Make ACP/Zed status obvious before users go source-diving

ROADMAP #21, #22, and #23 were already closed on current main, so the next real repo-local backlog item was the ACP/Zed discoverability gap. This adds a local `claw acp` status surface plus aliases, updates help/docs, and separates the shipped discoverability fix from the still-open daemon/protocol follow-up so editor-first users get a crisp answer immediately.

Constraint: No ACP/Zed daemon or protocol server exists in claw-code yet, so the new surface must be explicit status guidance rather than a fake implementation
Rejected: Add a pretend `acp serve` daemon path | would imply supported protocol behavior that does not exist
Rejected: Docs-only clarification | still leaves `claw --help` unable to answer the editor-launch question directly
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep ROADMAP discoverability fixes separate from future ACP daemon/protocol work so help text and backlog IDs stay unambiguous
Tested: cargo fmt --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; cargo run -q -p rusty-claude-cli -- acp; cargo run -q -p rusty-claude-cli -- --output-format json acp; architect review APPROVED
Not-tested: Real ACP/Zed daemon launch because no protocol-serving surface exists yet
This commit is contained in:
Yeachan-Heo
2026-04-16 03:13:50 +00:00
parent 64e058f720
commit ac45bbec15
5 changed files with 129 additions and 2 deletions

View File

@@ -33,6 +33,8 @@ The canonical implementation lives in [`rust/`](./rust), and the current source
> [!IMPORTANT]
> Start with [`USAGE.md`](./USAGE.md) for build, auth, CLI, session, and parity-harness workflows. Make `claw doctor` your first health check after building, use [`rust/README.md`](./rust/README.md) for crate-level details, read [`PARITY.md`](./PARITY.md) for the current Rust-port checkpoint, and see [`docs/container.md`](./docs/container.md) for the container-first workflow.
>
> **ACP / Zed status:** `claw-code` does not ship an ACP/Zed daemon entrypoint yet. Run `claw acp` (or `claw --acp`) for the current status instead of guessing from source layout; `claw acp serve` is currently a discoverability alias only, and real ACP support remains tracked separately in `ROADMAP.md`.
## Current repository shape

View File

