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)
## 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.
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
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.
## 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.
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
#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
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
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
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
The repo-local backlog was effectively exhausted, so this sweep promoted the
newly observed test-lock poisoning pain point into ROADMAP #74 and fixed it in
place. Test-only env/cwd lock acquisition now recovers poisoned mutexes in the
remaining strict call sites, and each affected surface has a regression that
proves a panic no longer permanently poisons later tests.
Constraint: Keep the fix test-only and avoid widening runtime behavior changes
Rejected: Refactor shared helper signatures across broader call paths | unnecessary churn beyond the remaining strict test sites
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: These guards only recover the mutex; tests that mutate env or cwd still must restore process-global state explicitly
Tested: cargo fmt --all --check
Tested: cargo clippy --workspace --all-targets -- -D warnings
Tested: cargo test --workspace
Tested: Architect review (APPROVE)
Not-tested: Additional fault-injection around partially restored env/cwd state after panic
Related: ROADMAP #74
The next repo-local sweep target was ROADMAP #72: the `latest`
managed-session alias could depend on filesystem mtime before the
session's own persisted recency markers, which made the selection
path vulnerable to coarse or misleading file timestamps. The fix
promotes `updated_at_ms` into the summary/order path, keeps CLI
wrappers in sync, and locks the mtime-vs-session-recency case with
regression coverage.
Constraint: Preserve existing managed-session storage layout while changing only the ordering signal
Rejected: Keep sorting by filesystem mtime and just sleep longer in tests | hides the semantic ordering bug instead of fixing it
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Any future managed-session ordering change must keep runtime and CLI summary structs aligned on the same recency fields
Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE
Not-tested: Cross-filesystem behavior where persisted session JSON cannot be read and fallback ordering uses mtime only
The next repo-local backlog item was ROADMAP #70: users could
mistake third-party pages or the deprecated `cargo install
claw-code` path for the official install route. The CLI now
surfaces the source of truth directly in `claw doctor` and
`claw --help`, and the roadmap closeout records the change.
Constraint: Keep the fix inside repo-local Rust CLI surfaces instead of relying on docs alone
Rejected: Close#70 with README-only wording | the bug was user-facing CLI ambiguity, so the warning needed to appear in runtime help/doctor output
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If install guidance changes later, update both the doctor check payload and the help-text warning together
Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE
Not-tested: Third-party websites outside this repo that may still present stale install instructions
The backlog sweep found that the user-cited #21-#23 items were already
closed, and the next real pain point was `claw dump-manifests` failing
without a direct way to point at the upstream manifest source. This adds
an explicit `--manifests-dir` path, upgrades the failure messages to say
whether the source root or required files are missing, and updates the
ROADMAP closeout to reflect that #45 is now fixed.
Constraint: Preserve existing dump-manifests behavior when no explicit override is supplied
Rejected: Require CLAUDE_CODE_UPSTREAM for every invocation | breaks existing build-tree workflows and is unnecessarily rigid
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep manifest-source override guidance centralized so future error-path edits do not drift
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE
Not-tested: Manual invocation against every legacy env-based manifest lookup layout
ROADMAP #36 remained open even though current main already dispatches bare
skill names in the REPL through skill resolution instead of forwarding them
to the model. This change adds a direct regression test for that behavior
and marks the backlog item done with fresh verification evidence.
Constraint: User required fresh cargo fmt, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace before closeout
Rejected: Leave #36 open because the implementation already existed | keeps the immediate backlog inaccurate and invites duplicate work
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Reopen#36 only with a fresh repro showing a listed project skill still falls through to plain prompt handling on current main
Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: No interactive manual REPL session beyond the new bare-skill unit coverage
The ROADMAP #38 closeout was rebased onto a moving main branch. That pulled in
new workspace files whose clippy/rustfmt fixes were required for the exact
verification gate the user asked for. This follow-up records those remaining
cleanups so the pushed branch matches the green tree that was actually tested.
Constraint: The user-required full-workspace fmt/clippy/test sequence had to stay green after rebasing onto newer origin/main
Rejected: Leave the rebase cleanup uncommitted locally | working tree would stay dirty and the pushed branch would not match the verified code
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: When rebasing onto a moving main, commit any gate-fixing follow-up so pushed history matches the verified tree
Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: No additional behavior beyond the already-green verification sweep
ROADMAP #37 was still open even though several earlier backlog items were
already closed. This change removes the local login/logout surface, stops
startup auth resolution from treating saved OAuth credentials as a supported
path, and updates diagnostics/help to point users at ANTHROPIC_API_KEY or
ANTHROPIC_AUTH_TOKEN only.
While proving the change with the user-requested workspace gates, clippy
surfaced additional pre-existing warning failures across the Rust workspace.
Those were cleaned up in-place so the required `cargo fmt`, `cargo clippy
--workspace --all-targets -- -D warnings`, and `cargo test --workspace`
sequence now passes end to end.
Constraint: User explicitly required full-workspace fmt/clippy/test before commit/push
Constraint: Existing dirty leader worktree had to be stashed before attempted OMX team worktree launch
Rejected: Keep login/logout but hide them from help | left unsupported auth flow and saved OAuth fallback intact
Rejected: Stop after ROADMAP #37 targeted tests | did not satisfy required full-workspace verification gate
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Do not reintroduce saved OAuth as a silent Anthropic startup fallback without an explicit supported auth policy
Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Remote push effects beyond origin/main update
ROADMAP #41 was still leaving a phantom-completion class open: managed
sessions could be resumed from the wrong workspace, and the CLI/runtime
paths were split between partially isolated storage and older helper
flows. This squashes the verified team work into one deliverable that
routes managed session operations through the per-worktree SessionStore,
rejects workspace mismatches explicitly, extends lane-event taxonomy for
workspace mismatch reporting, and updates the affected CLI regression
fixtures/docs so the new contract is enforced without losing same-
workspace legacy coverage.
Constraint: Keep same-workspace legacy flat sessions readable while blocking cross-worktree misuse
Constraint: No new dependencies; stay within the ROADMAP #41 changed-file scope
Rejected: Leave team auto-checkpoint history as final branch state | noisy/non-lore history for a single roadmap fix
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Preserve workspace_root validation on future resume/load helpers; do not reintroduce path-only fallback without equivalent mismatch checks
Tested: cargo test -p runtime session_control -- --nocapture; cargo test -p rusty-claude-cli resume -- --nocapture; cargo test -p rusty-claude-cli --test cli_flags_and_config_defaults; cargo test -p rusty-claude-cli --test output_format_contract; cargo test -p rusty-claude-cli --test resume_slash_commands; cargo test --workspace --exclude compat-harness; cargo check --workspace --all-targets; git diff --check
Not-tested: cargo clippy --workspace --all-targets -- -D warnings (pre-existing failures in unchanged rust/crates/rusty-claude-cli/build.rs)
Related: ROADMAP #41
Add 'model: Option<String>' to Session struct. The model used is now
saved in the session_meta JSONL record and surfaced in resumed /status:
- JSON mode: {model: 'claude-sonnet-4-6'} instead of null
- Text mode: shows actual model instead of 'restored-session'
Model is set in build_runtime_with_plugin_state() before the runtime
is constructed, and only when not already set (preserves model through
fork/resume cycles).
Backward compatible: old sessions without a model field load cleanly
with model: None (shown as null in JSON, 'restored-session' in text).
All workspace tests pass.
On Windows, HOME is often unset. The CLI crashed at startup with
'error: io error: HOME is not set' because three paths only checked
HOME:
- config_home_dir() in tools crate (config/settings loading)
- credentials_home_dir() in runtime crate (OAuth credentials)
- detect_broad_cwd() in CLI (CWD-is-home-dir check)
- skill lookup roots in tools crate
All now fall through to USERPROFILE when HOME is absent. Error message
updated to suggest USERPROFILE or CLAW_CONFIG_HOME on Windows.
Source: MaxDerVerpeilte in #claw-code (Windows user, 2026-04-10).
claw --output-format json --resume <session> (no command) was printing:
'Restored session from <path> (N messages).'
to stdout as prose, regardless of output format.
Now emits:
{"kind":"restored","session_id":"...","path":"...","message_count":N}
159 CLI tests pass.
/session list previously returned 'unsupported resumed slash command' in
--output-format json --resume mode. It only reads the sessions directory
so does not need a live runtime session.
Adds a Session{action:"list"} arm in run_resume_command() before the
unsupported catchall. Emits:
{kind:session_list, sessions:[...ids], active:<current-session-id>}
159 CLI tests pass.
'failed to restore session' errors from both the path-resolution step
and the JSONL-load step now check output_format and emit:
{"type":"error","error":"failed to restore session: <detail>"}
instead of bare eprintln prose.
Covers: session not found, corrupt JSONL, permission errors.
/stats was accidentally listed in STUB_COMMANDS (both in the original list
and overlooked in 1e14d59). Since SlashCommand::Stats is fully implemented
with REPL and resume dispatch, it should not be intercepted as unimplemented.
/tokens and /cache alias to Stats and were already working correctly.
/stats now works again in all modes.