## 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.
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.
## Problem
#77 Phase 1 added machine-readable error `kind` discriminants to JSON error
payloads. Text-mode (stderr) errors still emit prose-only output with no
structured classification.
Observability tools (log aggregators, CI error parsers) parsing stderr can't
distinguish error classes without regex-scraping the prose.
## Fix
Added `[error-kind: <class>]` prefix line to all text-mode error output.
The prefix appears before the error prose, making it immediately parseable by
line-based log tools without any substring matching.
**Examples:**
## Impact
- Stderr observers (log aggregators, CI systems) can now parse error class
from the first line without regex or substring scraping
- Same classifier function used for JSON (#77 P1) and text modes
- Text-mode output remains human-readable (error prose unchanged)
- Prefix format follows syslog/structured-logging conventions
## Tests
All 179 rusty-claude-cli tests pass. Verified on 3 different error classes.
Closes ROADMAP #156.
## Problem
All JSON error payloads had the same three-field envelope:
```json
{"type": "error", "error": "<prose with hint baked in>"}
```
Five distinct error classes were indistinguishable at the schema level:
- missing_credentials (no API key)
- missing_worker_state (no state file)
- session_not_found / session_load_failed
- cli_parse (unrecognized args)
- invalid_model_syntax
Downstream claws had to regex-scrape the prose to route failures.
## Fix
1. **Added `classify_error_kind()`** — prefix/keyword classifier that returns a
snake_case discriminant token for 12 known error classes:
`missing_credentials`, `missing_manifests`, `missing_worker_state`,
`session_not_found`, `session_load_failed`, `no_managed_sessions`,
`cli_parse`, `invalid_model_syntax`, `unsupported_command`,
`unsupported_resumed_command`, `confirmation_required`, `api_http_error`,
plus `unknown` fallback.
2. **Added `split_error_hint()`** — splits multi-line error messages into
(short_reason, optional_hint) so the runbook prose stops being stuffed
into the `error` field.
3. **Extended JSON envelope** at 4 emit sites:
- Main error sink (line ~213)
- Session load failure in resume_session
- Stub command (unsupported_command)
- Unknown resumed command (unsupported_resumed_command)
## New JSON shape
```json
{
"type": "error",
"error": "short reason (first line)",
"kind": "missing_credentials",
"hint": "Hint: export ANTHROPIC_API_KEY..."
}
```
`kind` is always present. `hint` is null when no runbook follows.
`error` now carries only the short reason, not the full multi-line prose.
## Tests
Added 2 new regression tests:
- `classify_error_kind_returns_correct_discriminants` — all 9 known classes + fallback
- `split_error_hint_separates_reason_from_runbook` — with and without hints
All 179 rusty-claude-cli tests pass. Full workspace green.
Closes ROADMAP #77 Phase 1.
## Problem
Two session error messages advertised `.claw/sessions/` as the managed-session
location, but the actual on-disk layout is `.claw/sessions/<workspace_fingerprint>/`
where the fingerprint is a 16-char FNV-1a hash of the CWD path.
Users see error messages like:
```
no managed sessions found in .claw/sessions/
```
But the real directory is:
```
.claw/sessions/8497f4bcf995fc19/
```
The error copy was a direct lie — it made workspace-fingerprint partitioning
invisible and left users confused about whether sessions were lost or just in
a different partition.
## Fix
Updated two error formatters to accept the resolved `sessions_root` path
and extract the actual workspace-fingerprint directory:
1. **format_missing_session_reference**: now shows the actual fingerprint dir
and explains that it's a workspace-specific partition
2. **format_no_managed_sessions**: now shows the actual fingerprint dir and
includes a note that sessions from other CWDs are intentionally invisible
Updated all three call sites to pass `&self.sessions_root` to the formatters.
## Examples
**Before:**
```
no managed sessions found in .claw/sessions/
```
**After:**
```
no managed sessions found in .claw/sessions/8497f4bcf995fc19/
Start `claw` to create a session, then rerun with `--resume latest`.
Note: claw partitions sessions per workspace fingerprint; sessions from other CWDs are invisible.
```
```
session not found: nonexistent-id
Hint: managed sessions live in .claw/sessions/8497f4bcf995fc19/ (workspace-specific partition).
Try `latest` for the most recent session or `/session list` in the REPL.
```
## Impact
- Users can now tell from the error message that they're looking in the right
directory (the one their current CWD maps to)
- The workspace-fingerprint partitioning stops being invisible
- Operators understand why sessions from adjacent CWDs don't appear
- Error copy matches the actual on-disk structure
## Tests
All 466 runtime tests pass. Verified on two real workspaces with actual
workspace-fingerprint directories.
Closes ROADMAP #80.
## Problem
When a user types `claw --model gpt-4` or `--model qwen-plus`, they get:
```
error: invalid model syntax: 'gpt-4'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias
```
USAGE.md documents that "The error message now includes a hint that names the detected env var" — but this hint does not actually exist. The user has to re-read USAGE.md or guess the correct prefix.
## Fix
Enhance `validate_model_syntax` to detect when a model name looks like it belongs to a different provider:
1. **OpenAI models** (starts with `gpt-` or `gpt_`):
```
Did you mean `openai/gpt-4`? (Requires OPENAI_API_KEY env var)
```
2. **Qwen/DashScope models** (starts with `qwen`):
```
Did you mean `qwen/qwen-plus`? (Requires DASHSCOPE_API_KEY env var)
```
3. **Grok/xAI models** (starts with `grok`):
```
Did you mean `xai/grok-3`? (Requires XAI_API_KEY env var)
```
Unrelated invalid models (e.g., `asdfgh`) do not get a spurious hint.
## Verification
- `claw --model gpt-4` → hints `openai/gpt-4` + `OPENAI_API_KEY`
- `claw --model qwen-plus` → hints `qwen/qwen-plus` + `DASHSCOPE_API_KEY`
- `claw --model grok-3` → hints `xai/grok-3` + `XAI_API_KEY`
- `claw --model asdfgh` → generic error (no hint)
## Tests
Added 3 new assertions in `parses_multiple_diagnostic_subcommands`:
- GPT model error hints openai/ prefix and OPENAI_API_KEY
- Qwen model error hints qwen/ prefix and DASHSCOPE_API_KEY
- Unrelated models don't get a spurious hint
All 177 rusty-claude-cli tests pass.
Closes ROADMAP #154.
## Problem
Users commonly type `claw doctor --json`, `claw status --json`, or
`claw system-prompt --json` expecting JSON output. These fail with
`unrecognized argument \`--json\` for subcommand` with no hint that
`--output-format json` is the correct flag.
## Discovery
Filed as #152 during 21:17 dogfood nudge. The #127 worktree contained
a more comprehensive patch but conflicted with #141 (unified --help).
On re-investigation of main, Bugs 1 and 3 from #127 are already closed
(positional arg rejection works, no double "error:" prefix). Only
Bug 2 (the `--json` hint) remained.
## Fix
Two call sites add the hint:
1. `parse_single_word_command_alias`'s diagnostic-verb suffix path:
when rest[1] == "--json", append "Did you mean \`--output-format json\`?"
2. `parse_system_prompt_options` unknown-option path: same hint when
the option is exactly `--json`.
## Verification
Before:
$ claw doctor --json
error: unrecognized argument `--json` for subcommand `doctor`
Run `claw --help` for usage.
After:
$ claw doctor --json
error: unrecognized argument `--json` for subcommand `doctor`
Did you mean `--output-format json`?
Run `claw --help` for usage.
Covers: `doctor --json`, `status --json`, `sandbox --json`,
`system-prompt --json`, and any other diagnostic verb that routes
through `parse_single_word_command_alias`.
Other unrecognized args (`claw doctor garbage`) correctly don't
trigger the hint.
## Tests
- 2 new assertions in `parses_multiple_diagnostic_subcommands`:
- `claw doctor --json` produces hint
- `claw doctor garbage` does NOT produce hint
- 177 rusty-claude-cli tests pass
- Workspace tests green
Closes ROADMAP #152.
## Problem
`workspace_fingerprint(path)` hashes the raw path string without
canonicalization. Two equivalent paths (e.g. `/tmp/foo` vs
`/private/tmp/foo` on macOS) produce different fingerprints and
therefore different session stores. #150 fixed the test-side symptom;
this fixes the underlying product contract.
## Discovery path
#150 fix (canonicalize in test) was a workaround. Q's ack on #150
surfaced the deeper gap: the function itself is still fragile for
any caller passing a non-canonical path:
1. Embedded callers with a raw `--data-dir` path
2. Programmatic `SessionStore::from_cwd(user_path)` calls
3. NixOS store paths, Docker bind mounts, case-insensitive normalization
The REPL's default flow happens to work because `env::current_dir()`
returns canonical paths on macOS. But any caller passing a raw path
risks silent session-store divergence.
## Fix
Canonicalize inside `SessionStore::from_cwd()` and `from_data_dir()`
before computing the fingerprint. Kept `workspace_fingerprint()` itself
as a pure function for determinism — canonicalization is the entry
point's responsibility.
```rust
let canonical_cwd = fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
let sessions_root = canonical_cwd.join(".claw").join("sessions").join(workspace_fingerprint(&canonical_cwd));
```
Falls back to the raw path if canonicalize fails (directory doesn't
exist yet).
## Test-side updates
Three legacy-session tests expected the non-canonical base path to
match the store's workspace_root. Updated them to canonicalize
`base` after creation — same defensive pattern as #150, now
explicit across all three tests.
## Regression test
Added `session_store_from_cwd_canonicalizes_equivalent_paths` that
creates two stores from equivalent paths (raw vs canonical) and
asserts they resolve to the same sessions_dir.
## Verification
- `cargo test -p runtime session_store_` — 9/9 pass
- `cargo test --workspace` — all green, no FAILED markers
- No behavior change for existing users (REPL default flow already
used canonical paths)
## Backward compatibility
Users on macOS who always went through `env::current_dir()`:
no hash change, sessions resume identically.
Users who ever called with a non-canonical path: hash would change,
but those sessions were already broken (couldn't be resumed from a
canonical-path cwd). Net improvement.
Closes ROADMAP #151.
## #150 Fix: resume_latest test flake
**Problem:** `resume_latest_restores_the_most_recent_managed_session` intermittently
fails when run in the workspace suite or multiple times in sequence, but passes in
isolation.
**Root cause:** `workspace_fingerprint(path)` hashes the path string without
canonicalization. On macOS, `/tmp` is a symlink to `/private/tmp`. The test
creates a temp dir via `std::env::temp_dir().join(...)` which returns
`/var/folders/...` (non-canonical). When the subprocess spawns,
`env::current_dir()` returns the canonical path `/private/var/folders/...`.
The two fingerprints differ, so the subprocess looks in
`.claw/sessions/<hash1>` while files are in `.claw/sessions/<hash2>`.
Session discovery fails.
**Fix:** Call `fs::canonicalize(&project_dir)` after creating the directory
to ensure test and subprocess use identical path representations.
**Verification:** 5 consecutive runs of the full test suite — all pass.
Previously: 5/5 failed when run in sequence.
## #246 Filing: Reminder cron outcome ambiguity (control-loop blocker)
The `clawcode-dogfood-cycle-reminder` cron times out repeatedly with no
structured feedback on whether the nudge was delivered, skipped, or died in-flight.
**Phase 1 outcome schema** — add explicit field to cron result:
- `delivered` — nudge posted to Discord
- `timed_out_before_send` — died before posting
- `timed_out_after_send` — posted but cleanup timed out
- `skipped_due_to_active_cycle` — previous cycle active
- `aborted_gateway_draining` — daemon shutdown
Assigned to gaebal-gajae (cron/orchestration domain). Unblocks trustworthy
dogfood cycle observability.
Closes ROADMAP #150. Filed ROADMAP #246.
## Problem
`runtime::config::tests::validates_unknown_top_level_keys_with_line_and_field_name`
intermittently fails during `cargo test --workspace` (witnessed during
#147 and #148 workspace runs) but passes deterministically in isolation.
Example failure from workspace run:
test result: FAILED. 464 passed; 1 failed
## Root cause
`runtime/src/config.rs::tests::temp_dir()` used nanosecond timestamp
alone for namespace isolation:
std::env::temp_dir().join(format!("runtime-config-{nanos}"))
Under parallel test execution on fast machines with coarse clock
resolution, two tests start within the same nanosecond bucket and
collide on the same path. One test's `fs::remove_dir_all(root)` then
races another's in-flight `fs::create_dir_all()`.
Other crates already solved this pattern:
- plugins::tests::temp_dir(label) — label-parameterized
- runtime::git_context::tests::temp_dir(label) — label-parameterized
runtime/src/config.rs was missed.
## Fix
Added process id + monotonically-incrementing atomic counter to the
namespace, making every callsite provably unique regardless of clock
resolution or scheduling:
static COUNTER: AtomicU64 = AtomicU64::new(0);
let pid = std::process::id();
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("runtime-config-{pid}-{nanos}-{seq}"))
Chose counter+pid over the label-parameterized pattern to avoid
touching all 20 callsites in the same commit (mechanical noise with
no added safety — counter alone is sufficient).
## Verification
Before: one failure per workspace run (config test flake).
After: 5 consecutive `cargo test --workspace` runs — zero config
test failures. Only pre-existing `resume_latest` flake remains
(orthogonal, unrelated to this change).
for i in 1 2 3 4 5; do cargo test --workspace; done
# All 5 runs: config tests green. Only resume_latest flake appears.
cargo test -p runtime
# 465 passed; 0 failed
## ROADMAP.md
Added Pinpoint #149 documenting the gap, root cause, and fix.
Closes ROADMAP #149.
## Scope
Two deltas in one commit:
### #128 closure (docs)
Re-verified on main HEAD `4cb8fa0`: malformed `--model` strings already
rejected at parse time (`validate_model_syntax` in parse_args). All
historical repro cases now produce specific errors:
claw --model '' → error: model string cannot be empty
claw --model 'bad model' → error: invalid model syntax: 'bad model' contains spaces
claw --model 'sonet' → error: invalid model syntax: 'sonet'. Expected provider/model or known alias
claw --model '@invalid' → error: invalid model syntax: '@invalid'. Expected provider/model ...
claw --model 'totally-not-real-xyz' → error: invalid model syntax: ...
claw --model sonnet → ok, resolves to claude-sonnet-4-6
claw --model anthropic/claude-opus-4-6 → ok, passes through
Marked #128 CLOSED in ROADMAP with repro block. Residual provenance gap
split off as #148.
### #148 implementation
**Problem.** After #128 closure, `claw status --output-format json`
still surfaces only the resolved model string. No way for a claw to
distinguish whether `claude-sonnet-4-6` came from `--model sonnet`
(alias resolution) vs `--model claude-sonnet-4-6` (pass-through) vs
`ANTHROPIC_MODEL` env vs `.claw.json` config vs compiled-in default.
Debug forensics had to re-read argv instead of reading a structured
field. Clawhip orchestrators sending `--model` couldn't confirm the
flag was honored vs falling back to default.
**Fix.** Added two fields to status JSON envelope:
- `model_source`: "flag" | "env" | "config" | "default"
- `model_raw`: user's input before alias resolution (null on default)
Text mode appends a `Model source` line under `Model`, showing the
source and raw input (e.g. `Model source flag (raw: sonnet)`).
**Resolution order** (mirrors resolve_repl_model but with source
attribution):
1. If `--model` / `--model=` flag supplied → source: flag, raw: flag value
2. Else if ANTHROPIC_MODEL set → source: env, raw: env value
3. Else if `.claw.json` model key set → source: config, raw: config value
4. Else → source: default, raw: null
## Changes
### rust/crates/rusty-claude-cli/src/main.rs
- Added `ModelSource` enum (Flag/Env/Config/Default) with `as_str()`.
- Added `ModelProvenance` struct (resolved, raw, source) with
three constructors: `default_fallback()`, `from_flag(raw)`, and
`from_env_or_config_or_default(cli_model)`.
- Added `model_flag_raw: Option<String>` field to `CliAction::Status`.
- Parse loop captures raw input in `--model` and `--model=` arms.
- Extended `parse_single_word_command_alias` to thread
`model_flag_raw: Option<&str>` through.
- Extended `print_status_snapshot` signature to accept
`model_flag_raw: Option<&str>`. Resolves provenance at dispatch time
(flag provenance from arg; else probe env/config/default).
- Extended `status_json_value` signature with
`provenance: Option<&ModelProvenance>`. On Some, adds `model_source`
and `model_raw` fields; on None (legacy resume paths), omits them
for backward compat.
- Extended `format_status_report` signature with optional provenance.
On Some, renders `Model source` line after `Model`.
- Updated all existing callers (REPL /status, resume /status, tests)
to pass None (legacy paths don't carry flag provenance).
- Added 2 regression assertions in parse_args test covering both
`--model sonnet` and `--model=...` forms.
### ROADMAP.md
- Marked #128 CLOSED with re-verification block.
- Filed #148 documenting the provenance gap split, fix shape, and
acceptance criteria.
## Live verification
$ claw --model sonnet --output-format json status | jq '{model,model_source,model_raw}'
{"model": "claude-sonnet-4-6", "model_source": "flag", "model_raw": "sonnet"}
$ claw --output-format json status | jq '{model,model_source,model_raw}'
{"model": "claude-opus-4-6", "model_source": "default", "model_raw": null}
$ ANTHROPIC_MODEL=haiku claw --output-format json status | jq '{model,model_source,model_raw}'
{"model": "claude-haiku-4-5-20251213", "model_source": "env", "model_raw": "haiku"}
$ echo '{"model":"claude-opus-4-7"}' > .claw.json && claw --output-format json status | jq '{model,model_source,model_raw}'
{"model": "claude-opus-4-7", "model_source": "config", "model_raw": "claude-opus-4-7"}
$ claw --model sonnet status
Status
Model claude-sonnet-4-6
Model source flag (raw: sonnet)
Permission mode danger-full-access
...
## Tests
- rusty-claude-cli bin: 177 tests pass (2 new assertions for #148)
- Full workspace green except pre-existing resume_latest flake (unrelated)
Closes ROADMAP #128, #148.
## Problem
The `"prompt"` subcommand arm enforced `if prompt.trim().is_empty()`
and returned a specific error. The fallthrough `other` arm in the same
match block — which routes any unrecognized first positional arg to
`CliAction::Prompt` — had no such guard. Result:
$ claw ""
error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN ...
$ claw " "
error: missing Anthropic credentials; ...
$ claw "" ""
error: missing Anthropic credentials; ...
$ claw --output-format json ""
{"error":"missing Anthropic credentials; ...","type":"error"}
An empty prompt should never reach the credentials check. Worse: with
valid credentials, the literal empty string gets sent to Claude as a
user prompt, either burning tokens for nothing or triggering a model-
side refusal. Same prompt-misdelivery family as #145.
## Root cause
In `parse_subcommand()`, the final `other =>` arm in the top-level
match only guards against typos (#108 guard via `looks_like_subcommand_typo`)
and then unconditionally builds `CliAction::Prompt { prompt: rest.join(" ") }`.
An empty/whitespace-only join passes through.
## Changes
### rust/crates/rusty-claude-cli/src/main.rs
Added the same `if joined.trim().is_empty()` guard already used in the
`"prompt"` arm to the fallthrough path. Error message distinguishes it
from the `prompt` subcommand path:
empty prompt: provide a subcommand (run `claw --help`) or a
non-empty prompt string
Runs AFTER the typo guard (so `claw sttaus` still suggests `status`)
and BEFORE CliAction::Prompt construction (so no network call ever
happens for empty inputs).
### Regression tests
Added 4 assertions in the existing parse_args test:
- parse_args([""]) → Err("empty prompt: ...")
- parse_args([" "]) → Err("empty prompt: ...")
- parse_args(["", ""]) → Err("empty prompt: ...")
- parse_args(["sttaus"]) → Err("unknown subcommand: ...") [verifies #108 typo guard still takes precedence]
### ROADMAP.md
Added Pinpoint #147 documenting the gap, verification, root cause,
fix shape, and acceptance. Joins the prompt-misdelivery cluster
alongside #145.
## Live verification
$ claw ""
error: empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string
$ claw " "
error: empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string
$ claw --output-format json ""
{"error":"empty prompt: provide a subcommand ...","type":"error"}
$ claw prompt "" # unchanged: subcommand-specific error preserved
error: prompt subcommand requires a prompt string
$ claw hello # unchanged: typo guard still fires
error: unknown subcommand: hello.
Did you mean help
$ claw "real prompt here" # unchanged: real prompts still reach API
error: api returned 401 Unauthorized (with dummy key, as expected)
All empty/whitespace-only paths exit 1. No network call. No misleading
credentials error.
## Tests
- rusty-claude-cli bin: 177 tests pass (4 new assertions)
- Full workspace green except pre-existing resume_latest flake (unrelated)
Closes ROADMAP #147.
## Problem
`claw config` and `claw diff` are pure-local read-only introspection
commands (config merges .claw.json + .claw/settings.json from disk; diff
shells out to `git diff --cached` + `git diff`). Neither needs a session
context, yet both rejected direct CLI invocation:
$ claw config
error: `claw config` is a slash command. Use `claw --resume SESSION.jsonl /config` ...
$ claw diff
error: `claw diff` is a slash command. ...
This forced clawing operators to spin up a full session just to inspect
static disk state, and broke natural pipelines like
`claw config --output-format json | jq`.
## Root cause
Sibling of #145: `SlashCommand::Config { section }` and
`SlashCommand::Diff` had working renderers (`render_config_report`,
`render_config_json`, `render_diff_report`, `render_diff_json_for`)
exposed for resume sessions, but the top-level CLI parser in
`parse_subcommand()` had no arms for them. Zero-arg `config`/`diff`
hit `parse_single_word_command_alias`'s fallback to
`bare_slash_command_guidance`, producing the misleading guidance.
## Changes
### rust/crates/rusty-claude-cli/src/main.rs
- Added `CliAction::Config { section, output_format }` and
`CliAction::Diff { output_format }` variants.
- Added `"config"` / `"diff"` arms to the top-level parser in
`parse_subcommand()`. `config` accepts an optional section name
(env|hooks|model|plugins) matching SlashCommand::Config semantics.
`diff` takes no positional args. Both reject extra trailing args
with a clear error.
- Added `"config" | "diff" => None` to
`parse_single_word_command_alias` so bare invocations fall through
to the new parser arms instead of the slash-guidance error.
- Added dispatch in run() that calls existing renderers: text mode uses
`render_config_report` / `render_diff_report`; JSON mode uses
`render_config_json` / `render_diff_json_for` with
`serde_json::to_string_pretty`.
- Added 5 regression assertions in parse_args test covering:
parse_args(["config"]), parse_args(["config", "env"]),
parse_args(["config", "--output-format", "json"]),
parse_args(["diff"]), parse_args(["diff", "--output-format", "json"]).
### ROADMAP.md
Added Pinpoint #146 documenting the gap, verification, root cause,
fix shape, and acceptance. Explicitly notes which other slash commands
(`hooks`, `usage`, `context`, etc.) are NOT candidates because they
are session-state-modifying.
## Live verification
$ claw config # no config files
Config
Working directory /private/tmp/cd-146-verify
Loaded files 0
Merged keys 0
Discovered files
user missing ...
project missing ...
local missing ...
Exit 0.
$ claw config --output-format json
{
"cwd": "...",
"files": [...],
...
}
$ claw diff # no git
Diff
Result no git repository
Detail ...
Exit 0.
$ claw diff --output-format json # inside claw-code
{
"kind": "diff",
"result": "changes",
"staged": "",
"unstaged": "diff --git ..."
}
Exit 0.
## Tests
- rusty-claude-cli bin: 177 tests pass (5 new assertions in parse_args)
- Full workspace green except pre-existing resume_latest flake (unrelated)
## Not changed
`hooks`, `usage`, `context`, `tasks`, `theme`, `voice`, `rename`,
`copy`, `color`, `effort`, `branch`, `rewind`, `ide`, `tag`,
`output-style`, `add-dir` — all session-mutating or interactive-only;
correctly remain slash-only.
Closes ROADMAP #146.
## Problem
`claw plugins` (and `claw plugins list`, `claw plugins --help`,
`claw plugins info <name>`, etc.) fell through the top-level subcommand
match and got routed into the prompt-execution path. Result: a purely
local introspection command triggered an Anthropic API call and surfaced
`missing Anthropic credentials` to the user. With valid credentials, it
would actually send the literal string "plugins" as a user prompt to
Claude, burning tokens for a local query.
$ claw plugins
error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API
$ ANTHROPIC_API_KEY=dummy claw plugins
⠋ 🦀 Thinking...
✘ ❌ Request failed
error: api returned 401 Unauthorized
Meanwhile siblings (`agents`, `mcp`, `skills`) all worked correctly:
$ claw agents
No agents found.
$ claw mcp
MCP
Working directory ...
Configured servers 0
## Root cause
`CliAction::Plugins` exists, has a working dispatcher
(`LiveCli::print_plugins`), and is produced inside the REPL via
`SlashCommand::Plugins`. But the top-level CLI parser in
`parse_subcommand()` had arms for `agents`, `mcp`, `skills`, `status`,
`doctor`, `init`, `export`, `prompt`, etc., and **no arm for
`plugins`**. The dispatch never ran from the CLI entry point.
## Changes
### rust/crates/rusty-claude-cli/src/main.rs
Added a `"plugins"` arm to the top-level match in `parse_subcommand()`
that produces `CliAction::Plugins { action, target, output_format }`,
following the same positional convention as `mcp` (`action` = first
positional, `target` = second). Rejects >2 positional args with a clear
error.
Added four regression assertions in the existing `parse_args` test:
- `plugins` alone → `CliAction::Plugins { action: None, target: None }`
- `plugins list` → action: Some("list"), target: None
- `plugins enable <name>` → action: Some("enable"), target: Some(...)
- `plugins --output-format json` → action: None, output_format: Json
### ROADMAP.md
Added Pinpoint #145 documenting the gap, verification, root cause,
fix shape, and acceptance.
## Live verification
$ claw plugins # no credentials set
Plugins
example-bundled v0.1.0 disabled
sample-hooks v0.1.0 disabled
$ claw plugins --output-format json # no credentials set
{
"action": "list",
"kind": "plugin",
"message": "Plugins\n example-bundled ...\n sample-hooks ...",
"reload_runtime": false,
"target": null
}
Exit 0 in all modes. No network call. No "missing credentials" error.
## Tests
- rusty-claude-cli bin: 177 tests pass (new plugin assertions included)
- Full workspace green except pre-existing resume_latest flake (unrelated)
Closes ROADMAP #145.
Filing + Phase 1 fix in one commit (sibling of #143).
## Context
With #143 Phase 1 landed (`claw status` degrades), `claw mcp` was the
remaining diagnostic surface that hard-failed on a malformed `.claw.json`.
Same input, same parse error, same partial-success violation. Fresh
dogfood at 18:59 KST caught it on main HEAD `e2a43fc`.
## Changes
### ROADMAP.md
Added Pinpoint #144 documenting the gap and acceptance criteria. Joins
the partial-success / Principle #5 cluster with #143.
### rust/crates/commands/src/lib.rs
`render_mcp_report_for()` + `render_mcp_report_json_for()` now catch the
ConfigError at loader.load() instead of propagating:
- **Text mode** prepends a "Config load error" block (same shape as
#143's status output) before the MCP listing. The listing still renders
with empty servers so the output structure is preserved.
- **JSON mode** adds top-level `status: "ok" | "degraded"` +
`config_load_error: string | null` fields alongside existing fields
(`kind`, `action`, `working_directory`, `configured_servers`,
`servers[]`). On clean runs, `status: "ok"` and
`config_load_error: null`. On parse failure, `status: "degraded"`,
`config_load_error: "..."`, `servers: []`, exit 0.
- Both list and show actions get the same treatment.
### Regression test
`commands::tests::mcp_degrades_gracefully_on_malformed_mcp_config_144`:
- Injects the same malformed .claw.json as #143 (one valid + one broken
mcpServers entry).
- Asserts mcp list returns Ok (not Err).
- Asserts top-level status: "degraded" and config_load_error names the
malformed field path.
- Asserts show action also degrades.
- Asserts clean path returns status: "ok" with config_load_error null.
## Live verification
$ claw mcp --output-format json
{
"action": "list",
"kind": "mcp",
"status": "degraded",
"config_load_error": ".../.claw.json: mcpServers.missing-command: missing string field command",
"working_directory": "/Users/yeongyu/clawd",
"configured_servers": 0,
"servers": []
}
Exit 0.
## Contract alignment after this commit
All three diagnostic surfaces match now:
- `doctor` — degraded envelope with typed check entries ✅
- `status` — degraded envelope with config_load_error ✅ (#143)
- `mcp` — degraded envelope with config_load_error ✅ (this commit)
Phase 2 (typed-error object joining taxonomy §4.44) tracked separately
across all three surfaces.
Full workspace test green except pre-existing resume_latest flake (unrelated).
Closes ROADMAP #144 phase 1.
Previously `claw status` hard-failed on any config parse error, emitting
a bare error string and exiting 1. This took down the entire health
surface for a single malformed MCP entry, even though workspace, git,
model, permission, and sandbox state could all be reported independently.
`claw doctor` already degraded gracefully on the exact same input.
This commit matches `claw status` to that contract.
Changes:
- Add `StatusContext::config_load_error: Option<String>` to capture parse
errors without aborting.
- Rewrite `status_context()` to match on `ConfigLoader::load()`: on Err,
fall back to default `SandboxConfig` for sandbox resolution and record
the parse error, then continue populating workspace/git/memory fields.
- JSON output gains top-level `status: "ok" | "degraded"` marker and a
`config_load_error` string (null on clean runs). All other existing
fields preserved for backward compat.
- Text output prepends a "Config load error" block with Details + Hint
when config failed to parse, then a "Status (degraded)" header on the
main block. Clean runs show the usual "Status" header.
- Doctor path updated to pass the config load error through StatusContext.
Regression test `status_degrades_gracefully_on_malformed_mcp_config_143`:
- Injects a .claw.json with one valid + one malformed mcpServers entry
- Asserts status_context() returns Ok (not Err)
- Asserts config_load_error names the malformed field path
- Asserts workspace/sandbox fields still populated in JSON
- Asserts top-level status is 'degraded'
- Asserts clean config path still returns status: 'ok'
Verified live on /Users/yeongyu/clawd (contains deliberately broken MCP entries):
$ claw status --output-format json
{ "status": "degraded",
"config_load_error": ".../mcpServers.missing-command: missing string field command",
"model": "claude-opus-4-6",
"workspace": {...},
"sandbox": {...},
... }
Phase 2 (typed error object joining #4.44 taxonomy) tracked separately.
Full workspace test green except pre-existing resume_latest flake (unrelated).
Closes ROADMAP #143 phase 1.
Previously `claw state` errored with "no worker state file found ... — run a
worker first" but there is no `claw worker` subcommand, so claws had no
discoverable path from the error to a fix.
Changes:
- Rewrite the missing-state error to name the two concrete commands that
produce .claw/worker-state.json:
* `claw` (interactive REPL, writes state on first turn)
* `claw prompt <text>` (one non-interactive turn)
Also tell the user what to rerun: `claw state [--output-format json]`.
- Expand the State --help topic with "Produces state", "Observes state",
and "Exit codes" lines so the worker-state contract is discoverable
before the user hits the error.
- Add regression test state_error_surfaces_actionable_worker_commands_139
asserting the error contains `claw prompt`, REPL mention, and the
rerun path, plus that the help topic documents the producer contract.
Verified live:
$ claw state
error: no worker state file found at .claw/worker-state.json
Hint: worker state is written by the interactive REPL or a non-interactive prompt.
Run: claw # start the REPL (writes state on first turn)
Or: claw prompt <text> # run one non-interactive turn
Then rerun: claw state [--output-format json]
JSON mode preserves the full hint inside the error envelope so CI/claws
can match on `claw prompt` without losing the canonical prefix.
Full workspace test green except pre-existing resume_latest flake (unrelated).
Closes ROADMAP #139.
Previously `claw init --output-format json` emitted a valid JSON envelope but
packed the entire human-formatted output into a single `message` string. Claw
scripts had to substring-match human language to tell `created` from `skipped`.
Changes:
- Add InitStatus::json_tag() returning machine-stable "created"|"updated"|"skipped"
(unlike label() which includes the human " (already exists)" suffix).
- Add InitReport::NEXT_STEP constant so claws can read the next-step hint
without grepping the message string.
- Add InitReport::artifacts_with_status() to partition artifacts by state.
- Add InitReport::artifact_json_entries() for the structured artifacts[] array.
- Rewrite run_init + init_json_value to emit first-class fields alongside the
legacy message string (kept for text consumers): project_path, created[],
updated[], skipped[], artifacts[], next_step, message.
- Update the slash-command Init dispatch to use the same structured JSON.
- Add regression test artifacts_with_status_partitions_fresh_and_idempotent_runs
asserting both fresh + idempotent runs produce the right partitioning and
that the machine-stable tag is bare 'skipped' not label()'s phrasing.
Verified output:
- Fresh dir: created[] has 4 entries, skipped[] empty
- Idempotent call: created[] empty, skipped[] has 4 entries
- project_path, next_step as first-class keys
- message preserved verbatim for backward compat
Full workspace test green except pre-existing resume_latest flake (unrelated).
Closes ROADMAP #142.
Previously, `claw <subcommand> --help` had 5 different behaviors:
- 7 subcommands returned subcommand-specific help (correct)
- init/export/state/version silently fell back to global `claw --help`
- system-prompt/dump-manifests errored with `unknown <cmd> option: --help`
- bootstrap-plan printed its phase list instead of help text
Changes:
- Extend LocalHelpTopic enum with Init, State, Export, Version, SystemPrompt,
DumpManifests, BootstrapPlan variants.
- Extend parse_local_help_action() to resolve those 7 subcommands to their
local help topic instead of falling through to the main dispatch.
- Remove init/state/export/version from the explicit wants_help=true matcher
so they reach parse_local_help_action() before being routed to global help.
- Add render_help_topic() entries for the 7 new topics with consistent
Usage/Purpose/Output/Formats/Related structure.
- Add regression test subcommand_help_flag_has_one_contract_across_all_subcommands_141
asserting every documented subcommand + both --help and -h variants resolve
to a HelpTopic with non-empty text that contains a Usage line.
Verification:
- All 14 subcommands now return subcommand-specific help (live dogfood).
- Full workspace test green except pre-existing resume_latest flake.
Closes ROADMAP #141.
Previously this test inherited the cargo test runner's CWD, which could contain
a stale .claw/settings.json with "permissionMode": "acceptEdits" written by
another test. The deprecated-field resolver then silently downgraded the
default permission mode to WorkspaceWrite, breaking the test's assertion.
Fix: wrap the assertion in with_current_dir() + env_lock() so the test runs in
an isolated temp directory with no stale config.
Full workspace test now passes except for pre-existing resume_latest flake
(unrelated to #140, environment-dependent, tracked separately).
Closes ROADMAP #140.
Updated LocalHelpTopic help strings to surface --output-format support:
- Status, Sandbox, Doctor, Acp all now show [--output-format <format>]
- Added 'Formats: text (default), json' line to each
Diagnostic verbs support JSON output but help text didn't advertise it.
Post-#127 fix: help text now matches actual CLI surface.
Verified: cargo build passes, claw doctor --help shows output-format.
Refs: #127
Diagnostic verbs (help, version, status, sandbox, doctor, state) now
reject unrecognized suffix arguments at parse time instead of silently
falling through to Prompt dispatch.
Fixes: claw doctor --json (and similar) no longer accepts --json silently
and attempts to send it to the LLM as a prompt. Now properly emits:
'unrecognized argument `--json` for subcommand `doctor`'
Joined parser-level trust gap quintet #108 + #117 + #119 + #122 + #127.
Prevents token burn on rejected arguments.
Verified: cargo build --workspace passes, claw doctor --json errors cleanly.
Refs: #127, ROADMAP
Adds ship provenance detection and emission in execute_bash_async():
- Detects git push to main/master commands
- Captures current branch, HEAD commit, git user as actor
- Emits ship.prepared event with ShipProvenance payload
- Logs to stderr as interim routing (event stream integration pending)
This is the first wired provenance event — schema (§4.44.5) now has
runtime emission at actual git operation boundary.
Verified: cargo build --workspace passes.
Next: wire ship.commits_selected, ship.merged, ship.pushed_main events.
Refs: §4.44.5.1, ROADMAP #4.44.5
#122: doctor invocation now checks stale-base condition
- Calls run_stale_base_preflight(None) in render_doctor_report()
- Emits stale-base warnings to stderr when branch is behind main
- Fixes inconsistency: doctor 'ok' vs prompt 'stale base' warning
#125: git_state field reflects non-git directories
- When !in_git_repo, git_state = 'not in git repo' instead of 'clean'
- Fixes contradiction: in_git_repo: false but git_state: 'clean'
- Applied in both doctor text output and status JSON
Verified: cargo build --workspace passes.
Refs: ROADMAP #122 (dd73962), #125 (debbcbe)
Adds run_stale_base_preflight(None) call to render_doctor_report() so that
claw doctor emits stale-base warnings to stderr when the current branch is
behind main. Previously doctor reported 'ok' even when branch was stale,
creating inconsistency with prompt path warnings.
Fixes silent-state inventory gap: doctor now consistent with prompt/repl
stale-base checking. No behavior change for non-stale branches.
Verified: cargo build --workspace passes, no test failures.
Ref: ROADMAP #122 dogfood filing @ dd73962
Adds structured ship provenance surface to eliminate delivery-path opacity:
New lane events:
- ship.prepared — intent to ship established
- ship.commits_selected — commit range locked
- ship.merged — merge completed with provenance
- ship.pushed_main — delivery to main confirmed
ShipProvenance struct carries:
- source_branch, base_commit
- commit_count, commit_range
- merge_method (direct_push/fast_forward/merge_commit/squash_merge/rebase_merge)
- actor, pr_number
Constructor methods added to LaneEvent for all four ship events.
Tests:
- Wire value serialization for ship events
- Round-trip deserialization
- Canonical event name coverage
Runtime: 465 tests pass
ROADMAP updated with IMPLEMENTED status
This closes the gap where 56 commits pushed to main had no structured
provenance trail — now emits first-class events for clawhip consumption.
Added structured delivery-path contract to surface branch → merge → main-push
provenance as first-class events. Filed from the 56-commit 2026-04-20 push
that exposed the gap.
Also fixes: ApiError test compilation — add suggested_action: None to 4 sites
- Line ~8414: opaque_provider_wrapper_surfaces_failure_class_session_and_trace
- Line ~8436: retry_exhaustion_uses_retry_failure_class_for_generic_provider_wrapper
- Line ~8499: provider_context_window_errors_are_reframed_with_same_guidance
- Line ~8533: retry_wrapped_context_window_errors_keep_recovery_guidance
Add ModelTokenLimit entries for kimi-k2.5 and kimi-k1.5 to enable
preflight context window validation. Per Moonshot AI documentation:
- Context window: 256,000 tokens
- Max output: 16,384 tokens
Includes 3 unit tests:
- returns_context_window_metadata_for_kimi_models
- kimi_alias_resolves_to_kimi_k25_token_limits
- preflight_blocks_oversized_requests_for_kimi_models
All tests pass, clippy clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add "kimi" to the strip_routing_prefix matches so that models like
"kimi/kimi-k2.5" have their prefix stripped before sending to the
DashScope API (consistent with qwen/openai/xai/grok handling).
Also add unit test strip_routing_prefix_strips_kimi_provider_prefix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Changes in rust/crates/api/src/providers/mod.rs:
- Add 'kimi' alias to MODEL_REGISTRY resolving to 'kimi-k2.5' with DashScope config
- Add kimi/kimi- prefix routing to DashScope endpoint in metadata_for_model()
- Add resolve_model_alias() handling for kimi -> kimi-k2.5
- Add unit tests: kimi_prefix_routes_to_dashscope, kimi_alias_resolves_to_kimi_k2_5
Users can now use:
- --model kimi (resolves to kimi-k2.5)
- --model kimi-k2.5 (auto-routes to DashScope)
- --model kimi/kimi-k2.5 (explicit provider prefix)
All 127 tests pass, clippy clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add structured error context to API failures:
- Request ID tracking across retries with full context in error messages
- Provider-specific error code mapping with actionable suggestions
- Suggested user actions for common error types (401, 403, 413, 429, 500, 502-504)
- Added suggested_action field to ApiError::Api variant
- Updated enrich_bearer_auth_error to preserve suggested_action
Files changed:
- rust/crates/api/src/error.rs: Add suggested_action field, update Display
- rust/crates/api/src/providers/openai_compat.rs: Add suggested_action_for_status()
- rust/crates/api/src/providers/anthropic.rs: Update error handling
All tests pass, clippy clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added criterion benchmarks and optimized flatten_tool_result_content:
- Added criterion dev-dependency and request_building benchmark suite
- Optimized flatten_tool_result_content to pre-allocate capacity and avoid
intermediate Vec construction (was collecting to Vec then joining)
- Made key functions public for benchmarking: translate_message,
build_chat_completion_request, flatten_tool_result_content,
is_reasoning_model, model_rejects_is_error_field
Benchmark results:
- flatten_tool_result_content/single_text: ~17ns
- translate_message/text_only: ~200ns
- build_chat_completion_request/10 messages: ~16.4µs
- is_reasoning_model detection: ~26-42ns
All 119 unit tests and 29 integration tests pass.
cargo clippy passes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added 4 unit tests to verify is_error field handling for kimi models:
- model_rejects_is_error_field_detects_kimi_models: Detects kimi-k2.5, kimi-k1.5, dashscope/kimi-k2.5 (case insensitive)
- translate_message_includes_is_error_for_non_kimi_models: Verifies gpt-4o, grok-3, claude include is_error
- translate_message_excludes_is_error_for_kimi_models: Verifies kimi models exclude is_error (prevents 400 Bad Request)
- build_chat_completion_request_kimi_vs_non_kimi_tool_results: Full integration test for request building
All 119 unit tests and 29 integration tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds typed worker.startup_no_evidence event with evidence bundle when worker
startup times out. The classifier attempts to down-rank the vague bucket into
specific failure classifications:
- trust_required
- prompt_misdelivery
- prompt_acceptance_timeout
- transport_dead
- worker_crashed
- unknown
Evidence bundle includes:
- Last known worker lifecycle state
- Pane/command being executed
- Prompt-send timestamp
- Prompt-acceptance state
- Trust-prompt detection result
- Transport health summary
- MCP health summary
- Elapsed seconds since worker creation
Includes 6 regression tests covering:
- Evidence bundle serialization
- Transport dead classification
- Trust required classification
- Prompt acceptance timeout
- Worker crashed detection
- Unknown fallback
Closes Phase 1.6 from ROADMAP.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ROADMAP #21, #22, and #23 were already closed on current main, so the next real repo-local backlog item was the ACP/Zed discoverability gap. This adds a local `claw acp` status surface plus aliases, updates help/docs, and separates the shipped discoverability fix from the still-open daemon/protocol follow-up so editor-first users get a crisp answer immediately.
Constraint: No ACP/Zed daemon or protocol server exists in claw-code yet, so the new surface must be explicit status guidance rather than a fake implementation
Rejected: Add a pretend `acp serve` daemon path | would imply supported protocol behavior that does not exist
Rejected: Docs-only clarification | still leaves `claw --help` unable to answer the editor-launch question directly
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep ROADMAP discoverability fixes separate from future ACP daemon/protocol work so help text and backlog IDs stay unambiguous
Tested: cargo fmt --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; cargo run -q -p rusty-claude-cli -- acp; cargo run -q -p rusty-claude-cli -- --output-format json acp; architect review APPROVED
Not-tested: Real ACP/Zed daemon launch because no protocol-serving surface exists yet
Malformed hook stdout that looks like JSON was collapsing into low-signal failure text during hook execution. This change preserves plain-text hook feedback for normal text hooks, but upgrades malformed JSON-like output into an explicit hook_invalid_json diagnostic that includes phase, tool, command, and bounded stdout/stderr previews. It also adds a regression test for malformed-but-nonempty output.
Constraint: User scoped the implementation to rust/crates/runtime/src/hooks.rs and tests only
Constraint: Existing plain-text hook feedback must remain intact for non-JSON hook output
Rejected: Treat every non-JSON stdout payload as invalid JSON | would break legitimate plain-text hook feedback
Confidence: high
Scope-risk: narrow
Directive: Keep malformed-hook diagnostics bounded and preserve the plain-text fallback for hooks that intentionally emit text
Tested: cargo test --manifest-path rust/Cargo.toml -p runtime hooks::tests:: -- --nocapture
Tested: cargo test --manifest-path rust/Cargo.toml -p runtime -- --nocapture
Tested: cargo clippy --manifest-path rust/Cargo.toml -p runtime --all-targets -- -D warnings
Not-tested: Full workspace clippy/test sweep outside runtime crate
Recent OMX dogfooding kept surfacing raw `[OMX_TMUX_INJECT]`
messages as lane results, which told operators that tmux reinjection
happened but not why or what lane/state it applied to. The lane-finished
persistence path now recognizes that control prose, stores structured
recovery metadata, and emits a human-meaningful fallback summary instead
of preserving the raw marker as the primary result.
Constraint: Keep the fix in the existing lane-finished metadata surface rather than inventing a new runtime channel
Rejected: Treat all reinjection prose as ordinary quality-floor mush | loses the recovery cause and target lane operators actually need
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Recovery classification is heuristic; extend the parser only when new operator phrasing shows up in real dogfood evidence
Tested: cargo fmt --all --check
Tested: cargo clippy --workspace --all-targets -- -D warnings
Tested: cargo test --workspace
Tested: LSP diagnostics on rust/crates/tools/src/lib.rs (0 errors)
Tested: Architect review (APPROVE)
Not-tested: Additional reinjection phrasings beyond the currently observed `[OMX_TMUX_INJECT]` / current-mode-state variants
Related: ROADMAP #68
Dogfooding kept reproducing OMX team merge conflicts on
`.clawhip/state/prompt-submit.json`, so the init bootstrap now
teaches repos to ignore `.clawhip/` alongside the existing local
`.claw/` artifacts. This also updates the current repo ignore list
so the fix helps immediately instead of only on future `claw init`
runs.
Constraint: Keep the fix narrow and centered on repo-local ignore hygiene
Rejected: Broader team merge-hygiene changes | unnecessary for the proven local root cause
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If more runtime-local artifact directories appear, extend the shared init gitignore list instead of patching repos ad hoc
Tested: cargo fmt --all --check
Tested: cargo clippy --workspace --all-targets -- -D warnings
Tested: cargo test --workspace
Tested: Architect review (APPROVE)
Not-tested: Existing clones with already-tracked `.clawhip` files still need manual cleanup
Related: ROADMAP #75