@@ -1145,9 +1145,10 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
63. **Droid session completion semantics broken: code arrives after "status: completed"** — dogfooded 2026-04-12. Ultraclaw droid sessions (use-droid via acpx) report `session.status: completed` before file writes are fully flushed/synced to the working tree. Discovered +410 lines of "late-arriving" droid output that appeared after I had already assessed 8 sessions as "no code produced." This creates false-negative assessments and duplicate work. **Fix shape:** (a) droid agent should only report completion after explicit file-write confirmation (fsync or existence check); (b) or, claw-code should expose a `pending_writes` status that indicates "agent responded, disk flush pending"; (c) lane orchestrators should poll for file changes for N seconds after completion before final assessment. **Blocker:** none. Source: Jobdori ultraclaw dogfood 2026-04-12.
64. **ACP/Zed editor integration entrypoint is too implicit**dogfooded 2026-04-13 from a user request for a `-acp` parameter to support ACP protocol integration in editor-first workflows such as Zed. The gap is not generic "please add another integration" churn; it is a **discoverability and launch-contract problem**. Right now the product surface does not make it obvious whether ACP is already supported, how an editor should invoke claw-code, or whether a dedicated flag/mode exists at all. That forces evaluators into repo archaeology instead of giving them a crisp editor-facing invocation contract. **Fix shape:** either (a) add an explicit ACP/editor entrypoint such as `--acp` / `acp serve` with help text that states the contract, or (b) if the protocol path already exists, surface it prominently in CLI help/README with a concrete Zed/editor integration example so users do not have to guess. **Acceptance bar:** an editor-first user can answer "how do I launch claw-code for ACP/Zed?" from `claw --help` or the first screen of docs without reading source. **Blocker:** none; currently recorded as a roadmap follow-up because the repo-local entrypoint was not obvious during dogfood.
64a. **ACP/Zed editor integration entrypoint is too implicit****done (verified 2026-04-16):** `claw` now exposes a local `acp` discoverability surface (`claw acp`, `claw acp serve`, `claw --acp`, `claw -acp`) that answers the editor-first question directly without starting the runtime, and `claw --help` / `rust/README.md` now surface the ACP/Zed status in first-screen command/docs text. The current contract is explicit: claw-code does **not** ship an ACP/Zed daemon entrypoint yet; `claw acp serve` is only a status alias, while real ACP protocol support is tracked separately as #76. Fresh proof: parser coverage for `acp`/`acp serve`/flag aliases, help rendering coverage, and JSON output coverage for `claw --output-format json acp`.
Original filing (2026-04-13): user requested a `-acp` parameter to support ACP protocol integration in editor-first workflows such as Zed. The gap was a **discoverability and launch-contract problem**: the product surface did not make it obvious whether ACP was supported, how an editor should invoke claw-code, or whether a dedicated flag/mode existed at all.
64. **Artifact provenance is post-hoc narration, not structured events****done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now attaches structured `artifactProvenance` metadata to `lane.finished`, including `sourceLanes`, `roadmapIds`, `files`, `diffStat`, `verification`, and `commitSha`, while keeping the existing `lane.commit.created` provenance event intact. Regression coverage locks a successful completion payload that carries roadmap ids, file paths, diff stat, verification states, and commit sha without relying on prose re-parsing. **Original filing below.**
64b. **Artifact provenance is post-hoc narration, not structured events****done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now attaches structured `artifactProvenance` metadata to `lane.finished`, including `sourceLanes`, `roadmapIds`, `files`, `diffStat`, `verification`, and `commitSha`, while keeping the existing `lane.commit.created` provenance event intact. Regression coverage locks a successful completion payload that carries roadmap ids, file paths, diff stat, verification states, and commit sha without relying on prose re-parsing. **Original filing below.**
65. **Backlog-scanning team lanes emit opaque stops, not structured selection outcomes****done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now recognizes backlog-scan selection summaries and records structured `selectionOutcome` metadata on `lane.finished`, including `chosenItems`, `skippedItems`, `action`, and optional `rationale`, while preserving existing non-selection and review-lane behavior. Regression coverage locks the structured backlog-scan payload alongside the earlier quality-floor and review-verdict paths. **Original filing below.**
@@ -1169,3 +1170,5 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
74. **Poisoned test locks cascade into unrelated Rust regressions****done (verified 2026-04-12):** test-only env/cwd lock acquisition in `rust/crates/tools/src/lib.rs`, `rust/crates/plugins/src/lib.rs`, `rust/crates/commands/src/lib.rs`, and `rust/crates/rusty-claude-cli/src/main.rs` now recovers poisoned mutexes via `PoisonError::into_inner`, and new regressions lock that behavior so one panic no longer causes later tests to fail just by touching the shared env/cwd locks. Source: Jobdori dogfood 2026-04-12.
75. **`claw init` leaves `.clawhip/` runtime artifacts unignored** — **done (verified 2026-04-12):** `rust/crates/rusty-claude-cli/src/init.rs` now treats `.clawhip/` as a first-class local artifact alongside `.claw/` paths, and regression coverage locks both the create and idempotent update paths so `claw init` adds the ignore entry exactly once. The repo `.gitignore` now also ignores `.clawhip/` for immediate dogfood relief, preventing repeated OMX team merge conflicts on `.clawhip/state/prompt-submit.json`. Source: Jobdori dogfood 2026-04-12.
76. **Real ACP/Zed daemon contract is still missing after the discoverability fix** — follow-up filed 2026-04-16. ROADMAP #64 made the current status explicit via `claw acp`, but editor-first users still cannot actually launch claw-code as an ACP/Zed daemon because there is no protocol-serving surface yet. **Fix shape:** add a real ACP entrypoint (for example `claw acp serve`) only when the underlying protocol/transport contract exists, then document the concrete editor wiring in `claw --help` and first-screen docs. **Acceptance bar:** an editor can launch claw-code for ACP/Zed from a documented, supported command rather than a status-only alias. **Blocker:** protocol/runtime work not yet implemented; current `acp serve` spelling is intentionally guidance-only.

