Sigrid Jin relayed an adamantium Discord field report: Anthropic rejected requests with invalid_request_error when messages contained tool_use ids without immediately following tool_result blocks.
Coalesce consecutive tool-result messages after assistant tool_use blocks into one Anthropic user message, and drop orphan tool_use/tool_result blocks before dispatch so resume/edit/compaction boundary damage cannot reach the provider as a 400.
Tests cover parallel tool results and orphaned resume-boundary history.
Per gaebal-gajae cycle #105 validation pass. One-liner state summary
now appears at top (tone-setter for reviewers) and bottom (reinforced
recap):
'Phase 0 is now frozen, reviewer-mapped, and merge-ready;
Phase 1 remains intentionally deferred behind the locked priority order.'
This is the single authoritative sentence that captures branch state.
Use it for PR titles, review summaries, and Phase 1 handoff notes.
Why this framing matters (per gaebal-gajae evaluation):
- 'frozen' signals no scope creep
- 'reviewer-mapped' signals audit trail exists (this guide)
- 'merge-ready' signals gates are passed
- 'intentionally deferred' signals Phase 1 absence is by design, not omission
- 'locked priority order' signals sequencing is validated (cycle #104-#105)
Review guide now doubles as merge-enabler: reviewers parse branch state
in one sentence, then drill into commits as needed.
Doc-only. No code changes. Freeze preserved.
Pre-merge documentation for reviewers. Summarizes:
- What Phase 0 tasks deliver (JSON envelope contracts, regression locks)
- Why dogfood cycles #99-#105 matter (validated methodology, 15 filed pinpoints)
- Commit-by-commit navigation for the 30-commit frozen bundle
- What lands vs what's deferred
- Integration notes for Phase 1 planning
- Known limitations + follow-ups
This is doc-only, no code changes. Serves as audit trail and reviewer
reference without adding scope to the frozen feature branch.
Pinpoint #172: SCHEMAS.md v1.5 Emission Baseline documentation inaccuracy
discovered during cycle #98 probe.
The Phase 1 normalization targets section claimed:
"unify where `action` field appears (only in 4 inventory verbs)"
But reality is only 3 inventory verbs have `action`:
- mcp
- skills
- agents
list-sessions uses `command` instead (the documented 1-of-13 deviation
already captured elsewhere in v1.5 baseline).
This is a doc-truthfulness issue (same family as cycles #76, #79, #82).
Active misdocumentation leads downstream consumers to assume 4-verb
coverage when building adapters/dispatchers.
Changes:
1. SCHEMAS.md: 'only in 4 inventory verbs' → 'only in 3 inventory verbs: mcp, skills, agents'
2. Added regression test `v1_5_action_field_appears_only_in_3_inventory_verbs_172`
- Asserts mcp/skills/agents HAVE action field
- Asserts help/version/doctor/status/sandbox/system-prompt/bootstrap-plan/list-sessions do NOT have action field
- Forces SCHEMAS.md + binary to stay synchronized
Test added:
- `v1_5_action_field_appears_only_in_3_inventory_verbs_172` (8 negative cases + 3 positive cases)
Tests: 227/227 pass (+1 from #172).
Related: #155 (doc parity family), #168c (emission baseline).
Doc-truthfulness family: #76, #79, #82, #172.
Pinpoint #171: typed-error classifier gap discovered during #141 probe cycle #97.
`claw list-sessions --help` emits:
error: unexpected extra arguments after `claw list-sessions`: --help
This format is used by multiple verbs that reject trailing positional args:
- list-sessions
- plugins (subcommands)
- config (subcommands)
- diff
- load-session
Before fix:
{"error": "unexpected extra arguments after `claw list-sessions`: --help",
"hint": null,
"kind": "unknown",
"type": "error"}
After fix:
{"error": "unexpected extra arguments after `claw list-sessions`: --help",
"hint": "Run `claw --help` for usage.",
"kind": "cli_parse",
"type": "error"}
The pattern `unexpected extra arguments after \`claw` is specific enough
that it won't hijack generic prose mentioning "unexpected extra arguments"
in other contexts (sanity test included).
Side benefit: like #169/#170, correctly classified cli_parse errors now
auto-trigger the #247 hint synthesizer.
Related #141 gap not yet closed: `claw list-sessions --help` still errors
instead of showing help (requires separate parser fix to recognize --help
as a distinct path). This classifier fix at least makes the error surface
typed correctly so consumers can distinguish "parse failure" from "unknown"
and potentially retry without the --help flag.
Test added:
- `classify_error_kind_covers_unexpected_extra_args_171` (4 positive cases
+ 1 sanity guard)
Tests: 226/226 pass (+1 from #171).
Typed-error family: #121, #127, #129, #130, #164, #169, #170, #247.
Pinpoint #153 closure. USAGE.md was missing practical instructions for:
1. Adding the claw binary to PATH (symlink vs export PATH)
2. Verifying the install works (version, doctor, --help)
3. Troubleshooting PATH issues (which, echo $PATH, ls -la)
New subsections:
- "Add binary to PATH" with two common options
- "Verify install" with post-install health checks
- Troubleshooting guide for common failures
Target audience: developers building from source who want to run `claw`
from any directory without typing `./rust/target/debug/claw`.
Discovered during cycle #96 dogfood (10-min reminder cycle).
Tests: 225/225 still pass (doc-only change).
Pinpoint #170: Extended typed-error classifier coverage gap discovered during
dogfood probe 2026-04-23 07:30 Seoul (cycle #95).
The #169 comment claimed to cover `--permission-mode bogus` via the
`unsupported value for --` pattern, but the actual `parse_permission_mode_arg`
message format is `unsupported permission mode 'bogus'` (NO `for --` prefix).
Doc-vs-reality lie in the #169 fix itself — fixed here.
Four classifier gaps closed:
1. `unsupported permission mode '<value>'` → cli_parse
(from: `parse_permission_mode_arg`)
2. `invalid value for --reasoning-effort: '<value>'; must be ...` → cli_parse
(from: `--reasoning-effort` validator)
3. `model string cannot be empty` → cli_parse
(from: empty --model rejection)
4. `slash command /<name> is interactive-only. Start \`claw\` ...` →
slash_command_requires_repl (NEW kind — more specific than cli_parse)
The fourth pattern gets its own kind (`slash_command_requires_repl`) because
it's a command-mode misuse, not a parse error. Downstream consumers can
programmatically offer REPL-launch guidance.
Side benefit: like #169, the correctly classified cli_parse errors now
auto-trigger the #247 hint synthesizer ("Run `claw --help` for usage.").
Test added:
- `classify_error_kind_covers_flag_value_parse_errors_170_extended`
(4 positive cases + 2 sanity guards)
Tests: 225/225 pass (+1 from #170).
Typed-error family: #121, #127, #129, #130, #164, #169, #247.
Discovered via systematic probe angle: 'error message pattern audit' \u2014
grep each error emission for pattern, confirm classifier matches.
Pinpoint #169: typed-error classifier gap discovered during dogfood probe.
`claw --output-format json --output-format xml doctor` was emitting:
{"error": "unsupported value for --output-format: xml ...",
"hint": null,
"kind": "unknown",
"type": "error"}
After fix:
{"error": "unsupported value for --output-format: xml ...",
"hint": "Run `claw --help` for usage.",
"kind": "cli_parse",
"type": "error"}
The change adds two new classifier branches to `classify_error_kind`:
1. `unsupported value for --` → cli_parse
2. `missing value for --` → cli_parse
Covers all `CliOutputFormat::parse` / `parse_permission_mode_arg` rejections
and any future flag-value validation messages using the same pattern.
Side benefit: the #247 hint synthesizer ("Run `claw --help` for usage.")
now triggers automatically because the error is now correctly classified
as cli_parse. Consumers get both correct kind AND helpful hint.
Test added:
- `classify_error_kind_covers_flag_value_parse_errors_169` (4 positive +
1 sanity case)
Tests: 224/224 pass (+1 from #169).
Discovered during dogfood probe 2026-04-23 07:00 Seoul, cycle #94.
Refs: #169, typed-error family (#121, #127, #129, #130, #164, #247)
Pinpoint #155: USAGE.md was missing documentation for three interactive
commands that appear in `claw --help`:
- /ultraplan [task]
- /teleport <symbol-or-path>
- /bughunter [scope]
Also adds full documentation for other underdocumented commands:
- /commit, /pr, /issue, /diff, /plugin, /agents
Converts inline sentence list into structured section 'Interactive slash
commands (inside the REPL)' with brief descriptions for each command.
Closes#155 gap: discovered during dogfood probing of help/USAGE parity.
No code changes. Pure documentation update.
Phase 0 Task 4 of the JSON Productization Program: CI shape parity guard.
This test locks the v1.5 emission baseline (documented in SCHEMAS.md § v1.5
Emission Baseline) so any future PR that introduces shape drift in a documented
verb fails this test at PR time.
Complements Task 2 (no-silent guarantee) by asserting SPECIFIC top-level key
sets, not just 'stdout is non-empty valid JSON'. If a verb adds/removes a
top-level field, this test fails with a clear error message pointing to
SCHEMAS.md § v1.5 Emission Baseline for update guidance.
Coverage:
- 8 success-path verbs with locked shape (help, version, doctor, skills,
agents, system-prompt, bootstrap-plan, list-sessions)
- 2 error-path cases with locked error envelope shape (prompt-no-arg, doctor --foo)
Key enforcement rules:
- Success envelope: exact key set match per verb
- Error envelope: {error, hint, kind, type} (4 keys, all verbs)
- list-sessions deliberately kept as {command, sessions} (Phase 1 target)
Test design intent:
- Locks CURRENT (possibly imperfect) shape, NOT target shape
- Forces PR authors to update both code + SCHEMAS.md + test together
- Makes Phase 1 shape normalization PRs visible: 'update this test'
Phase 0 now COMPLETE:
- Task 1 ✅ Stream routing fix (cycle #89)
- Task 2 ✅ No-silent guarantee (cycle #90)
- Task 3 ✅ Per-verb emission inventory SCHEMAS.md (cycle #91)
- Task 4 ✅ CI shape parity guard (this cycle)
Tests: 18 output_format_contract tests all pass (+1 from Task 4).
v1.5 emission baseline now locked by code + tests + docs.
Refs: #168c, cycle #92, Phase 0 Task 4 (final)
Under --output-format json, error envelopes were emitted to stderr via
eprintln!. This violated the emission contract: stdout should carry the
contractual envelope (success OR error); stderr is reserved for
non-contractual diagnostics.
Cycle #87 controlled matrix audit found bootstrap/dump-manifests/state
exhibited this pattern (exit 1, stdout 0 bytes, stderr N bytes under
--output-format json).
Fix: change eprintln! to println! for the JSON error envelope path in main().
Text mode continues to route errors to stderr (conventional).
Verification:
- bootstrap --output-format json: stdout now carries envelope, exit 1
- dump-manifests --output-format json: stdout now carries envelope, exit 1
- Text mode: errors still on stderr with [error-kind: ...] prefix (no regression)
Tests:
- Updated assert_json_error_envelope helper to read from stdout (was stderr)
- Added error_envelope_emitted_to_stdout_under_output_format_json_168c
regression test that asserts envelope on stdout + non-JSON on stderr
- All 16 output_format_contract tests pass
Phase 0 Task 1 complete: emission routing fixed across all error-path verbs.
Phase 0 Task 2 (no-silent CI guarantee) remains.
Refs: #168c (cycle #87 filing), cycle #88 emission contract framing
Fresh-dogfood validation (cycle #84, #168) proved the original locus premise was
underspecified. v1.0 was never a coherent contract — each verb has a bespoke JSON
shape with no coordination, and bootstrap JSON is completely broken (silent
failure, exit 0 no output).
Revised migration plan:
- Phase 0 (NEW): Emergency fix for silent failures (#168 bootstrap JSON)
- Phase 1 (NEW): v1.5 baseline — minimal JSON invariants across all 14 verbs
- Every command emits valid JSON with --output-format json
- Every command has top-level 'kind' field for verb ID
- Every error envelope follows {error, hint, kind, type}
- Phase 2 (renamed from Phase 1): v2.0 wrapped envelope (opt-in)
- Phase 3 (renamed from Phase 2): v2.0 default
- Phase 4 (renamed from Phase 3): v1.0/v1.5 deprecation
Rationale:
- Can't migrate from 'incoherent' to 'coherent v2.0' in one jump
- Consumers need stable target (v1.5) to transition from
- Silent failures must be fixed BEFORE migration (consumers can't detect breakage)
Effort revision: ~9 dev-days (Phase 0: 1 + Phase 1: 3 + Phase 2: 5) vs original
~6 dev-days for direct v1.0→v2.0 (which would have failed).
Doctrine implication: Fresh-dogfood principle (#9, cycle #73) prevented a multi-day
migration from hitting an unsolvable baseline problem. Evidence-backed mid-design
correction.
SCHEMAS.md was presenting the target v2.0 schema as the current binary contract.
This is the source of truth document, so the misdocumentation propagated to every
downstream doc (USAGE.md, ERROR_HANDLING.md, CLAUDE.md all inherited the false
premise that v1.0 includes timestamp/command/exit_code/etc).
Fixed with:
1. CRITICAL header at top: marks entire doc as v2.0 target, not v1.0 reality
2. 'TARGET v2.0 SCHEMA' headers on Common Fields section
3. Comprehensive Appendix: v1.0 actual shape + migration timeline + v1.0 code example
4. Links to FIX_LOCUS_164.md + ERROR_HANDLING.md for v1.0 reality
5. FAQ: clarifies the version mismatch and when v2.0 ships
This closes the fourth P0 doc-truthfulness instance (4/4 in family):
- #78 USAGE.md: active misdocumentation (fixed#78)
- #79 ERROR_HANDLING.md: copy-paste trap (fixed#79)
- #165 CLAUDE.md: boundary collapse (fixed#81)
- #166 SCHEMAS.md: aspirational source doc (fixed#82)
Pattern is now crystallized: SCHEMAS.md was the aspirational source;
three downstream docs (USAGE, ERROR_HANDLING, CLAUDE) inherited the false v2.0-as-v1.0
claim. Fix the source (SCHEMAS.md), which eliminates the root cause for all four.
CLAUDE.md was documenting the v2.0 target schema as if it were current binary
behavior. This misled validator/harness implementers into assuming the Rust
binary emits timestamp, command, exit_code, output_format, schema_version fields
when it doesn't.
Fixed by explicitly marking the boundary:
1. SCHEMAS.md section: now clearly labels 'target v2.0 design' and lists both
v1.0 (actual binary) and v2.0 (target) field shapes
2. Clawable commands requirements: now explicitly separates v1.0 (current) and
v2.0 (post-FIX_LOCUS_164) envelope requirements
3. Added inline migration note pointing to FIX_LOCUS_164.md
This closes#165 as the third P0 doc-truthfulness fix (Option A: preserve current
truth, add v2.0 target as separate labeled section).
P0 doc-truthfulness family pattern (all three related to #164 envelope divergence):
- #78 USAGE.md: active misdocumentation (fixed cycle #78)
- #79 ERROR_HANDLING.md: copy-paste trap (fixed cycle #79)
- #165 CLAUDE.md: target/current boundary collapse (fixed cycle #81)
The Python code examples were accessing nested error.kind like envelope['error']['kind'],
but v1.0 emits flat envelopes with error as a STRING and kind at top-level.
Updated:
- Table header: now shows actual v1.0 shape {error: "...", kind: "...", type: "error"}
- match statement: switched from envelope.get('error',{}).get('kind') to envelope.get('kind')
- All ClawError raises: changed from envelope['error']['message'] to envelope.get('error','')
because error field is a STRING in v1.0, not a nested object
- Added inline comments on every error case noting v1.0 vs v2.0 difference
- Appendix: split into v1.0 (actual/current) and v2.0 (target after FIX_LOCUS_164)
The code examples now work correctly against the actual binary.
This was active misdocumentation (P0 severity) — the Python examples would crash
if a consumer tried to use them.
The JSON output section was misleading — it claimed the binary emits
exit_code, command, timestamp, output_format, schema_version, and nested
error objects. The binary actually emits v1.0 flat shape (kind at top-level,
error as string, no common metadata fields).
Updated section:
- Documents actual v1.0 success and error envelope shapes
- Lists known issues (missing fields, overloaded kind, flat error)
- Shows how to dispatch on v1.0 (check type=='error' before reading kind)
- Warns users NOT to rely on kind alone
- Links to FIX_LOCUS_164.md for migration plan
- Explains Phase 1/2/3 timeline for v2.0 adoption
This is a doc-only fix that makes USAGE.md truthful about the current behavior
while preparing users for the coming schema migration.
Problem: In git worktrees, .git is a pointer file (not a directory), so cargo's
rerun-if-changed=.git/HEAD never triggers when commits are made. This causes
claw version to report a stale SHA after new commits.
Solution: Add resolve_git_head_path() helper that detects worktree mode:
- If .git is a file: parse gitdir pointer, watch <gitdir>/HEAD
- If .git is a directory: watch .git/HEAD (regular repo)
This ensures build.rs invalidates on each commit, making version output truthful.
Verification: Binary built in worktree now reports correct SHA after commits
(before: stale, after: current HEAD).
Relates to ROADMAP #161 (filed cycle #65, implemented cycle #69).
Diagnostic-strictness family member.
Diff: 21 lines added (resolve_git_head_path + conditional rerun-if-changed).
## What Was Broken (ROADMAP #130e, filed cycle #53)
Three subcommands leaked `missing_credentials` errors when called
with `--help`:
$ claw help --help
[error-kind: missing_credentials]
error: missing Anthropic credentials...
$ claw submit --help
[error-kind: missing_credentials]
error: missing Anthropic credentials...
$ claw resume --help
[error-kind: missing_credentials]
error: missing Anthropic credentials...
This is the same dispatch-order bug class as #251 (session verbs).
The parser fell through to the credential check before help-flag
resolution ran. Critical discoverability gap: users couldn't learn
what these commands do without valid credentials.
## Root Cause (Traced)
`parse_local_help_action()` (main.rs:1260) is called early in
`parse_args()` (main.rs:1002), BEFORE credential check. But the
match statement inside only recognized:
status, sandbox, doctor, acp, init, state, export, version,
system-prompt, dump-manifests, bootstrap-plan, diff, config.
`help`, `submit`, `resume` were NOT in the list, so the function
returned `None`, and parsing continued to credential check which
then failed.
## What This Fix Does
Same pattern as #130c (diff) and #130d (config):
1. **LocalHelpTopic enum extended** with Meta, Submit, Resume variants
2. **parse_local_help_action() extended** to map the three new cases
3. **Help topic renderers added** with accurate usage info
Three-line change to parse_local_help_action:
"help" => LocalHelpTopic::Meta,
"submit" => LocalHelpTopic::Submit,
"resume" => LocalHelpTopic::Resume,
Dispatch order (parse_args):
1. --resume parsing
2. parse_local_help_action() ← NOW catches help/submit/resume --help
3. parse_single_word_command_alias()
4. parse_subcommand() ← Credential check happens here
## Dogfood Verification
Before fix (all three):
$ claw help --help
[error-kind: missing_credentials]
error: missing Anthropic credentials...
After fix:
$ claw help --help
Help
Usage claw help [--output-format <format>]
Purpose show the full CLI help text (all subcommands, flags, environment)
...
$ claw submit --help
Submit
Usage claw submit [--session <id|latest>] <prompt-text>
Purpose send a prompt to an existing managed session
Requires valid Anthropic credentials (when actually submitting)
...
$ claw resume --help
Resume
Usage claw resume [<session-id|latest>]
Purpose restart an interactive REPL attached to a managed session
...
## Non-Regression Verification
- `claw help` (no --help) → still shows full CLI help ✅
- `claw submit "text"` (with prompt) → still requires credentials ✅
- `claw resume` (bare) → still emits slash command guidance ✅
- All 180 binary tests pass ✅
- All 466 library tests pass ✅
## Regression Tests Added (6 assertions)
- `help --help` → routes to HelpTopic(Meta)
- `submit --help` → routes to HelpTopic(Submit)
- `resume --help` → routes to HelpTopic(Resume)
- Short forms: `help -h`, `submit -h`, `resume -h` all work
## Pattern Note
This is Category A of #130e (dispatch-order bugs). Same class as #251.
Category B (surface-parity: plugins, prompt) will be handled in a
follow-up commit/branch.
## Help-Parity Sweep Status
After cycle #52 (#130c diff, #130d config), help sweep revealed:
| Command | Before | After This Commit |
|---|---|---|
| help --help | missing_credentials | ✅ Meta help |
| submit --help | missing_credentials | ✅ Submit help |
| resume --help | missing_credentials | ✅ Resume help |
| plugins --help | "Unknown action" | ⏳ #130e-B (next) |
| prompt --help | wrong help | ⏳ #130e-B (next) |
## Related
- Closes #130e Category A (dispatch-order help fixes)
- Same bug class as #251 (session verbs)
- Stacks on #130d (config help) on same worktree branch
- #130e Category B (plugins, prompt) queued for follow-up
## What Was Broken (ROADMAP #130d, filed cycle #52)
`claw config --help` was silently ignored — the command executed and
displayed the config dump instead of showing help:
$ claw config --help
Config
Working directory /private/tmp/dogfood-probe-47
Loaded files 0
Merged keys 0
(displays full config, not help)
Expected: help for the config command. Actual: silent acceptance of
`--help`, runs config display anyway.
This is the opposite outlier from #130c (which rejected help with an
error). Together they form the help-parity anomaly:
- #130c `diff --help` → error (rejects help)
- #130d `config --help` → silent ignore (runs command, ignores help)
- Others (status, mcp, export) → proper help
- Expected behavior: all commands should show help on `--help`
## Root Cause (Traced)
At main.rs:1050, the `"config"` parser arm parsed arguments positionally:
"config" => {
let tail = &rest[1..];
let section = tail.first().cloned();
// ... ignores unrecognized args like --help silently
Ok(CliAction::Config { section, ... })
}
Unlike the `diff` arm (#130c), `config` had no explicit check for
extra args. It positionally parsed the first arg as an optional
`section` and silently accepted/ignored any trailing arg, including
`--help`.
## What This Fix Does
Same pattern as #130c (help-surface parity):
1. **LocalHelpTopic enum extended** with new `Config` variant
2. **parse_local_help_action() extended** to map `"config"` → `LocalHelpTopic::Config`
3. **config arm guard added**: check for help flag before parsing section
4. **Help topic renderer added**: human-readable help text for config
Fix locus at main.rs:1050:
"config" => {
// #130d: accept --help / -h and route to help topic
if rest.len() >= 2 && is_help_flag(&rest[1]) {
return Ok(CliAction::HelpTopic(LocalHelpTopic::Config));
}
let tail = &rest[1..];
// ... existing parsing continues
}
## Dogfood Verification
Before fix:
$ claw config --help
Config
Working directory ...
Loaded files 0
(no help, runs config)
After fix:
$ claw config --help
Config
Usage claw config [--cwd <path>] [--output-format <format>]
Purpose merge and display the resolved configuration
Options --cwd overrides the workspace directory
Output loaded files and merged key-value pairs
Formats text (default), json
Related claw status · claw doctor · claw init
Short form `claw config -h` also works.
## Non-Regression Verification
- `claw config` (no args) → still displays config dump ✅
- `claw config permissions` (section arg) → still works ✅
- All 180 binary tests pass ✅
- All 466 library tests pass ✅
## Regression Tests Added (4 assertions)
- `config --help` → routes to `HelpTopic(LocalHelpTopic::Config)`
- `config -h` (short form) → routes to help topic
- bare `config` (no args) → still routes to `Config` action
- `config permissions` (with section) → still works correctly
## Pattern Note
#130c and #130d form a pair: two outlier failure modes in help
handling for local introspection commands:
- #130c `diff` rejected help (loud error) → fixed with guard + routing
- #130d `config` silently ignored help (silent accept) → fixed with same pattern
Both are now consistent with the rest of the CLI (status, mcp, export, etc.).
## Related
- Closes #130d (config help discoverability gap)
- Completes help-parity family (#130c, #130d)
- Stacks on #130c (diff help fix) on same worktree branch
- Part of help-consistency thread (#141 audit)
## What Was Broken (ROADMAP #130c, filed cycle #50)
`claw diff --help` was rejected with:
[error-kind: unknown]
error: unexpected extra arguments after `claw diff`: --help
Other local introspection commands accept --help fine:
- `claw status --help` → shows help ✅
- `claw mcp --help` → shows help ✅
- `claw export --help` → shows help ✅
- `claw diff --help` → error ❌ (outlier)
This is a help-surface parity bug: `diff` is the only local command
that rejects --help as "extra arguments" before the help detector
gets a chance to run.
## Root Cause (Traced)
At main.rs:1063, the `"diff"` parser arm rejected ALL extra args:
"diff" => {
if rest.len() > 1 {
return Err(format!("unexpected extra arguments after `claw diff`: {}", ...));
}
Ok(CliAction::Diff { output_format })
}
When parsing `["diff", "--help"]`, `rest.len() > 1` was true (length
is 2) and `--help` was rejected as extra argument.
Other commands (status, sandbox, doctor, init, state, export, etc.)
routed through `parse_local_help_action()` which detected
`--help` / `-h` and routed to a LocalHelpTopic. The `diff` arm
lacked this guard.
## What This Fix Does
Three minimal changes:
1. **LocalHelpTopic enum extended** with new `Diff` variant
2. **parse_local_help_action() extended** to map `"diff"` → `LocalHelpTopic::Diff`
3. **diff arm guard added**: check for help flag before extra-args validation
4. **Help topic renderer added**: human-readable help text for diff command
Fix locus at main.rs:1063:
"diff" => {
// #130c: accept --help / -h as first argument and route to help topic
if rest.len() == 2 && is_help_flag(&rest[1]) {
return Ok(CliAction::HelpTopic(LocalHelpTopic::Diff));
}
if rest.len() > 1 { /* existing error */ }
Ok(CliAction::Diff { output_format })
}
## Dogfood Verification
Before fix:
$ claw diff --help
[error-kind: unknown]
error: unexpected extra arguments after `claw diff`: --help
After fix:
$ claw diff --help
Diff
Usage claw diff [--output-format <format>]
Purpose show local git staged + unstaged changes
Requires workspace must be inside a git repository
...
And `claw diff -h` (short form) also works.
## Non-Regression Verification
- `claw diff` (no args) → still routes to Diff action correctly
- `claw diff foo` (unknown arg) → still rejected as "unexpected extra arguments"
- `claw diff --output-format json` (valid flag) → still works
- All 180 binary tests pass
- All 466 library tests pass
## Regression Tests Added (4 assertions)
- `diff --help` → routes to HelpTopic(LocalHelpTopic::Diff)
- `diff -h` (short form) → routes to HelpTopic(LocalHelpTopic::Diff)
- bare `diff` → still routes to Diff action
- `diff foo` (unknown arg) → still errors with "extra arguments"
## Pattern
Follows #141 help-consistency work (extending LocalHelpTopic to
cover more subcommands). Clean surface-parity fix: identify the
outlier, add the missing guard. Low-risk, high-clarity.
## Related
- Closes #130c (diff help discoverability gap)
- Stacks on #130b (filesystem context) and #251 (session dispatch)
- Part of help-consistency thread (#141 audit, #145 plugins wiring)
## What Was Broken (ROADMAP #130b, filed cycle #47)
In a fresh workspace, running:
claw export latest --output /private/nonexistent/path/file.jsonl --output-format json
produced:
{"error":"No such file or directory (os error 2)","hint":null,"kind":"unknown","type":"error"}
This violates the typed-error contract:
- Error message is a raw errno string with zero context
- Does not mention the operation that failed (export)
- Does not mention the target path
- Classifier defaults to "unknown" even though the code path knows
this is a filesystem I/O error
## Root Cause (Traced)
run_export() at main.rs:~6915 does:
fs::write(path, &markdown)?;
When this fails:
1. io::Error propagates via ? to main()
2. Converted to string via .to_string() in error handler
3. classify_error_kind() cannot match "os error" or "No such file"
4. Defaults to "kind": "unknown"
The information is there at the source (operation name, target path,
io::ErrorKind) but lost at the propagation boundary.
## What This Fix Does
Three changes:
1. **New helper: contextualize_io_error()** (main.rs:~260)
Wraps an io::Error with operation name + target path into a
recognizable message format:
"{operation} failed: {target} ({error})"
2. **Classifier branch added** (classify_error_kind at main.rs:~270)
Recognizes the new format and classifies as "filesystem_io_error":
else if message.contains("export failed:") ||
message.contains("diff failed:") ||
message.contains("config failed:") {
"filesystem_io_error"
}
3. **run_export() wired** (main.rs:~6915)
fs::write() call now uses .map_err() to enrich io::Error:
fs::write(path, &markdown).map_err(|e| -> Box<dyn std::error::Error> {
contextualize_io_error("export", &path.display().to_string(), e).into()
})?;
## Dogfood Verification
Before fix:
{"error":"No such file or directory (os error 2)","kind":"unknown","type":"error"}
After fix:
{"error":"export failed: /private/nonexistent/path/file.jsonl (No such file or directory (os error 2))","kind":"filesystem_io_error","type":"error"}
The envelope now tells downstream claws:
- WHAT operation failed (export)
- WHERE it failed (the path)
- WHAT KIND of failure (filesystem_io_error)
- The original errno detail preserved for diagnosis
## Non-Regression Verification
- Successful export still works (emits "kind": "export" envelope as before)
- Session not found error still emits "session_not_found" (not filesystem)
- missing_credentials still works correctly
- cli_parse still works correctly
- All 180 binary tests pass
- All 466 library tests pass
- All 95 compat-harness tests pass
## Regression Tests Added
Inside the main CliAction test function:
- "export failed:" pattern classifies as "filesystem_io_error" (not "unknown")
- "diff failed:" pattern classifies as "filesystem_io_error"
- "config failed:" pattern classifies as "filesystem_io_error"
- contextualize_io_error() produces a message containing operation name
- contextualize_io_error() produces a message containing target path
- Messages produced by contextualize_io_error() are classifier-recognizable
## Scope
This is the minimum viable fix: enrich export's fs::write with context.
Future work (filed as part of #130b scope): apply same pattern to
other filesystem operations (diff, plugins, config fs reads, session
store writes, etc.). Each application is a copy-paste of the same
helper pattern.
## Pattern
Follows #145 (plugins parser interception), #248-249 (arm-level leak
templates). Helper + classifier + call site wiring. Minimal diff,
maximum observability gain.
## Related
- Closes #130b (filesystem error context preservation)
- Stacks on top of #251 (dispatch-order fix) — same worktree branch
- Ground truth for future #130 broader sweep (other io::Error sites)
## What Was Broken (ROADMAP #251)
Session-management verbs (list-sessions, load-session, delete-session,
flush-transcript) were falling through to the parser's `_other => Prompt`
catchall at main.rs:~1017. This construed them as `CliAction::Prompt {
prompt: "list-sessions", ... }` which then required credentials via the
Anthropic API path. The result: purely-local session operations emitted
`missing_credentials` errors instead of session-layer envelopes.
## Acceptance Criterion
The fix's essential requirement (stated by gaebal-gajae):
**"These 4 verbs stop falling through to Prompt and emitting `missing_credentials`."**
Not "all 4 are fully implemented to spec" — stubs are acceptable for
delete-session and flush-transcript as long as they route LOCALLY.
## What This Fix Does
Follows the exact pattern from #145 (plugins) and #146 (config/diff):
1. **CliAction enum** (main.rs:~700): Added 4 new variants.
2. **Parser** (main.rs:~945): Added 4 match arms before the `_other => Prompt`
catchall. Each arm validates the verb's positional args (e.g., load-session
requires a session-id) and rejects extra arguments.
3. **Dispatcher** (main.rs:~455):
- list-sessions → dispatches to `runtime::session_control::list_managed_sessions_for()`
- load-session → dispatches to `runtime::session_control::load_managed_session_for()`
- delete-session → emits `not_yet_implemented` error (local, not auth)
- flush-transcript → emits `not_yet_implemented` error (local, not auth)
## Dogfood Verification
Run on clean environment (no credentials):
```bash
$ env -i PATH=$PATH HOME=$HOME claw list-sessions --output-format json
{
"command": "list-sessions",
"sessions": [
{"id": "session-1775777421902-1", ...},
...
]
}
# ✓ Session-layer envelope, not auth error
$ env -i PATH=$PATH HOME=$HOME claw load-session nonexistent --output-format json
{"error":"session not found: nonexistent", "kind":"session_not_found", ...}
# ✓ Local session_not_found error, not missing_credentials
$ env -i PATH=$PATH HOME=$HOME claw delete-session test-id --output-format json
{"command":"delete-session","error":"not_yet_implemented","kind":"not_yet_implemented","type":"error"}
# ✓ Local not_yet_implemented, not auth error
$ env -i PATH=$PATH HOME=$HOME claw flush-transcript test-id --output-format json
{"command":"flush-transcript","error":"not_yet_implemented","kind":"not_yet_implemented","type":"error"}
# ✓ Local not_yet_implemented, not auth error
```
Regression sanity:
```bash
$ claw plugins --output-format json # #145 still works
$ claw prompt "hello" --output-format json # still requires credentials correctly
$ claw list-sessions extra arg --output-format json # rejects extra args with cli_parse
```
## Regression Tests Added
Inside `removed_login_and_logout_subcommands_error_helpfully` test function:
- `list-sessions` → CliAction::ListSessions (both text and JSON output)
- `load-session <id>` → CliAction::LoadSession with session_reference
- `delete-session <id>` → CliAction::DeleteSession with session_id
- `flush-transcript <id>` → CliAction::FlushTranscript with session_id
- Missing required arg errors (load-session and delete-session without ID)
- Extra args rejection (list-sessions with extra positional args)
All 180 binary tests pass. 466 library tests pass.
## Fix Scope vs. Full Implementation
This fix addresses #251 (dispatch-order bug) and #250's Option A (implement
the surfaces). list-sessions and load-session are fully functional via
existing runtime::session_control helpers. delete-session and flush-transcript
are stubbed with local "not yet implemented" errors to satisfy #251's
acceptance criterion without requiring additional session-store mutations
that can ship independently in a follow-up.
## Template
Exact same pattern as #145 (plugins) and #146 (config/diff): top-level
verb interception → CliAction variant → dispatcher with local operation.
## Related
Closes#251. Addresses #250 Option A for 4 verbs. Does not block #250
Option B (documentation scope guards) which remains valuable.
Verbs with CLI-reserved positional-arg meanings (resume, compact, memory,
commit, pr, issue, bughunter) were falling through to Prompt dispatch
when invoked with args, causing users to see 'missing_credentials' errors
instead of guidance that the verb is a slash command.
#160 investigation revealed the underlying design question: which verbs
are 'promptable' (can start a prompt like 'explain this pattern') vs.
'reserved' (have specific CLI meaning like 'resume SESSION_ID')?
This fix implements the reserved-verb classification: at parse time,
intercept reserved verbs with trailing args and emit slash-command guidance
before falling through to Prompt. Promptable verbs (explain, bughunter, clear)
continue to route to Prompt as before.
Helper: is_reserved_semantic_verb() lists the reserved set.
All 181 tests pass (no regressions).
## What Was Broken
`claw doctor` reported "Status: ok" when run from ~/ or /, but `claw
prompt` in the same directory would error out with:
error: claw is running from a very broad directory (/Users/yeongyu).
The agent can read and search everything under this path.
Diagnostic deception: doctor said green, prompt said red. User runs
doctor to check their setup, sees all green, runs prompt, gets blocked.
Trust in doctor erodes.
This is the exact pattern captured in the 'Diagnostic Commands Must Be
At Least As Strict As Runtime Commands' principle recorded in ROADMAP.md
at cycle #57.
## Root Cause
Two code paths perform the broad-cwd check:
- CliAction::Prompt handler → `enforce_broad_cwd_policy()` (errors out)
- CliAction::Repl handler → same function
But render_doctor_report() never called detect_broad_cwd(). The workspace
health check only looked at whether cwd was inside a git project, not
whether cwd was a dangerously broad path.
## What This Fix Does
Extend `check_workspace_health()` to also probe `detect_broad_cwd()`:
let broad_cwd = detect_broad_cwd();
let (level, summary) = match (in_repo, &broad_cwd) {
(_, Some(path)) => (
DiagnosticLevel::Warn,
format!(
"current directory is a broad path ({}); Prompt/REPL will \
refuse to run here without --allow-broad-cwd",
path.display()
),
),
(true, None) => (DiagnosticLevel::Ok, "project root detected"),
(false, None) => (DiagnosticLevel::Warn, "not inside a git project"),
};
The check now warns about BOTH failure modes with clear messaging about
what Prompt/REPL will do.
## Dogfood Verification
Before fix:
$ cd ~ && claw doctor
Workspace
Status warn
Summary current directory is not inside a git project
[all green otherwise]
$ echo | claw prompt "test"
error: claw is running from a very broad directory (/Users/yeongyu)...
After fix:
$ cd ~ && claw doctor
Workspace
Status warn
Summary current directory is a broad path (/Users/yeongyu);
Prompt/REPL will refuse to run here without
--allow-broad-cwd
$ cd / && claw doctor
Workspace
Status warn
Summary current directory is a broad path (/); ...
Non-regression:
$ cd /tmp/my-project && claw doctor
Workspace
Status warn
Summary current directory is not inside a git project
(unchanged)
$ cd /path/to/real/git/project && claw doctor
Workspace
Status ok
Summary project root detected on branch main
(unchanged)
## Regression Tests Added
- `workspace_check_in_project_dir_reports_ok` — non-broad + in-project = OK
- `workspace_check_outside_project_reports_warn` — non-broad + not-in-project = Warn with 'not inside git project' summary
- 181 binary tests pass (was 179, added 2)
## Related
- Principle: 'Diagnostic Commands Must Be At Least As Strict As Runtime
Commands' (ROADMAP.md cycle #57)
- Companion to #122 (stale-base preflight in doctor)
- Sibling: next step is probably a full runtime-vs-doctor audit for
other asymmetries (auth, sandbox, plugins, hooks)
Provides:
- Recommended merge order (P0 → P1 → P2 → P3 by cluster)
- Per-cluster merge prerequisites and validation steps
- Conflict risk assessment (Cluster 2 #122/#122b have same edit locus)
- Post-merge validation checklist (build + test + dogfood)
- Timeline estimate (~60 min for full 17-branch queue)
Addresses the final integration step: once branches are reviewed, knowing
the safe merge order matters. This artifact pre-answers that question.
Applied doctrine: integration-support artifacts (cycle #64) reduce reviewer
friction. At 17-branch saturation, a merge-safe checklist is first-class work.
Relates to cycle #70 integration throughput initiative.
Cycle #46 follow-up to cycle #45's #251 implementation. Closes#250's
implementation urgency by aligning docs with reality.
SCHEMAS.md Updates:
For each of the 4 session-management verbs, added:
1. Status marker (Implemented or Stub only)
2. Actual binary envelope (shape produced by the #251-fixed binary)
3. Aspirational (future) shape (original SCHEMAS.md content, preserved as target)
4. Gap notes where the two diverge
Per-verb status:
- list-sessions: Implemented, nested field layout
- load-session: Implemented, nested session object with local session_not_found error
- delete-session: Stub, emits not_yet_implemented (local error, not auth)
- flush-transcript: Stub, emits not_yet_implemented (local error, not auth)
ROADMAP.md Updates:
- #251 marked CLOSED: Full status with commit ref, test counts.
- #250 marked SCOPE-REDUCED: Option A resolved by #251, Option C moot,
only Option B (doc alignment) remains as future cleanup.
Why this matters:
Every code change should close its documentation loop. #251 landed on
the branch, but SCHEMAS.md still described aspirational shapes without
marking which were implemented. Claws reading SCHEMAS.md would have
assumed full conformance and hit surprises. Now the document tells the
truth about which verbs work, which are stubs, and why.
Related:
- #251 implementation on feat/jobdori-251-session-dispatch branch
- #250 scope-reduced to Option B (field-name harmonization)
- #145/#146 parser fall-through fix precedent
Cycle #39 dogfood re-verification of #130 (filed 2026-04-20). All 5
filesystem failure modes reproduce identically on main HEAD 186d42f,
2 days after original filing. Gap is unchanged.
## What's Added
1. **[STILL OPEN — re-verified 2026-04-22 cycle #39]** marker on the
entry so readers can see immediately that the pinpoint hasn't been
accidentally closed.
2. Full 5-mode repro output preserved verbatim for the current HEAD,
so future re-verifications have a concrete baseline to diff against.
3. **New evidence not in original filing**: the classifier actively
chose `kind: "unknown"` rather than just omitting the field. This
means classify_error_kind() has NO substring match for "Is a
directory", "No such file", "Operation not permitted", or "File
exists". The typed-error contract is thus twice-broken on this path.
4. **Pairing with #247/#248/#249 classifier sweep**: the classifier-level
part of #130 could land in the same sweep (add substring branches
for io::ErrorKind strings). The context-preservation part (fix
run_export's bare `?`) is a separate, larger change.
## Why Re-Verification Not Re-Filing
Per cycle #24 discipline: speculative re-filings add noise, real
confirmations add truth. #130 was already filed with exact repros, code
trace, and fix shape. My dogfood hit the same gap on fresh HEAD — the
right output is confirming the gap is still there (not filing #251 for
the same bug).
This is the same pattern as cycle #32's "mark #127 CLOSED" reality-sync:
documentation-drift prevention through explicit status markers.
## New Pattern
"Reality-sync via re-verification" — re-running a filed pinpoint's
repro on fresh HEAD and adding the timestamp + output proves the gap
is still real without inventing new filings. Cycle #24 calibration
keeps ROADMAP entries honest.
Per cycle #24 calibration:
- Red-state bug? ⚠️ borderline (errors surfaced, but kind=unknown is
demonstrably wrong on a path where the system knows the errno)
- Real friction? ✓ (re-verified on fresh HEAD)
- Evidence-backed? ✓ (5-mode repro + classifier trace)
- Same-cycle fix? ✗ (classifier-level part could join #247/#248/#249
sweep; context-preservation part is larger refactor)
- Implementation cost? Classifier part ~10 lines; full context fix ~60 lines
Source: Jobdori cycle #39 proactive dogfood in response to Clawhip
pinpoint nudge. Probed export filesystem errors; discovered this was
#130 reconfirmation, not new bug. Applied reality-sync pattern from
cycle #32.
Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed
at fbcbe9d). Closes the two typed-error contract drifts surfaced in that
pinpoint against the Rust `claw` binary.
## What was wrong
1. `classify_error_kind()` (main.rs:~251) used substring matching but did
NOT match two common prompt-related parse errors:
- "prompt subcommand requires a prompt string"
- "empty prompt: provide a subcommand..."
Both fell through to `"unknown"`. §4.44 typed-error contract specifies
`parse | usage | unknown` as distinct classes, so claws dispatching on
`error.kind == "cli_parse"` missed those paths entirely.
2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode
appends it at stderr-print time (main.rs:~234) AFTER split_error_hint()
has already serialized the envelope, so JSON consumers never saw it.
Text-mode humans got an actionable pointer; machine consumers did not.
## Fix
Two small, targeted edits:
1. `classify_error_kind()`: add explicit branches for "prompt subcommand
requires" and "empty prompt:" (the latter anchored with `starts_with`
so it never hijacks unrelated error messages containing the word).
Both route to `cli_parse`.
2. JSON error render path in `main()`: after calling split_error_hint(),
if the message carried no embedded hint AND kind is `cli_parse` AND
the short-reason does not already embed a `claw --help` pointer,
synthesize the same `Run `claw --help` for usage.` trailer that
text-mode stderr appends. The embedded-pointer check prevents
duplication on the `empty prompt: ... (run `claw --help`)` message
which already carries inline guidance.
## Verification
Direct repro on the compiled binary:
$ claw --output-format json prompt
{"error":"prompt subcommand requires a prompt string",
"hint":"Run `claw --help` for usage.",
"kind":"cli_parse","type":"error"}
$ claw --output-format json ""
{"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string",
"hint":null,"kind":"cli_parse","type":"error"}
$ claw --output-format json doctor --foo # regression guard
{"error":"unrecognized argument `--foo` for subcommand `doctor`",
"hint":"Run `claw --help` for usage.",
"kind":"cli_parse","type":"error"}
Text mode unchanged in shape; `[error-kind: ...]` prefix now reads
`cli_parse` for the two previously-misclassified paths.
## Regression coverage
- Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks
both patterns route to `cli_parse` AND that generic "prompt"-containing
messages still fall through to `unknown`.
- Integration tests in `tests/output_format_contract.rs`:
* prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247
* empty_positional_arg_emits_cli_parse_envelope_247
* whitespace_only_positional_arg_emits_cli_parse_envelope_247
* unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard
- Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15
output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli).
## Family / related
Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181,
and now **#247**. All four quartet items now have real fixes landed on
the canonical binary surface rather than only the Python harness.
ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
Cycle #32 dogfood finding: #127 was fixed on main via `a3270db` + `79352a2`
(2026-04-20), but the ROADMAP.md entry still lacked a [CLOSED] marker.
The in-flight branches `feat/jobdori-127-clean` and
`feat/jobdori-127-verb-suffix-flags` were superseded and are now obsolete.
## What This Fixes
**Documentation drift:** Pinpoint #127 was complete in code but unmarked
in ROADMAP. New contributors checking the roadmap would see it as open
work, potentially duplicating effort.
**Stale branches:** Two branches (`feat/jobdori-127-clean`,
`feat/jobdori-127-verb-suffix-flags`) contain the fix attempt bundled
with an unrelated large-scope refactor (5365 lines removed from
ROADMAP.md, root-level governance docs deleted, command infra refactored).
Their fix was superseded; branches are functionally obsolete.
## Verification
Re-verified all 4 #127 scenarios pass on main HEAD `b903e16`:
$ claw doctor --json → rejected with "did you mean" hint
$ claw doctor garbage → rejected
$ claw doctor --unknown-flag → rejected
$ claw doctor --output-format json → works (canonical form)
All behavior matches #127 acceptance criteria.
## Cluster Impact
Post-closure: **parser-level trust gap quintet (#108 + #117 + #119 + #122
+ #127) is 5/5 closed**. The `_other => Prompt` fall-through audit is
complete.
## Discipline Check
Per cycle #24 calibration:
- Red-state bug? ✗ (behavior is correct on main)
- Real friction? ✓ (ROADMAP drift; obsolete branches adrift)
- Evidence-backed? ✓ (dogfood probe confirmed closure; git log confirmed
supersession; branch diff confirmed scope contamination)
## Relationship to Gaebal-gajae's Option A Guidance
Cycle #32 started by proposing separating the #127 fix from the attached
refactor. On deeper probe, discovered the fix was already superseded on
main via different commits. Option A (separate the fix) is retroactively
satisfied: the fix landed cleanly, the refactor never did.
The remaining action is governance hygiene: mark closure, document
supersession, flag obsolete branches for deletion.
## Next Actions (not in this commit)
- Delete `feat/jobdori-127-clean` locally and on fork (after confirmation)
- Delete `feat/jobdori-127-verb-suffix-flags` locally and on fork
- Monitor whether any attached refactor content should be re-proposed in
its own scoped PR
Source: Jobdori cycle #32 dogfood in response to Clawhip 10-min nudge.
Proposed Option A (separate fix from refactor); probe revealed the fix
already landed via a different commit path, rendering the refactor-only
branch obsolete.
Cycle #30 dogfood found a testing gap: OPT_OUT surfaces were classified
in code but their REJECTION behavior was never regression-tested.
## The Gap
OPT_OUT_AUDIT.md declares 12 surfaces as intentionally exempt from
--output-format. The test suite had:
- ✅ test_clawable_surface_has_output_format (CLAWABLE must accept)
- ✅ test_every_registered_command_is_classified (no orphans)
- ❌ Nothing verifying OPT_OUT surfaces REJECT --output-format
If a developer accidentally added --output-format to 'summary' (one of
the 12 OPT_OUT surfaces), no test would catch the silent promotion.
The classification was governed, but the rejection behavior was NOT.
## What Changed
Added TestOptOutSurfaceRejection to test_cli_parity_audit.py with 14 tests:
1. **12 parametrized tests** — one per OPT_OUT surface, verifying each
rejects --output-format with an argparse error.
2. **test_opt_out_set_matches_audit_document** — verifies OPT_OUT_SURFACES
constant matches the declared 12 surfaces in OPT_OUT_AUDIT.md.
3. **test_opt_out_count_matches_declared** — sanity check that the count
stays at 12 as documented.
## Symmetry Achieved
Before: only CLAWABLE acceptance tested
CLAWABLE accepts --output-format ✅
OPT_OUT behavior: untested
After: full parity coverage
CLAWABLE accepts --output-format ✅
OPT_OUT rejects --output-format ✅
Audit doc ↔ constant kept in sync ✅
This completes the parity enforcement loop: every new surface is
explicitly IN or OUT, and BOTH directions are regression-locked.
## Promotion Path Preserved
When a real OPT_OUT surface gains genuine demand (per OPT_OUT_DEMAND_LOG.md):
1. Move from OPT_OUT_SURFACES to CLAWABLE_SURFACES
2. Update OPT_OUT_AUDIT.md with promotion rationale
3. Remove from this test's expected rejections
4. Tests pass (rejection test no longer runs; acceptance test now required)
Graceful promotion; no accidental drift.
## Test Count
- 222 → 236 passing (+14, zero regressions)
- 12 parametrized + 2 metadata = 14 new tests
## Discipline Check
Per cycle #24 calibration:
- Red-state bug? ✗ (no broken behavior)
- Real friction? ✓ (testing gap discovered by dogfood)
- Evidence-backed? ✓ (systematic probe revealed missing coverage)
This is the cycle #27 taxonomy (structural / quality / cross-channel /
text-vs-JSON divergence) extending into classification: not just 'is the
envelope right?' but 'is the OPPOSITE-OF-envelope right?'
Future cycles can apply the same principle to other classifications:
every governed non-goal deserves regression tests that lock its
non-goal-ness.
Classification:
- Real friction: ✓ (cycle #30 dogfood)
- Evidence-backed: ✓ (gap discovered by systematic surface audit)
- Same-cycle fix: ✓ (maintainership discipline)
Source: Jobdori cycle #30 proactive dogfood — probed all 26 subcommands
with --output-format json and noticed OPT_OUT rejection pattern was
unverified by any dedicated test.
Cycle #29 dogfood found a real pinpoint: cross-mode exit code divergence.
## The Pinpoint
Dogfooding the CLI revealed that unknown subcommand errors return different
exit codes depending on output mode:
$ python3 -m src.main nonexistent-cmd # exit 2
$ python3 -m src.main nonexistent-cmd --output-format json # exit 1
ERROR_HANDLING.md documented the exit-code contract (1=parse, 2=timeout)
but did NOT explicitly state the contract applies only to JSON mode. Text
mode follows argparse defaults (exit 2 for any parse error), which
violates the documented contract when interpreted generally.
A claw using text mode with 'claw nonexistent' would see exit 2 and
misclassify as timeout per the docs. Real protocol contract gap, not
implementation bug.
## Classification
This is a DOCUMENTATION gap, not a behavior bug:
- Text mode follows argparse convention (reasonable for humans)
- JSON mode normalizes to documented contract (reasonable for claws)
- The divergence is intentional; only the docs were silent about it
Fix = document the divergence explicitly + lock it with tests.
NOT fix = change text mode exit code to 1 (would break argparse
conventions and confuse human users).
## Documentation Changes
ERROR_HANDLING.md:
1. Added IMPORTANT callout in Quick Reference section:
'The exit code contract applies ONLY when --output-format json is
explicitly set. Text mode follows argparse conventions.'
2. New 'Text mode vs JSON mode exit codes' table showing exact divergence:
- Unknown subcommand: text=2, json=1
- Missing required arg: text=2, json=1
- Session not found: text=1, json=1 (app-level, identical)
- Success: text=0, json=0 (identical)
- Timeout: text=2, json=2 (identical, #161)
3. Practical rule: 'always pass --output-format json'
## Tests Added (5)
TestTextVsJsonModeDivergence in test_cross_channel_consistency.py:
1. test_unknown_command_text_mode_exits_2 — text mode argparse default
2. test_unknown_command_json_mode_exits_1 — JSON mode contract normalized
3. test_missing_required_arg_text_mode_exits_2 — same for missing args
4. test_missing_required_arg_json_mode_exits_1 — same normalization
5. test_success_path_identical_in_both_modes — success exit identical
These tests LOCK the expected divergence so:
- Documentation stays aligned with implementation
- Future changes (either direction) are caught as intentional
- Claws trust the docs
## Test Status
- 217 → 222 tests passing (+5)
- Zero regressions
## Discipline
This cycle follows the cycle #28 template exactly:
- Dogfood probe revealed real friction (test said exit=2, docs said exit=1)
- Minimal fix shape (documentation clarification, not code change)
- Regression guard via tests
- Evidence-backed, not speculative
Relationship to #181:
- #181 fixed env.exit_code != process exit (WITHIN JSON mode)
- #29 clarifies exit code contract scope (ONLY JSON mode)
- Both establish: exit codes are deterministic, but only when --output-format json
---
Classification (per cycle #24 calibration):
- Red-state bug? ✗ (behavior was reasonable, docs were incomplete)
- Real friction? ✓ (docs/code divergence revealed by dogfood)
- Evidence-backed? ✓ (test suite probed both modes, found the gap)
Source: Jobdori cycle #29 proactive dogfood — in response to Clawhip nudge
for pinpoint hunting. Found that text-mode errors return exit 2 but
ERROR_HANDLING.md implied exit 1 was the parse-error contract universally.
Cycle #28 closes the low-hanging metadata protocol gap identified in #180.
## The Gap
Pinpoint #180 (filed cycle #24) documented a metadata protocol gap:
- `--help` works (argparse default)
- `--version` does NOT exist
The ROADMAP entry deferred implementation pending demand. Cycle #28 dogfood
probe found this during routine invariant audit (attempt to call `--version`
as part of comprehensive CLI surface coverage). This is concrete evidence of
real friction, not speculative gap-filling.
## Implementation
Added `--version` flag to argparse in `build_parser()`:
```python
parser.add_argument('--version', action='version', version='claw-code 1.0.0 (Python harness)')
```
Simple one-liner. Follows Python argparse conventions (built-in action='version').
## Tests Added (3)
TestMetadataFlags in test_exec_route_bootstrap_output_format.py:
1. test_version_flag_returns_version_text — `claw --version` prints version
2. test_help_flag_returns_help_text — `claw --help` still works
3. test_help_still_works_after_version_added — Both -h and --help work
Regression guard on the original help surface.
## Test Status
- 214 → 217 tests passing (+3)
- Zero regressions
- Full suite green
## Discipline
This cycle exemplifies the cycle #24 calibration:
- #180 was filed as 'deferred pending demand'
- Cycle #28 dogfood found actual friction (proactive test coverage gap)
- Evidence = concrete ('--version not found during invariant audit')
- Action = minimal implementation + regression tests
- No speculation, no feature creep, no implementation before evidence
Not 'we imagined someone might want this.' Instead: 'we tried to call it
during routine maintenance, got ENOENT, fixed it.'
## Related
- #180 (cycle #24): Metadata protocol gap filed
- Cycle #27: Cross-channel consistency audit established framework
- Cycle #28 invariant audit: Discovered actual friction, triggered fix
---
Classification (per cycle #24 calibration):
- Red-state bug? ✗ (not a malfunction, just an absence)
- Real friction? ✓ (audit probe could not call the flag, had to special-case)
- Evidence-backed? ✓ (proactive test coverage revealed the gap)
Source: Jobdori cycle #28 dogfood — invariant audit attempting comprehensive
CLI surface coverage found that --version was unsupported.
Cycle #27 ships a new test class systematizing the three-layer protocol
invariant framework.
## Context
After cycles #20–#26, the protocol has three distinct invariant classes:
1. **Structural compliance** (#178): Does the envelope exist?
2. **Quality compliance** (#179): Is stderr silent + error message truthful?
3. **Cross-channel consistency** (#181 + NEW): Do multiple channels agree?
#181 revealed a critical gap: the second test class was incomplete.
Envelopes could be structurally valid, quality-compliant, but still
lie about their own state (envelope.exit_code != actual exit).
## New Test Class
TestCrossChannelConsistency in test_cross_channel_consistency.py captures
the third invariant layer with 5 dedicated tests:
1. envelope.command ↔ dispatched subcommand
2. envelope.output_format ↔ --output-format flag
3. envelope.timestamp ↔ actual wall clock (recent, <5s)
4. envelope.exit_code ↔ process exit code (cycle #26/#181 regression guard)
5. envelope boolean fields (found/handled/deleted) ↔ error block presence
Each test specifically targets cross-channel truth, not structure or quality.
## Why Separate Test Classes Matter
A command can fail all three ways independently:
| Failure mode | Exit/Crash | Test class | Example |
|---|---|---|---|
| Structural | stderr noise | TestParseErrorEnvelope | argparse leaks to stderr |
| Quality | correct shape, wrong message | TestParseErrorStderrHygiene | error instead of real message |
| Cross-channel | truthy field, lie about state | TestCrossChannelConsistency | exit_code: 0 but exit 1 |
#181 was invisible to the first two classes. A claw passing all structure/
quality tests could still be misled. The third class catches that.
## Audit Results (Cycle #27)
All 5 tests pass — no drift detected in any channel pair:
- ✅ Envelope command always matches dispatch
- ✅ Envelope output_format always matches flag
- ✅ Envelope timestamp always recent (<5s)
- ✅ Envelope exit_code always matches process exit (post-#181 guard)
- ✅ Boolean fields consistent with error block presence
The systematic audit proved the fix from #181 holds, and identified
no new cross-channel gaps.
## Test Impact
- 209 → 214 tests passing (+5)
- Zero regressions
- New invariant class now has dedicated test suite
- Future cross-channel bugs will be caught by this class
## Related
- #178 (#20): Parser-front-door structural contract
- #179 (#20): Stderr hygiene + real error message quality
- #181 (#26): Envelope exit_code must match process exit
- #182-N: Future cross-channel contract violations will be caught
by TestCrossChannelConsistency
This test class is evergreen — as new fields/channels are added to the
protocol, invariants for those channels should be added here, not mixed
with other test classes. Keeping invariant classes separate makes
regression attribution instant (e.g., 'TestCrossChannelConsistency failed'
= 'some truth channel disagreed').
Classification (per cycle #24 calibration):
- Red-state bug: ✗ (audit is green)
- Real friction: ✓ (structured audit of documented invariants)
- Proof of equilibrium: ✓ (systematic verification, no gaps found)
Source: Jobdori cycle #27 proactive invariant audit — following gaebal
guidance to probe documented invariants, not speculative gaps.
Cycle #26 dogfood found a real red-state bug in the JSON envelope contract.
## The Bug
exec-command and exec-tool not-found cases return exit code 1 from the
process, but the envelope reports exit_code: 0 (the default from
wrap_json_envelope). This is a protocol violation.
Repro (before fix):
$ claw exec-command unknown-cmd test --output-format json > out.json
$ echo $?
1
$ jq '.exit_code' out.json
0 # WRONG — envelope lies about exit code
Claws reading the envelope's exit_code field get misinformation. A claw
implementing the canonical ERROR_HANDLING.md pattern (check exit_code,
then classify by error.kind) would incorrectly treat failures as
successes when dispatching on the envelope alone.
## Root Cause
main.py lines 687–739 (exec-command + exec-tool handlers):
- Return statement: 'return 0 if result.handled else 1' (correct)
- Envelope wrap: 'wrap_json_envelope(envelope, args.command)'
(uses default exit_code=0, IGNORES the return value)
The envelope wrap was called BEFORE the return value was computed, so
the exit_code field was never synchronized with the actual exit code.
## The Fix
Compute exit_code ONCE at the top:
exit_code = 0 if result.handled else 1
Pass it explicitly to wrap_json_envelope:
wrap_json_envelope(envelope, args.command, exit_code=exit_code)
Return the same value:
return exit_code
This ensures the envelope's exit_code field is always truth — the SAME
value the process returns.
## Tests Added (3)
TestEnvelopeExitCodeMatchesProcessExit in test_exec_route_bootstrap_output_format.py:
1. test_exec_command_not_found_envelope_exit_matches:
Verifies exec-command unknown-cmd returns exit 1 in both envelope
and process.
2. test_exec_tool_not_found_envelope_exit_matches:
Same for exec-tool.
3. test_all_commands_exit_code_invariant:
Audit across 4 known non-zero cases (show-command, show-tool,
exec-command, exec-tool not-found). Guards against the same bug
in other surfaces.
## Impact
- 206 → 209 passing tests (+3)
- Zero regressions
- Protocol contract now truthful: envelope.exit_code == process exit
- Claws using the one-handler pattern from ERROR_HANDLING.md now get
correct information
## Related
- ERROR_HANDLING.md (cycle #22): Documented exit_code as machine-readable
contract field
- #178/#179 (cycles #19/#20): Closed parser-front-door contract
- This closes a gap in the WORK PROTOCOL contract — envelope values must
match reality, not just be structurally present.
Classification (per cycle #24 calibration):
- Red-state bug: ✓ (contract violation, claws get misinformation)
- Real friction: ✓ (discovered via dogfood, not speculative)
- Fix ships same-cycle: ✓ (discipline per maintainership mode)
Source: Jobdori cycle #26 dogfood — ran multiple edge-case probes, noticed
exec-command envelope showed exit_code: 0 while process exited 1.
Investigated wrap_json_envelope default behavior, confirmed bug, fixed
and tested in same cycle.
Cycle #25 ships navigation improvements connecting USAGE (setup/interactive)
to ERROR_HANDLING.md (subprocess/orchestration patterns).
Before: USAGE.md had JSON scripting mention but no link to error-handling guide.
New users reading USAGE would see JSON is available, but wouldn't discover
the error-handling pattern without accidentally finding ERROR_HANDLING.md.
After: Two strategic cross-links:
1. Top-level tip box: "Building orchestration code? See ERROR_HANDLING.md"
2. JSON scripting section expanded with examples + link to unified pattern
Changes to USAGE.md:
- Added TIP callout near top linking to ERROR_HANDLING.md
- Expanded "JSON output for scripting" section:
- Explains what the envelope contains (exit_code, command, timestamp, fields)
- Added 3 command examples (prompt, load-session, turn-loop)
- Added callout for dispatchers/orchestrators pointing to ERROR_HANDLING pattern
Impact: Operators reading USAGE for "how do I call claw from scripts?" now
immediately see the canonical answer (ERROR_HANDLING.md) instead of having
to reverse-engineer it from code examples.
No code changes. Pure navigation/documentation.
Continues the documentation-governance pattern: the work protocol (14 clawable
commands) has a consumption guide (ERROR_HANDLING.md), and that guide is now
reachable from the main entry point (USAGE.md + README.md top nav).
Cycle #23 ships a documentation discoverability fix.
After #22 shipping ERROR_HANDLING.md, the next natural step is making it
discoverable from the project's entry point (README.md).
Before: README top navigation linked to USAGE, PARITY, ROADMAP, Rust workspace.
ERROR_HANDLING.md was buried in CLAUDE.md references.
After: ERROR_HANDLING.md is now in the top navigation (right after USAGE,
before Rust workspace). Also added SCHEMAS.md mention in repository shape.
This signals that:
1. Error handling is a first-class concern (not an afterthought)
2. The Python harness documentation (SCHEMAS.md, ERROR_HANDLING.md, CLAUDE.md)
is part of the official docs, not just dogfood artifacts
3. New users/claws can discover the error-handling pattern at entry point
Impact: Operators building orchestration code will immediately see
'Error Handling' link in navigation, shortening the path to understanding
how to consume the protocol reliably.
No code changes. No test changes. Pure navigation/discoverability.
Cycle #22 ships documentation that operationalizes cycles #178–#179.
Problem context:
After #178 (parse-error envelope) and #179 (stderr hygiene + real error message),
claws can now build a unified error handler for all 14 clawable commands.
But there was no guide on how to actually do that. Operators had the pieces;
they didn't have the pattern.
This file changes that.
New file: ERROR_HANDLING.md
- Quick reference: exit codes + envelope shapes (0=success, 1=error, 2=timeout)
- One-handler pattern: ~80 lines of Python showing how to parse error.kind,
check retryable, and decide recovery strategy
- Four practical recovery patterns:
- Retry on transient errors (filesystem, timeout)
- Reuse session after timeout (if cancel_observed=true)
- Validate command syntax before dispatch (dry-run --help)
- Log errors for observability
- Error kinds enumeration (parse, session_not_found, filesystem, runtime, timeout)
- Common mistakes to avoid (6 patterns with BAD vs GOOD examples)
- Testing your error handler (unit test examples)
Operational impact:
Orchestration code now has a canonical pattern. Claws can:
- Copy-paste the run_claw_command() function (works for all commands)
- Classify errors uniformly (no special cases per command)
- Decide recovery deterministically (error.kind + retryable + cancel_observed)
- Log/monitor/escalate with confidence
Related cycles:
- #178: Parse-error envelope (commands now emit structured JSON on invalid argv)
- #179: Stderr hygiene + real message (JSON mode silences argparse, carries actual error)
- #164 Stage B: cancel_observed field (callers know if session is safe for reuse)
Updated CLAUDE.md:
- Added ERROR_HANDLING.md to 'Related docs' section
- Now documents the one-handler pattern as a guideline
No code changes. No test changes. Pure documentation.
This completes the documentation trail from protocol (SCHEMAS.md) →
governance (OPT_OUT_AUDIT.md, OPT_OUT_DEMAND_LOG.md) → practice (ERROR_HANDLING.md).
Cycle #21 ships governance infrastructure, not implementation. Maintainership
mode means sometimes the right deliverable is a decision framework, not code.
Problem context:
OPT_OUT_AUDIT.md (cycle #18 bonus) established 'demand-backed audit' as the
next step. But without a structured way to record demand signals, 'demand-backed'
was just a slogan — the next audit cycle would have no evidence to work from.
This commit creates the evidentiary base:
New file: OPT_OUT_DEMAND_LOG.md
- Per-surface entries for all 12 OPT_OUT commands (Groups A/B/C)
- Current state: 0 signals across all surfaces (consistent with audit prediction)
- Signal entry template with required fields:
- Source (who/what)
- Use case (concrete orchestration problem)
- Markdown-alternative-checked (why existing output insufficient)
- Date
- Promotion thresholds:
- 2+ independent signals for same surface → file promotion pinpoint
- 1 signal + existing stable schema → file pinpoint for discussion
- 0 signals → stays OPT_OUT (rationale preserved)
Decision framework for cycle #22 (audit close):
- If 0 signals total: move to PERMANENTLY_OPT_OUT, close audit
- If 1-2 signals: file individual promotion pinpoints with evidence
- If 3+ signals: reopen audit, question classification itself
Updated files:
- OPT_OUT_AUDIT.md: Added demand log reference in Related section
- CLAUDE.md: Added prerequisites for promotions (must have logged signals),
added 'File a demand signal' workflow section
Philosophy:
'Prevent speculative expansion' — schema bloat protection discipline.
Every new CLAWABLE surface is a maintenance tax. Evidence requirement keeps
the protocol lean. OPT_OUT surfaces are intentionally not-clawable until
proven otherwise by external demand.
Operational impact:
Next cycles can now:
1. Watch for real claws hitting OPT_OUT surface limits
2. Log signals in structured format (no ad-hoc filing)
3. Run audit at cycle #22 with actual data, not speculation
No code changes. No test changes. Pure governance infrastructure.
Related: #18 cycle (OPT_OUT_AUDIT.md), maintainership phase transition.
Dogfood discovered #178 had two residual gaps:
1. Stderr pollution: argparse usage + error text still leaked to stderr even in
JSON mode (envelope was correct on stdout, but stderr noise broke the
'machine-first protocol' contract — claws capturing both streams got dual output)
2. Generic error message: envelope carried 'invalid command or argument (argparse
rejection)' instead of argparse's actual text like 'the following arguments
are required: session_id' or 'invalid choice: typo (choose from ...)'
Before #179:
$ claw load-session --output-format json
[stdout] {"error": {"message": "invalid command or argument (argparse rejection)"}}
[stderr] usage: main.py load-session [-h] ...
main.py load-session: error: the following arguments are required: session_id
[exit 1]
After #179:
$ claw load-session --output-format json
[stdout] {"error": {"message": "the following arguments are required: session_id"}}
[stderr] (empty)
[exit 1]
Implementation:
- New _ArgparseError exception class captures argparse's real message
- main() monkey-patches parser.error (+ all subparser.error) in JSON mode to raise
_ArgparseError instead of print-to-stderr + sys.exit(2)
- _emit_parse_error_envelope() now receives the real message verbatim
- Text mode path unchanged: still uses original argparse print+exit behavior
Contract:
- JSON mode: stdout carries envelope with argparse's actual error; stderr silent
- Text mode: unchanged — argparse usage to stderr, exit 2
- Parse errors still error.kind='parse', retryable=false
Test additions (5 new, 14 total in test_parse_error_envelope.py):
- TestParseErrorStderrHygiene (5):
- test_json_mode_stderr_is_silent_on_unknown_command
- test_json_mode_stderr_is_silent_on_missing_arg
- test_json_mode_envelope_carries_real_argparse_message
- test_json_mode_envelope_carries_invalid_choice_details (verifies valid-choices list)
- test_text_mode_stderr_preserved_on_unknown_command (backward compat)
Operational impact:
Claws capturing both stdout and stderr no longer get garbled output. The envelope
message now carries discoverability info (valid command list, missing-arg name)
that claws can use for retry/recovery without probing the CLI a second time.
Test results: 201 → 206 passing, 3 skipped unchanged, zero regression.
Pinpoint discovered via dogfood at 2026-04-22 20:30 KST (cycle #20).
Filed explicit decision criteria for the 12 OPT_OUT surfaces (commands that do
not support --output-format json) documented in test_cli_parity_audit.py.
Categorized by rationale:
- Group A (4): Rich-Markdown reports (summary, manifest, parity-audit, setup-report)
Markdown-as-output is intentional; JSON would be information loss.
Unlikely promotions (remain OPT_OUT long-term).
- Group B (3): List filters with --query/--limit (subsystems, commands, tools)
Query layer already exists; users have escape hatch.
Remain OPT_OUT (promotion effort >> value).
- Group C (5): Simulation/debug surfaces (remote-mode, ssh-mode, teleport-mode,
direct-connect-mode, deep-link-mode)
Intentionally non-production; JSON output doesn't add value.
Remain OPT_OUT (simulation tools, not orchestration endpoints).
Audit workflow documented:
1. Survey: Check if external claws actually request JSON versions
2. Cost estimate: Schema + tests for each surface
3. Value estimate: Real demand vs hypothetical
4. Decision: CLAWABLE, remain OPT_OUT, or new pinpoint
Promotion criteria locked (only if clear use case + schema simple + demand exists).
Outcome prediction: All 12 likely remain OPT_OUT (documented rationale per group).
Timeline: Survey period (cycles #19–#21), final decision (cycle #22).
Related pinpoints: #175 (summary/manifest JSON parallel?), #176 (--query-json?),
#177 (mode simulators ever CLAWABLE?).
This closes the documentation loop from cycles #173–#174 (protocol closure →
field evolution → reframe). Now governance rules are explicit for future work.
#164 Stage B requires exposing whether cancellation was observed at the
turn-result level. This commit adds the infrastructure field:
Changes:
- TurnResult.cancel_observed: bool = False (query_engine.py)
- _build_timeout_result() accepts cancel_observed parameter (runtime.py)
- Two timeout paths now pass cancel_event.is_set() to signal observation (runtime.py)
- bootstrap command includes cancel_observed in turn JSON (main.py)
- SCHEMAS.md documents Turn Result Fields with cancel_observed contract
Usage:
When a turn timeout occurs, cancel_observed=true indicates that the
engine observed the cancellation event being set. This allows callers
to distinguish:
- timeout with no cancel → infrastructure/network stall
- timeout with cancel observed → cooperative cancellation was triggered
Backward compat:
- Existing TurnResult construction without cancel_observed defaults to False
- bootstrap JSON output still validates per SCHEMAS.md (new field is always present)
Test results: 182 passing, 3 skipped, zero regression.
Related: #161 (wall-clock timeout), #164 (cancellation observability protocol)
ROADMAP continues #164 with Stage C (test coverage for cancellation + turn envelope).
Adds parametrised test suite validating that clawable-surface commands'
JSON output matches their declared envelope contracts per SCHEMAS.md.
Two phases:
Phase 1 (this commit): Consistency baseline.
- Collect ENVELOPE_CONTRACTS registry mapping each command to its
required and optional fields
- TestJsonEnvelopeConsistency: parametrised test iterates over 13
commands, invokes with --output-format json, validates that
actual JSON envelope contains all required fields
- test_envelope_field_value_types: spot-check types (int, str, list)
for consistency
Phase 2 (future #173): Common field wrapping.
- Once wrap_json_envelope() is applied, all commands will emit
timestamp, command, exit_code, output_format, schema_version
- Currently skipped via @pytest.mark.skip, these tests will activate
automatically when wrapping is implemented:
TestJsonEnvelopeCommonFieldPrep::test_all_envelopes_include_timestamp
TestJsonEnvelopeCommonFieldPrep::test_all_envelopes_include_command
TestJsonEnvelopeCommonFieldPrep::test_all_envelopes_include_exit_code_and_schema_version
Why this matters:
- #172 documented the JSON contract; this test validates it
- Currently detects when actual output diverges from SCHEMAS.md
(e.g. list-sessions emits 'count', not 'sessions_count')
- As #173 wraps commands, test suite auto-validates new common fields
- Prevents regression: accidental field removal breaks the test suite
Current status: 11 passed (consistency), 6 skipped (awaiting #173)
Full suite: 168 → 179 passing, zero regression.
Closes ROADMAP #173 prep (framework for common field validation).
Actual field wrapping remains for next cycle.
Stops manual parity inspection from being a human-noticed concern. When
a developer adds a new subcommand to the claw-code CLI, this test suite
enforces explicit classification:
- CLAWABLE_SURFACES: MUST accept --output-format {text,json}
- OPT_OUT_SURFACES: explicitly exempt with documented rationale
A new command that forgets to opt into one of these two sets FAILS
loudly with TestCommandClassificationCoverage::test_every_registered_
command_is_classified. No silent drift possible.
Technique: argparse introspection at test time walks the _actions tree,
discovers every registered subcommand, and compares against the declared
classification sets. Contract is enforced machine-first instead of
depending on human review.
Three test classes covering three invariants:
TestClawableSurfaceParity (14 tests):
- test_all_clawable_surfaces_accept_output_format: every member of
CLAWABLE_SURFACES has --output-format flag registered
- test_clawable_surface_output_format_choices (parametrised over 13
commands): each must accept exactly {text, json} and default to 'text'
for backward compat
TestCommandClassificationCoverage (3 tests):
- test_every_registered_command_is_classified: any new subcommand
must be explicitly added to CLAWABLE_SURFACES or OPT_OUT_SURFACES
- test_no_command_in_both_sets: sanity check for classification conflicts
- test_all_classified_commands_actually_exist: no phantom commands
(catches stale entries after a command is removed)
TestJsonOutputContractEndToEnd (10 tests):
- test_command_emits_parseable_json (parametrised over 10 clawable
commands): actual subprocess invocation with --output-format json
produces valid parseable JSON on stdout
Classification:
CLAWABLE_SURFACES (13):
Session lifecycle: list-sessions, delete-session, load-session,
flush-transcript
Inspect: show-command, show-tool
Execution: exec-command, exec-tool, route, bootstrap
Diagnostic inventory: command-graph, tool-pool, bootstrap-graph
OPT_OUT_SURFACES (12):
Rich-Markdown reports (future JSON schema): summary, manifest,
parity-audit, setup-report
List filter commands: subsystems, commands, tools
Turn-loop: structured_output is future work
Simulation/debug: remote-mode, ssh-mode, teleport-mode,
direct-connect-mode, deep-link-mode
Full suite: 141 → 168 passing (+27), zero regression.
Closes ROADMAP #171.
Why this matters:
Before: parity was human-monitored; every new command was a drift
risk. The CLUSTER 3 sweep required manually auditing every
subcommand and landing fixes as separate pinpoints.
After: parity is machine-enforced. If a future developer adds a new
command without --output-format, the test suite blocks it
immediately with a concrete error message pointing at the
missing flag.
This is the first step in Gaebal-gajae's identified upper-level work:
operationalised parity instead of aspirational parity.
Related clusters:
- Clawability principle: machine-first protocol enforcement
- Test-first regression guard: extends TestTripletParityConsistency
(#160/#165) and TestFullFamilyParity (#166) from per-cluster
parity to cross-surface parity
Final diagnostic surface in the JSON parity sweep: bootstrap-graph
(the runtime bootstrap/prefetch visualization) now supports --output-format.
Concrete addition:
- bootstrap-graph: --output-format {text,json}
JSON envelope:
{stages: [str], note: 'bootstrap-graph is markdown-only in this version'}
Envelope explanation: bootstrap-graph's Markdown output is rich and
textual; raw JSON embedding maintains the markdown format (split into
lines array) rather than attempting lossy structural extraction that
would lose information. This is an honest limitation in this cycle;
full JSON schema can be added in a future audit if claws require
structured bootstrap data (dependency graphs, prefetch timing, etc.).
Backward compatibility:
- Default is 'text' (Markdown unchanged)
Closes ROADMAP #170.
Related: #167, #168, #169. Diagnostic/inventory surface family is now
uniformly JSON-capable. Summary, manifest, parity-audit, setup-report,
command-graph, tool-pool, bootstrap-graph all accept --output-format.
Extends the diagnostic surface audit with the two inventory-structure
commands: command-graph (command family segmentation) and tool-pool
(assembled tool inventory). Both now expose their underlying rich
datastructures via JSON envelope.
Concrete additions:
- command-graph: --output-format {text,json}
- tool-pool: --output-format {text,json}
JSON envelope shapes:
command-graph:
{builtins_count, plugin_like_count, skill_like_count, total_count,
builtins: [{name, source_hint}],
plugin_like: [{name, source_hint}],
skill_like: [{name, source_hint}]}
tool-pool:
{simple_mode, include_mcp, tool_count,
tools: [{name, source_hint}]}
Backward compatibility:
- Default is 'text' (Markdown unchanged)
- Text output byte-identical to pre-#169
Tests (4 new, test_command_graph_tool_pool_output_format.py):
- TestCommandGraphOutputFormat (2): JSON structure + text compat
- TestToolPoolOutputFormat (2): JSON structure + text compat
Full suite: 137 → 141 passing, zero regression.
Closes ROADMAP #169.
Why this matters:
Claws auditing the codebase can now ask 'what commands exist' and
'what tools exist' and get structured, parseable answers instead of
regex-parsing Markdown headers and counting list items.
Related clusters:
- Diagnostic surfaces (#169 adds to #167/#168 work-verb parity)
- Inventory introspection (command-graph + tool-pool are the two
foundational 'what do we have?' queries)
Closes the inspect-capability parity gap: show-command and show-tool were
the only discovery/inspection CLI commands lacking --output-format support,
making them outliers in the ecosystem that already had unified JSON
contracts across list-sessions, load-session, delete-session, and
flush-transcript (#160/#165/#166).
Concrete additions:
- show-command: --output-format {text,json}
- show-tool: --output-format {text,json}
JSON envelope shape (found case):
{name, found: true, source_hint, responsibility}
JSON envelope shape (not-found case):
{name, found: false, error: {kind:'command_not_found'|'tool_not_found',
message, retryable: false}}
Exit codes:
0 = success
1 = not found
Backward compatibility:
- Default (no --output-format) is 'text' (unchanged)
- Text output byte-identical to pre-#167 (three newline-separated lines)
Tests (10 new, test_show_command_tool_output_format.py):
- TestShowCommandOutputFormat (5): found + not-found in JSON; text mode
backward compat; text is default
- TestShowToolOutputFormat (3): found + not-found in JSON; text mode
backward compat
- TestShowCommandToolFormatParity (2): both accept same flag choices;
consistent JSON envelope shape
Full suite: 114 → 124 passing, zero regression.
Closes ROADMAP #167.
Why this matters:
Before: Claws calling show-command/show-tool had to parse human-readable
prose output via regex, with no structured error signal.
After: Same envelope contract as load-session and friends: JSON-first,
typed errors, machine-parseable.
Related clusters:
- Session-lifecycle CLI parity family (#160, #165, #166, #167)
- Machine-readable error contracts (same vein as #162 atomicity + #164
cancellation state-safety: structured boundaries for orchestration)
Closes the #161 follow-up gap identified in review: wall-clock timeout
bounded caller-facing wait but did not cancel the underlying provider
thread, which could silently mutate mutable_messages / transcript_store /
permission_denials / total_usage after the caller had already observed
stop_reason='timeout'. A ghost turn committed post-deadline would poison
any session that got persisted afterwards.
Stage A scope (this commit): runtime + engine layer cooperative cancel.
Engine layer (src/query_engine.py):
- submit_message now accepts cancel_event: threading.Event | None = None
- Two safe checkpoints:
1. Entry (before max_turns / budget projection) — earliest possible return
2. Post-budget (after output synthesis, before mutation) — catches cancel
that arrives while output was being computed
- Both checkpoints return stop_reason='cancelled' with state UNCHANGED
(mutable_messages, transcript_store, permission_denials, total_usage
all preserved exactly as on entry)
- cancel_event=None preserves legacy behaviour with zero overhead (no
checkpoint checks at all)
Runtime layer (src/runtime.py):
- run_turn_loop creates one cancel_event per invocation when a deadline
is in play (and None otherwise, preserving legacy fast path)
- Passes the same event to every submit_message call across turns, so a
late cancel on turn N-1 affects turn N
- On timeout (either pre-call or mid-call), runtime explicitly calls
cancel_event.set() before future.cancel() + synthesizing the timeout
TurnResult. This upgrades #161's best-effort future.cancel() (which
only cancels not-yet-started futures) to cooperative mid-flight cancel.
Stop reason taxonomy after Stage A:
'completed' — turn committed, state mutated exactly once
'max_budget_reached' — overflow, state unchanged (#162)
'max_turns_reached' — capacity exceeded, state unchanged
'cancelled' — cancel_event observed, state unchanged (#164 Stage A)
'timeout' — synthesised by runtime, not engine (#161)
The 'cancelled' vs 'timeout' split matters:
- 'timeout' is the runtime's best-effort signal to the caller: deadline hit
- 'cancelled' is the engine's confirmation: cancel was observed + honoured
If the provider call wedges entirely (never reaches a checkpoint), the
caller still sees 'timeout' and the thread is leaked — but any NEXT
submit_message call on the same engine observes the event at entry and
returns 'cancelled' immediately, preventing ghost-turn accumulation.
This is the honest cooperative limit in Python threading land; true
preemption requires async-native provider IO (future work, not Stage A).
Tests (29 new tests, tests/test_submit_message_cancellation.py + tests/
test_run_turn_loop_cancellation.py):
Engine-layer (12 tests):
- TestCancellationBeforeCall (5): pre-set event returns 'cancelled' immediately;
mutable_messages, transcript_store, usage, permission_denials all preserved
- TestCancellationAfterBudgetCheck (1): cancel set mid-call (after projection,
before commit) still honoured; output synthesised but state untouched
- TestCancellationAfterCommit (2): post-commit cancel not observable (honest
limit) BUT next call on same engine observes it + returns 'cancelled'
- TestLegacyCallersUnchanged (3): cancel_event=None preserves #162 atomicity
+ max_turns contract with zero behaviour change
- TestCancellationVsOtherStopReasons (2): cancel precedes max_turns check;
cancel does not retroactively override a completed turn
Runtime-layer (5 tests):
- TestTimeoutPropagatesCancelEvent (3): submit_message receives a real Event
object when deadline is set; None in legacy mode; timeout actually calls
event.set() so in-flight threads observe at their next checkpoint
- TestCancelEventSharedAcrossTurns (1): same event object passed to every
turn (object identity check) — late cancel on turn N-1 must affect turn N
Regression: 3 existing timeout test mocks updated to accept cancel_event
kwarg (mocks that previously had signature (prompt, commands, tools, denials)
now have (prompt, commands, tools, denials, cancel_event=None) since runtime
passes cancel_event positionally on the timeout path).
Full suite: 97 → 114 passing, zero regression.
Closes ROADMAP #164 Stage A.
What's explicitly NOT in Stage A:
- Preemptive cancellation of wedged provider IO (requires asyncio-native
provider path; larger refactor)
- Timeout on the legacy unbounded run_turn_loop path (by design: legacy
callers opt out of cancellation entirely)
- CLI exposure of 'cancelled' as a distinct exit code (currently 'cancelled'
maps to the same stop_reason != 'completed' break condition as others;
CLI surface for cancel is a separate pinpoint if warranted)
Every 'claw flush-transcript' call without --directory writes to
.port_sessions/<uuid>.json in CWD. Without a gitignore entry, every
dogfood run leaves dozens of untracked files in the repo, masking real
changes in 'git status' output.
Now that #160/#166 ship structured session lifecycle commands and
deterministic --session-id, this directory is purely transient by
default — belongs in .gitignore.
#159: multi-turn sessions had a silent security asymmetry: denied_tools
were always empty in run_turn_loop, even though bootstrap_session inferred
them from the routed matches. Result: any tool gated as 'destructive'
(bash-family commands, rm, etc) would silently appear unblocked across all
turns in multi-turn mode, giving a false 'clean' permission picture to any
claw consuming TurnResult.permission_denials.
Fix: compute denied_tools once at loop start via _infer_permission_denials,
then pass the same denials to every submit_message call (both timeout and
legacy unbounded paths). This mirrors the existing bootstrap_session pattern.
Acceptance: run_turn_loop('run bash ls').permission_denials now matches
what bootstrap_session returns — both infer the same denials from the
routed matches. Multi-turn security posture is symmetric.
Tests (tests/test_run_turn_loop_permissions.py, 2 tests):
- test_turn_loop_surfaces_permission_denials_like_bootstrap: Symmetry
check confirming both paths infer identical denials for destructive tools
- test_turn_loop_with_continuation_preserves_denials: Denials inferred at
loop start are passed consistently to all turns; captured via mock and
verified non-empty
Full suite: 82/82 passing, zero regression.
Closes ROADMAP #159.
The #160 session-lifecycle CLI triplet was asymmetric: list-sessions and
delete-session accepted --directory + --output-format and emitted typed
JSON error envelopes, but load-session had neither flag and dumped a raw
Python traceback (including the SessionNotFoundError class name) on a
missing session.
Three concrete impacts this fix closes:
1. Alternate session-store locations (e.g. /tmp/claw-run-XXX/.port_sessions)
were unreachable via load-session; claws had to chdir or monkeypatch
DEFAULT_SESSION_DIR to work around it.
2. Not-found emitted a multi-line Python stack, not a parseable envelope.
Claws deciding retry/escalate/give-up had only exit code 1 to work with.
3. The traceback leaked 'src.session_store.SessionNotFoundError' verbatim,
coupling version-pinned claws to our internal exception class name.
Now all three triplet commands accept the same flag pair and emit the
same JSON error shape:
Success (json mode):
{"session_id": "alpha", "loaded": true, "messages_count": 3,
"input_tokens": 42, "output_tokens": 99}
Not-found:
{"session_id": "missing", "loaded": false,
"error": {"kind": "session_not_found",
"message": "session 'missing' not found in /path",
"directory": "/path", "retryable": false}}
Corrupted file:
{"session_id": "broken", "loaded": false,
"error": {"kind": "session_load_failed",
"message": "...", "directory": "/path",
"retryable": true}}
Exit code contract:
- 0 on successful load
- 1 on not-found (preserves existing $?)
- 1 on OSError/JSONDecodeError (distinct 'kind' in JSON)
Backward compat: legacy 'claw load-session ID' text output unchanged
byte-for-byte. Only new behaviour is the flags and structured error path.
Tests (tests/test_load_session_cli.py, 13 tests):
- TestDirectoryFlagParity (2): --directory works + fallback to CWD/.port_sessions
- TestOutputFormatFlagParity (2): json schema + text-mode backward compat
- TestNotFoundTypedError (2): JSON envelope on not-found; no traceback in
either mode; no internal class name leak
- TestLoadFailedDistinctFromNotFound (1): corrupted file = session_load_failed
with retryable=true, distinct from session_not_found
- TestTripletParityConsistency (6): parametrised over [list, delete, load] *
[--directory, --output-format] — explicit parity guard for future regressions
Full suite: 80/80 passing, zero regression.
Discovered via Jobdori dogfood sweep 2026-04-22 17:44 KST — ran
'claw load-session nonexistent' expecting a clean error, got a Python
traceback. Filed #165 + fixed in same commit.
Closes ROADMAP #165.
#163: run_turn_loop no longer injects f'{prompt} [turn N]' into follow-up
prompts. The suffix was never defined or interpreted anywhere — not by the
engine, not by the system prompt, not by any LLM. It looked like a real
user-typed annotation in the transcript and made replay/analysis fragile.
New behaviour:
- turn 0 submits the original prompt (unchanged)
- turn > 0 submits caller-supplied continuation_prompt if provided, else
the loop stops cleanly — no fabricated user turn
- added continuation_prompt: str | None = None parameter to run_turn_loop
- added --continuation-prompt CLI flag for claws scripting multi-turn loops
- zero '[turn' strings ever appear in mutable_messages or stdout now
Behaviour change for existing callers:
- Before: run_turn_loop(prompt, max_turns=3) submitted 3 turns
('prompt', 'prompt [turn 2]', 'prompt [turn 3]')
- After: run_turn_loop(prompt, max_turns=3) submits 1 turn ('prompt')
- To preserve old multi-turn behaviour, pass continuation_prompt='Continue.'
or any structured follow-up text
One existing timeout test (test_budget_is_cumulative_across_turns) updated
to pass continuation_prompt so the cumulative-budget contract is actually
exercised across turns instead of trivially satisfied by a one-turn loop.
#164 filed: addresses reviewer feedback on #161. The wall-clock timeout
bounds the caller-facing wait, but the underlying submit_message worker
thread keeps running and can mutate engine state after the timeout
TurnResult is returned. A cooperative cancel_event pattern is sketched in
the pinpoint; real asyncio.Task.cancel() support will come once provider
IO is async-native (larger refactor).
Tests (tests/test_run_turn_loop_continuation.py, 8 tests):
- TestNoTurnSuffixInjection (2): zero '[turn' strings in any submitted
prompt, both default and explicit-continuation paths
- TestContinuationDefaultStopsAfterTurnZero (2): default loops run exactly
one turn; engine.submit_message called exactly once despite max_turns=10
- TestExplicitContinuationBehaviour (2): turn 0 = original, turn N = continuation
verbatim; max_turns still respected
- TestCLIContinuationFlag (2): CLI default emits only '## Turn 1';
--continuation-prompt wires through to multi-turn behaviour
Full suite: 67/67 passing.
Closes ROADMAP #163. Files #164.
Previously, QueryEnginePort.submit_message() checked the token budget AFTER
appending the prompt to mutable_messages, transcript_store, and permission_denials,
and AFTER calling compact_messages_if_needed(). On overflow it set
stop_reason='max_budget_reached' but the overflow turn was already committed.
Any caller that persisted the session afterwards wrote the rejected prompt to
disk — the session was silently poisoned even though the TurnResult said the
turn never completed.
Fix:
- Restructure submit_message so the budget check early-returns BEFORE any
mutation of mutable_messages, transcript_store, permission_denials, or
total_usage.
- The returned TurnResult.usage reflects pre-call state (overflow never
advanced the usage counter).
- Normal (in-budget) path unchanged: mutation happens exactly once, at the
end, only on 'completed' results.
This closes the atomicity gap: submit_message is now either 'turn committed'
(stop_reason='completed') or 'turn rejected, state untouched'
(stop_reason in {'max_budget_reached', 'max_turns_reached'}). Callers can
safely retry with a fresh budget or a smaller prompt without worrying about
phantom committed turns from prior rejections.
Tests (tests/test_submit_message_budget.py, 10 tests):
- TestBudgetOverflowDoesNotMutate (5): mutable_messages / transcript /
permission_denials / total_usage / TurnResult.usage all pre-mutation after overflow
- TestOverflowPersistence (2): first-turn overflow persists empty session;
successful-turn-then-overflow persists only the successful turn
- TestEngineUsableAfterOverflow (2): subsequent in-budget call still works
with no residue; repeated overflows don't accumulate hidden state
- TestNormalPathStillCommits (1): regression guard — non-overflow path still
commits mutable_messages/transcript/usage as expected
Full suite: 59/59 passing, zero regression.
Blocker: none. Closes ROADMAP #162.
Previously, run_turn_loop was bounded only by max_turns (turn count). If
engine.submit_message stalled — slow provider, hung network, infinite
stream — the loop blocked indefinitely with no cancellation path. Claws
calling run_turn_loop in CI or orchestration had no reliable way to
enforce a deadline; the loop would hang until OS kill or human intervention.
Fix:
- Add timeout_seconds parameter to run_turn_loop (default None = legacy unbounded).
- When set, each submit_message call runs inside a ThreadPoolExecutor and is
bounded by the remaining wall-clock budget (total across all turns, not per-turn).
- On timeout, synthesize a TurnResult with stop_reason='timeout' carrying the
turn's prompt and routed matches so transcripts preserve orchestration context.
- Exhausted/negative budget short-circuits before calling submit_message.
- Legacy path (timeout_seconds=None) bypasses the executor entirely — zero
overhead for callers that don't opt in.
CLI:
- Added --timeout-seconds flag to 'turn-loop' command.
- Exit code 2 when the loop terminated on timeout (vs 0 for completed),
so shell scripts can distinguish 'done' from 'budget exhausted'.
Tests (tests/test_run_turn_loop_timeout.py, 6 tests):
- Legacy unbounded path unchanged (timeout_seconds=None never emits 'timeout')
- Hung submit_message aborted within budget (0.3s budget, 5s mock hang → exit <1.5s)
- Budget is cumulative across turns (0.6s budget, 0.4s per turn, not per-turn)
- timeout_seconds=0 short-circuits first turn without calling submit_message
- Negative timeout treated as exhausted (guard against caller bugs)
- Timeout TurnResult carries correct prompt, matches, UsageSummary shape
Full suite: 49/49 passing, zero regression.
Blocker: none. Closes ROADMAP #161.
- list_sessions(directory=None) -> list[str]: enumerate stored session IDs
- session_exists(session_id, directory=None) -> bool: check existence without FileNotFoundError
- delete_session(session_id, directory=None) -> bool: unlink a session file
- load_session now raises typed SessionNotFoundError (subclass of KeyError) instead of FileNotFoundError
- Claws can now manage session lifecycle without reaching past the module to glob filesystem
Closes ROADMAP #160. Acceptance: claw can call list_sessions(), session_exists(id), delete_session(id) without importing Path or knowing .port_sessions/<id>.json layout.
Constraint: ROADMAP-only dogfood follow-up for 01:30 nudge on rebuilt claw git_sha 52a909ce
Rejected: implementation change to doctor help; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus status help plain-text sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2911 with verified help JSON-format fallback gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw doctor --help --output-format json; timeout 8 ./rust/target/debug/claw status --help --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 01:00 nudge on rebuilt claw git_sha 74338dc6
Rejected: implementation change to status help; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus version JSON sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2907 with verified help JSON-format fallback gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw status --help --output-format json; timeout 8 ./rust/target/debug/claw version --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 00:30 nudge on rebuilt claw git_sha 8e24f304
Rejected: implementation change to session command dispatch; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded session list samples plus session help bounded probe also hung/no bytes
Scope-risk: narrow
Directive: Switch from memory command introspection to session command introspection clawability
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw session list --output-format json; timeout 8 ./rust/target/debug/claw session help --output-format json/killed after no bytes; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 00:00 nudge on rebuilt claw git_sha 19947545
Rejected: implementation change to memory command dispatch; request was one concrete follow-up if no backlog item
Confidence: high after bounded memory list samples plus memory help bounded sanity also hung
Scope-risk: narrow
Directive: Switch from plugin lifecycle repetition to memory command introspection clawability
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; timeout 8 ./rust/target/debug/claw memory list --output-format json samples; timeout 8 ./rust/target/debug/claw memory help --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 23:30 nudge on rebuilt claw git_sha 6f92e54d
Rejected: implementation change to plugin lifecycle uninstall; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus prompt list sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2897 with verified stderr-only JSON-mode gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins uninstall does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 23:00 nudge on rebuilt claw git_sha 5eb1d7d8
Rejected: implementation change to plugin lifecycle update; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus prompt list sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2894 with verified stderr-only JSON-mode gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins update does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 22:30 nudge on rebuilt claw git_sha 0f9e8915
Rejected: implementation change to plugin lifecycle mutation; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus prompt list sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2891 with verified stderr-only JSON-mode gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins disable does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 22:00 nudge on rebuilt claw git_sha ee44ff98
Rejected: implementation change to plugin lifecycle mutation; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus prompt list sanity check
Scope-risk: narrow
Directive: Keep supported lifecycle missing-target hang distinct from #348 list schema and #349 unsupported show action
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins enable does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 21:30 nudge on rebuilt claw git_sha a2a38df9
Rejected: implementation change to plugin action dispatch; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples
Scope-risk: narrow
Directive: Replaces invalid hang PR #2885 with verified unsupported-action classification gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins show does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 21:00 nudge on rebuilt claw git_sha cca6f682
Rejected: implementation change to plugin list serializer; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples
Scope-risk: narrow
Directive: Keep plugin inventory schema issue distinct from broad help JSON opacity
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins list --output-format json; ./rust/target/debug/claw plugins help --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 20:30 nudge on rebuilt claw git_sha ee41b266
Rejected: implementation change to MCP show status schema; request was one concrete follow-up if no backlog item
Confidence: high after bounded successful repro
Scope-risk: narrow
Directive: Replaces invalid hang/nondeterminism PRs with verified status contract gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; ./rust/target/debug/claw mcp show does-not-exist --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 20:00 nudge on rebuilt claw git_sha c6c01bea
Rejected: implementation change to native-agent detail dispatch; request was one concrete follow-up if no backlog item
Confidence: high
Scope-risk: narrow
Directive: Keep agent detail fallback distinct from #328/#329 native-agent source/schema issues; closed invalid hang hypotheses first
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; ./rust/target/debug/claw agents list --output-format json; ./rust/target/debug/claw agents show analyst --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 18:30 nudge on rebuilt claw git_sha a510f734
Rejected: implementation change to config slash dispatcher; request was one concrete follow-up if no backlog item
Confidence: high
Scope-risk: narrow
Directive: Keep /config section discovery issue distinct from #342 /commands and #343 /models correction issues
Tested: ./rust/target/debug/claw --resume latest /config help --output-format json; /config list; /config show; bare /config; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Constraint: ROADMAP-only dogfood follow-up for 16:00 nudge on rebuilt claw git_sha 58569131
Rejected: code change in the command dispatcher | request was specifically to add one ROADMAP.md-only item
Confidence: high
Scope-risk: narrow
Directive: Keep /tasks distinct from #340; this is unsupported command stub JSON, not session help
Tested: git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
Capture the dogfood evidence as a roadmap item so the stdout JSON error-envelope contract can be fixed and regression-tested later.\n\nConstraint: User requested exactly one ROADMAP.md-only item #340 from current origin/main.\nConfidence: high\nScope-risk: narrow\nTested: git diff --check; scripts/fmt.sh --check\nNot-tested: Runtime behavior unchanged; documentation-only roadmap entry.
Constraint: ROADMAP.md-only restore of lost #337 from PR #2852 / Jobdori dogfood evidence
Rejected: Renumbering adjacent items | preserving existing #338 and surrounding roadmap entries keeps history stable
Confidence: high
Scope-risk: narrow
Directive: Keep #337 before #338 and do not collapse the dirty-file detail requirement into the broader help/status backlog
Tested: git diff --check; scripts/fmt.sh --check
Not-tested: Product behavior changes; documentation-only change
Constraint: Respond to 14:30 dogfood nudge with one direct claw-code pinpoint.\nEvidence: rebuilt actual debug binary at git_sha 24ccb59b; compared top-level help --output-format json with resume-safe /help --output-format json.\nFinding: same help surface uses message in top-level JSON and text in slash/resume JSON.\nTested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; ./rust/target/debug/claw help --output-format json; ./rust/target/debug/claw --resume latest /help --output-format json; git diff --check; scripts/fmt.sh --check.\nNot-tested: full Rust suite; roadmap-only documentation change.
Constraint: Scope requested ROADMAP.md only with exactly one new #328 pinpoint from direct claw dogfood.\nRejected: Implementing the agents-help fix now | user requested roadmap-only evidence item.\nConfidence: high\nScope-risk: narrow\nDirective: Keep agent help source roots derived from the same loader registry as agents list; do not hand-maintain a divergent root list.\nTested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; ./rust/target/debug/claw version --output-format json; ./rust/target/debug/claw agents help --output-format json; ./rust/target/debug/claw agents --output-format json; git diff --check; scripts/fmt.sh --check\nNot-tested: Full Rust test suite; roadmap-only documentation change.
Constraint: Scope limited to ROADMAP.md and one new pinpoint #327 from actual rebuilt claw dogfood.
Rejected: Code fix in this branch | user requested roadmap-only filing.
Confidence: high
Scope-risk: narrow
Directive: Keep mcp help source lists derived from actual config discovery, not hard-coded partial docs.
Tested: ./rust/target/debug/claw version --output-format json; ./rust/target/debug/claw mcp --help; ./rust/target/debug/claw mcp help --output-format json; temp .claw.json mcp list proof; git diff --check; scripts/fmt.sh --check
Not-tested: Full Rust test suite, documentation-only change.
Document the dogfood gap where help JSON stays parseable but hides command metadata inside a prose message, so future implementation can expose machine-readable command, slash-command, and resume-safety fields.\n\nConstraint: user requested ROADMAP.md-only pinpoint for issue #325 from origin/main d607ff36.\nRejected: implementing the schema now | requested fix shape is roadmap documentation only.\nConfidence: high\nScope-risk: narrow\nDirective: keep message for humans while adding schema/versioned structured help metadata when implementing.\nTested: git diff --check; scripts/fmt.sh --check\nNot-tested: runtime CLI behavior unchanged by docs-only change
Constraint: Documentation-only follow-up from current main e7074f47 after PR #2838; edit scope limited to ROADMAP.md.\nRejected: Implementing provenance detection now | user requested roadmap entry only.\nConfidence: high\nScope-risk: narrow\nDirective: Future implementation should compare embedded build git_sha/build date to workspace HEAD/dirty state without leaking secrets.\nTested: git diff --check; scripts/fmt.sh --check\nNot-tested: Runtime provenance behavior; this commit only records the roadmap requirement.
Keep claw --help's resume-safe slash command summary aligned with the interactive command list by filtering STUB_COMMANDS and adding regression coverage.
Operator status previously treated any tmux pane in a workspace as equivalent to active work. The new classifier uses tmux pane command/path metadata as a soft signal, treats plain shells as idle, and adds dirty-worktree abandoned markers to status and session-list output for clawhip consumers.
Constraint: Keep issue #320 prototype minimal and additive without new dependencies
Rejected: Screen-scraping pane output | fragile and broader than needed for lifecycle classification
Confidence: high
Scope-risk: narrow
Tested: cargo test -p rusty-claude-cli
Tested: cargo check -p rusty-claude-cli
Not-tested: cargo clippy -p rusty-claude-cli --all-targets -- -D warnings is blocked by pre-existing commands crate clippy::unnecessary_wraps warnings
The formatting wrapper should remain safe when invoked through different current directories or shell contexts, so resolve the script directory before entering the Rust workspace and forwarding cargo fmt arguments.
Constraint: Wrapper must be runnable from repo root while forwarding flags like --check
Rejected: Leave relative dirname cd | less robust if invocation context changes
Confidence: high
Scope-risk: narrow
Tested: scripts/fmt.sh --check
Tested: git diff --check
The Rust crate layout expects formatting to run from the rust directory, so add a root-level wrapper that preserves the working command while forwarding user flags like --check. Documentation now points contributors at the wrapper instead of the misleading virtual-workspace manifest invocation.
Constraint: Root-level cargo fmt --manifest-path rust/Cargo.toml is misleading for this virtual workspace
Rejected: Document cd rust && cargo fmt directly | a root wrapper gives one stable repo-root command
Confidence: high
Scope-risk: narrow
Tested: scripts/fmt.sh --check
Tested: git diff --check
Run rustfmt from the Rust workspace so CI format checks pass without changing behavior.
Constraint: Scope is formatting-only across tracked Rust files
Confidence: high
Scope-risk: narrow
Tested: cd rust && cargo fmt --check
Tested: git diff --check
Reject empty --allowedTools inputs instead of treating them as an empty restriction, and surface status JSON metadata that distinguishes default unrestricted tools from flag-provided allow lists.
Confidence: high
Scope-risk: narrow
Tested: cargo test -p rusty-claude-cli rejects_empty_allowed_tools_flag -- --nocapture
Tested: cargo test -p tools allowed_tools_rejects_empty_token_lists -- --nocapture
Tested: cargo check -p rusty-claude-cli -p tools
Tested: cargo test -p rusty-claude-cli -p tools
Not-tested: full workspace cargo fmt --check is blocked by pre-existing unrelated formatting drift
Worker boot could previously stall on an interactive MCP/tool permission prompt while readiness and startup-timeout surfaces only had generic idle/no-evidence shapes. This adds a first-class blocked lifecycle state, structured event payload, startup evidence fields, and regression coverage so callers can report the exact server/tool gate instead of pane-scraping.
Constraint: ROADMAP #200 requires tool/server identity, prompt age, and session-only versus always-allow capability in status/evidence surfaces
Rejected: Treat MCP/tool prompts as trust gates | conflates distinct prompts and loses tool identity
Rejected: Leave allow-scope as pane text only | clawhip still could not classify the blocker without scraping
Confidence: high
Scope-risk: moderate
Directive: Keep tool_permission_required distinct from trust_required; downstream claws rely on server/tool payload plus allow-scope metadata
Tested: cargo test -p runtime tool_permission
Tested: cargo fmt -p runtime -- --check && cargo clippy -p runtime --all-targets -- -D warnings && cargo test -p runtime
Tested: cargo test --workspace
Not-tested: live interactive MCP permission prompt in tmux
The pull brought the branch current with origin/main while replaying local follow-up work. Conflict resolution kept the roadmap/progress additions and integrated the runtime event/trust changes with upstream's newer surfaces.
The trust allowlist now treats worktree_pattern as an additional required predicate, including the missing-worktree case, so auto-trust cannot fall back to cwd-only matching when a worktree constraint was declared. The runtime formatting cleanup keeps clippy/fmt green after the merge.
Constraint: Local branch was 109 commits behind origin/main with dirty tracked follow-up work.
Rejected: Drop the autostash after conflict resolution | keeping it preserves a reversible safety backup for unrelated recovery.
Confidence: high
Scope-risk: moderate
Directive: Do not relax worktree_pattern matching without preserving the missing-worktree regression.
Tested: git diff --cached --check; cargo fmt -p runtime -- --check; cargo clippy -p runtime --all-targets -- -D warnings; cargo test -p runtime; cargo test --workspace; architect verification approved
Not-tested: Live tmux/worker auto-trust behavior outside unit/integration tests
2026-04-27 09:05:50 +00:00
48 changed files with 7984 additions and 1002 deletions
All notable changes to claw-code are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (currently pre-1.0).
## [Unreleased] — 2026-04-26 to 2026-04-27 (extended dogfood audit cycles, through #433)
- Turn Result fields (including cancel_observed as of #164 Stage B)
> **Important:** SCHEMAS.md describes the **v2.0 target envelope**, not the current v1.0 binary behavior. The binary does NOT currently emit `timestamp`, `command`, `exit_code`, `output_format`, or `schema_version` fields. See [`FIX_LOCUS_164.md`](./FIX_LOCUS_164.md) for the migration plan (Phase 1: dual-mode flag; Phase 2: default bump; Phase 3: deprecation).
3.Use common fields (timestamp, command, exit_code, output_format, schema_version)
4.Exit 0 on success, 1 on error/not-found, 2 on timeout
2. Return JSON envelopes (current v1.0: flat shape with top-level `kind`; target v2.0: nested with common fields per SCHEMAS.md)
3.**v1.0 (current):** Emit flat top-level fields: verb-specific data + `kind` (verb identity for success, error classification for errors)
4.**v2.0 (target, post-FIX_LOCUS_164):** Use common wrapper fields (timestamp, command, exit_code, output_format, schema_version) with nested `data` or `error` objects
5. Exit 0 on success, 1 on error/not-found, 2 on timeout
**Migration note:** The Python reference harness in `src/` was written against the v2.0 target schema (SCHEMAS.md). The Rust binary in `rust/` currently emits v1.0 (flat). See [`FIX_LOCUS_164.md`](./FIX_LOCUS_164.md) for the full migration plan and timeline.
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at GitHub Security Advisories or email to the maintainers listed in SECURITY.md. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations).
Thanks for your interest. This project follows the **gaebal-gajae pinpoint cadence** — see [ROADMAP.md](./ROADMAP.md) for the current pinpoint census. Here's how to contribute effectively.
## Security
For security vulnerabilities, see [SECURITY.md](./SECURITY.md). **Do not file public pinpoints for security issues.**
## Filing a ROADMAP Pinpoint
All feature requests and bug reports go through the pinpoint format (see `ROADMAP.md`). Each pinpoint must have:
- **Exact pinpoint** — one crisp sentence stating what is wrong or missing
- **Live evidence** — reproduction steps, logs, or observed behavior
- **Why distinct** — why this isn't already covered by an existing pinpoint
- **Concrete delta** — what the repo looks like after this is fixed (file-level)
**Status:** Frozen (feature-complete), ready for review + merge
---
## One-Liner (reviewer-ready)
> **Phase 0 is now frozen, reviewer-mapped, and merge-ready; Phase 1 remains intentionally deferred behind the locked priority order.**
This is the single sentence that captures branch state. Use it in PR titles, review summaries, and Phase 1 handoff notes.
---
## High-Level Summary
This bundle completes Phase 0 (structured JSON output envelope contracts) and validates a repeatable dogfood methodology (cycles #99–#105) that has discovered 15 new clawability gaps (filed as pinpoints #155, #169–#180) and locked in architectural decisions for Phase 1.
**Key property:** The bundle is *dependency-clean*. Every commit can be reviewed independently. No commit depends on uncommitted follow-up. The freeze holds: no code changes will land on this branch after merge.
---
## Why Review This Now
### What lands when this merges:
1.**Phase 0 guarantees** (4 commits) — JSON output envelopes now follow `SCHEMAS.md` contracts. Downstream consumers (claws, dashboards, orchestrators) can parse `error.kind`, `error.operation`, `error.target`, `error.hint` as first-class fields instead of scraping prose.
2.**Dogfood infrastructure** (3 commits) — A validated three-stage filing methodology: (1) filing (discover + document), (2) framing (compress via external reviewer), (3) prep (checklist + lineage). Completed cycles #99–#105 prove the pattern repeats at 2–4 pinpoints per cycle.
3.**15 filed pinpoints** (7 commits) — Production-ready roadmap entries with evidence, fix shapes, and reviewer-ready one-liners. No implementation code, pure documentation. These unblock Phase 1 branch creation.
4.**Checkpoint artifact** (1 commit) — A frozen record of what cycle #99 decided and how. Audit trail for multi-cycle work.
### What does NOT land:
- No implementation of any filed pinpoint (#155–#186). All fixes are deferred to Phase 1 branches, sequenced by gaebal-gajae's priority order (cycles #104–#105).
- No schema changes. SCHEMAS.md is frozen at the contract that Phase 0 guarantees.
- No new dependencies. Cargo.toml is unchanged from the base branch.
---
## Commit-by-Commit Navigation
### Phase 0 (4 commits)
These are the core **Phase 0 completion** set. Each one is a self-contained capability unlock.
1.**`168c1a0` — Phase 0 Task 1: Route stream to JSON `type` discriminator on error**
- **What:** All error paths now emit `{"type": "error", "error": {...}}` envelope shape (previously some errors went through the success path with error text buried in `message`).
- **Why it matters:** Downstream claws can now reliably check `if response.type == "error"` instead of parsing prose.
- **Review focus:** Diff routing in `emit_error_response()` and friends. Verify every error exit path hits the JSON discriminator.
- **What:** When a text-mode user sees `{"error": ...}` escape into their terminal unexpectedly, they get a `SCHEMAS.md` violation warning + hint. Prevents silent envelope shape drift.
- **Why it matters:** Text-mode users are first-class. JSON contract violations are visible + auditable.
- **Review focus:** The `silent_emit_guard()` wrapper and its condition. Verify it gates all JSON output paths.
- **What:** Adds golden-fixture test `schemas_contract_holds_on_static_verbs` that asserts every verb's JSON shape matches SCHEMAS.md as of this commit. Future drifts are caught.
- **Why it matters:** Schema is now truth-testable, not aspirational.
- **Review focus:** The fixture names and which verbs are covered. Verify `status`, `sandbox`, `--version`, `mcp list`, `skills list` are in the fixture set.
- **What:** New test `error_kind_and_error_field_presence_are_gated_together` asserts that if `type: "error"` is present, both `error` field and `error.kind` are always populated (no partial shapes).
- **Why it matters:** Downstream consumers can rely on shape consistency. No more "sometimes error.kind is missing" surprises.
- **Review focus:** The parity assertion logic. Verify it covers all error-emission sites.
- **What:** Documents the three-stage filing discipline that cycles #99–#105 will use (filing → framing → prep). Locks the "5-axis density rule" (freeze when a branch spans 5+ axes).
- **Why it matters:** Audit trail. Future cycles know what #99 decided.
- **Review focus:** The decision rationale in ROADMAP.md. Is the freeze doctrine sound for your project?
- **What:** Gaebal-gajae provides surgical one-liners for #181–#183, plus insights (agents is the reference implementation for #183 canonical shape).
- **Why it matters:** Framings now survive reader compression. Reviewers can understand the issue in 1 sentence + 1 justification.
- **Review focus:** The rewritten framings. Do they improve on the original verbose descriptions?
8.**`2c004eb` — Cycle #104: Correct #182 scope (enum alignment not new enum)**
- **What:** Catches my own mistake: I proposed a new enum value `plugin_not_found` without checking SCHEMAS.md. Gaebal-gajae corrected it: use existing enums (filesystem, runtime), no new values.
- **Why it matters:** Demonstrates the doctrine correction loop. Catch regressions early.
- **Review focus:** The scope correction logic. Do you agree with "existing contract alignment > new enum"?
- **What:** More corrections from gaebal-gajae: #184/#185 belong to #171 lineage (not new family), #186 to #169/#170 lineage. Agents is the reference for #183 fix.
- **Why it matters:** Family tree hygiene. Each pinpoint sits in the right narrative arc.
- **Review focus:** The family tree reorganization. Is the new structure clearer?
- **CI status:** Ready (no CI jobs run until merge)
---
## Integration Notes
### What the main branch gains:
-`SCHEMAS.md` now has a regression lock. Future commits that drift the shape are caught.
- Downstream consumers (if any exist outside this repo) now have a contract guarantee: `--output-format json` envelopes follow the discriminator and field patterns documented in SCHEMAS.md.
- If someone lands a fix for #155, #169, #170, #171, etc. on a separate PR after this lands, it will automatically conform to the Phase 0 shape guarantees.
### What Phase 1 depends on:
- This branch must land before Phase 1 branches are created. Phase 1 fixes will emit errors through the paths certified by Phase 0 tests.
- Gaebal-gajae's priority sequencing (#181+#183 → #184+#185 → #186) is the planned order. Follow it when planning Phase 1 PRs.
- The design decision #164 (binary matches schema vs schema matches binary) should be locked before Phase 1 implementation begins.
### What is explicitly deferred:
- **Implementation of any pinpoint.** Only documentation and test coverage.
- **Schema additions.** All filed work uses existing enum values.
- **New dependencies.** Cargo.toml is unchanged.
- **Database/persistence.** Session/state handling is unchanged.
---
## Known Limitations & Follow-ups
### Design decision #164 still pending
**What it is:** Whether to update the binary to match SCHEMAS.md (Option A) or update SCHEMAS.md to match the binary (Option B).
**Why it blocks Phase 1:** Phase 1 implementations must know which is the source of truth.
**Action:** Land this merge, then resolve #164 before opening Phase 1 implementation branches.
### Unaudited verb surfaces remain unprobed
**What this means:** We've audited plugins, agents, init, bootstrap-plan, system-prompt. Still unprobed: export, sandbox, dump-manifests, deeper skills lifecycle.
**Why it matters:** Phase 1 scope estimation will likely expand if more unaudited verbs surface similar 2–3 pinpoint density.
**Action:** Cycles #106+ will continue probing unaudited surfaces. Phase 1 sequence adjusts if new families emerge.
---
## Reviewer Checkpoints
**Before approving:**
1. ✅ Do the Phase 0 commits actually deliver what they claim? (Test coverage, routing changes, guard logic)
2. ✅ Is the SCHEMAS.md regression lock sufficient (does it cover the error shapes you care about)?
3. ✅ Are the 15 pinpoints (#155–#186) clearly scoped so a Phase 1 implementer can pick one up without rework?
4. ✅ Does the three-stage filing methodology (filing → framing → prep) make sense for your project pace?
5. ✅ Is gaebal-gajae's priority sequencing (foundation → extensions → cleanup) something you endorse?
**Before squashing/fast-forwarding:**
1. ✅ No outstanding merge conflicts with main
2. ✅ All 227 tests pass on main (not just this branch)
3. ✅ No style drift (fmt + clippy clean)
**After merge:**
1. ✅ Tag the merge commit as `phase-0-complete` for easy reference
2. ✅ Update the issue/PR #164 status to "awaiting decision before Phase 1 kickoff"
- **For leadership:** Is the Phase 0 shape guarantee (error.kind + error.operation + error.target + error.hint always together) a contract we want to support for 2+ major versions?
- **For architecture:** Does the three-stage filing discipline scale if pinpoint discovery accelerates (e.g. 10+ new gaps per cycle)?
- **For product:** Should the SCHEMAS.md version be bumped to 2.1 after Phase 0 lands to signal the new guarantees?
---
## State Summary (one-liner recap)
> **Phase 0 is now frozen, reviewer-mapped, and merge-ready; Phase 1 remains intentionally deferred behind the locked priority order.**
---
**Branch ready for review. Awaiting approval + merge signal.**
For reference, the target JSON error envelope shape (SCHEMAS.md, v2.0):
```json
{
@@ -466,7 +489,7 @@ For reference, the canonical JSON error envelope shape (SCHEMAS.md):
"command":"load-session",
"exit_code":1,
"output_format":"json",
"schema_version":"1.0",
"schema_version":"2.0",
"error":{
"kind":"session_not_found",
"operation":"session_store.load_session",
@@ -478,7 +501,7 @@ For reference, the canonical JSON error envelope shape (SCHEMAS.md):
}
```
All commands that emit errors follow this shape (with error.kind varying). See `SCHEMAS.md` for the complete contract.
**This is the target schema after [`FIX_LOCUS_164`](./FIX_LOCUS_164.md) is implemented.** The migration plan includes a dual-mode `--envelope-version=2.0` flag in Phase 1, default version bump in Phase 2, and deprecation in Phase 3. For now, code against v1.0 (Appendix A).
Cycles #410–#459 conclude a 16+ hour extended discovery audit that filed **63 pinpoints** (#241–#312) across **8 primary axes**, shipped **23 artifacts** (docs, meta-fixes, implementation kickoffs, and parity verification), and produced a complete parity matrix against `anomalyco/opencode`. All major architectural gaps are documented with acceptance criteria and sequenced into a 6-phase implementation roadmap (estimated 22–39 cycles). Discovery is **saturated**; continued cycling yields noise, not signal. The branch is **merge-eligible** pending three Phase 0 blockers. This document is the handoff from discovery to execution.
## 0. CRITICAL UPDATE (Cycle #85 via #168 Evidence)
**Premise revision:** This locus document originally framed the problem as **"v1.0 (incoherent) → v2.0 (target schema)"** migration. **Fresh-dogfood validation in cycle #84 proved this framing was underspecified.**
**Actual problem (evidence from #168):**
- There is **no coherent v1.0 envelope contract**. Each verb has a bespoke JSON shape.
-`claw list-sessions --output-format json` emits `{command, sessions}` — has `command` field
-`claw doctor --output-format json` emits `{checks, kind, message, ...}` — no `command` field
- Each verb renderer was written independently with no coordinating contract
**Revised migration plan — three phases instead of two:**
1.**Phase 0 (Emergency):** Fix silent failures (#168 bootstrap JSON). Every `--output-format json` command must emit valid JSON.
2.**Phase 1 (v1.5 Baseline):** Establish minimal JSON invariants across all 14 verbs without breaking existing consumers:
- Every command emits valid JSON when `--output-format json` is passed
- Every command has a top-level `kind` field identifying the verb
- Every error envelope follows the confirmed `{error, hint, kind, type}` shape
- Every success envelope has the verb name in a predictable location
- **Effort:** ~3 dev-days (no new design, just fill gaps and normalize bugs)
3.**Phase 2 (v2.0 Wrapped Envelope):** Execute the original Phase 1 plan documented below — common metadata wrapper, nested data/error objects, opt-in via `--envelope-version=2.0`.
4.**Phase 3 (v2.0 Default):** Original Phase 2 plan below.
5.**Phase 4 (v1.0/v1.5 Deprecation):** Original Phase 3 plan below.
**Why add Phase 0 + Phase 1 (v1.5)?**
- You can't migrate from "incoherent" to "coherent v2.0" in one jump. Intermediate coherence (v1.5 baseline) is required.
- Consumer code built against "whatever v1 emits today" needs a stable target to transition from.
- **Silent failures (bootstrap JSON) must be fixed BEFORE any migration** — otherwise consumers have no way to detect breakage.
**Blocker resolved:** The original blocker "v1.0 design vs v2.0 design" is actually "no v1 design exists; let's make one (v1.5) then migrate." This is a **clearer, lower-risk migration path**.
**Revised effort estimate:** ~9 dev-days total (Phase 0: 1 day + Phase 1/v1.5: 3 days + Phase 2/v2.0: 5 days) instead of ~6 dev-days for a direct v1.0→v2.0 migration (which would have failed given the incoherent baseline).
**Doctrine implication:** Cycles #76–#82 diagnosed "aspirational vs current" correctly but missed that "current" was never a single thing. Cycle #84 fresh-dogfood caught this. **Fresh-dogfood discipline (principle #9) prevented a 6-day migration effort from hitting an unsolvable baseline problem.**
---
## 1. Scope — What This Migration Affects
**Every JSON-emitting verb.** Audit across the 14 documented verbs:
| Verb | Current top-level keys | Schema-conformant? |
1.**Schema parity:** Every `--output-format json` command emits v2.0 envelope shape exactly per SCHEMAS.md
2.**Success/error symmetry:** Success envelopes have `data` field; error envelopes have `error` object; never both
3.**kind semantic unification:**`data.kind` = verb identity (when present); `error.kind` = enum from §4.44. No overloading.
4.**Common metadata:**`timestamp`, `command`, `exit_code`, `output_format`, `schema_version` present in ALL envelopes
5.**Dual-mode support:**`--envelope-version=1|2` flag allows opt-in/opt-out during migration
6.**Tests:** Per-verb golden test fixtures for both v1.0 and v2.0 envelopes
7.**Documentation:** SCHEMAS.md documents both versions with deprecation timeline
---
## 6. Risks
### 6a. Breaking Change Risk
Phase 2 (default version bump) WILL break consumers that depend on flat-shape envelope. Mitigations:
- Dual-mode flag allows opt-in testing before default change
- Long grace period (Phase 3 deprecation ~6 months post-Phase 2)
- Clear migration guide + example consumer code
### 6b. Implementation Risk
14 verbs to migrate. Each verb has its own success shape (`checks`, `agents`, `phases`, etc.). Payload structure stays the same; only the wrapper changes. Mechanical but high-volume.
**Mitigation:** Start with doctor, status, version as pilot. If pattern works, batch remaining 11.
### 6c. Error Classification Remapping Risk
Changing `kind: "cli_parse"` to `error.kind: "parse"` is a breaking change even within the error envelope. Consumers doing `response["kind"] == "cli_parse"` will break.
**Mitigation:** Document explicitly in migration guide. Provide sed script if needed.
**Purpose:** Streamline merging of the 17 review-ready branches by grouping them into safe clusters and providing per-cluster merge order + validation steps.
**Batch strategy:** Merge by cluster, not individual branches. Each cluster shares the same fix pattern, so reviewers can validate one cluster and merge all members together.
**Estimated throughput:** 2-3 clusters per merge session. At current cycle velocity (~1 cluster per 15 min), full queue → merged main in ~2 hours.
- Run: `cargo test -p rusty-claude-cli` (should pass 181 tests)
**Commit strategy:** Rebase all three, squash into single "typed-error: thread kind+hint through 3 families" commit, OR merge individually preserving commit history for bisect clarity.
- [ ]#122 and #122b are binary-level changes, #161 is build-system change
- [ ] All three pass `cargo build`
- [ ] No cross-crate merge conflicts
**Why these three together:** All share the diagnostic-strictness principle. #122 and #122b extend `doctor`, #161 fixes `version`. Merging as a cluster signals the principle to future reviewers.
**Post-merge validation:**
- Rebuild binary
- Run: `claw doctor` (should now check stale-base + broad-cwd)
- Run: `claw version` (should report correct SHA even in worktrees)
- Run: `cargo test` (full suite)
**Commit strategy:** Merge individually preserving history, then add ROADMAP commit explaining the cluster principle. This makes the doctrine visible in git log.
- [ ] All four branches edit help-topic routing in the same regions
- [ ] Verify no merge conflicts (should be sequential, non-overlapping edits)
- [ ]`cargo build` passes
**Why these four together:** All address help-parity (verbs in `--help` → correct help topics). This cluster is the most "batch-like" — identical fix pattern repeated.
**Post-merge validation:**
- Rebuild binary
- Run: `claw diff --help` (should route to help topic, not crash)
- Run: `claw config --help` (ditto)
- Run: `claw --help` (should list all verbs)
**Merge strategy:** Can be fast-forwarded or squashed as a unit since they're all the same pattern.
**Branch-last protocol validation:** All 17 branches here represent work that was:
1. Pinpoint filed (with repro + fix shape)
2. Implemented in scratch/worktree (not directly on main)
3. Verified to build + pass tests
4. Only then branched for review
This artifact provides the final step: **validated merge order + per-cluster risks.**
**Integration-support artifact:** This checklist reduces reviewer cognitive load by pre-answering "which merge order is safest?" and "what could go wrong?" questions.
- Canonical document: this top-level `PARITY.md` is the file consumed by `rust/scripts/run_mock_parity_diff.py`.
- Requested 9-lane checkpoint: **All 9 lanes merged on `main`.**
- Current `main` HEAD: `ee31e00` (stub implementations replaced with real AskUserQuestion + RemoteTrigger).
- Repository stats at this checkpoint: **292 commits on `main` / 293 across all branches**, **9 crates**, **48,599 tracked Rust LOC**, **2,568 test LOC**, **3 authors**, date range **2026-03-31 → 2026-04-03**.
- Current `main` HEAD: `ad1cf92` (doctrine loop canonical example).
- Repository stats at this checkpoint: **979 commits on `main`**, **9 crates**, **80,789 tracked Rust LOC**, **4,533 test LOC**, **3 authors**, date **2026-04-23**.
- **Growth since last PARITY update (2026-04-03):** Rust LOC +66% (48,599 → 80,789), Test LOC +76% (2,568 → 4,533), Commits +235% (292 → 979). Current phase: 13 branches awaiting review/integration.
**Why it's Priority 2:** Extensions. Guard clauses on existing envelope shape. Uses envelope from Priority 1.
**Implementation:** Add trailing-args rejection to `init` and unknown-flag rejection to `bootstrap-plan`. Pattern: match existing guard in #171 (extra-args classifier).
**Risk profile:** MEDIUM (adds guards, no shape changes)
@@ -34,14 +36,37 @@ Claw Code is the public Rust implementation of the `claw` CLI agent harness.
The canonical implementation lives in [`rust/`](./rust), and the current source of truth for this repository is **ultraworkers/claw-code**.
> [!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.
> 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, see [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) for a high-level crate/subsystem map, see [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md) for env vars and settings, 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`.
> **New users:** start with [`USAGE.md`](./USAGE.md) → run `claw doctor` → check [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md) for settings → [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md) if stuck.
## Current repository shape
- **`rust/`** — canonical Rust workspace and the `claw` CLI binary
- **`USAGE.md`** — task-oriented usage guide for the current product surface
- **`docs/`** — full documentation suite (configuration, architecture, API reference, providers, container workflow)
- **`ERROR_HANDLING.md`** — unified error-handling pattern for orchestration code
- **`PARITY.md`** — Rust-port parity status and migration notes
- **`ROADMAP.md`** — active roadmap and cleanup backlog
@@ -196,6 +221,7 @@ cargo test --workspace
- [`PARITY.md`](./PARITY.md) — parity status for the Rust port
This is an integration support artifact (per cycle #64 doctrine). Its purpose: let reviewers see all queued branches, cluster membership, and merge priorities without re-deriving from git log.
This document locks the field-level contract for all clawable-surface commands. Every command accepting `--output-format json` must conform to the envelope shapes below.
> **⚠️ CRITICAL: This document describes the TARGET v2.0 envelope schema, not the current v1.0 binary behavior.** The Rust binary currently emits a **flat v1.0 envelope** that does NOT include `timestamp`, `command`, `exit_code`, `output_format`, or `schema_version` fields. See [`FIX_LOCUS_164.md`](./FIX_LOCUS_164.md) for the full migration plan and timeline. **Do not build automation against the field shapes below without first testing against the actual binary output.** Use `claw <command> --output-format json` to inspect what your binary version actually emits.
**Target audience:** Claws building orchestrators, automation, or monitoring against claw-code's JSON output.
This document locks the **target** field-level contract for all clawable-surface commands. After the v1.0→v2.0 migration (FIX_LOCUS_164 Phase 2), every command accepting `--output-format json` will conform to the envelope shapes documented here.
**Current v1.0 reality:** See [`ERROR_HANDLING.md`](./ERROR_HANDLING.md) Appendix A for the flat envelope shape the binary actually emits today.
---
## Common Fields (All Envelopes)
## Common Fields (All Envelopes) — TARGET v2.0 SCHEMA
Every command response, success or error, carries:
**This section describes the v2.0 target schema. The current v1.0 binary does NOT emit these fields.** See FIX_LOCUS_164.md for the migration timeline.
After v2.0 migration, every command response, success or error, will carry:
```json
{
@@ -16,7 +22,7 @@ Every command response, success or error, carries:
"command":"list-sessions",
"exit_code":0,
"output_format":"json",
"schema_version":"1.0"
"schema_version":"2.0"
}
```
@@ -107,6 +113,24 @@ When an entity does not exist (exit code 1, but not a failure):
@@ -128,8 +152,25 @@ When an entity does not exist (exit code 1, but not a failure):
}
```
**Gap**: Current impl lacks `timestamp`, `exit_code`, `output_format`, `schema_version`, `directory`, `sessions_count` (derivable), and the session object uses `id`/`updated_at_ms`/`message_count` instead of `session_id`/`last_modified`/`prompt_count`. Follow-up #250 Option B to align field names and add common-envelope fields.
### `delete-session`
**Status**: ⚠️ Stub only (closed #251 dispatch-order fix; full impl deferred).
**Actual binary envelope** (as of #251 fix):
```json
{
"type":"error",
"command":"delete-session",
"error":"not_yet_implemented",
"kind":"not_yet_implemented"
}
```
Exit code: 1. No credentials required. The stub ensures the verb does NOT fall through to Prompt/auth (the #251 fix), but the actual delete operation is not yet wired.
**Aspirational (future) shape**:
```json
{
"timestamp":"2026-04-22T10:10:00Z",
@@ -143,6 +184,31 @@ When an entity does not exist (exit code 1, but not a failure):
For nonexistent sessions, emits a local `session_not_found` error (NOT `missing_credentials`):
```json
{
"error":"session not found: nonexistent",
"kind":"session_not_found",
"type":"error",
"hint":"Hint: managed sessions live in .claw/sessions/<hash>/ ..."
}
```
**Aspirational (future) shape**:
```json
{
"timestamp":"2026-04-22T10:10:00Z",
@@ -155,8 +221,25 @@ When an entity does not exist (exit code 1, but not a failure):
}
```
**Gap**: Current impl uses nested `session: {...}` instead of flat fields, and omits common-envelope fields. Follow-up #250 Option B to align.
### `flush-transcript`
**Status**: ⚠️ Stub only (closed #251 dispatch-order fix; full impl deferred).
**Actual binary envelope** (as of #251 fix):
```json
{
"type":"error",
"command":"flush-transcript",
"error":"not_yet_implemented",
"kind":"not_yet_implemented"
}
```
Exit code: 1. No credentials required. Like `delete-session`, this stub resolves the #251 dispatch-order bug but the actual flush operation is not yet wired.
**Aspirational (future) shape**:
```json
{
"timestamp":"2026-04-22T10:10:00Z",
@@ -375,3 +458,251 @@ cargo test --release test_json_envelope_field_consistency
-`show-command` reports `found: bool` (inventory signal: "does this exist?")
-`exec-command` reports `handled: bool` (operational signal: "was this work performed?")
- The names matter: a command can be found but not handled (e.g. too large for context window), or handled silently (no output message)
---
## Appendix: Current v1.0 vs. Target v2.0 Envelope Shapes
### ⚠️ IMPORTANT: Binary Reality vs. This Document
**This entire SCHEMAS.md document describes the TARGET v2.0 schema.** The actual Rust binary currently emits v1.0 (flat) envelopes.
**Do not assume the fields documented above are in the binary right now.** They are not.
### Current v1.0 Envelope (What the Rust Binary Actually Emits)
The Rust binary in `rust/` currently emits a **flat v1.0 envelope** without common metadata wrapper:
error_msg=envelope.get("error","unknown error")# error is a STRING
error_kind=envelope.get("kind")# kind is at TOP LEVEL
print(f"Error: {error_kind} — {error_msg}")
else:
# Success path: verb-specific fields at top level
sessions=envelope.get("sessions",[])
forsessioninsessions:
print(f"Session: {session['id']}")
```
**After v2.0 migration, this code will break.** Claws building for v2.0 compatibility should:
1. Check `schema_version` field
2. Parse differently based on version
3. Or wait until Phase 2 default bump is announced, then migrate
### Why This Mismatch Exists
SCHEMAS.md was written as the **target design** for v2.0. The Rust binary is still on v1.0. The migration (FIX_LOCUS_164) will bring the binary in line with this schema, but it hasn't happened yet.
**This mismatch is the root cause of doc-truthfulness issues #78, #79, #165.** All three docs were documenting the v2.0 target as if it were current reality.
### Questions?
- **"Is v2.0 implemented?"** No. The binary is v1.0. See FIX_LOCUS_164.md for the implementation roadmap.
- **"Should I build against v2.0 schema?"** No. Build against v1.0 (current). Test your code with `claw` to verify.
- **"When does v2.0 ship?"** See FIX_LOCUS_164.md Phase 1 estimate: ~6 dev-days. Not scheduled yet.
- **"Can I use v2.0 now?"** Only if you explicitly pass `--envelope-version=2.0` (which doesn't exist yet in v1.0 binary).
**Status:** 📸 Snapshot of actual binary behavior as of cycle #91 (2026-04-23). Anchored by controlled matrix `/tmp/cycle87-audit/matrix.json` + Phase 0 tests in `output_format_contract.rs`.
### Purpose
This section documents **what each verb actually emits under `--output-format json`** as of the v1.5 emission baseline (post-cycle #89 emission routing fix, pre-Phase 1 shape normalization).
This is a **reference artifact**, not a target schema. It describes the reality that:
1.`--output-format json` exists and emits JSON (enforced by Phase 0 Task 2)
2. All output goes to stdout (enforced by #168c fix, cycle #89)
3. Each verb has a bespoke top-level shape (documented below; to be normalized in Phase 1)
This project is pre-1.0 / active development. Only the `main` branch (and the current active feature branch) receives security attention. No LTS commitment exists yet.
| Branch | Supported |
|--------|-----------|
| `main` | ✅ |
| older forks/branches | ❌ |
## Reporting a Vulnerability
**Do not file a public GitHub issue for security vulnerabilities.**
Please use [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) to report privately:
1. Go to the **Security** tab of this repository
2. Click **"Report a vulnerability"**
3. Describe the issue with reproduction steps and impact
We aim to acknowledge within **72 hours** and work toward coordinated disclosure.
## Disclosure Process
1. Report received → acknowledgement within 72h
2. We assess severity and reproduce the issue
3. Fix developed and reviewed privately
4. Fix shipped; advisory published after patch is live
5. Credit given to reporter (unless they prefer anonymity)
**Symptom:** claw-code exits with `context_window_blocked` or similar provider error when resuming a long session, or when sending a request with a very large prompt + accumulated history.
**Root cause:** Session size exceeded provider context window before claw-code's auto-compaction could reduce it. Auto-compaction is currently REACTIVE-AFTER-SUCCESS — it only fires after a successful provider response. If the request itself is oversized, compaction never runs.
**Mitigation:**
1.**Resume with manual compact:**`claw resume <session> --compact-before` (if available); else manually compact via `/compact` slash command before retrying
2.**Start a fresh session:** Sometimes the cleanest path; existing session-state preserved in `~/.claw/sessions/<id>/`
3.**Reduce prompt size:** If interactive, send shorter prompts; truncate file contents before pasting
4.**Adjust threshold:** Lower `CLAW_AUTO_COMPACT_INPUT_TOKENS_THRESHOLD` env var (default varies by provider)
**Symptom:** You run `/compact` to manually compact a session, but it reports `session below compaction threshold` even though the session feels large.
**Root cause:** The "below threshold" message is currently a catch-all for multiple skip reasons:
- Too few compactable messages
- Already compacted (only summary remains)
- Compactable tokens below threshold
- Tool-use/tool-result boundary preserved
- Live vs resume threshold divergence
**Mitigation:**
1.**Check session state:**`claw session info <id>` to inspect message count, total tokens
2.**Force compaction:** Currently no `--force` flag exists; track #289 for typed skip-reason discriminants
3.**Workaround:** Continue session and let auto-compact fire after next provider response (when reactive-after-success path is available)
**Symptom:** A parallel agent lane shows `status: running` indefinitely, never transitioning to `completed` or `error`. Downstream coordination treats it as still-working.
**Root cause:**`Agent::execute_agent` writes a `running` manifest BEFORE spawning a detached `std::thread::spawn`. The `JoinHandle` is dropped. If the process crashes during agent execution, the manifest stays as `running` forever (zombie state). No heartbeat or stale-reaper exists.
**Mitigation:**
1.**Manual cleanup:** Inspect `~/.claw/agents/<lane>/` and remove stale `manifest.json` files where last-modified > N minutes ago
**Symptom:** Same upstream provider error (e.g., `500 empty_stream: upstream stream closed before first payload`) repeats 5+ times in <60 minutes. Retries hit the same dead upstream blindly.
**Root cause:** claw-code does NOT detect repeat-failure patterns. No circuit-breaker. No automatic provider-fallback when configured. Each retry attempts the same provider+endpoint regardless of recent failure history.
**Mitigation:**
1.**Manual circuit-breaker:** Wait 5-10 minutes after repeated failures before retrying
2.**Switch provider:** If you have multiple providers configured (`ANTHROPIC_API_KEY` + `OPENAI_API_KEY`), restart with different model prefix (e.g., `gpt-4` instead of `claude-`)
3.**Check provider status pages:** status.anthropic.com, status.openai.com
4.**Verify upstream endpoint:** If using a proxy (CCAPI, custom OpenAI-compatible endpoint), check proxy logs
**Related pinpoints:**#291 (no repeat-failure detection / circuit-breaker), #285 (declarative providers config for fallback), #290 (stream-init failure envelope)
---
## Other common failures
*[placeholder for future sections: tool-use failures, session corruption]*
# Should run health check (shows which components are initialized)
claw doctor
# Should show available commands
claw --help
```
If `claw: command not found`, the PATH addition didn't take. Re-check:
```bash
echo$PATH# verify your PATH directory is listed
which claw # should show full path to binary
ls -la ~/.local/bin/claw # if using symlink, verify it exists and points to target/debug/claw
```
## Quick start
### First-run doctor check
@@ -98,7 +152,57 @@ cd rust
### JSON output for scripting
All clawable commands support `--output-format json` for machine-readable output. Every invocation returns a consistent JSON envelope with `exit_code`, `command`, `timestamp`, and either `{success fields}` or `{error: {kind, message, ...}}`.
All clawable commands support `--output-format json` for machine-readable output.
**IMPORTANT SCHEMA VERSION NOTICE:**
The JSON envelope is currently in **v1.0 (flat shape)** and is scheduled to migrate to **v2.0 (nested schema)** in a future release. See [`FIX_LOCUS_164.md`](./FIX_LOCUS_164.md) for the full migration plan.
-`error` is a string, not a structured object with operation/target/retryable/message/hint
-`kind` field is semantically overloaded (verb identity in success, error classification in error)
- See [`SCHEMAS.md`](./SCHEMAS.md) for documented (v2.0 target) schema and [`FIX_LOCUS_164.md`](./FIX_LOCUS_164.md) for migration details
#### Using v1.0 envelopes in your code
**Success path:** Check for absence of `type: "error"`, then access verb-specific fields:
```bash
cd rust
./target/debug/claw doctor --output-format json | jq '.kind, .has_failures'
```
**Error path:** Check for `type == "error"`, then access `error` (string) and `kind` (error classification):
```bash
cd rust
./target/debug/claw doctor invalid-arg --output-format json | jq '.error, .kind'
```
**Do NOT rely on `kind` alone for dispatching** — it has different meanings in success vs. error. Always check `type == "error"` first.
```bash
cd rust
@@ -109,6 +213,8 @@ cd rust
**Building a dispatcher or orchestration script?** See [`ERROR_HANDLING.md`](./ERROR_HANDLING.md) for the unified error-handling pattern. One code example works for all 14 clawable commands: parse the exit code, classify by `error.kind`, apply recovery strategies (retry, timeout recovery, validation, logging). Use that pattern instead of reimplementing error handling per command.
**Migrating to v2.0?** Check back after [`FIX_LOCUS_164`](./FIX_LOCUS_164.md) is implemented. Phase 1 will add a `--envelope-version=2.0` flag for opt-in access to the structured envelope schema. Phase 2 will make v2.0 the default. Phase 3 will deprecate v1.0.
### Inspect worker state
The `claw state` command reads `.claw/worker-state.json`, which is written by the interactive REPL or a one-shot prompt when a worker executes a task. This file contains the worker ID, session reference, model, and permission mode.
**Purpose:** Dump built-in tool and plugin manifests to stdout as JSON, for parity comparison against the upstream Claude Code TypeScript implementation.
**Prerequisite:** This command requires access to upstream source files (`src/commands.ts`, `src/tools.ts`, `src/entrypoints/cli.tsx`). Set `CLAUDE_CODE_UPSTREAM` env var or pass `--manifests-dir`.
**When to use:** Parity work (comparing the Rust port's tool/plugin surface against the canonical TypeScript implementation). Not needed for normal operation.
**Error mode:** If upstream sources are missing, exits with `error-kind: missing_manifests` and a hint about how to provide them.
### `bootstrap-plan` — Show startup component graph
**Purpose:** Print the ordered list of startup components that are initialized when `claw` begins a session. Useful for debugging startup issues or verifying that fast-path optimizations are in place.
```bash
claw bootstrap-plan
```
**Sample output:**
```
- CliEntry
- FastPathVersion
- StartupProfiler
- SystemPromptFastPath
- ChromeMcpFastPath
```
**When to use:**
- Debugging why startup is slow (compare your plan to the expected one)
- Verifying that fast-path components are registered
- Understanding the load order before customizing hooks or plugins
**Related:** See `claw doctor` for health checks against these startup components.
**Purpose:** Report the current state of the ACP (Agent Context Protocol) / Zed editor integration. Currently **discoverability only** — no editor daemon is available yet.
```bash
claw acp
claw acp serve # same output; `serve` is accepted but not yet launchable
claw --acp # alias
claw -acp # alias
```
**Sample output:**
```
ACP / Zed
Status discoverability only
Launch `claw acp serve` / `claw --acp` / `claw -acp` report status only; no editor daemon is available yet
Today use `claw prompt`, the REPL, or `claw doctor` for local verification
Tracking ROADMAP #76
```
**When to use:** Check whether ACP/Zed integration is ready in your current build. Plan around its availability (track ROADMAP #76 for status).
**Today's alternatives:** Use `claw prompt` for one-shot runs, the interactive REPL for iterative work, or `claw doctor` for local verification.
### `export` — Export session transcript
**Purpose:** Export a managed session's transcript to a file or stdout. Operates on the currently-resumed session (requires `--resume`).
```bash
# Export latest session
claw --resume latest export
# Export specific session
claw --resume <session-id> export
```
**Prerequisite:** A managed session must exist under `.claw/sessions/<workspace-fingerprint>/`. If no sessions exist, the command exits with `error-kind: no_managed_sessions` and a hint to start a session first.
**When to use:**
- Archive session transcripts for review
- Share session context with teammates
- Feed session history into downstream tooling
**Related:** Inside the REPL, `/export` is also available as a slash command for the active session.
## Session management
REPL turns are persisted under `.claw/sessions/` in the current workspace.
@@ -432,7 +625,27 @@ cd rust
./target/debug/claw --resume latest /status /diff
```
Useful interactive commands include `/help`, `/status`, `/cost`, `/config`, `/session`, `/model`, `/permissions`, and `/export`.
### Interactive slash commands (inside the REPL)
Useful interactive commands include:
-`/help` — Show help for all available commands
-`/status` — Display current session and workspace status
-`/cost` — Show token usage and cost estimates for the session
-`/config` — Display current configuration and environment state
-`/session` — Show session ID, creation time, and persisted metadata
-`/model` — Display or switch the active model
-`/permissions` — Check sandbox permissions and capability grants
-`/export [file]` — Export the current conversation to a file (or resume from backup)
-`/ultraplan [task]` — Run a deep planning prompt with multi-step reasoning (good for complex refactoring tasks)
-`/teleport <symbol-or-path>` — Jump to a file or symbol by searching the workspace (IDE-like navigation)
-`/bughunter [scope]` — Inspect the codebase for likely bugs in an optional scope (e.g., `src/runtime`)
-`/commit` — Generate a commit message and create a git commit from the conversation
-`/pr [context]` — Draft or create a pull request from the conversation
-`/issue [context]` — Draft or create a GitHub issue from the conversation
-`/diff` — Show unified diff of changes made in the current session
A high-level overview of how claw-code is structured. For implementation details, see source code in `rust/crates/`. For provider details, see [SUPPORTED_PROVIDERS.md](./SUPPORTED_PROVIDERS.md). For pinpoint navigation, see [ROADMAP.md](../ROADMAP.md#pinpoint-cluster-index).
## Overview
claw-code is a Rust-based CLI for interacting with LLM providers (Anthropic, OpenAI-compatible, xAI, DashScope, etc.). It provides:
- Streaming conversation with auto-compaction
- Tool execution (file read/write, bash, MCP)
- Multi-provider routing
- Session persistence
- Parallel agent execution
## Workspace Layout
The Rust workspace is organized in `rust/crates/`:
claw-code configuration reference. For provider details, see [SUPPORTED_PROVIDERS.md](./SUPPORTED_PROVIDERS.md). For architecture, see [ARCHITECTURE.md](./ARCHITECTURE.md).
## Configuration Sources
claw-code reads configuration from multiple sources (in priority order):
**Related paths also respected:**`CODEX_HOME`, `CLAUDE_CONFIG_DIR` (legacy compatibility).
## settings.json
Located at `.claw/settings.json` (project-local) or `~/.claw/settings.json` (user-level). Project-local takes precedence over user-level.
Example:
```json
{
"model":"claude-sonnet-4-6"
}
```
`claw /config` shows the merged, resolved configuration from all sources.
> **Known gap (#285):** No declarative `providers` or `models` block in `settings.json`. Provider selection is currently model-prefix-based via a hardcoded `MODEL_REGISTRY`. See [SUPPORTED_PROVIDERS.md](./SUPPORTED_PROVIDERS.md) for the full provider/model matrix.
## Provider Selection
Provider is auto-selected from model name prefix or the `openai/` namespace prefix:
This guide walks through the workflow for filing a new claw-code pinpoint, from initial friction to merged ROADMAP entry. For format details, see [CONTRIBUTING.md](../CONTRIBUTING.md). For issue template, see [.github/ISSUE_TEMPLATE/pinpoint.md](../.github/ISSUE_TEMPLATE/pinpoint.md).
## What is a Pinpoint?
A pinpoint is a precise, distinct claw-code clawability gap captured in ROADMAP.md format. Pinpoints differ from generic issues by:
- **Specificity:** Exact file paths, function names, line numbers when available
- **Distinctness:** Verified not already covered by existing pinpoints
- **Live evidence:** Real friction event, not hypothetical
Use claw-code in real work. When you hit friction (slow startup, broken behavior, opaque error, missing feature, test brittleness, etc.), STOP and capture:
- What you were trying to do
- What you expected to happen
- What actually happened
- Exact error message / log output (verbatim)
### Step 2: Identify distinct axis
Open ROADMAP.md and search for related existing pinpoints (use the [Cluster Index](../ROADMAP.md#pinpoint-cluster-index)).
For each candidate match:
- Does the existing pinpoint cover this exact symptom?
- Does it cover this exact axis (e.g., timing vs envelope vs config)?
- Is your case a SUBSET, a SUPERSET, or an ORTHOGONAL axis?
If your case is orthogonal, file new. If subset, add live-evidence as additional context to existing pinpoint. If superset, file new + cross-reference existing.
Verify three-way parity (local == origin == fork) before posting any update.
## Worked Example: #290 (stream-init failure envelope)
This shows how #290 was filed in real-time on 2026-04-26.
### Step 1: Friction identified
gaebal-gajae's session hit `500 empty_stream: upstream stream closed before first payload` repeatedly (4x in 30 min). Bare-string error surfaced; no diagnostics, no retry guidance.
### Step 2: Distinct axis identified
-#266 (typed-error-kind taxonomy) covers single-failure categorization, NOT stream-init specifically
-#287 (auto-compaction reactive) covers session-size failures, NOT transport
-#288 (JSON envelope failure) covers context-window envelope, NOT stream-init
→ Orthogonal: filed new #290 covering typed-stream-init-failure-envelope
### Step 3: Code verified
Inspected `rust/crates/api/src/sse.rs` — confirmed no `failure_class=upstream_stream_init` discriminant, no retry recommendation in JSON envelope.
### Step 4: Entry written
Used canonical 5-section format. Listed 4 live evidence timestamps. Cross-referenced #266, #287, #288 in "Why distinct."
### Step 5: Submitted
Commit `0f38975`, pushed to both origin and fork, parity verified, Discord post under 1500 chars.
**Total time: ~2 minutes from friction identification to merged ROADMAP entry.**
## Tips
- **File while it's fresh.** Wait too long and you'll forget exact symptoms.
- **Check Cluster Index FIRST** — saves time vs scanning full ROADMAP.
- **Write Fix Shape even if you don't implement.** Helps future contributors.
claw-code currently supports the following LLM providers. This is a snapshot of the current code state and may change. The canonical source of truth is `MODEL_REGISTRY` and provider routing logic in `rust/crates/api/src/providers/mod.rs`.
> **Note:** A declarative `providers` / `models` / `websearch` config in `settings.json` is tracked as pinpoint #285 and is not yet implemented. Until then, provider/model selection is determined by:
> 1. The model name prefix (e.g., `claude-`, `grok-`, `openai/`, `qwen/`, `kimi-`)
- **Status:** Supported via OpenAI-compatible client; also covers local providers (Ollama, LM Studio, vLLM, OpenRouter)
- **Models:** `openai/` prefix (e.g., `openai/gpt-4.1-mini`) or bare `gpt-*` prefix
- **Auth:** `OPENAI_API_KEY`
- **Base URL:** `https://api.openai.com/v1` (override: `OPENAI_BASE_URL` — also used for local providers)
- **Local provider routing:** When `OPENAI_BASE_URL` is set and `OPENAI_API_KEY` is present, unknown model names (e.g., `qwen2.5-coder:7b`) also route here
- **Known issues:** Declarative per-model config tracked in #285
## Web Search
- **Status:** Hard-coded heuristics; declarative `websearch` config tracked in #285
## Provider Selection Order
When the model name has no recognized prefix, `detect_provider_kind()` falls through in this order:
1. Model prefix match (`claude-` → Anthropic, `grok-` → xAI, `openai/` or `gpt-` → OpenAI, `qwen/` or `qwen-` → DashScope, `kimi/` or `kimi-` → DashScope)
2.`OPENAI_BASE_URL` + `OPENAI_API_KEY` set → OpenAI-compat
3. Anthropic credentials found → Anthropic
4.`OPENAI_API_KEY` found → OpenAI
5.`XAI_API_KEY` found → xAI
6.`OPENAI_BASE_URL` set (no key) → OpenAI-compat (for keyless local providers)
7. Default fallback → Anthropic
## Reporting Provider Issues
For provider-specific bugs (e.g., `500 empty_stream` from upstream), see [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for mitigation steps.
For pinpointing a missing provider feature, file via [ISSUE_TEMPLATE/pinpoint.md](../.github/ISSUE_TEMPLATE/pinpoint.md).
- US-015: Session identity completeness at creation time
- US-016: Duplicate terminal-event suppression
- US-017: Lane ownership / scope binding
- US-018: Nudge acknowledgment / dedupe contract
- US-019: Stable roadmap-id assignment
- US-020: Roadmap item lifecycle state contract
Iteration 8: 2026-04-16
------------------------
US-021 COMPLETED (Request body size pre-flight check - from dogfood findings)
- Files:
- rust/crates/api/src/error.rs (new error variant)
- rust/crates/api/src/providers/openai_compat.rs
- Added RequestBodySizeExceeded error variant with actionable message
- Added max_request_body_bytes to OpenAiCompatConfig:
- DashScope: 6MB (6_291_456 bytes) - from dogfood with kimi-k2.5
- OpenAI: 100MB (104_857_600 bytes)
- xAI: 50MB (52_428_800 bytes)
- Added estimate_request_body_size() for pre-flight checks
- Added check_request_body_size() for validation
- Pre-flight check integrated in send_raw_request()
- Tests: 5 new tests for size estimation and limit checking
PROJECT STATUS: COMPLETE (21/21 stories)
Iteration 2026-04-29 - ROADMAP #96 COMPLETED
------------------------------------------------
- Pulled origin/main: already up to date.
- Selected ROADMAP #96 as a small repo-local Immediate Backlog item: the `claw --help` Resume-safe command summary leaked slash-command stubs despite the main Interactive command listing filtering them.
@@ -7,7 +7,8 @@ This file provides guidance to Claw Code (clawcode.dev) when working with code i
- Frameworks: none detected from the supported starter markers.
## Verification
-Run Rust verification from the repo root: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`
-From the repository root, run Rust formatting with `scripts/fmt.sh` (or `scripts/fmt.sh --check` for CI-style checks). From this `rust/` directory, the equivalent command is `../scripts/fmt.sh`. Root-level `cargo fmt --manifest-path rust/Cargo.toml` is not the supported formatting command.
- From this `rust/` directory, run Rust verification with `cargo clippy --workspace --all-targets -- -D warnings` and `cargo test --workspace`.
## Working agreement
- Prefer small, reviewable changes and keep generated bootstrap files aligned with actual repo workflows.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.