diff --git a/README.md b/README.md index dcf4038e..0b090077 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,87 @@ export ANTHROPIC_API_KEY="sk-ant-..." **Git Bash / WSL** are optional alternatives, not requirements. If you prefer bash-style paths (`/c/Users/you/...` instead of `C:\Users\you\...`), Git Bash (ships with Git for Windows) works well. In Git Bash, the `MINGW64` prompt is expected and normal — not a broken install. +## Post-build: locate the binary and verify + +After running `cargo build --workspace`, the `claw` binary is built but **not** automatically installed to your system. Here's where to find it and how to verify the build succeeded. + +### Binary location + +After `cargo build --workspace` in `claw-code/rust/`: + +**Debug build (default, faster compile):** +- **macOS/Linux:** `rust/target/debug/claw` +- **Windows:** `rust/target/debug/claw.exe` + +**Release build (optimized, slower compile):** +- **macOS/Linux:** `rust/target/release/claw` +- **Windows:** `rust/target/release/claw.exe` + +If you ran `cargo build` without `--release`, the binary is in the `debug/` folder. + +### Verify the build succeeded + +Test the binary directly using its path: + +```bash +# macOS/Linux (debug build) +./rust/target/debug/claw --help +./rust/target/debug/claw doctor + +# Windows PowerShell (debug build) +.\rust\target\debug\claw.exe --help +.\rust\target\debug\claw.exe doctor +``` + +If these commands succeed, the build is working. `claw doctor` is your first health check — it validates your API key, model access, and tool configuration. + +### Optional: Add to PATH + +If you want to run `claw` from any directory without the full path, choose one of these approaches: + +**Option 1: Symlink (macOS/Linux)** +```bash +ln -s $(pwd)/rust/target/debug/claw /usr/local/bin/claw +``` +Then reload your shell and test: +```bash +claw --help +``` + +**Option 2: Use `cargo install` (all platforms)** + +Build and install to Cargo's default location (`~/.cargo/bin/`, which is usually on PATH): +```bash +# From the claw-code/rust/ directory +cargo install --path . --force + +# Then from anywhere +claw --help +``` + +**Option 3: Update shell profile (bash/zsh)** + +Add this line to `~/.bashrc` or `~/.zshrc`: +```bash +export PATH="$(pwd)/rust/target/debug:$PATH" +``` + +Reload your shell: +```bash +source ~/.bashrc # or source ~/.zshrc +claw --help +``` + +### Troubleshooting + +- **"command not found: claw"** — The binary is in `rust/target/debug/claw`, but it's not on your PATH. Use the full path `./rust/target/debug/claw` or symlink/install as above. +- **"permission denied"** — On macOS/Linux, you may need `chmod +x rust/target/debug/claw` if the executable bit isn't set (rare). +- **Debug vs. release** — If the build is slow, you're in debug mode (default). Add `--release` to `cargo build` for faster runtime, but the build itself will take 5–10 minutes. + > [!NOTE] > **Auth:** claw requires an **API key** (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) — Claude subscription login is not a supported auth path. -Run the workspace test suite: +Run the workspace test suite after verifying the binary works: ```bash cd rust diff --git a/ROADMAP.md b/ROADMAP.md index 421beaa6..688fe166 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -107,6 +107,20 @@ Acceptance: - long silent gaps between ready-state and first-task execution become machine-visible - recovery can trigger on acceptance timeout before humans start scraping panes +### 1.6. `startup-no-evidence` evidence bundle + classifier +Dogfooded 2026-04-16. Team worker startup can stall in a vague `startup-no-evidence` state even when the real failure mode is more specific (trust prompt, shell misdelivery, prompt never accepted, transport death, or crashed worker). Claws currently have to infer too much from pane noise after the fact. + +Required behavior: +- when startup times out, emit a typed `worker.startup_no_evidence` event with an attached evidence bundle +- evidence bundle should include last known worker lifecycle state, pane command, prompt-send timestamp, prompt-acceptance state, trust-prompt detection result, and transport/MCP health summary +- classifier should attempt to down-rank the vague bucket into one of: `trust_required`, `prompt_misdelivery`, `prompt_acceptance_timeout`, `transport_dead`, `worker_crashed`, or `unknown` +- keep the raw `startup-no-evidence` marker only as a fallback when no stronger classification exists + +Acceptance: +- a claw can report why startup stalled without manually scraping tmux output +- repeated `startup-no-evidence` incidents become clusterable by real root-cause class +- clawhip summaries can say `worker stalled: prompt acceptance timeout` instead of a human-only mystery label + ### 2. Trust prompt resolver Add allowlisted auto-trust behavior for known repos/worktrees. @@ -830,6 +844,49 @@ Acceptance: - channel status updates stay short and machine-grounded - claws stop inferring state from raw build spam +### 140. Deprecated `permissionMode` migration silently downgrades `DangerFullAccess` to `WorkspaceWrite` + +**Filed:** 2026-04-21 from dogfood cycle — `cargo test --workspace` on `main` HEAD `36b3a09` shows 1 deterministic failure. + +**Problem:** `tests::punctuation_bearing_single_token_still_dispatches_to_prompt` fails with: +``` +assert left == right failed + left: ... permission_mode: WorkspaceWrite ... + right: ... permission_mode: DangerFullAccess ... +warning: .claw/settings.json: field "permissionMode" is deprecated (line 1). Use "permissions.defaultMode" instead +``` +The test fixture writes a `.claw/settings.json` with the deprecated `permissionMode: "dangerFullAccess"` key. The migration/deprecation shim reads it but resolves to `WorkspaceWrite` instead of `DangerFullAccess`. Result: `cargo test --workspace` is red on `main` with 172 passing, 1 failing. + +**Root cause hypothesis:** The deprecated field reader in `parse_args` or `ConfigLoader` applies the `permissionMode` value through a permission-mode resolver that does not map `"dangerFullAccess"` to `PermissionMode::DangerFullAccess`, likely defaulting or falling back to `WorkspaceWrite`. + +**Fix shape:** +- Ensure the deprecated-key migration path correctly maps `permissionMode: "dangerFullAccess"` → `PermissionMode::DangerFullAccess` (same as `permissions.defaultMode: "dangerFullAccess"`). +- Alternatively, update the test fixture to use the canonical `permissions.defaultMode` key so it exercises the migration shim rather than depending on it. +- Verify `cargo test --workspace` returns 0 failures. + +**Acceptance:** +- `cargo test --workspace` passes with 0 failures on `main`. +- Deprecated `permissionMode: "dangerFullAccess"` migrates cleanly to `DangerFullAccess` without downgrading to `WorkspaceWrite`. + +### 137. Model-alias shorthand regression in test suite — bare alias parsing broken on `feat/134-135-session-identity` branch + +**Filed:** 2026-04-21 from dogfood cycle — `cargo test --workspace` on `feat/134-135-session-identity` HEAD (`91ba54d`) shows 3 failing tests. + +**Problem:** `tests::parses_bare_prompt_and_json_output_flag`, `tests::multi_word_prompt_still_uses_shorthand_prompt_mode`, and `tests::env_permission_mode_overrides_project_config_default` all panic with: +``` +args should parse: "invalid model syntax: 'claude-opus'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku)" +``` +The #134/#135 session-identity work tightened model-syntax validation but the test fixtures still pass bare `claude-opus` style strings that the new validator rejects. 162 tests pass; only the three tests using legacy bare-alias model names fail. + +**Fix shape:** +- Update the three failing test fixtures to use either a valid alias (`opus`, `sonnet`, `haiku`) or a fully-qualified model id (`anthropic/claude-opus-4-6`) +- Alternatively, if `claude-opus` is an intended supported alias, add it to the alias registry +- Verify `cargo test --workspace` returns 0 failures before merging the feat branch to `main` + +**Acceptance:** +- `cargo test --workspace` passes with 0 failures on the `feat/134-135-session-identity` branch +- No regression on the 162 tests currently passing + ### 133. Blocked-state subphase contract (was §6.5) **Filed:** 2026-04-20 from dogfood cycle — previous cycle identified §4.44.5 provenance gap, this cycle targets §6.5 implementation. @@ -1053,6 +1110,7 @@ Priority order: P0 = blocks CI/green state, P1 = blocks integration wiring, P2 = 27. **`dev/rust` `cargo test -p rusty-claude-cli` reads host `~/.claude/plugins/installed/` from real `$HOME` and fails parse-time on any half-installed user plugin** — dogfooding on 2026-04-08 (filed from gaebal-gajae's clawhip bullet at message `1491322807026454579` after the provider-matrix branch QA surfaced it) reproduced 11 deterministic failures on clean `dev/rust` HEAD of the form `panicked at crates/rusty-claude-cli/src/main.rs:3953:31: args should parse: "hook path \`/Users/yeongyu/.claude/plugins/installed/sample-hooks-bundled/./hooks/pre.sh\` does not exist; hook path \`...\post.sh\` does not exist"` covering `parses_prompt_subcommand`, `parses_permission_mode_flag`, `defaults_to_repl_when_no_args`, `parses_resume_flag_with_slash_command`, `parses_system_prompt_options`, `parses_bare_prompt_and_json_output_flag`, `rejects_unknown_allowed_tools`, `parses_resume_flag_with_multiple_slash_commands`, `resolves_model_aliases_in_args`, `parses_allowed_tools_flags_with_aliases_and_lists`, `parses_login_and_logout_subcommands`. **Same failures do NOT reproduce on `main`** (re-verified with `cargo test --release -p rusty-claude-cli` against `main` HEAD `79da4b8`, all 156 tests pass). **Root cause is two-layered.** First, on `dev/rust` `parse_args` eagerly walks user-installed plugin manifests under `~/.claude/plugins/installed/` and validates that every declared hook script exists on disk before returning a `CliAction`, so any half-installed plugin in the developer's real `$HOME` (in this case `~/.claude/plugins/installed/sample-hooks-bundled/` whose `.claude-plugin` manifest references `./hooks/pre.sh` and `./hooks/post.sh` but whose `hooks/` subdirectory was deleted) makes argv parsing itself fail. Second, the test harness on `dev/rust` does not redirect `$HOME` or `XDG_CONFIG_HOME` to a fixture for the duration of the test — there is no `env_lock`-style guard equivalent to the one `main` already uses (`grep -n env_lock rust/crates/rusty-claude-cli/src/main.rs` returns 0 hits on `dev/rust` and 30+ hits on `main`). Together those two gaps mean `dev/rust` `cargo test -p rusty-claude-cli` is non-deterministic on every clean clone whose owner happens to have any non-pristine plugin in `~/.claude/`. **Action (two parts).** (a) Backport the `env_lock`-based test isolation pattern from `main` into `dev/rust`'s `rusty-claude-cli` test module so each test runs against a temp `$HOME`/`XDG_CONFIG_HOME` and cannot read host plugin state. (b) Decouple `parse_args` from filesystem hook validation on `dev/rust` (the same decoupling already on `main`, where hook validation happens later in the lifecycle than argv parsing) so even outside tests a partially installed user plugin cannot break basic CLI invocation. **Branch scope.** This is a `dev/rust` catchup against `main`, not a `main` regression. Tracking it here so the dev/rust merge train picks it up before the next dev/rust release rather than rediscovering it in CI. 28. **Auth-provider truth: error copy fails real users at the env-var-vs-header layer** — dogfooded live on 2026-04-08 in #claw-code (Sisyphus Labs guild), two separate new users hit adjacent failure modes within minutes of each other that both trace back to the same root: the `MissingApiKey` / 401 error surface does not teach users how the auth inputs map to HTTP semantics, so a user who sets a "reasonable-looking" env var still hits a hard error with no signpost. **Case 1 (varleg, Norway).** Wanted to use OpenRouter via the OpenAI-compat path. Found a comparison table claiming "provider-agnostic (Claude, OpenAI, local models)" and assumed it Just Worked. Set `OPENAI_API_KEY` to an OpenRouter `sk-or-v1-...` key and a model name without an `openai/` prefix; claw's provider detection fell through to Anthropic first because `ANTHROPIC_API_KEY` was still in the environment. Unsetting `ANTHROPIC_API_KEY` got them `ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY is not set` instead of a useful hint that the OpenAI path was right there. Fix delivered live as a channel reply: use `main` branch (not `dev/rust`), export `OPENAI_BASE_URL=https://openrouter.ai/api/v1` alongside `OPENAI_API_KEY`, and prefix the model name with `openai/` so the prefix router wins over env-var presence. **Case 2 (stanley078852).** Had set `ANTHROPIC_AUTH_TOKEN="sk-ant-..."` and was getting 401 `Invalid bearer token` from Anthropic. Root cause: `sk-ant-` keys are `x-api-key`-header keys, not bearer tokens. `ANTHROPIC_API_KEY` path in `anthropic.rs` sends the value as `x-api-key`; `ANTHROPIC_AUTH_TOKEN` path sends it as `Authorization: Bearer` (for OAuth access tokens from `claw login`). Setting an `sk-ant-` key in the wrong env var makes claw send it as `Bearer sk-ant-...` which Anthropic rejects at the edge with 401 before it ever reaches the completions endpoint. The error text propagated all the way to the user (`api returned 401 Unauthorized (authentication_error) ... Invalid bearer token`) with zero signal that the problem was env-var choice, not key validity. Fix delivered live as a channel reply: move the `sk-ant-...` key to `ANTHROPIC_API_KEY` and unset `ANTHROPIC_AUTH_TOKEN`. **Pattern.** Both cases are failures at the *auth-intent translation* layer: the user chose an env var that made syntactic sense to them (`OPENAI_API_KEY` for OpenAI, `ANTHROPIC_AUTH_TOKEN` for Anthropic auth) but the actual wire-format routing requires a more specific choice. The error messages surface the HTTP-layer symptom (401, missing-key) without bridging back to "which env var should you have used and why." **Action.** Three concrete improvements, scoped for a single `main`-side PR: (a) In `ApiError::MissingCredentials` Display, when the Anthropic path is the one being reported but `OPENAI_API_KEY`, `XAI_API_KEY`, or `DASHSCOPE_API_KEY` are present in the environment, extend the message with "— but I see `$OTHER_KEY` set; if you meant to use that provider, prefix your model name with `openai/`, `grok`, or `qwen/` respectively so prefix routing selects it." (b) In the 401-from-Anthropic error path in `anthropic.rs`, when the failing auth source is `BearerToken` AND the bearer token starts with `sk-ant-`, append "— looks like you put an `sk-ant-*` API key in `ANTHROPIC_AUTH_TOKEN`, which is the Bearer-header path. Move it to `ANTHROPIC_API_KEY` instead (that env var maps to `x-api-key`, which is the correct header for `sk-ant-*` keys)." Same treatment for OAuth access tokens landing in `ANTHROPIC_API_KEY` (symmetric mis-assignment). (c) In `rust/README.md` on `main` and the matrix section on `dev/rust`, add a short "Which env var goes where" paragraph mapping `sk-ant-*` → `ANTHROPIC_API_KEY` and OAuth access token → `ANTHROPIC_AUTH_TOKEN`, with the one-line explanation of `x-api-key` vs `Authorization: Bearer`. **Verification path.** Both improvements can be tested with unit tests against `ApiError::fmt` output (the prefix-routing hint) and with a targeted integration test that feeds an `sk-ant-*`-shaped token into `BearerToken` and asserts the fmt output surfaces the correction hint (no HTTP call needed). **Source.** Live users in #claw-code at `1491328554598924389` (varleg) and `1491329840706486376` (stanley078852) on 2026-04-08. **Partial landing (`ff1df4c`).** Action parts (a), (b), (c) shipped on `main`: `MissingCredentials` now carries an optional hint field and renders adjacent-provider signals, Anthropic 401 + `sk-ant-*` bearer gets a correction hint, USAGE.md has a "Which env var goes where" section. BUT the copy fix only helps users who fell through to the Anthropic auth path by accident — it does NOT fix the underlying routing bug where the CLI instantiates `AnthropicRuntimeClient` unconditionally and ignores prefix routing at the runtime-client layer. That deeper routing gap is tracked separately as #29 below and was filed within hours of #28 landing when live users still hit `missing Anthropic credentials` with `--model openai/gpt-4` and all `ANTHROPIC_*` env vars unset. 29. **CLI provider dispatch is hardcoded to Anthropic, ignoring prefix routing** — **done at `8dc6580` on 2026-04-08**. Changed `AnthropicRuntimeClient.client` from concrete `AnthropicClient` to `ApiProviderClient` (the api crate's `ProviderClient` enum), which dispatches to Anthropic / xAI / OpenAi at construction time based on `detect_provider_kind(&resolved_model)`. 1 file, +59 −7, all 182 rusty-claude-cli tests pass, CI green at run `24125825431`. Users can now run `claw --model openai/gpt-4.1-mini prompt "hello"` with only `OPENAI_API_KEY` set and it routes correctly. **Original filing below for the trace record.** Dogfooded live on 2026-04-08 within hours of ROADMAP #28 landing. Users in #claw-code (nicma at `1491342350960562277`, Jengro at `1491345009021030533`) followed the exact "use main, set OPENAI_API_KEY and OPENAI_BASE_URL, unset ANTHROPIC_*, prefix the model with `openai/`" checklist from the #28 error-copy improvements AND STILL hit `error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API`. **Reproduction on `main` HEAD `ff1df4c`:** `unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN; export OPENAI_API_KEY=sk-...; export OPENAI_BASE_URL=https://api.openai.com/v1; claw --model openai/gpt-4 prompt 'test'` → reproduces the error deterministically. **Root cause (traced).** `rust/crates/rusty-claude-cli/src/main.rs` at `build_runtime_with_plugin_state` (line ~6221) unconditionally builds `AnthropicRuntimeClient::new(session_id, model, ...)` without consulting `providers::detect_provider_kind(&model)`. `BuiltRuntime` at line ~2855 is statically typed as `ConversationRuntime`, so even if the dispatch logic existed there would be nowhere to slot an alternative client. `providers/mod.rs::metadata_for_model` correctly identifies `openai/gpt-4` as `ProviderKind::OpenAi` at the metadata layer — the routing decision is *computed* correctly, it's just *never used* to pick a runtime client. The result is that the CLI is structurally single-provider (Anthropic only) even though the `api` crate's `openai_compat.rs`, `XAI_ENV_VARS`, `DASHSCOPE_ENV_VARS`, and `send_message_streaming` all exist and are exercised by unit tests inside the `api` crate. The provider matrix in `rust/README.md` is misleading because it describes the api-crate capabilities, not the CLI's actual dispatch behaviour. **Why #28 didn't catch this.** ROADMAP #28 focused on the `MissingCredentials` error *message* (adding hints when adjacent provider env vars are set, or when a bearer token starts with `sk-ant-*`). None of its tests exercised the `build_runtime` code path — they were all unit tests against `ApiError::fmt` output. The routing bug survives #28 because the `Display` improvements fire AFTER the hardcoded Anthropic client has already been constructed and failed. You need the CLI to dispatch to a different client in the first place for the new hints to even surface at the right moment. **Action (single focused commit).** (1) New `OpenAiCompatRuntimeClient` struct in `rust/crates/rusty-claude-cli/src/main.rs` mirroring `AnthropicRuntimeClient` but delegating to `openai_compat::send_message_streaming`. One client type handles OpenAI, xAI, DashScope, and any OpenAI-compat endpoint — they differ only in base URL and auth env var, both of which come from the `ProviderMetadata` returned by `metadata_for_model`. (2) New enum `DynamicApiClient { Anthropic(AnthropicRuntimeClient), OpenAiCompat(OpenAiCompatRuntimeClient) }` that implements `runtime::ApiClient` by matching on the variant and delegating. (3) Retype `BuiltRuntime` from `ConversationRuntime` to `ConversationRuntime`, update the Deref/DerefMut/new spots. (4) In `build_runtime_with_plugin_state`, call `detect_provider_kind(&model)` and construct either variant of `DynamicApiClient`. Prefix routing wins over env-var presence (that's the whole point). (5) Integration test using a mock OpenAI-compat server (reuse `mock_parity_harness` pattern from `crates/api/tests/`) that feeds `claw --model openai/gpt-4 prompt 'test'` with `OPENAI_BASE_URL` pointed at the mock and no `ANTHROPIC_*` env vars, asserts the request reaches the mock, and asserts the response round-trips as an `AssistantEvent`. (6) Unit test that `build_runtime_with_plugin_state` with `model="openai/gpt-4"` returns a `BuiltRuntime` whose inner client is the `DynamicApiClient::OpenAiCompat` variant. **Verification.** `cargo test --workspace`, `cargo fmt --all`, `cargo clippy --workspace`. **Source.** Live users nicma (`1491342350960562277`) and Jengro (`1491345009021030533`) in #claw-code on 2026-04-08, within hours of #28 landing. +30. **Immediate-backlog visibility gap: active dogfood pinpoints are easy to rediscover because ROADMAP lacks a concise in-progress board** — dogfooding on 2026-04-21 surfaced a softer but recurring clawability failure: there are real active branches/sessions (`claw-code-issue-21-resumed-status-json`, `claw-code-issue-24-plugin-lifecycle-flake`, `claw-code-issue-33-xai-integration`), but a claw doing a fresh sweep still has to scrape tmux names, branch diffs, and long-form ROADMAP prose to answer a simple question: "what pinpoint is already active right now, and what delta is in flight?" The result is rediscovery churn, duplicate reporting, and weak handoff quality even when the actual engineering work is already moving. **Concrete gap.** `ROADMAP.md` has rich long-form entries and a large done/archive surface, but no compact machine-friendly `In Progress Now` section that binds `{roadmap_id, pinpoint, owner/session, branch, status, blocker}`. **Action.** Add a small top-of-file/current-work section (or generated JSON companion) that lists only active dogfood items with stable ids and lifecycle state, and require dogfood updates to reference that id when reporting progress. Minimum fields: item id, lifecycle state, current session/branch, one-line delta, blocker/none, last-updated timestamp. **Acceptance.** A fresh claw can answer "what is active now?" from one short section without scraping panes, and repeat dogfood nudges can distinguish `already in progress` from `new pinpoint` automatically. 41. **Phantom completions root cause: global session store has no per-worktree isolation** — @@ -4772,7 +4830,19 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label], **Source.** Jobdori dogfood 2026-04-20 against `/tmp/claw-dogfood` (env-cleaned, no git, no config) on main HEAD `7370546` in response to Clawhip pinpoint nudge at `1495620050424434758`. Joins **Silent-flag / documented-but-unenforced** (#96–#101, #104, #108, #111, #115, #116, #117, #118, #119, #121, #122, #123, #124, #126) as 18th — `--json` silently swallowed into Prompt dispatch instead of being recognized or rejected. Joins **Parser-level trust gap quintet** (#108, #117, #119, #122, **#127**) as 5th — same `_other => Prompt` fall-through arm, fifth distinct entry case (#108 = typoed verb, #117 = `-p` greedy, #119 = bare slash + arg, #122 = `--base-commit` greedy, **#127 = valid verb + unrecognized suffix arg**). Joins **Cred-error misdirection / failure-classification gaps** as a sibling of #99 (system-prompt unvalidated) — same family of "local diagnostic verb pretends to need API creds." Joins **Truth-audit / diagnostic-integrity** (#80–#87, #89, #100, #102, #103, #105, #107, #109, #110, #112, #114, #115, #125) — `claw --help` lies about per-verb accepted flags. Joins **Parallel-entry-point asymmetry** (#91, #101, #104, #105, #108, #114, #117, #122, #123, #124) as 11th — three working forms and one broken form for the same logical intent (`--json` doctor output). Joins **Claude Code migration parity** (#103, #109, #116) as 4th — Claude Code's `--json` convention shorthand is unrecognized in claw-code's verb-suffix position; users migrating get cred errors instead. Cross-cluster with **README/USAGE doc-vs-implementation gap** — README explicitly recommends `claw doctor` as the first health check; the natural JSON form of that exact command is broken. Natural bundle: **#108 + #117 + #119 + #122 + #127** — parser-level trust gap quintet: complete `_other => Prompt` fall-through audit (typoed verb + greedy `-p` + bare slash-verb + greedy `--base-commit` + valid verb + unrecognized suffix). Also **#99 + #127** — local-diagnostic cred-error misdirection pair: `system-prompt` and verb-suffix `--json` both pretend to need creds for pure-local operations. Also **#126 + #127** — diagnostic-verb surface integrity pair: `/config` section args ignored (#126) + verb-suffix args silently mis-dispatched (#127). Session tally: ROADMAP #127. -128. **`claw --model ` (spaces, empty string, special chars, invalid provider/model syntax) silently falls through to API-layer cred error instead of rejecting at parse time** — dogfooded 2026-04-20 on main HEAD `d284ef7` from a fresh environment (no config, no auth). The `--model` flag accepts any string without syntactic validation: spaces (`claw --model "bad model"`), empty strings (`claw --model ""`), special characters (`claw --model "@invalid"`), non-existent provider/model combinations all parse successfully. The malformed model string then flows into the runtime's provider-detection layer, which silently accepts it as Anthropic fallback or passes it to an API layer that fails with `missing Anthropic credentials` (misdirection) rather than a clear "invalid model syntax" error at parse time. With API credentials configured, a malformed model string gets sent to the API, billing tokens against a request that should have failed client-side. +128. **[CLOSED 2026-04-21]** **`claw --model ` (spaces, empty string, special chars, invalid provider/model syntax) silently falls through to API-layer cred error instead of rejecting at parse time** — dogfooded 2026-04-20 on main HEAD `d284ef7` from a fresh environment (no config, no auth). The `--model` flag accepts any string without syntactic validation: spaces (`claw --model "bad model"`), empty strings (`claw --model ""`), special characters (`claw --model "@invalid"`), non-existent provider/model combinations all parse successfully. The malformed model string then flows into the runtime's provider-detection layer, which silently accepts it as Anthropic fallback or passes it to an API layer that fails with `missing Anthropic credentials` (misdirection) rather than a clear "invalid model syntax" error at parse time. With API credentials configured, a malformed model string gets sent to the API, billing tokens against a request that should have failed client-side. + + **Closure (2026-04-21):** Re-verified on main HEAD `4cb8fa0`. All cases now rejected at parse time: + ``` + $ claw --model '' status → error: model string cannot be empty + $ claw --model 'bad model' status → error: invalid model syntax: 'bad model' contains spaces + $ claw --model 'sonet' status → error: invalid model syntax: 'sonet'. Expected provider/model ... + $ claw --model '@invalid' status → error: invalid model syntax: '@invalid'. Expected provider/model ... + $ claw --model 'totally-not-real-xyz' status → error: invalid model syntax ... + $ claw --model sonnet status → ok, resolves to claude-sonnet-4-6 + $ claw --model anthropic/claude-opus-4-6 status → ok, passes through + ``` + Validation happens in `validate_model_syntax()` before `resolve_model_alias_with_config()`. All `--model` and `--model=` parse paths call it. No API call ever reached with malformed input. Residual gap (model provenance in status JSON — raw input vs resolved value) was split off as #148 (see below). 129. **MCP server startup blocks credential validation — `claw ` with any `.claw.json` `mcpServers` entry awaits the MCP server's stdio handshake BEFORE checking whether the operator has Anthropic credentials. With no `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY` set and `mcpServers.everything = { command: "npx", args: ["-y", "@modelcontextprotocol/server-everything"] }` configured, the CLI hangs forever (verified via `timeout 30s` — still in MCP startup at 30s with three repeated `"Starting default (STDIO) server..."` lines), instead of fail-fasting with the same `missing Anthropic credentials` error that fires in milliseconds when no MCP is configured. A misconfigured-but-running MCP server (one that spawns successfully but never completes its `initialize` handshake) wedges every `claw ` invocation permanently. A misconfigured MCP server with a slow-but-eventually-succeeding init (npx download, container pull, network roundtrip) burns startup latency on every Prompt invocation regardless of whether the LLM call would even succeed. This is the runtime-side companion to #102's config-time MCP diagnostic gap: #102 says doctor doesn't surface MCP reachability; #129 says the Prompt path's reachability check is implicit, blocking, retried, and runs *before* the cheaper auth precondition that should run first** — dogfooded 2026-04-20 on main HEAD `d284ef7` from `/tmp/claw-mcp-test` with `env -i PATH=$PATH HOME=$HOME` (all auth env vars unset). @@ -5014,3 +5084,1173 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label], **Blocker.** None. Reuses existing `stale_base` module; no new logic needed, just a missing call site. **Source.** Jobdori dogfood 2026-04-20 against `/tmp/jobdori-129-mcp-cred-order` + `/tmp/stale-branch` in response to 10-min cron cycle. Confirmed: `claw doctor` on branch 5 commits behind main says "Status: ok" but `prompt` dispatch would warn "worktree HEAD does not match expected base commit." Gap is a missing invocation of the already-correct `run_stale_base_preflight()` in the `doctor` action handler. Joins **Boot preflight / doctor contract (#80–#83, #114)** family — doctor is the single machine-readable preflight surface; missing checks degrade operator trust. Also relates to **Silent-state inventory** cluster (#102/#127/#129/#245) because stale-base is a runtime truth ("my branch is behind main") that the preflight surface (doctor) does not expose. + +## Pinpoint #135. `claw status --json` missing `active_session` boolean and `session.id` cross-reference — two surfaces that should be unified are inconsistent + +**Gap.** `claw status --json` exposes a snapshot of the runtime state but does not include (1) a stable `session.id` field (filed as #134 — the fix from the other side is to emit it in lane events; the consumer side needs it queryable via `status` too) and (2) an `active_session: bool` that tells an orchestrator whether the runtime currently has a live session in flight. An external orchestrator (Clawhip, remote agent) running `claw status --json` after sending a prompt has no machine-readable way to confirm whether the session is alive, idle, or stalled without parsing log output. + +**Trace path.** +- `claw status --json` (dispatcher in `main.rs` `CliAction::Status`) renders a `StatusReport` struct that includes `git_state`, `config`, `model`, `provider` — but no `session_id` or `active_session` fields. +- `claw status` (text mode) also omits both. +- The `session.id` fix from #134 introduces a UUID at session init; it should be threaded through to `StatusReport` so the round-trip is complete: emit on startup event → queryable via `status --json` → correlatable in lane events. + +**Fix shape (~30 lines).** +1. Add `session_id: Option` and `active_session: bool` to `StatusReport` struct. Both `null`/`false` when no session is active. When a session is running, `session_id` is the same UUID emitted in the startup lane event (#134). +2. Thread the session state into the `status` handler via a shared `Arc>` or equivalent (same mechanism #134 uses for startup event emission). +3. Text-mode `claw status` surfaces the value: `Session: active (id: abc123)` or `Session: idle`. +4. Regression tests: (a) `claw status --json` before any prompt → `active_session: false, session_id: null`. (b) `claw status --json` during a prompt session → `active_session: true, session_id: `. (c) UUID matches the `session.id` in the first lane event of the same run. + +**Acceptance.** An orchestrator can poll `claw status --json` and determine: is there a live session? What is its correlation ID? Does it match the ID from the last startup event? This closes the round-trip opened by #134. + +**Blocker.** Depends on #134 (session.id generation at init). Can be filed and implemented together. + +**Source.** Jobdori dogfood 2026-04-21 06:53 KST on main HEAD `2c42f8b` during recurring cron cycle. Direct sibling of #134 — #134 covers the event-emission side, #135 covers the query side. Joins **Session identity completeness** (§4.7) and **status surface completeness** cluster (#80/#83/#114/#122). Natural bundle: **#134 + #135** closes the full session-identity round-trip. Session tally: ROADMAP #135. + +## Pinpoint #134. No run/correlation ID at session boundary — every observer must infer session identity from timing or prompt content + + **Gap.** When a `claw` session starts, no stable correlation ID is emitted in the first structured event (or any event). Every observer — lane event consumer, log aggregator, Clawhip router, test harness — has to infer session identity from timing proximity or prompt content. If two sessions start in close succession there is no unambiguous way to attribute subsequent events to the correct session. `claw status --json` returns session metadata but does not expose an opaque stable ID that could be used as a correlation key across the event stream. + + **Fix shape.** + - Emit `session.id` (opaque, stable, scoped to this boot) in the first structured event at startup + - Include same ID in all subsequent lane events as `session_id` field + - Expose via `claw status --json` so callers can retrieve the active session's ID from outside + - Add regression: golden-fixture asserting `session.id` is present in startup event and value matches across a multi-event trace + + **Acceptance.** Any observer can correlate all events from a session using `session_id` without parsing prompt content or relying on timestamp proximity. `claw status --json` exposes the current session's ID. + + **Blocker.** None. Requires a UUID/nanoid generated at session init and threaded through the event emitter. + + **Source.** Jobdori dogfood 2026-04-21 01:54 KST on main HEAD `50e3fa3` during recurring cron cycle. Joins **Session identity completeness at creation time** (ROADMAP §4.7) — §4.7 covers identity fields at creation time; #134 covers the stable correlation handle that ties those fields to downstream events. Joins **Event provenance / environment labeling** (§4.6) — provenance requires a stable anchor; without `session.id` the provenance chain is broken at the root. Natural bundle with **#241** (no startup run/correlation id, filed by gaebal-gajae 2026-04-20) — #241 approached from the startup cluster; #134 approaches from the event-stream observer side. Same root fix closes both. Session tally: ROADMAP #134. + +## Pinpoint #136. `--compact` flag output is not machine-readable — compact turn emits plain text instead of JSON when `--output-format json` is also passed + +**Gap.** `claw --compact ` runs a prompt turn with compacted output (tool-use suppressed, final assistant text only). But `run_with_output()` routes on `(output_format, compact)` with an explicit early-return match: `CliOutputFormat::Text if compact => run_prompt_compact(input)`. The `CliOutputFormat::Json` branch is never reached when `--compact` is set. Result: passing `--compact --output-format json` silently produces plain-text output — the compact flag wins and the format flag is silently ignored. No warning or error is emitted. + +**Trace path.** +- `rust/crates/rusty-claude-cli/src/main.rs:3872-3879` — `run_with_output()` match: + ``` + CliOutputFormat::Text if compact => self.run_prompt_compact(input), + CliOutputFormat::Text => self.run_turn(input), + CliOutputFormat::Json => self.run_prompt_json(input), + ``` + The `Json` arm is unreachable when `compact = true` because the first arm matches first regardless of `output_format`. +- `run_prompt_compact()` at line 3879 calls `println!("{final_text}")` — always plain text, no JSON envelope. +- `run_prompt_json()` at line 3891 wraps output in a JSON object with `message`, `model`, `iterations`, `usage`, `tool_uses`, `tool_results`, etc. + +**Fix shape (~20 lines).** +1. Add a `CliOutputFormat::Json if compact` arm (or merge compact flag into `run_prompt_json` as a parameter) that produces a JSON object with `message: ` and a `compact: true` marker. Tool-use fields remain present but empty arrays (consistent with compact semantics — tools ran but are not returned verbatim). +2. Emit a warning or `error.kind: "flag_conflict"` if conflicting flags are passed in a way that silently wins (or document the precedence explicitly in `--help`). +3. Regression tests: `claw --compact --output-format json ` must produce valid JSON with at minimum `{message: "...", compact: true}`. + +**Acceptance.** An orchestrator that requests compact output for token efficiency AND machine-readable JSON gets both. Silent flag override is never a correct behavior for a tool targeting machine consumers. + +**Blocker.** None. Additive change to existing match arms. + +**Source.** Jobdori dogfood 2026-04-21 12:25 KST on main HEAD `8b52e77` during recurring cron cycle. Joins **Output format completeness** cluster (#90/#91/#92/#127/#130) — all surfaces that produce inconsistent or plain-text fallbacks when JSON is requested. Also joins **CLI/REPL parity** (§7.1) — compact is available as both `--compact` flag and `/compact` REPL command; JSON output gap affects only the flag path. Session tally: ROADMAP #136. + +## Pinpoint #138. Dogfood cycle report-gate opacity — nudge surface collapses "bundle converged", "follow-up landed", and "pre-existing flake only" into single closure shape + +**Gap.** When a dogfood nudge triggers on a branch with landed work, the report surface emits status like "fixed 3 tests, pushed branch, 1 unrelated red remains" — but downstream nudges cannot distinguish: +1. `bundle converged, merge-ready` (e.g., #134/#135 branch after fixes) +2. `follow-up landed on main, branch still valid` (e.g., #137 + #136 fixes after #134/#135 was ready) +3. `only pre-existing flake remains, no new regressions` (e.g., `resume_latest...` test failure on main that also fails on feature branch) +4. `work still in flight, blocker not yet resolved` +5. `merged and closed, re-nudge is a dup` + +Result: repeat nudges look identical whether the prior work converged or is still broken. Claws re-open what was already resolved, burning cycles on rediscovery. + +**Concrete example from this session:** +- 14:30 nudge triggered on bundle already clear (14:25) +- Reported finding was "nudge closure-state opacity" but manifested as "should we re-nudge or not?" +- No explicit surface like "status: done", "last-updated: 2026-04-21T14:25", "next-action: none" that stops re-nudges on unchanged state + +**Fix shape (~30-50 lines, surfaces not code).** +1. Dogfood report should carry an explicit **closure state** field: `converged`, `follow-up-landed`, `pre-existing-flake-only`, `in-flight`, `merged`, `dup`. +2. Each state has a **last-updated timestamp** (when report was filed) and **next-action** (null if converged, or describe blocker). +3. Nudge logic checks prior report state: if `converged` + timestamp < 10 min old, skip nudge and post "still converged as of HH:MM, no action". +4. If state changed (e.g., new commits landed), emit **state transition** explicitly: "bundle done (14:25) → follow-up landed (14:42)". +5. Store closure state in a **shared metadata surface** (Discord message edit, ROADMAP inline, or compact JSON file) so next cycle can read it. + +**Acceptance.** +- Repeat nudges on converged work are replaced with "no change since last report" (skip). +- State transitions are explicit: "was X, now Y" instead of ambiguous "X and also Y". +- Claws can scan closure states and prioritize fresh work over already-handled bundles. + +**Blocker.** Design question: **where should closure state live?** Options: +- Edit the prior Discord message with a closure tag (e.g., 🟢 CONVERGED). +- Add a `.dogfood-closure.json` file to the worktree branch that tracks state. +- File a new ROADMAP entry per bundle completion (meta-tracking). +- Embedded in claw-code CLI output (machine-readable, but creates coupling). + +Current state is **design question unresolved**. Implementation is straightforward once closure-state model is settled. + +**Source.** Jobdori dogfood 2026-04-21 14:25-14:47 KST — multi-cycle convergence pattern exposed by repeat nudges on #134/#135 bundle. Joins **Dogfood loop observability** (related to earlier §4.7 session-identity, but one level up — session-identity is plumbing, closure-state is the **reporting contract**). Also joins **False-green report gating** (from 14:05 finding) — this is the downstream effect: unclear reports beget re-nudges on stale work. + +Session tally: ROADMAP #138. + +### Evidence for #138 — feat/134-135-session-identity branch is pushed but no PR was opened (2026-04-21 15:05) + +**Concrete gap observed:** +- Branch `feat/134-135-session-identity` pushed to `origin` at `7235260` (commits `f55612e`, `2b7095e`, `230d97a`, `7235260`) +- Dogfood loop declared bundle "merge-ready" at 14:25 +- ~40 min elapsed; no PR opened, no merge, branch still unmerged +- Meanwhile #136 and #137 landed directly on main (`a8beca1`, `21adae9`) without going through the branch + +**Direct verification of #135 on main:** +- `env -i $BIN status --output-format json` on main HEAD `768c1ab` shows `active_session: null, session_id: null` +- Fields exist in JSON schema (added by schema-only?) but values are None because the producer plumbing (`#134`) is not on main +- #135 consumer relies on #134 producer; both live on feat/134-135 only + +**Impact:** +- `claw status --output-format json` on main returns JSON without the #135 session identity signals (because they're only on feat/134-135) +- Orchestrators that shipped using the 13:00 "round-trip proof" report believing #134+#135 was merge-ready will get null fields +- Evidence for #138: "closure-state" = "pushed branch" ≠ "merged" ≠ "in-PR" — nudge surface collapses all three + +**Proposed closure-state transition:** +1. `pushed` — branch exists on origin but no PR (current state for feat/134-135) +2. `in-PR` — PR open, review pending +3. `approved` — PR approved, awaiting merge +4. `merged` — in main +5. `deployed` — if applicable +6. `abandoned` — PR closed without merge + +Nudge surface should report explicit state + timestamp: `"feat/134-135 state=pushed (no PR) since 13:00; no closure action taken"` instead of ambiguous "merge-ready." + +**Token/permission note:** +- `code-yeongyu` token has write access to push branches to `ultraworkers/claw-code` but lacks `createPullRequest` permission (GraphQL 404) +- Issues are disabled on the repo (can't open issue-based tracking) +- Means closure-state tracking must live inside the repo (ROADMAP) or in an external surface (Discord message edits, `.dogfood-closure.json`) + +**Filed:** 2026-04-21 15:05 KST as evidence for #138 by Jobdori dogfood loop. + +## Pinpoint #139. `claw state` error message refers to "worker" concept that is not discoverable via `--help` or any documented command — error is unactionable for claws and CI + +**Gap.** `claw state` (both text and JSON output modes) returns this error when no worker-state.json exists: +``` +error: no worker state file found at /private/tmp/cd-16/.claw/worker-state.json — run a worker first +``` + +**The problem:** "worker" is a concept that has **zero discoverability path** from the CLI surface: +1. `claw --help` has no mention of workers, `claw worker`, or worker state +2. There is no `claw worker` subcommand (not listed in help, not in the 16 known subcommands) +3. No hint in the error itself about what command triggers worker state creation +4. A claw, CI pipeline, or first-time user hitting this error has no actionable next step + +**Verified on main HEAD `f3f6643` (2026-04-21 15:58 KST):** +``` +$ claw state --output-format json +{"error":"no worker state file found at /private/tmp/cd-16/.claw/worker-state.json — run a worker first","type":"error"} +``` + +**Trace path.** +- `rust/crates/rusty-claude-cli/src/main.rs` — `handle_state()` or equivalent returns this error when `.claw/worker-state.json` is missing. +- No internal documentation on what produces `worker-state.json` (likely background worker session, but not surfaced) +- `claw bootstrap-plan` mentions phases like `DaemonWorkerFastPath` and `BackgroundSessionFastPath` — suggesting workers are part of daemon/background execution — but this is internal architecture jargon, not user-facing + +**Why this is a clawability gap.** +1. **Error references concept that is not discoverable.** Product Principle violation: "Errors must be actionable." Current error is descriptive but unactionable. +2. **Claws can't self-heal.** A claw orchestrator that gets this error cannot construct a follow-up command because the remediation is not in the error or in `--help`. +3. **Dogfood blocker.** Automated test setups that include `claw state` as a health check will fail silently for users who haven't triggered the worker path. +4. **Internal architecture leaks into user surface.** The `worker` / `daemon` / `background session` distinction is internal runtime nomenclature, not user-facing workflow. + +**Fix shape (~20-40 lines).** +1. **Error message should include remediation.** Change error to: + ```json + { + "error": "no worker state file found at — run `claw` (interactive REPL) or `claw prompt ` to produce worker state", + "type": "error", + "hint": "Worker state is created when claw executes a prompt (REPL or one-shot). If you have run claw but still see this, check that your session wrote to .claw/worker-state.json.", + "next_action": "claw prompt \"hello\"" + } + ``` +2. **Add `claw --help` reference.** Document under `Flags` or `Subcommand overview` that `claw state` requires prior execution. +3. **Consistency with typed-error envelope** (ROADMAP §4.44): include `operation: "state-read"`, `target: ""`, `retryable: false` fields for machine consumers. + +**Acceptance.** +- `claw state` error text explicitly names the command(s) that produce worker state +- `--help` has at least one line documenting the state/worker relationship +- A claw reading the JSON error gets a structured `next_action` field + +**Blocker.** None. Pure error-text + doc fix. ~30 lines. + +**Source.** Jobdori dogfood 2026-04-21 16:00 KST on main HEAD `f3f6643`. Joins **error-message-quality** cluster (related to §4.44 typed error taxonomy and §5 failure class enumeration). Joins **CLI discoverability** cluster (#108 did-you-mean for typos, #127 --json on diagnostic verbs). Session tally: ROADMAP #139. + +## Pinpoint #141. `claw --help` has 5 different behaviors — inconsistent help surface breaks discoverability + +**Gap.** Running ` --help` has five different behaviors depending on which subcommand you pick. This breaks the expected CLI contract that ` --help` returns subcommand-specific help. + +**Matrix (verified on main HEAD `27ffd75` 2026-04-21 16:59 KST):** + +| Subcommand | Behavior | Status | +|---|---|---| +| `status`, `sandbox`, `doctor`, `skills`, `agents`, `mcp`, `acp` | Subcommand-specific help | ✅ correct | +| `version` | Global `claw --help` | ⚠️ inconsistent | +| `init`, `export`, `state` | Global `claw --help` | ⚠️ inconsistent | +| `dump-manifests`, `system-prompt` | `error: unknown option: --help` | ❌ broken | +| `bootstrap-plan` | Prints phases JSON (not help at all) | ❌ broken | + +**Concrete repro:** +``` +$ claw system-prompt --help +error: unknown system-prompt option: --help + +$ claw dump-manifests --help +error: unknown dump-manifests option: --help + +$ claw bootstrap-plan --help +- CliEntry +- FastPathVersion +... + +$ claw init --help +claw v0.1.0 +Usage: + claw [--model MODEL] ... # this is global help, not init-specific +``` + +**Why this is a clawability gap.** +1. **Product principle violation**: every CLI subcommand should have a consistent ` --help` contract that returns subcommand-specific help. +2. **CI/orchestration hazard**: a claw script that tries ` --help | grep