View File

@@ -135,6 +135,7 @@ Top-level commands:
version
status
sandbox
acp [serve]
dump-manifests
bootstrap-plan
agents
@@ -144,6 +145,8 @@ Top-level commands:
init
```
`claw acp` is a local discoverability surface for editor-first users: it reports the current ACP/Zed status without starting the runtime. As of April 16, 2026, claw-code does **not** ship an ACP/Zed daemon entrypoint yet, and `claw acp serve` is only a status alias until the real protocol surface lands.
The command surface is moving quickly. For the canonical live help text, run:
```bash

View File

@@ -95,6 +95,8 @@ const CLI_OPTION_SUGGESTIONS: &[&str] = &[
"--allowedTools",
"--allowed-tools",
"--resume",
"--acp",
"-acp",
"--print",
"--compact",
"--base-commit",
@@ -248,6 +250,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
cli.run_turn_with_output(&effective_prompt, output_format, compact)?;
}
CliAction::Doctor { output_format } => run_doctor(output_format)?,
CliAction::Acp { output_format } => print_acp_status(output_format)?,
CliAction::State { output_format } => run_worker_state(output_format)?,
CliAction::Init { output_format } => run_init(output_format)?,
CliAction::Export {
@@ -337,6 +340,9 @@ enum CliAction {
Doctor {
output_format: CliOutputFormat,
},
Acp {
output_format: CliOutputFormat,
},
State {
output_format: CliOutputFormat,
},
@@ -368,6 +374,7 @@ enum LocalHelpTopic {
Status,
Sandbox,
Doctor,
Acp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -547,6 +554,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
rest.push(flag[9..].to_string());
index += 1;
}
"--acp" | "-acp" => {
rest.push("acp".to_string());
index += 1;
}
"--allowedTools" | "--allowed-tools" => {
let value = args
.get(index + 1)
@@ -661,6 +672,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}
}
"system-prompt" => parse_system_prompt_args(&rest[1..], output_format),
"acp" => parse_acp_args(&rest[1..], output_format),
"login" | "logout" => Err(removed_auth_surface_error(rest[0].as_str())),
"init" => Ok(CliAction::Init { output_format }),
"export" => parse_export_args(&rest[1..], output_format),
@@ -715,6 +727,7 @@ fn parse_local_help_action(rest: &[String]) -> Option<Result<CliAction, String>>
"status" => LocalHelpTopic::Status,
"sandbox" => LocalHelpTopic::Sandbox,
"doctor" => LocalHelpTopic::Doctor,
"acp" => LocalHelpTopic::Acp,
_ => return None,
};
Some(Ok(CliAction::HelpTopic(topic)))
@@ -785,6 +798,16 @@ fn removed_auth_surface_error(command_name: &str) -> String {
)
}
fn parse_acp_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
match args {
[] => Ok(CliAction::Acp { output_format }),
[subcommand] if subcommand == "serve" => Ok(CliAction::Acp { output_format }),
_ => Err(String::from(
"unsupported ACP invocation. Use `claw acp`, `claw acp serve`, `claw --acp`, or `claw -acp`.",
)),
}
}
fn try_resolve_bare_skill_prompt(cwd: &Path, trimmed: &str) -> Option<String> {
let bare_first_token = trimmed.split_whitespace().next().unwrap_or_default();
let looks_like_skill_name = !bare_first_token.is_empty()
@@ -5175,6 +5198,13 @@ fn render_help_topic(topic: LocalHelpTopic) -> String {
Output local-only health report; no provider request or session resume required
Related /doctor · claw --resume latest /doctor"
.to_string(),
LocalHelpTopic::Acp => "ACP / Zed
Usage claw acp [serve]
Aliases claw --acp · claw -acp
Purpose explain the current editor-facing ACP/Zed launch contract without starting the runtime
Status discoverability only; `serve` is a status alias and does not launch a daemon yet
Related ROADMAP #64a (discoverability) · ROADMAP #76 (real ACP support) · claw --help"
.to_string(),
}
}
@@ -5182,6 +5212,39 @@ fn print_help_topic(topic: LocalHelpTopic) {
println!("{}", render_help_topic(topic));
}
fn print_acp_status(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let message = "ACP/Zed editor integration is not implemented in claw-code yet. `claw acp serve` is only a discoverability alias today; it does not launch a daemon or Zed-specific protocol endpoint. Use the normal terminal surfaces for now and track ROADMAP #76 for real ACP support.";
match output_format {
CliOutputFormat::Text => {
println!(
"ACP / Zed\n Status discoverability only\n Launch `claw acp serve` / `claw --acp` / `claw -acp` report status only; no editor daemon is available yet\n Today use `claw prompt`, the REPL, or `claw doctor` for local verification\n Tracking ROADMAP #76\n Message {message}"
);
}
CliOutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "acp",
"status": "discoverability_only",
"supported": false,
"serve_alias_only": true,
"message": message,
"launch_command": serde_json::Value::Null,
"aliases": ["acp", "--acp", "-acp"],
"discoverability_tracking": "ROADMAP #64a",
"tracking": "ROADMAP #76",
"recommended_workflows": [
"claw prompt TEXT",
"claw",
"claw doctor"
],
}))?
);
}
}
Ok(())
}
fn render_config_report(section: Option<&str>) -> Result<String, Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
@@ -8148,6 +8211,11 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
out,
" Diagnose local auth, config, workspace, and sandbox health"
)?;
writeln!(out, " claw acp [serve]")?;
writeln!(
out,
" Show ACP/Zed editor integration status (currently unsupported; aliases: --acp, -acp)"
)?;
writeln!(out, " Source of truth: {OFFICIAL_REPO_SLUG}")?;
writeln!(
out,
@@ -9200,6 +9268,34 @@ mod tests {
);
}
#[test]
fn parses_acp_command_surfaces() {
assert_eq!(
parse_args(&["acp".to_string()]).expect("acp should parse"),
CliAction::Acp {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["acp".to_string(), "serve".to_string()]).expect("acp serve should parse"),
CliAction::Acp {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["--acp".to_string()]).expect("--acp should parse"),
CliAction::Acp {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["-acp".to_string()]).expect("-acp should parse"),
CliAction::Acp {
output_format: CliOutputFormat::Text,
}
);
}
#[test]
fn local_command_help_flags_stay_on_the_local_parser_path() {
assert_eq!(
@@ -9217,6 +9313,10 @@ mod tests {
.expect("doctor help should parse"),
CliAction::HelpTopic(LocalHelpTopic::Doctor)
);
assert_eq!(
parse_args(&["acp".to_string(), "--help".to_string()]).expect("acp help should parse"),
CliAction::HelpTopic(LocalHelpTopic::Acp)
);
}
#[test]
@@ -10125,6 +10225,7 @@ mod tests {
assert!(help.contains("claw status"));
assert!(help.contains("claw sandbox"));
assert!(help.contains("claw init"));
assert!(help.contains("claw acp [serve]"));
assert!(help.contains("claw agents"));
assert!(help.contains("claw mcp"));
assert!(help.contains("claw skills"));

View File

@@ -46,6 +46,24 @@ fn status_and_sandbox_emit_json_when_requested() {
assert!(sandbox["filesystem_mode"].as_str().is_some());
}
#[test]
fn acp_guidance_emits_json_when_requested() {
let root = unique_temp_dir("acp-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let acp = assert_json_command(&root, &["--output-format", "json", "acp"]);
assert_eq!(acp["kind"], "acp");
assert_eq!(acp["status"], "discoverability_only");
assert_eq!(acp["supported"], false);
assert_eq!(acp["serve_alias_only"], true);
assert_eq!(acp["discoverability_tracking"], "ROADMAP #64a");
assert_eq!(acp["tracking"], "ROADMAP #76");
assert!(acp["message"]
.as_str()
.expect("acp message")
.contains("discoverability alias"));
}
#[test]
fn inventory_commands_emit_structured_json_when_requested() {
let root = unique_temp_dir("inventory-json");