Compare commits

...

88 Commits

Author SHA1 Message Date
YeonGyu-Kim
63931c74fb fix: mcp show (missing server name) emits missing_argument error_kind (#830)
`claw --output-format json mcp show` (no server name) previously emitted
error_kind:"unknown_mcp_action" — misleading because `show` IS a known
action; the problem is a missing required argument.

Fix:
- `render_mcp_report_json_for`: `Some("show")` arm now emits a dedicated
  JSON response with error_kind:"missing_argument" + usage hint
- `classify_error_kind`: add classifier arm for "missing_argument:" prefix

One new test: mcp_show_missing_server_name_emits_missing_argument

572+ tests pass.
2026-05-29 17:07:15 +09:00
YeonGyu-Kim
4d3dc5b873 docs: record #830 - mcp show missing server name emits unknown_mcp_action instead of missing_argument 2026-05-29 16:57:22 +09:00
YeonGyu-Kim
ac5b19dee1 fix: interactive_only hint omits --resume for non-resume-safe commands (#829)
Commands like /commit, /pr, /issue, /bughunter, /ultraplan are
interactive-only and NOT resume-safe. Previously the generic
interactive_only error always suggested 'claw --resume SESSION.jsonl
/commit', which would just re-trigger interactive_only.

Fix: check commands::resume_supported_slash_commands() in the
SlashCommand::Ok(Some(cmd)) arm. Resume-safe commands get the full
--resume suggestion; non-resume-safe commands only say 'Start claw'.

Also update two existing unit tests whose assertions checked for the old
'interactive-only' substring (now 'interactive_only:' prefix).

Two new integration tests:
- non_resume_safe_interactive_only_hint_omits_resume_suggestion
- resume_safe_interactive_only_hint_includes_resume_suggestion

572 tests pass, 1 pre-existing worker_boot failure unrelated.
2026-05-29 16:55:57 +09:00
YeonGyu-Kim
fdfb9f4dc1 docs: record #829 - interactive_only hint incorrectly suggests --resume for non-resume-safe commands 2026-05-29 16:38:04 +09:00
YeonGyu-Kim
187aebd74f fix: /approve and /deny outside REPL emit interactive_only error_kind (#828)
/approve, /yes, /deny, /no (and /y, /n) are valid REPL-only slash
commands. Outside the REPL they were falling through to
format_unknown_direct_slash_command -> error_kind:unknown_slash_command.

Fix: intercept them in the SlashCommand::Unknown arm and emit
interactive_only: prefix so classify_error_kind returns the correct kind.

One new test: approve_deny_outside_repl_emits_interactive_only (covers
/approve, /yes, /deny, /no)

572 tests pass, 1 pre-existing worker_boot failure unrelated.
2026-05-29 16:36:54 +09:00
YeonGyu-Kim
9d05573f24 fix: unknown slash command emits unknown_slash_command error_kind (#827)
Both direct-slash CLI path (claw /boguscommand) and resume slash path
(claw --resume session /boguscommand) previously emitted error_kind:unknown
(opaque fallback). Machine consumers could not distinguish unrecognized
slash commands from other error classes.

Fix:
- format_unknown_direct_slash_command: prefix with 'unknown_slash_command:'
- format_unknown_slash_command (resume path): prefix with 'unknown_slash_command:'
- Add classifier arm for 'unknown_slash_command:' prefix

One new regression test: direct_unknown_slash_command_emits_typed_error_kind
Uses the direct-slash CLI path (no session load needed; reproducible on CI).

572 tests pass, 1 pre-existing worker_boot failure unrelated.
2026-05-29 16:00:37 +09:00
YeonGyu-Kim
58902915f6 docs: record #827 - resume unknown slash command emits opaque error_kind:unknown 2026-05-29 14:59:12 +09:00
YeonGyu-Kim
d47b015100 fix: unknown single-word subcommand emits command_not_found (#825/#826)
Single-word all-alpha/dash tokens that don't match any known subcommand
now always emit command_not_found (with or without fuzzy suggestions).

Multi-word cases fall through to CliAction::Prompt (natural language
prompt passthrough like 'claw explain this' must still work). The
multi-word gap is documented as ROADMAP #826 (known limitation).

Tests:
- unknown_subcommand_json_emits_command_not_found (new)
- unknown_subcommand_text_emits_command_not_found_on_stderr (new)
- unknown_subcommand_typo_with_suggestions_json_emits_command_not_found (new)
- multi_word_unknown_subcommand_falls_through_to_prompt_826 (documents gap)

572 tests pass, 1 pre-existing worker_boot failure unrelated.
2026-05-29 14:58:07 +09:00
YeonGyu-Kim
5458d3547a docs: record #826 - multi-word unknown subcommand falls through to missing_credentials 2026-05-29 14:38:09 +09:00
YeonGyu-Kim
70d64be033 fix: unknown single-word subcommand emits command_not_found instead of missing_credentials (#825)
When looks_like_subcommand_typo fires on a single word with no close
fuzzy matches, the fallthrough reached CliAction::Prompt → provider
startup → misleading missing_credentials error.

Fix: always return Err with command_not_found: prefix from the typo
guard (with or without suggestions). Added command_not_found classifier
arm in classify_error_kind. Unified existing unknown_subcommand kind
under command_not_found in #825.

Three new regression tests in output_format_contract.rs:
- unknown_subcommand_json_emits_command_not_found
- unknown_subcommand_text_emits_command_not_found_on_stderr
- unknown_subcommand_typo_with_suggestions_json_emits_command_not_found

Updated pre-existing unit test assertion (starts_with → contains) and
classifier unit test (unknown_subcommand → command_not_found).

572 tests pass, 1 pre-existing worker_boot failure unrelated.
2026-05-29 14:37:29 +09:00
YeonGyu-Kim
de7edd5bb1 fix: suppress config deprecation stderr in JSON mode globally (#824)
Add SUPPRESS_CONFIG_WARNINGS_STDERR AtomicBool flag in runtime/config.rs
and expose suppress_config_warnings_for_json_mode() via runtime crate.

In main.rs, scan raw argv for --output-format json before parse_args
and activate the flag so no settings-load warnings reach stderr on any
JSON-mode surface (status, sandbox, system-prompt, mcp list, skills list,
agents list, --resume /config*, etc.).

Text-mode surfaces are unaffected; prose deprecation warnings continue
to appear on stderr.

All 572+ tests pass (one pre-existing worker_boot failure unrelated).
2026-05-29 14:00:32 +09:00
YeonGyu-Kim
f0e6671538 docs: record #824 - global settings-load deprecation leaks to stderr in JSON mode 2026-05-29 13:34:29 +09:00
YeonGyu-Kim
b4b1ba10f6 fix: route all JSON-mode abort envelopes to stdout (#819 #820 #823) (#3197)
* fix: route all JSON-mode abort envelopes to stdout (#819 #820 #823)

All handled errors in --output-format json mode now write the structured
abort envelope to stdout (rc=1) and keep stderr empty. Previously the
top-level error handler and resume_session JSON branches used eprintln!
which sent the envelope to stderr, breaking machine consumers that read
stdout for command payloads.

Surfaces fixed:
- Top-level abort handler (main.rs): export --session <missing>,
  session <subcommand>, prompt (no text), unknown subcommand fallthrough,
  flag errors, and all other run() failures
- resume_session JSON branches: session load errors, unsupported commands,
  parse errors, command execution errors

Test changes: updated 24 failing contract tests to assert JSON envelopes
on stdout. Added stderr-clean assertions where appropriate. 70 contract
tests pass (was 68; 2 additional from regression coverage).

ROADMAP: #819 (export session-not-found), #820 (interactive_only class),
#823 (missing prompt)

* style: cargo fmt on main.rs after eprintln->println fix

* fix(tests): fmt + update compact_output test for stdout abort envelope routing

* fix(tests): update resume_slash_commands stub test for stdout envelope routing
2026-05-29 13:30:35 +09:00
YeonGyu-Kim
e50c46c1ed docs: extend #821 - config/providers also leak deprecation warning in JSON mode 2026-05-29 12:01:09 +09:00
YeonGyu-Kim
3dbb35c3aa docs: record prompt missing-text JSON stderr routing gap (#823) 2026-05-29 11:31:28 +09:00
YeonGyu-Kim
3a76c4f4fd docs: record unknown subcommand falls through to provider startup (#822) 2026-05-29 11:01:13 +09:00
YeonGyu-Kim
69b59079c5 docs: record status/sandbox/system-prompt JSON stderr deprecation leak (#821) 2026-05-29 10:31:13 +09:00
YeonGyu-Kim
42aff269d1 docs: record interactive_only error class JSON stderr routing gap (#820) 2026-05-29 10:01:01 +09:00
YeonGyu-Kim
efe59c22e4 docs: record export session-not-found JSON stderr routing gap (#819) 2026-05-29 09:33:25 +09:00
YeonGyu-Kim
37a9a543d6 docs: record AGENTS.md and .claude/CLAUDE.md instruction cascade gap (#818) 2026-05-29 08:09:51 +09:00
Bellman
0800d7ae88 Route plugins list JSON parse errors to stdout (#3194) 2026-05-28 22:35:58 +09:00
Bellman
69b8b367c1 docs: record plugins trailing dash json routing (#3193) 2026-05-28 21:35:25 +09:00
Bellman
9494e3c26f Suppress config warnings on JSON local surfaces (#3192) 2026-05-28 20:34:18 +09:00
Bellman
ed3a616e62 docs: record global json warning leak (#3191) 2026-05-28 18:36:30 +09:00
Bellman
89e7f415a9 Avoid duplicate config warnings for JSON consumers (#3190)
JSON config output already carries collected config diagnostics in warnings[], so prose stderr emission must be reserved for text/local paths. Lazy permission-mode default resolution prevents an earlier config load from leaking the same deprecation before the JSON renderer runs.\n\nConstraint: ROADMAP #815 requires text mode to keep human stderr warnings while JSON config/list suppresses duplicate app-level config prose.\nRejected: Filtering all stderr in JSON mode | would hide cargo/compiler or unrelated diagnostics outside the app config warning path.\nConfidence: high\nScope-risk: narrow\nDirective: Keep load_collecting_warnings side-effect-free; use load() for human stderr emission.\nTested: cargo fmt; cargo test -p rusty-claude-cli --test output_format_contract config_json_reports_deprecations_structurally_without_stderr_duplicate_815; cargo test -p rusty-claude-cli --test output_format_contract; manual target/debug/claw JSON config fixture.\nNot-tested: cargo clippy -p rusty-claude-cli --all-targets -- -D warnings is blocked by pre-existing runtime dead_code/trident warnings.
2026-05-28 18:09:59 +09:00
Bellman
c3e7b6af60 docs: record config json warning duplication (#3189) 2026-05-28 17:05:57 +09:00
Bellman
3af2d9f986 docs: verify trailing json inventory gap resolved (#3188) 2026-05-28 16:36:11 +09:00
Bellman
09ff1caf42 docs: record trailing json inventory timeout (#3187) 2026-05-28 16:05:50 +09:00
Bellman
0e6d48d9dc docs: record argv-safe dogfood probe gap (#3186) 2026-05-28 15:34:04 +09:00
Bellman
b7ea04661a test: cover doctor help JSON flag order (#3185) 2026-05-28 14:34:19 +09:00
Bellman
73d8d6e638 Keep doctor help machine-discoverable locally (#3184)
Doctor help was already on the local help path in current source, but the exact #702 dogfood surface lacked a focused guard and the JSON help envelope was still too prose-oriented for wrappers. Strengthen the JSON contract while preserving text help.\n\nConstraint: Preserve unrelated dirty rust/Cargo.lock from prior #701 work.\nRejected: Starting runtime/provider/session to inspect doctor semantics | help must be local and credential-free.\nConfidence: high\nScope-risk: narrow\nDirective: Keep doctor help routed through parse_local_help_action and print_help_topic; do not call run_doctor for --help.\nTested: cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract doctor_help -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract help -- --nocapture; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p rusty-claude-cli; timeout 5s cargo run -q --bin claw -- --output-format json doctor --help; timeout 5s cargo run -q --bin claw -- doctor --help.\nNot-tested: full workspace test suite.
2026-05-28 13:31:39 +09:00
Bellman
9ac66cbeb3 docs: quote dogfood build trap cleanup guidance (#3183) 2026-05-28 12:35:47 +09:00
Bellman
773aa021be docs: use trap cleanup in dogfood build guidance (#3182) 2026-05-28 12:06:17 +09:00
Bellman
5c3e1c1444 fix: add dogfood build help handling (#3181) 2026-05-28 11:36:13 +09:00
Bellman
3260258b56 fix: make cc2 renderer path errors concise (#3180) 2026-05-28 11:08:26 +09:00
Bellman
a88d52fe88 fix: make cc2 validator directory board error concise (#3179) 2026-05-28 10:38:57 +09:00
Bellman
60f44d314b fix: avoid cc2 generator dirs on missing source (#3178) 2026-05-28 10:07:38 +09:00
Bellman
d4e9829329 fix: suppress partial cc2 wrapper validate pass output (#3177) 2026-05-28 09:36:18 +09:00
Bellman
e17098cc70 fix: resolve cc2 wrapper tools from script root (#3176) 2026-05-28 08:36:16 +09:00
Bellman
e17936158a fix: make cc2 validator board read errors concise (#3175) 2026-05-28 08:06:28 +09:00
Bellman
760e69675c fix: make cc2 generator missing source error concise (#3174) 2026-05-28 07:36:47 +09:00
Bellman
193f11171a fix: reject extra roadmap helper paths (#3173) 2026-05-28 06:34:44 +09:00
Bellman
f11ac23e1f fix: add roadmap next-id help handling (#3172) 2026-05-28 06:06:42 +09:00
Bellman
c4770e6571 docs(roadmap): add #811 json error envelope nontty hangs (#3171) 2026-05-28 05:35:57 +09:00
Bellman
b0e94c996b docs(roadmap): add #810 json stdout warning contamination (#3169) 2026-05-28 05:05:17 +09:00
Bellman
85d63b071c docs(roadmap): add #809 help mcp plugin json hangs (#3168) 2026-05-28 04:30:30 +09:00
Bellman
db81598525 docs(roadmap): add #808 control-plane json hangs (#3166) 2026-05-28 03:32:15 +09:00
Bellman
86f45a11ef docs(roadmap): add #807 model json hang (#3163) 2026-05-28 01:33:01 +09:00
YeonGyu-Kim
87b7e74770 fix(#806): plugins show <not-found> in text mode returned empty success instead of error 2026-05-27 22:34:10 +09:00
Bellman
ae6a207d4e fix(#3129): handle trailing json format for diff errors (#3161)
Keep malformed diff invocations with trailing JSON format flags on the parser error path and lock the contract with focused output-format regressions.

Constraint: Do not touch tracked .omx state files.

Rejected: Repeating direct binary smoke loops | local auth/provider configuration intercepts those invocations and obscures parser behavior.

Confidence: high

Scope-risk: narrow

Tested: git diff --check; cargo fmt --check; cargo test -p rusty-claude-cli diff_extra_args_have_typed_error_kind_and_hint_766 --test output_format_contract; cargo test -p rusty-claude-cli diff_trailing_json_after_malformed_args_is_bounded_json_3129 --test output_format_contract; cargo test -p rusty-claude-cli diff_non_git_dir_has_error_kind_and_hint_801 --test output_format_contract
2026-05-27 21:57:26 +09:00
YeonGyu-Kim
efd34c151a fix(#805): skills show <not-found> in text mode silently returned empty success instead of error 2026-05-27 21:05:41 +09:00
YeonGyu-Kim
2c3c0f60e7 fix(#804): agents/skills show <name> <extra> in text mode returned wrong error instead of unexpected_extra_args 2026-05-27 20:05:39 +09:00
YeonGyu-Kim
bad1b97f8e fix(#803): agents/skills/plugins list --flag in text mode silently returned empty success 2026-05-27 19:38:31 +09:00
YeonGyu-Kim
fcebf64468 fix(#802): four resume-mode and broad-cwd error envelopes now include hint field 2026-05-27 19:04:15 +09:00
YeonGyu-Kim
53953a8157 fix(#801): diff non-git-dir error envelope now includes error_kind, hint, and message fields 2026-05-27 18:34:58 +09:00
YeonGyu-Kim
1201dc60ef docs(roadmap): add deferred entries #798-#800 (plugins extra-arg, empty-prompt, classifier coverage) 2026-05-27 18:21:35 +09:00
Bellman
23d7761e50 docs(roadmap): add #786 installed binary provenance gap (#3126) 2026-05-27 18:21:02 +09:00
YeonGyu-Kim
6ee67d6c61 test: add unit test coverage for invalid_history_count and unknown_option classifier arms
Two classifier arms had no corresponding assert_eq! in
test_classify_error_kind_returns_correct_discriminants: invalid_history_count
(both prefix and contains paths) and unknown_option (#790). Now 49/39 = full
coverage of all classify_error_kind return values.
2026-05-27 18:05:33 +09:00
YeonGyu-Kim
efb1542a39 fix: empty-prompt error now returns non-null hint via newline-delimited usage string
claw '' and claw '   ' returned empty_prompt + hint:null because the
error message had no newline delimiter. Added usage hint. 61 CLI
contract tests pass.
2026-05-27 16:34:37 +09:00
YeonGyu-Kim
bff370003b fix: plugins extra-arg errors now return non-null hint via newline-delimited usage string
Parity with #791 (config extra-arg fix). The plugins arg parser emitted
'unexpected extra arguments after claw plugins show ...' with no newline
delimiter, so split_error_hint returned None. Added usage hint after newline.
60 CLI contract tests pass.
2026-05-27 15:04:03 +09:00
YeonGyu-Kim
9976585f87 fix(#796): agents/skills show <name> <extra> returned wrong not-found instead of unexpected_extra_args 2026-05-27 14:07:04 +09:00
YeonGyu-Kim
18b4cee5fd fix(#795): skill_not_found and unsupported_skills_action now return non-null hints via fallback table 2026-05-27 13:34:09 +09:00
YeonGyu-Kim
491f179a03 fix(#794): plugins install not-found path returns typed plugin_source_not_found instead of unknown+null 2026-05-27 13:08:14 +09:00
YeonGyu-Kim
57a57ef771 fix(#793): plugins list --flag silent success + uninstall not-found hint:null 2026-05-27 12:34:35 +09:00
YeonGyu-Kim
abfa2e4cf7 fix(#792): agents/skills list --flag silently returned empty success; now returns unknown_option error 2026-05-27 11:39:44 +09:00
YeonGyu-Kim
93a159dca5 fix(#791): config extra-arg errors now return non-null hint via \n-delimited usage string 2026-05-27 11:04:50 +09:00
YeonGyu-Kim
9968a27e92 fix(#790): system-prompt unknown-option errors now return typed unknown_option kind + non-null hint 2026-05-27 10:36:12 +09:00
YeonGyu-Kim
e4c3c1aa80 fix(#789): agents show and plugins show not-found now exit 1; parity with skills (#788) and mcp (#68) 2026-05-27 10:07:51 +09:00
YeonGyu-Kim
abdbf61acf fix(#788): skills show not-found emitted duplicate JSON error envelope; use exit(1) instead of Err propagation 2026-05-27 09:36:11 +09:00
YeonGyu-Kim
113145a42a fix(#787): --resume with directory path returns session_path_is_directory kind + hint; wire fallback_hint_for_error_kind into both resume error emission sites 2026-05-27 09:06:28 +09:00
YeonGyu-Kim
22b423b651 fix(#786): dump-manifests --manifests-dir missing-value errors now return typed missing_flag_value kind + non-null hint 2026-05-27 08:39:11 +09:00
YeonGyu-Kim
87f4334728 fix(#785): add unknown_subcommand classifier arm for unknown subcommand: prose prefix 2026-05-27 08:36:41 +09:00
YeonGyu-Kim
e628b4bb68 fix(#784): export --output missing-value and extra-positional errors now return typed error_kind + non-null hint 2026-05-27 08:07:32 +09:00
YeonGyu-Kim
81fe0ccbb7 fix(#783): init JSON envelope now includes hint and already_initialized fields for orchestrator parity 2026-05-27 08:04:15 +09:00
YeonGyu-Kim
32c9276fdb fix(#782): acp unsupported invocation now returns non-null hint with newline-delimited remediation text 2026-05-27 07:37:26 +09:00
YeonGyu-Kim
16c1117af6 fix(#781): sub-classify api_auth_error/api_rate_limit_error from api_http_error; add fallback_hint_for_error_kind for hint-less API errors 2026-05-27 07:34:57 +09:00
YeonGyu-Kim
d9844cfe8d fix(#780): classifier arm ordering bug — legacy_session_no_workspace_binding and no_managed_sessions shadowed by generic session_load_failed arm 2026-05-27 05:34:49 +09:00
YeonGyu-Kim
364e7909f4 fix(#779): resumed /skills invocation returns interactive_only error_kind + non-null hint 2026-05-27 05:09:07 +09:00
YeonGyu-Kim
fded4f6b11 fix(#778): doctor check JSON objects now include hint field with stable remediation text for warn/fail checks 2026-05-27 05:07:02 +09:00
YeonGyu-Kim
e02030364d fix(#777): resumed /plugins mutations return interactive_only error_kind + non-null hint instead of unknown+null 2026-05-27 04:44:06 +09:00
YeonGyu-Kim
2684737d9e fix(#776): resume command errors now return typed error_kind + non-null hint (invalid_history_count, session action errors) 2026-05-27 04:39:43 +09:00
YeonGyu-Kim
028998d040 test(#775): integration tests for #769-#771 interactive-only guards and #774 hint fields; fix stale classifier unit test string 2026-05-27 04:03:52 +09:00
YeonGyu-Kim
c760a49c47 fix(#774): agents/plugins/mcp unknown-subcommand errors now include non-null hint 2026-05-27 03:37:00 +09:00
YeonGyu-Kim
727a1ea4a3 fix(#773): config --output-format json now surfaces deprecation warnings in warnings[] array instead of only stderr text 2026-05-27 03:05:14 +09:00
YeonGyu-Kim
212f0b2ad4 fix(#772): slash command aliases now resolve to canonical forms in interactive_only guidance 2026-05-27 02:37:17 +09:00
YeonGyu-Kim
bf212b986d fix(#771): init rejects extra args; usage/stats/fork return interactive_only instead of credential check 2026-05-27 02:33:55 +09:00
YeonGyu-Kim
3a1d88386c fix(#770): cost/clear/memory/ultraplan/model with args now return interactive_only instead of falling to credential check 2026-05-27 02:10:41 +09:00
YeonGyu-Kim
9e1be05634 fix(#769): claw session <arg> now returns interactive_only instead of falling to credential check 2026-05-27 02:05:14 +09:00
15 changed files with 3290 additions and 174 deletions

View File

@@ -55,8 +55,15 @@ REQUIRED_ITEM_FIELDS = [
def load_board(path: Path) -> dict[str, Any]:
with path.open() as f:
board = json.load(f)
try:
with path.open() as f:
board = json.load(f)
except FileNotFoundError:
raise ValueError(f"board not found at {path}") from None
except IsADirectoryError:
raise ValueError(f"board path is a directory: {path}") from None
except json.JSONDecodeError as exc:
raise ValueError(f"invalid board JSON at {path}: {exc}") from None
if not isinstance(board, dict):
raise ValueError("board JSON root must be an object")
items = board.get("items")
@@ -226,7 +233,11 @@ def main() -> int:
parser.add_argument("--check", action="store_true", help="fail if board_md is not up to date")
args = parser.parse_args()
board = load_board(args.board_json)
try:
board = load_board(args.board_json)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
errors = validate_board(board)
if errors:
for error in errors:
@@ -234,14 +245,22 @@ def main() -> int:
return 1
rendered = render(board)
if args.check:
existing = args.board_md.read_text() if args.board_md.exists() else ""
try:
existing = args.board_md.read_text() if args.board_md.exists() else ""
except IsADirectoryError:
print(f"ERROR: board markdown path is a directory: {args.board_md}", file=sys.stderr)
return 1
if existing != rendered:
print(f"ERROR: {args.board_md} is not up to date", file=sys.stderr)
return 1
print(f"PASS: {args.board_md} is up to date and roadmap coverage is complete")
return 0
args.board_md.parent.mkdir(parents=True, exist_ok=True)
args.board_md.write_text(rendered)
try:
args.board_md.write_text(rendered)
except IsADirectoryError:
print(f"ERROR: board markdown path is a directory: {args.board_md}", file=sys.stderr)
return 1
print(f"wrote {args.board_md}")
return 0

View File

@@ -7703,3 +7703,206 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
767. **`claw session bogus --output-format json` ignores JSON flag and falls through to credential check** — dogfooded 2026-05-27 on `d29a8e21`. `claw --output-format json session bogus` dispatches to the full interactive REPL runtime instead of rejecting `bogus` as an unknown session subcommand. Output is `error_kind:"missing_credentials"` rather than `error_kind:"unknown_session_subcommand"`. Root cause: `session` arg parser has no unknown-subcommand guard before dispatch; `bogus` is silently accepted as a session ID / switch target and reaches the credential-check gate. Fix needed: validate known session subcommands (`list`, `exists`, `switch`, `fork`, `delete`) before dispatch, return structured `unknown_session_subcommand` error for unrecognized tokens. [SCOPE: claw-code] Source: Jobdori probe on `d29a8e21`, 2026-05-27.
768. **`claw --resume latest compact` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `89735dbd` (gaebal-gajae pinpoint against `d29a8e21`, revised ID after #766/#767 landed). Resume trailing-arg validator emitted single-line `"--resume trailing arguments must be slash commands"` with no typed prefix and no `\n` hint. Fix: (1) changed error to `"invalid_resume_argument: \`{token}\` is not a slash command.\nUsage: claw --resume <session-id|latest> /<slash-command>"` so `split_error_hint()` extracts the hint; (2) added `invalid_resume_argument` classifier arm; (3) unit test assertion + integration test `resume_non_slash_trailing_arg_has_typed_error_kind_and_hint_768` added. 34 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae + Jobdori probe on `89735dbd`, 2026-05-27.
769. **`claw session bogus` fell through to credential check instead of interactive-only guidance** — dogfooded 2026-05-27 on `b778d4e3` (tracked as #767). `claw session <anything>` with more than one token bypassed `parse_single_word_command_alias` (which only fires for `rest.len()==1`) and had no match arm in `parse_args`, so `rest.join(" ")` became a prompt literal dispatched to `CliAction::Prompt`, hitting `missing_credentials` at the gate. Fix: added `"session"` match arm that emits `interactive_only:` error with `\n`-delimited hint referencing `--resume SESSION.jsonl /session` and REPL usage. Integration test `session_with_unknown_subcommand_returns_interactive_only_not_credentials_767` asserts `error_kind:interactive_only` + non-null hint for `bogus`, `nuke`, `delete-all`. 35 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori probe on `b778d4e3`, 2026-05-27.
770. **`claw cost/clear/memory/ultraplan/model` with trailing args fell to credential check** — dogfooded 2026-05-27 on `9e1be056`. Same fallthrough gap as #767/#769: these slash-only verbs had no multi-arg match arms, so `claw cost breakdown`, `claw clear --force`, `claw memory reset`, `claw ultraplan bogus`, `claw model opus extra` all became `CliAction::Prompt` literals, hitting `missing_credentials` at the gate. Fix: added `"cost"`, `"clear"`, `"memory"`, `"ultraplan"`, `"model" if rest.len() > 1` match arms, each returning `interactive_only:` + `\n`-delimited hint. Integration test `slash_only_verbs_with_args_return_interactive_only_not_credentials_770` asserts all five cases. 36 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori sweep on `9e1be056`, 2026-05-27.
771. **`init extraarg` silently succeeded; `usage`/`stats`/`fork` with args fell to credential check** — dogfooded 2026-05-27 on `3a1d8838`. Two distinct gaps: (1) `claw init extraarg` returned `status:ok` with trailing positional ignored — `"init"` arm always returned `Ok(CliAction::Init)` regardless of `rest[1..]`; (2) `claw usage extra`, `claw stats extra`, `claw fork newbranch` had no match arms and fell to `CliAction::Prompt` + credential gate. Fixes: (1) added extra-arg check in `"init"` arm — rejects with `unexpected_extra_args:` prefix + `\n` usage hint; (2) added `"usage"`, `"stats"`, `"fork"` interactive-only arms. All four now return correct `error_kind` + non-null hint. 36 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori sweep on `3a1d8838`, 2026-05-27.
772. **Slash command aliases bypassed `bare_slash_command_guidance` lookup** — dogfooded 2026-05-27 on `bf212b98`. `bare_slash_command_guidance()` only checked `spec.name == command_name`, not `spec.aliases`, so `claw yes`, `claw no`, `claw y`, `claw n`, `claw skill`, `claw cwd` all fell through (either to typo suggestions or `missing_credentials`). Should have returned `interactive_only:` guidance referencing the canonical form. Fix: (1) lookup changed to `spec.name == command_name || spec.aliases.contains(&command_name)`; (2) capture `canonical_name = slash_command.name`; (3) guidance strings updated to reference canonical form in remediation (e.g., `claw yes → /approve`, `claw n → /deny`, `claw skill → /skills`). 36 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint on `bf212b98`, 2026-05-27.
773. **Config deprecation warnings only emitted as unstructured stderr text in `--output-format json` mode** — dogfooded 2026-05-27 on `212f0b2a`. `emit_config_warning_once()` always wrote to stderr regardless of output format, causing JSON-mode callers to receive an unexpected `warning: ...` text line on stderr before the JSON object. Callers had to implement ad-hoc stripping. Fix: added `ConfigLoader::load_collecting_warnings()` method that returns `(RuntimeConfig, Vec<String>)` so callers can surface warnings structurally; `render_config_json()` now uses this and includes a `warnings: []` array in the config JSON envelope. Existing `load()` path unchanged (still emits to stderr for text-mode callers). 36 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori startup-friction probe on `212f0b2a`, 2026-05-27.
774. **`claw agents bogus`, `claw plugins bogus`, `claw mcp bogus` returned `hint: null`** — dogfooded 2026-05-27 on `727a1ea4`. Three "unknown subcommand" envelopes had `error_kind` correctly set but `hint: null`: (1) `unknown_agents_subcommand` — both text and JSON handler emitted single-line error with inline remediation after `.`, no `\n`; (2) `unknown_plugins_action` — same, period-delimited remediation; (3) `unknown_mcp_action``render_mcp_usage_json` never included a `hint` field at all. Fixes: (1)+(2) added `\n` before remediation suffix in `commands/src/lib.rs`; (3) added `hint` field to `render_mcp_usage_json` pointing at supported actions. All three now return non-null `hint`. 36 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori envelope-consistency probe on `727a1ea4`, 2026-05-27.
775. **Missing integration tests for #769-#771 interactive-only guards and #774 hint fields** — dogfooded 2026-05-27 on `c760a49c`. Fixes #769-#771 (session/cost/clear/memory/ultraplan/model/usage/stats/fork interactive-only guards) and #774 (agents/plugins/mcp unknown-subcommand hints) had no integration tests — a regression in any of those 10+ match arms would go undetected. Also: classify_error_kind unit test for `unknown_agents_subcommand` used the old single-line format string, not the `\n`-delimited format emitted after #774. Fixed: (1) updated unit test string to match new `\n`-delimited emission; (2) added `agents_plugins_mcp_unknown_subcommand_have_hint_774` asserting `error_kind` + non-null `hint` for all three; (3) added `interactive_only_guard_batch_769_to_771` asserting `interactive_only` + non-null `hint` for 10 cases. 38 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori test-coverage sweep on `c760a49c`, 2026-05-27.
776. **Resume-mode JSON errors had opaque `error_kind:"resume_command_error"` + `hint:null`** — dogfooded 2026-05-27 on `028998d0` (pinpoint identified by Gaebal-gajae). `run_resume_command` returned errors (e.g. from `parse_history_count`) with hardcoded `error_kind:"resume_command_error"` and the full error string in `error` with no hint extraction. Wrappers had to regex prose instead of switching on typed fields. Three co-located gaps fixed: (1) `resume_session` JSON error path now applies `classify_error_kind` + `split_error_hint` so errors get specific `error_kind` (e.g. `invalid_history_count`) and non-null `hint`; (2) `parse_history_count` errors now use `invalid_history_count:` prefix + `\n` usage hint; (3) `/session exists|delete|switch|fork` missing-arg and unsupported-action errors now use `\n`-delimited format with `unsupported_resumed_command:` prefix. Existing test updated to match new error message format. 38 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `028998d0`, 2026-05-27.
777. **Resumed `/plugins install|enable|disable|uninstall|update` returned opaque error_kind instead of interactive_only** — dogfooded 2026-05-27 on `2684737d` (pinpoint by Gaebal-gajae). The mutation arm in `run_resume_command` returned a bare single-line error; after #776 it was classified/split by the caller but fell to `error_kind:"unknown"` + `hint:null` because there was no `interactive_only:` prefix. Orchestrators had no stable signal to distinguish "command rejected — switch to REPL" from a transient error. Fix: each mutation verb now returns `interactive_only: /plugins {action} requires a live session...\n...hint...` so the caller emits `error_kind:"interactive_only"` + non-null hint pointing at REPL or direct CLI. Integration test `resume_plugin_mutations_are_typed_interactive_only_777` covers all 5 mutation verbs. 39 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `2684737d`, 2026-05-27.
778. **`claw doctor --output-format json` check objects had no `hint` field — all warn/fail remediation was buried in `details_prose`** — dogfooded 2026-05-27 on `e0203036`. Automation had to parse prose strings to find remediation text instead of reading a stable `hint` field. `DiagnosticCheck.json_value()` never emitted a `hint` field. Fix: added `hint: Option<String>` field to `DiagnosticCheck`, added `with_hint()` builder, populated for all warn/fail cases (auth: set env var; config: fix JSON syntax; workspace: git init; boot_preflight: install missing binaries; sandbox: expected on non-Linux). Empty hint string collapses to `null` (ok checks). 39 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori doctor-envelope probe on `e0203036`, 2026-05-27.
779. **Resumed `/skills <skill>` invocation returned bare prose → `error_kind:"unknown"` + `hint:null` after #776** — dogfooded 2026-05-27 on `fded4f6b` (pinpoint by Gaebal-gajae). Sibling of #777: the `/skills` invoke-dispatch guard emitted a single-line prose error identical in structure to the pre-#777 plugins mutation guard. After #776's classify/split it fell to `unknown+null` because no `interactive_only:` prefix was present. Fix: replaced with `interactive_only: /skills {skill_name} invocation requires a live session.\n...hint...` format. Integration test `resume_skills_invocation_is_typed_interactive_only_779` added. 40 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `fded4f6b`, 2026-05-27.
780. **`classify_error_kind` arm ordering bug: `"failed to restore session: legacy session is missing workspace binding: ..."` classified as `session_load_failed` instead of `legacy_session_no_workspace_binding`** — dogfooded 2026-05-27 on `364e7909`. The full error message from `resume_session` prepends `"failed to restore session: "` before `"legacy session is missing workspace binding: ..."`. The `contains("failed to restore session")` arm at line 278 matched first, returning `session_load_failed`; the more specific `legacy_session_no_workspace_binding` arm at line 282 was never reached. Same shadowing existed for `no_managed_sessions`. Fix: reordered the three arms — specific cases (`no_managed_sessions`, `legacy_session_no_workspace_binding`) before the generic `session_load_failed` catch-all. Unit test updated to assert corrected discriminants, plus new assertion covering the full prefixed message that exposed the bug. 40 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori classifier-ordering probe on `364e7909`, 2026-05-27.
781. **`api_http_error` was a single bucket for all HTTP errors; 401 auth and 429 rate-limit returned `hint:null` with no distinction** — dogfooded 2026-05-27 on `d9844cfe`. `classify_error_kind` had a single `api_http_error` arm for all API failures. 401 Unauthorized and 429 rate-limit errors emitted `error_kind:"api_http_error"` + `hint:null`, making it impossible for automation to distinguish auth misconfiguration from transient rate-limiting. Fixes: (1) added `api_auth_error` sub-classifier arm for 401/Unauthorized/authentication_error messages; (2) added `api_rate_limit_error` arm for 429/rate_limit messages; (3) added `fallback_hint_for_error_kind()` that derives a stable hint from the error kind when `split_error_hint` returns `None` (API layer never emits `\n`-delimited hints); (4) main JSON error emission path now calls `fallback_hint_for_error_kind` as fallback. Auth errors now return `api_auth_error` + env-var hint; rate-limit returns `api_rate_limit_error` + retry hint. Unit tests updated. 40 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori API error opacity probe on `d9844cfe`, 2026-05-27.
782. **`claw acp start` returned `error_kind:"unsupported_acp_invocation"` + `hint:null` — remediation text was on same line** — dogfooded 2026-05-27 on `16c1117a` (pinpoint by Gaebal-gajae). The error message `"unsupported ACP invocation. Use `claw acp`, `claw acp serve`, `claw --acp`, or `claw -acp`."` had no `\n` delimiter, so `split_error_hint` returned `hint:null`. Automation could tell ACP was unsupported but could not read the remediation structurally. Fix: inserted a `\n` before the remediation text: `"unsupported ACP invocation. Use ... claw -acp.\nACP/Zed editor integration is currently a discoverability alias only; ..."`. Integration test `acp_unsupported_invocation_has_hint_782` added. 41 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `16c1117a`, 2026-05-27.
783. **`claw --output-format json init` success envelope was missing `hint` field; idempotent re-init was not structurally detectable** — dogfooded 2026-05-27 on `32c9276f`. The init JSON envelope had no `hint` field (absent, not null), and no field to distinguish a fresh init from a re-init without checking `created.len() == 0`. Orchestrators had to inspect `created` array length to detect idempotent behavior. Fix: (1) added `hint` field to init JSON envelope — fresh path points at `CLAUDE.md + doctor`; idempotent path says "already initialised, run doctor"; (2) added `already_initialized: bool` field — `true` when `created` and `updated` are both empty (all artifacts skipped). Both test cases (fresh + re-init) covered by `init_json_envelope_has_hint_and_already_initialized_783`. 42 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori init-envelope probe on `32c9276f`, 2026-05-27.
784. **`claw export` had two opaque arg-error paths returning `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `81fe0ccb` (pinpoint by Gaebal-gajae). `claw export --output` (missing flag value) emitted plain `"missing value for --output"` with no typed prefix; `claw export a.md b.md` (extra positional) emitted plain `"unexpected export argument: second.md"`. Both classified as `unknown+null`. Fix: (1) `--output` missing-value error now uses `missing_flag_value:` prefix + `\n` usage hint; (2) extra positional now uses `unexpected_extra_args:` prefix + `\n` usage hint; (3) classifier `unexpected_extra_args` arm extended to match both `starts_with("unexpected extra arguments")` (prose form, #766) and `starts_with("unexpected_extra_args:")` (typed prefix form, #784). Integration test `export_arg_errors_have_typed_kind_and_hint_784` covers both paths. 43 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `81fe0ccb`, 2026-05-27.
785. **`claw dump` (typo/near-miss for dump-manifests) returned `error_kind:"unknown"` — no classifier arm for `"unknown subcommand:"` prose prefix** — dogfooded 2026-05-27 on `e628b4bb`. Any unknown top-level subcommand that triggers the suggestion path emitted `"unknown subcommand: <x>.\nDid you mean <y>"` but `classify_error_kind` had no arm for that prefix; all fell to the `"unknown"` catch-all. The hint was non-null (the suggestion text was extracted by `split_error_hint`) but `error_kind` was undifferentiated. Fix: added `starts_with("unknown subcommand:")``"unknown_subcommand"` arm. Unit test assertion + integration test `unknown_subcommand_returns_typed_kind_785` using `claw dump` as the trigger. 44 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori subcommand-classifier probe on `e628b4bb`, 2026-05-27.
786. **`claw dump-manifests --manifests-dir` (missing value) and `--manifests-dir=` (empty) both returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `87f43347` (pinpoint by Gaebal-gajae). Both missing `--manifests-dir` branches in `parse_dump_manifests_args` emitted plain `"--manifests-dir requires a path"` with no typed prefix; `classify_error_kind` had no matching arm so they fell to `"unknown"`. Fix: both branches now use `missing_flag_value:` prefix + `\n` usage hint. Integration test `dump_manifests_missing_dir_has_typed_kind_and_hint_786` covers both cases. 45 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `87f43347`, 2026-05-27.
787. **`claw --resume /tmp` (directory path) returned `error_kind:"session_load_failed"` + `hint:null`; resume error emission sites didn't apply `fallback_hint_for_error_kind`** — dogfooded 2026-05-27 on `22b423b6`. Two gaps: (1) the OS error `"Is a directory (os error 21)"` had no classifier arm, falling to generic `session_load_failed`; (2) both resume error emission paths (session load at line 3338, command execution at line 3484) called `split_error_hint` but not `fallback_hint_for_error_kind`, so API-layer errors with no `\n` always got `hint:null`. Fix: added `session_path_is_directory` classifier arm for `"Is a directory"` / `"os error 21"` messages; added `fallback_hint_for_error_kind` fallback to both resume error sites; added `session_path_is_directory` and `session_load_failed` to `fallback_hint_for_error_kind`. Unit test + integration test `resume_directory_path_returns_typed_kind_and_hint_787`. 46 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori resume-path probe on `22b423b6`, 2026-05-27.
788. **`claw --output-format json skills show <not-found>` emitted two JSON objects — one from the skills handler, one duplicate from the top-level error path** — dogfooded 2026-05-27 on `113145a4`. `print_skills` in JSON mode called `println!` to emit the `skill_not_found` error envelope, then returned `Err(...)`. The `?` propagation triggered the top-level error handler which emitted a second `action:"abort"` JSON envelope on stderr. Callers reading both stdout and stderr got two JSON objects with the same `error_kind` but different `action` fields — the first was the authoritative response, the second was a duplicate. Fix: replaced `return Err(...)` with `std::process::exit(1)` after the skills error JSON is emitted, mirroring the existing `is_help_action` guard pattern. Integration test `skills_show_not_found_emits_single_json_object_788` asserts exactly 1 JSON object on stdout and no JSON on stderr. 47 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori skills double-emission probe on `113145a4`, 2026-05-27.
789. **`claw --output-format json agents show <not-found>` and `plugins show <not-found>` both returned exit 0 despite `status:"error"` in the JSON** — dogfooded 2026-05-27 on `abdbf61a`. Skills was fixed in #788 (exit 1 via process::exit). Agents and plugins had the identical gap: `print_agents` had no error check at all (just println + Ok(())); `print_plugins`'s not-found branch used `return Ok(())`. MCP was already fixed in an earlier cycle (#68). Fix: added `is_error` check in `print_agents` JSON path (exit 1 when status=="error"); changed plugins not-found branch from `return Ok(())` to `std::process::exit(1)`. Existing `inventory_commands_emit_structured_json_when_requested` test updated to use `run_claw` directly for the not-found case. Two new tests added: `agents_show_not_found_exits_nonzero_789`, `plugins_show_not_found_exits_nonzero_789`. 49 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori exit-code consistency probe on `abdbf61a`, 2026-05-27.
790. **`claw --output-format json system-prompt <unknown-option>` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `e4c3c1aa`. The unknown-option branch in `parse_print_system_prompt_args` emitted plain `"unknown system-prompt option: {other}"` for all unrecognised options except `--json` (which appended a `\n`-delimited suggestion). All non-`--json` cases fell to `unknown+null`. Fix: replaced bare format string with `unknown_option: ... \n<usage hint>` format for all unknown options; `--json` special case preserves its `--output-format json` suggestion in the hint prefix. Integration test `system_prompt_unknown_option_returns_typed_kind_790` covers both paths. 50 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori system-prompt option probe on `e4c3c1aa`, 2026-05-27.
791. **`claw config show <extra>` and `claw config set <extra1> <extra2>` returned `unexpected_extra_args` + `hint:null`** — dogfooded 2026-05-27 on `9968a27e`. The config arg parser emitted `"unexpected extra arguments after `claw config {}`: {}"` with no `\n` delimiter, so `split_error_hint` returned `None` and `fallback_hint_for_error_kind("unexpected_extra_args")` also returns `None`. Fix: appended `\nUsage: claw config [env|hooks|model|plugins|mcp|settings]` to the error format string. Integration test `config_extra_args_have_non_null_hint_791` covers both paths. 51 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori config-arg probe on `9968a27e`, 2026-05-27.
792. **`claw agents list --bogus-flag` and `claw skills list --bogus-flag` silently returned `status:"ok" count:0` instead of an error** — dogfooded 2026-05-27 on `93a159dc`. The `list <filter>` arm in both handlers treated flag-shaped tokens (`--something`) as name substring filters. Since no agents/skills have `--bogus` in their name, result was empty success list — a false positive that masks typos and unknown flags. Fix: added flag-prefix guard at the top of both `list <args>` arms in `commands/src/lib.rs`; detected filter tokens starting with `-` return `unknown_option` + usage hint. Two new integration tests `agents_list_flag_shaped_filter_returns_unknown_option_792`, `skills_list_flag_shaped_filter_returns_unknown_option_792`. 53 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori agents/skills list probe on `93a159dc`, 2026-05-27.
793. **`claw plugins list --bogus-flag` silent empty success + `plugins uninstall <not-found>` had `hint:null`** — dogfooded 2026-05-27 on `abfa2e4c`. Two gaps: (1) `plugins list` filter branch in `print_plugins` treated `--bogus-flag` as an id substring filter, found no matches, returned `status:"ok"` empty list — same false-positive as #792 for agents/skills. (2) `plugins uninstall no-such` propagated `plugin_not_found` error via `?` with no `\n` delimiter; `plugin_not_found` was missing from `fallback_hint_for_error_kind` table. Fix: (1) added flag-prefix guard in `print_plugins` `is_list_action` branch (detects tokens starting with `-`, returns `unknown_option` + usage hint, exits 1); (2) added `"plugin_not_found"``"Run 'claw plugins list' to see installed plugins."` to fallback table. Two new tests `plugins_list_flag_shaped_filter_returns_unknown_option_793`, `plugins_uninstall_not_found_has_hint_793`. 55 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori plugins lifecycle probe on `abfa2e4c`, 2026-05-27.
794. **`claw plugins install /nonexistent/path` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `57a57ef7`. The error message `"plugin source '/path' was not found"` had no classifier arm, falling to `"unknown"`. Fix: added `plugin_source_not_found` classifier arm (`message.contains("plugin source") && message.contains("was not found")`); added `"plugin_source_not_found"``"Check that the path or URL is correct..."` to `fallback_hint_for_error_kind`. Unit test assertion added to `test_classify_error_kind`; integration test `plugins_install_not_found_path_returns_typed_kind_794` added. 56 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori plugins install probe on `57a57ef7`, 2026-05-27.
795. **`claw skills install /nonexistent` returned `skill_not_found + hint:null` and `claw skills uninstall x` returned `unsupported_skills_action + hint:null`** — dogfooded 2026-05-27 on `491f179a`. Both error kinds were missing from `fallback_hint_for_error_kind` table, so even though classify returned a typed kind, the hint field was always null. Fix: added `"skill_not_found"` → hint suggesting `claw skills list` / `claw skills install`; added `"unsupported_skills_action"` → hint listing supported actions. Integration test `skills_install_not_found_and_unsupported_action_have_hints_795` covers both paths. 57 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori skills lifecycle probe on `491f179a`, 2026-05-27.
796. **`claw agents show <name> <extra>` and `claw skills show <name> <extra>` returned confusing `agent_not_found`/`skill_not_found` for the concatenated "name extra" string** — dogfooded 2026-05-27 on `18b4cee5`. `join_optional_args` passes all tokens as a space-joined string; both `show` handlers called `split_once(' ')` to extract the name but did not check if the remainder (after the first split) contained additional tokens. Extra positional args (including `--flags`) became part of the "name", silently mangling the lookup. Fix: added second `split_once(' ')` on the extracted name; if the result has two parts, return `unexpected_extra_args` with a usage hint. Valid single-name lookups are unaffected. Two new integration tests `agents_show_extra_positional_arg_returns_unexpected_extra_796`, `skills_show_extra_positional_arg_returns_unexpected_extra_796`. 59 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori agents/skills show extra-arg probe on `18b4cee5`, 2026-05-27.
797. **Installed `claw version --output-format json` reports `git_sha:null` / `Git SHA unknown`, so dogfood cannot tie the binary under test to a source revision** — dogfooded 2026-05-27 from `#clawcode-building-in-public` using the installed `/home/bellman/.cargo/bin/claw` binary in a clean `ultraworkers/claw-code` checkout. `claw version --output-format json` returned `{"kind":"version","version":"0.1.0","git_sha":null,"target":null,"build_date":"2026-03-31"...}` while `claw status --output-format json` only reported workspace state (`git_branch`, clean/dirty counts) and did not provide any executable-vs-workspace provenance comparison. This is a clawability gap in event/log opacity and stale-binary confusion: an operator can run `doctor/status/version` successfully but still cannot prove which commit the installed CLI came from, whether it matches `origin/main`, or whether the observed behavior is from a stale packaged binary. **Required fix shape:** (a) embed build git SHA/target/build provenance in installed/release binaries whenever the source tree is available; (b) when provenance is missing, emit a typed `binary_provenance.status:"unknown"` rather than only `git_sha:null`; (c) have `status`/`doctor` include a redaction-safe comparison between executable provenance and workspace HEAD when running inside a git checkout; (d) add regression/packaging coverage proving release/local install paths preserve or explicitly classify provenance. **Why this matters:** dogfood reports and automation need to distinguish current-source failures from stale or unknown binary lineage before opening/rebasing/closing PRs. Source: gaebal-gajae live dogfood on 2026-05-27; active repo checkout had open PR #3124 DIRTY with no checks and PR #3125 CLEAN, but the installed binary itself could not identify its source revision.
798. **`claw plugins show <name> <extra-arg>` returned `unexpected_extra_args` + `hint:null`** — dogfooded 2026-05-27 on `9976585f`. The plugins arg parser at the top level emitted `"unexpected extra arguments after 'claw plugins show ...': ..."` with no `\n` delimiter (parity gap with #791 config fix). Fix: appended `\nUsage: claw plugins [list|show <id>|...]` to the error format string. Integration test `plugins_extra_args_have_non_null_hint_797`. Committed as `bff37000`. 60 CLI contract tests pass. [SCOPE: claw-code]
799. **`claw --output-format json ""` and `claw " "` returned `empty_prompt` + `hint:null`** — dogfooded 2026-05-27 on `bff37000`. The empty-prompt guard at the fallthrough path emitted `"empty prompt: provide a subcommand..."` with no `\n` delimiter. Fix: added `\n` + usage hint. Integration test `empty_prompt_has_non_null_hint_798`. Committed as `efb1542a`. 61 CLI contract tests pass. [SCOPE: claw-code]
800. **`classify_error_kind` unit test coverage gap: `invalid_history_count` and `unknown_option` arms had zero assertions** — found 2026-05-27 on `efb1542a`. Audit of 39 distinct classifier return values vs 37 unit test assertions revealed 2 untested arms. Fix: added 3 `assert_eq!` covering both arms (invalid_history_count prefix + contains paths, unknown_option prefix). Committed as `6ee67d6c`. All 39 return values now have unit test coverage. [SCOPE: claw-code]
801. **`claw --output-format json diff` in a non-git directory was missing `error_kind`, `hint`, and `message` fields** — dogfooded 2026-05-27 on `1201dc60`. The diff handler's no-git-repo JSON branch constructed a custom object with only `status:"error"` + `result:"no_git_repo"` + `detail`, violating the error envelope contract that every error has `error_kind` + `hint`. Fix: added `error_kind: "no_git_repo"`, `hint: "Run git init..."`, and `message` fields. Integration test `diff_non_git_dir_has_error_kind_and_hint_801`. 62 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori non-git-dir probe on `1201dc60`, 2026-05-27.
802. **Four `status:"error"` JSON sites in resume-mode and broad-cwd handlers were missing `hint` field** — found 2026-05-27 on `53953a81` via source audit of all `"status": "error"` sites in main.rs. The resume `unsupported_command` (L3433), `unsupported_resumed_command` (L3455), `cli_parse` (L3474), and `broad_cwd` (L4838) handlers all emitted JSON error envelopes with `error_kind` but no `hint` field. Fix: added contextual `hint` string to all four sites. Source audit now shows 0 `status:"error"` JSON objects missing `hint` across entire main.rs. 62 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori source-level audit of all error JSON sites, 2026-05-27.
803. **`claw agents list --bogus`, `skills list --bogus`, and `plugins list --bogus` in text mode silently returned empty success** — dogfooded 2026-05-27 on `fcebf644`. The JSON-mode flag guards added in #792/#793 only covered the JSON branch; the text-mode path through `handle_agents_slash_command`, `handle_skills_slash_command`, and `print_plugins` still passed flag-shaped tokens as substring filters. Fix: added flag-prefix guards to all three text-mode list handlers (agents and skills in `commands/src/lib.rs`, plugins in `main.rs print_plugins`). Also removed the now-redundant JSON-only guard from print_plugins (the early guard catches both modes). Updated `plugins_list_flag_shaped_filter_returns_unknown_option_793` test to check stderr. 62 CLI contract tests pass. [SCOPE: claw-code]
804. **`claw agents show <name> <extra>` and `claw skills show <name> <extra>` in text mode returned wrong `agent_not_found`/silent empty instead of catching extra args** — dogfooded 2026-05-27 on `bad1b97f`. Parity gap with JSON-mode fix #796: the text-mode show handlers in `commands/src/lib.rs` still used single-split `split_once(' ')` without checking for spaces in the extracted name. Fix: added `contains(' ')` guard to both text-mode show arms; extra tokens now return `unexpected extra arguments` with usage hint. 62 CLI contract tests pass. [SCOPE: claw-code]
805. **`claw skills show <not-found>` in text mode silently returned "No skills found." instead of an error** — dogfooded 2026-05-27 on `2c3c0f60`. The text-mode show handler in `handle_skills_slash_command` returned `render_skills_report(&matched)` with an empty vec instead of checking for empty match and returning an error. JSON mode already returned `skill_not_found` since #706. Fix: added `matched.is_empty()` guard with `skill_not_found` error + `\n` hint suggesting `claw skills list`. 62 CLI contract tests pass. [SCOPE: claw-code]
806. **`claw plugins show <not-found>` in text mode returned "No plugins installed." instead of an error** — dogfooded 2026-05-27 on `ae6a207d`. The text-mode path in `print_plugins` printed `payload.message` (the full list render) without checking if the requested plugin existed. JSON mode correctly returned `plugin_not_found`. Fix: added show-action filtering + not-found guard to text-mode path; added `starts_with("plugin_not_found:")` arm to classifier for the new error prefix. 63 CLI contract tests pass. [SCOPE: claw-code]
807. **`claw models` / `claw model` with `--output-format json` hang with zero stdout instead of returning bounded model discovery/help JSON or a typed unsupported response** — dogfooded 2026-05-27 on `ae6a207` while checking docs/usage model-alias surface after PR #3162 opened. Both `cargo run -q -p rusty-claude-cli -- models --output-format json` and the actual rebuilt `./rust/target/debug/claw models --output-format json` timed out under an 8s outer timeout with stdout `0`; stderr only contained config deprecation warnings. The same silent timeout reproduced for `models help --output-format json`, `model --output-format json`, and `model help --output-format json`. **Required fix shape:** (a) make `model(s)` help/list/discovery commands return bounded stdout JSON without entering prompt/provider/auth paths; (b) if the command is unsupported, return a standard typed JSON error envelope with `error_kind`, non-null `hint`, and `message`; (c) ensure docs model-alias tables and CLI model discovery surfaces do not diverge; (d) add regression coverage for `models --output-format json`, `models help --output-format json`, `model --output-format json`, and `model help --output-format json` proving they do not hang or emit zero-byte stdout. **Why this matters:** model selection is a setup/control-plane surface. If the natural model discovery commands hang silently, claws cannot verify aliases like `qwen-max` / `qwen-plus`, distinguish unsupported command spelling from provider startup, or safely guide users during first-run model setup. Source: gaebal-gajae 13:30/14:00 dogfood probe; GitHub issue creation was blocked by API rate limit, so the finding was recorded directly in ROADMAP.
808. **Control-plane commands `claw config`, `claw settings`, `claw status`, and `claw doctor` with `--output-format json` hang with zero stdout instead of returning bounded JSON/help or a typed unsupported envelope** — dogfooded 2026-05-27 on `86f45a1` after ROADMAP #807 landed. Each of `./rust/target/debug/claw config --output-format json`, `config help --output-format json`, `settings --output-format json`, `settings help --output-format json`, `status --output-format json`, and `doctor --output-format json` timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. **Required fix shape:** keep non-interactive control-plane/info commands out of prompt/provider startup paths; return bounded JSON stdout for supported status/config/help surfaces, or a standard typed JSON error envelope with `error_kind`, non-null `hint`, and `message` for unsupported spellings; add timeout/nonzero-stdout regression coverage for the six repro commands. **Why this matters:** claws and users need first-run diagnostics/config/status surfaces that are safe to call from scripts. Silent hangs make setup triage indistinguishable from provider startup, auth, or model discovery failures. Source: gaebal-gajae 17:00 dogfood probe; rechecked 17:30 after `cargo build --manifest-path rust/Cargo.toml -p rusty-claude-cli` produced `claw --version` Git SHA `23a7de6`, and the same timeout reproduced for current HOME and a clean `HOME=/tmp/claw-clean-home-1730` (clean HOME produced rc 124, stdout 0, stderr 0 for `config`, `status`, and `doctor`). [SCOPE: claw-code]
809. **Top-level help/version/MCP/plugin JSON spellings hang with zero stdout in trailing `--output-format json` form instead of returning bounded JSON/help or typed unsupported envelopes** — dogfooded 2026-05-27 on rebuilt main `db81598` (`cargo build --manifest-path rust/Cargo.toml -p rusty-claude-cli`; `claw --version` Git SHA `db81598`). `help --output-format json`, `version --output-format json`, `mcp --output-format json`, `mcp help --output-format json`, `plugins --output-format json`, and `plugins help --output-format json` each timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. Leading global-style probes (`--help --output-format json`, `--version --output-format json`) fail immediately as `[error-kind: cli_parse] unknown option`, so the hang is again in the trailing subcommand-style routing/startup path. **Required fix shape:** treat help/version/MCP/plugin discovery surfaces as bounded non-interactive control-plane commands; either return JSON help/list/version payloads or standard typed JSON unsupported envelopes with `error_kind`, non-null `hint`, and `message`; add timeout/nonzero-stdout regression coverage for the six trailing repro commands and parser-envelope coverage for leading global-style spellings. **Why this matters:** claws need safe scriptable help/version/plugin/MCP discovery before provider/session startup; silent hangs hide whether a command is unsupported, misparsed, or initializing runtime state. Source: gaebal-gajae 19:00 dogfood probe. [SCOPE: claw-code]
810. **TTY JSON success for `config`/`plugins --output-format json` contaminates stdout with deprecated-settings warnings before the JSON object** — dogfooded 2026-05-27 on rebuilt main `db81598` after #809. Under pseudo-TTY (`script -q -c "./rust/target/debug/claw config --output-format json"` and `plugins --output-format json`), the commands return rc `0` and bounded JSON, but stdout begins with `warning: /home/bellman/.claw/settings.json: field "enabledPlugins" is deprecated ...` before the JSON object (`first_json_index=121`). Parsing succeeds only after manually stripping the warning/prefix; raw stdout is not valid JSON. **Required fix shape:** in JSON mode, keep diagnostics/warnings on stderr or include structured warning fields inside the JSON envelope, but never prepend human warnings to stdout; add regression coverage that raw stdout from JSON commands parses from byte 0 under TTY and non-TTY modes. **Why this matters:** even when the TTY path avoids the hang from #807/#808/#809, claws and scripts still cannot safely `json.loads(stdout)` if configuration warnings are mixed into stdout. Source: gaebal-gajae 20:00 pseudo-TTY dogfood probe. [SCOPE: claw-code]
811. **Previously typed JSON error/list surfaces hang in plain non-TTY trailing `--output-format json` form instead of emitting their JSON envelopes** — dogfooded 2026-05-27 on rebuilt main `b0e94c9` after #810. In plain non-TTY automation, `agents list --bogus --output-format json`, `skills show does-not-exist --output-format json`, `plugins show does-not-exist --output-format json`, `diff --output-format json`, `sessions show does-not-exist --output-format json`, and `resume bogus --output-format json` each timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. Several of these surfaces had prior roadmap fixes for typed JSON/text envelopes, so this is a regression-class scriptability gap: the command-specific envelope may exist, but plain non-TTY trailing JSON invocation routes into interactive startup before reaching it. **Required fix shape:** ensure trailing `--output-format json` is honored before any interactive/provider/session startup for error/list surfaces; add plain non-TTY timeout regression coverage that asserts raw stdout is a parseable typed JSON envelope for the six repro commands, including `error_kind`, non-null `hint`, and `message` where applicable. **Why this matters:** claws primarily invoke CLI checks from non-TTY automation; a fix that only works in manual/TTY mode still leaves JSON error handling unusable for agents. Source: gaebal-gajae 20:30 dogfood probe. [SCOPE: claw-code]
812. **`claw --output-format json doctor --help` must stay a local help fast path and never fall through into runtime/provider startup** — dogfooded 2026-05-28 04:01 UTC after #701 worktree drift. The reported repro was `cargo run -q --bin claw -- --output-format json doctor --help`, which did not produce local help promptly and had to be killed, while the positive control `cargo run -q --bin claw -- --output-format json --help` emitted valid JSON help. Fresh bounded repro on branch `fix/doctor-help-json-local` did not reproduce the hang on current code (`timeout 5s cargo run -q --bin claw -- --output-format json doctor --help` exited 0 with a `kind:"help"`/`status:"ok"` doctor help envelope), which means the parser fast path is present but under-tested for this exact dogfood surface.
**Pinpoint.** The guarded path is `rust/crates/rusty-claude-cli/src/main.rs`: global `--output-format json` is parsed before `rest`, `parse_local_help_action()` maps `doctor --help` to `CliAction::HelpTopic { topic: Doctor }`, and `print_help_topic()` must return without calling `run_doctor()`, `LiveCli`, provider setup, session resume, or runtime startup. The previous risk class is help fallthrough: treating `doctor --help` as prompt text or as `doctor` diagnostics would either hit provider/session startup or run checks instead of local help.
**Fix.** Keep the local help interception and strengthen the doctor JSON help contract with structured, machine-readable metadata: `usage`, `formats`, `local_only:true`, `requires_credentials:false`, `requires_provider_request:false`, `requires_session_resume:false`, `mutates_workspace:false`, `output_fields`, `check_names`, and `status_values`. Preserve prose-only text help for `claw doctor --help`.
**Acceptance.** `timeout 5s cargo run -q --bin claw -- --output-format json doctor --help` exits 0 and parses as JSON with `.kind=="help"`, `.command=="doctor"`, `.local_only==true`, `.requires_provider_request==false`, and `output_fields` containing `checks`. `timeout 5s cargo run -q --bin claw -- doctor --help` exits 0 with plaintext beginning `Doctor` and no JSON parsing requirement. Neither command starts a provider request or session resume.
**Verification.** Regression tests: `doctor_help_json_is_local_structured_and_bounded_702` and `doctor_help_text_stays_plaintext_and_local_702` in `rust/crates/rusty-claude-cli/tests/output_format_contract.rs`; focused command repros recorded in `/tmp/claw_doctor_help_json.out` and `/tmp/claw_doctor_help_json.err` during the doctor-help fix branch.
813. **Dogfood probe shell-string loops can fabricate CLI argv failures for JSON help surfaces** — dogfooded 2026-05-28 after #3185 merged. A verification loop used a single shell string variable (`cmd="--output-format json doctor --help"`; then `cargo run -q --bin claw -- $cmd | python3 -c 'json.load(...)'`). The resulting channel transcript showed `unknown option: --output-format json doctor --help` and Python JSON parse stack noise, even though explicit argv invocations on fresh `main` all returned valid JSON: `cargo run -q --bin claw -- --output-format json doctor --help`, `cargo run -q --bin claw -- doctor --help --output-format json`, and `cargo run -q --bin claw -- help doctor --output-format json`. This is a dogfood-harness test-brittleness / event-log opacity gap, not a product parser regression. The log made a probe-construction mistake look like a claw-code failure.
**Required fix shape.** Add a tiny argv-safe dogfood helper (script or documented recipe) that runs CLI probes as explicit argv arrays rather than interpolated shell strings, captures stdout/stderr separately, and labels probe-construction failures distinctly from product failures. For ad-hoc shell loops, prefer arrays/functions (`run_probe --output-format json doctor --help`) over `$cmd` strings; never pipe unknown stdout directly into a JSON parser without first recording rc/stdout/stderr.
**Acceptance.** Future dogfood reports for argv-sensitive CLI surfaces include the exact argv vector and can distinguish `probe_error` from `product_error`; reproducing the three doctor-help forms through the helper yields three parseable JSON objects from byte 0 without Python parser stack noise. [SCOPE: claw-code dogfood harness]
814. **Plain non-TTY trailing `--output-format json` still times out for inventory/error surfaces after #3186** — dogfooded 2026-05-28 07:00 on fresh `main` `0e6d48d9d` after #3186 merged. Using explicit argv probes with separated stdout/stderr (per #813) reproduced the older #811 class on current main: `cargo run -q --bin claw -- agents list --bogus --output-format json`, `skills show does-not-exist --output-format json`, and `plugins show does-not-exist --output-format json` each hit the 5s timeout (`rc=124`) with `stdout` length 0; stderr contained only compile warnings plus the local deprecated `enabledPlugins` settings warning. This confirms the argv-safe probe harness can distinguish product failure from probe-construction failure, and the product gap remains for trailing JSON flag forms on inventory/error surfaces.
**Required fix shape.** Parse trailing `--output-format json` for local inventory/error commands before any REPL/provider startup in plain non-TTY mode, matching the already-working leading global form where applicable. Add timeout regression coverage for at least `agents list --bogus --output-format json`, `skills show does-not-exist --output-format json`, and `plugins show does-not-exist --output-format json` asserting nonzero stdout with a single parseable JSON envelope containing `status:"error"`, `error_kind`, and non-null `hint`. Keep deprecation/config warnings out of stdout in JSON mode.
**Acceptance.** All three repro commands exit within 5s under non-TTY automation, produce parseable JSON from stdout byte 0, and never require provider credentials/session startup. [SCOPE: claw-code]
**Follow-up verification (2026-05-28 07:30 on `main` `09ff1caf4`).** After #3187 merged, rerunning the same three commands with explicit argv showed the product path had already been fixed upstream: `agents list --bogus --output-format json` returned rc 1 with a JSON `unknown_option` envelope, `skills show does-not-exist --output-format json` returned rc 1 with `skill_not_found`, and `plugins show does-not-exist --output-format json` returned rc 1 with `plugin_not_found`. Stdout was nonzero and parseable in all three cases; warnings stayed on stderr. Remaining actionable lesson is process-level: ROADMAP record #814 is preserved as historical repro + verification, not an open product blocker.
815. **`claw --output-format json config` reports the same deprecated-settings warning twice: once structurally in `warnings[]` and once as prose on stderr** — dogfooded 2026-05-28 08:00 on current `main` after #3188. `timeout 5s cargo run -q --bin claw -- --output-format json config >out 2>err` exits 0 with parseable stdout JSON (`kind:"config", action:"list", status:"ok"`) and `warnings.length == 1`, but stderr still contains the same `enabledPlugins is deprecated` warning once. This is better than older stdout contamination, but still duplicates the same diagnostic across two channels in JSON mode. A machine consumer that reads the structured warning also sees an extra prose warning on stderr; a log scraper may count one config issue twice.
**Required fix shape.** In JSON mode for config/list surfaces that already include `warnings[]`, suppress eager prose emission of the same config warning on stderr or mark it as already collected. Text mode should keep the human stderr warning. Add regression coverage asserting `claw --output-format json config` returns exactly one structured warning and zero duplicate `enabledPlugins` prose lines on stderr.
**Acceptance.** With a deprecated `enabledPlugins` key present, `claw --output-format json config` exits 0, stdout parses from byte 0 and includes `warnings[]`, and stderr has no duplicate deprecation warning for the same file/key. [SCOPE: claw-code]
816. **JSON-mode local/list surfaces still leak deprecated config prose warnings on stderr outside `config`** — dogfooded 2026-05-28 09:30 on `main` `89e7f415a` after #3190. `./target/debug/claw --output-format json config` is now fixed (`rc=0`, parseable stdout, `warnings[]`, stderr empty), but sibling JSON surfaces still emit the same app-level config warning to stderr when `~/.claw/settings.json` contains deprecated `enabledPlugins`: `plugins list` (`kind:"plugin"`), `mcp list` (`kind:"mcp"`), and `doctor` (`kind:"doctor"`) all return parseable JSON with `rc=0` while stderr contains `enabledPlugins is deprecated`. `skills list` and `version` stay clean. This leaves machine consumers with a global JSON-mode cleanliness gap even after the config-specific duplicate was fixed.
**Required fix shape.** Treat JSON output mode as a global app-level diagnostic routing contract: local/list/status surfaces that successfully return structured JSON should not write config deprecation prose to stderr. Either collect those warnings into each relevant JSON envelope where a warnings field exists, or suppress config-warning emission during JSON-mode preloading/default resolution for surfaces that cannot represent warnings yet. Preserve human stderr warnings in text mode.
**Acceptance.** With deprecated `enabledPlugins` present, `claw --output-format json plugins list`, `claw --output-format json mcp list`, and `claw --output-format json doctor` exit 0, stdout parses from byte 0, and stderr contains zero `enabledPlugins is deprecated` app-level warning lines. Text mode still prints the warning. [SCOPE: claw-code]
817. **`claw --output-format json plugins list --` writes its JSON error envelope to stderr while sibling local inventory commands use stdout** — dogfooded 2026-05-28 12:30 on `main` `9494e3c26`. Trailing bare `--` is a useful parser edge because automation sometimes injects delimiter sentinels. `agents list --` and `skills list --` return rc 1 with parseable JSON on stdout and empty stderr. `mcp list --` also returns a parseable JSON error on stdout. `config --` returns rc 0 with a structured config error on stdout. But `plugins list --` returns rc 1, stdout empty, and writes the JSON error envelope to stderr: `{"action":"abort","error":"unknown option for `claw plugins list`: --", ...}`. This is machine-readable, but channel-inconsistent and surprising for JSON-mode consumers that read stdout for command payloads.
**Required fix shape.** Align `plugins list` parse-error routing with the other JSON inventory/local surfaces: in JSON mode, print the structured CLI error envelope to stdout and keep stderr empty for this handled parse error. Preserve text-mode stderr behavior. Add regression coverage for `claw --output-format json plugins list --` asserting rc 1, stdout parseable JSON with `error_kind:"cli_parse"`, and empty stderr.
**Acceptance.** `claw --output-format json plugins list --` exits 1, stdout parses from byte 0 as the existing JSON error envelope, stderr is empty, and text mode still reports the parse error to stderr. [SCOPE: claw-code]
818. **`AGENTS.md` and `.claude/CLAUDE.md` silently omitted from instruction file cascade** — dogfooded 2026-05-29 08:00. When a repo contains `AGENTS.md` (OpenAI Codex / multi-agent convention) or `.claude/CLAUDE.md` (scoped Claude Code convention), claw-code does not load either file as part of the instruction/context cascade on startup. Users following either convention discover this only by noticing their persona/context instructions have no effect — no warning, no missing-file diagnostic, no documentation note. This is a friction gap for any team migrating to or simultaneously using claw-code alongside Claude Code or Codex workflows, since the two most common non-CLAUDE.md instruction files are silently ignored.
**Required fix shape.** Add `AGENTS.md` (project root) and `.claude/CLAUDE.md` (`.claude/` subdirectory) to the instruction file cascade that already loads `CLAUDE.md`. Apply the same merge-and-precedence semantics as existing instruction files. Log a debug trace (not stderr noise) when either file is loaded. Add test coverage: a fixture repo with `AGENTS.md` only, `.claude/CLAUDE.md` only, and both present alongside `CLAUDE.md` should each have the relevant content visible in the resolved instruction context.
**Acceptance.** `claw` launched in a repo containing `AGENTS.md` or `.claude/CLAUDE.md` loads those files into the instruction context. No warning emitted for absent optional files. Existing `CLAUDE.md`-only repos unaffected. PR #3195. [SCOPE: claw-code]
819. **`claw --output-format json export --session <missing>` writes JSON error envelope to stderr, stdout empty** — dogfooded 2026-05-29 09:30 on `main` `37a9a543`. `claw --output-format json export --session does-not-exist` exits rc=1 with stdout length 0 and the full JSON error envelope on stderr: `{"action":"abort","error":"session not found: does-not-exist","error_kind":"session_not_found",...}`. This is the same channel-routing inconsistency class as #817 (plugins list trailing-dash, fixed in #3194): handled errors in JSON mode should go to stdout, not stderr, so machine consumers can parse the envelope from stdout byte 0 regardless of which surface triggered the error.
**Required fix shape.** Align `export --session <missing>` error routing with the inventory surfaces fixed in #817: in JSON mode, write the `session_not_found` error envelope to stdout (rc=1) and keep stderr empty. Preserve text-mode behavior (stderr message). Add regression coverage asserting rc=1, stdout parseable JSON with `error_kind:"session_not_found"`, and empty stderr.
**Acceptance.** `claw --output-format json export --session does-not-exist` exits 1, stdout contains the JSON error envelope from byte 0, stderr is empty. Text mode still prints the error to stderr. [SCOPE: claw-code]
820. **`interactive_only` error class always routes JSON envelope to stderr (stdout empty)** — dogfooded 2026-05-29 10:00 on `main` `efe59c22`. All `interactive_only` errors share the same routing gap as #819 (`export --session <missing>`): `claw --output-format json session list`, `session switch <id>`, `session delete <id>`, and `session fork <id>` each exit rc=1, stdout empty, JSON envelope on stderr. The envelope is well-formed (`error_kind:"interactive_only"`, `hint:...`, `action:"abort"`) but the channel is wrong for JSON mode. Any surface that returns `interactive_only` is affected; these are all the `claw session` subcommands. This is the same root cause as #817 (plugins) and #819 (export): the top-level error handler writes `Err(...)` to stderr instead of routing to stdout when `--output-format json` is active.
**Required fix shape.** In the top-level error handler (or the `interactive_only` classifier arm in `main.rs`), detect JSON output mode and write the structured error envelope to stdout (rc=1) instead of stderr. Scope the fix to the `interactive_only` error_kind so all affected surfaces are repaired in one pass. Add regression coverage for at least `claw --output-format json session list` asserting rc=1, stdout parseable JSON with `error_kind:"interactive_only"`, stderr empty.
**Acceptance.** All `claw --output-format json session <subcommand>` invocations exit 1 with the JSON envelope on stdout and empty stderr. Text mode continues to print the error to stderr. [SCOPE: claw-code]
821. **`status`, `sandbox`, and `system-prompt` in JSON mode still emit config deprecation warning to stderr** — dogfooded 2026-05-29 10:30 on `main` `42aff269`. After #816 fixed config deprecation stderr leakage for `plugins list`, `mcp list`, `doctor`, and `config`, three JSON-mode surfaces continue to emit the `enabledPlugins is deprecated` prose warning to stderr: `claw --output-format json status` (122 bytes stderr), `claw --output-format json sandbox` (122 bytes stderr), `claw --output-format json system-prompt` (122 bytes stderr). These surfaces return well-formed JSON on stdout (rc=0) but leak the config warning to stderr, leaving machine consumers with mixed-channel output. `version`, `acp`, `agents`, `skills`, `mcp`, `plugins`, and `doctor` all have clean stderr after #816.
**Required fix shape.** Extend the JSON-mode config-warning suppression applied in #816 to cover `status`, `sandbox`, and `system-prompt`. The fix should apply globally: any JSON-mode surface that completes successfully should not emit config deprecation prose to stderr. Text mode should keep the human stderr warning.
**Acceptance.** With deprecated `enabledPlugins` in `~/.claw/settings.json`, `claw --output-format json status`, `claw --output-format json sandbox`, and `claw --output-format json system-prompt` each exit 0, stdout parses from byte 0, and stderr is empty. Text mode still prints the deprecation warning. [SCOPE: claw-code]
**Follow-up (2026-05-29 12:00, `main` `3dbb35c3`).** Broader sweep confirms additional surfaces with the same 122-byte stderr leak in JSON mode: `--resume latest /config` (all subforms: bare, `env`, `hooks`, `model`, `plugins`) and `--resume latest /providers` (doctor alias). The fix must apply to all config-loading paths, not just the three originally documented surfaces. The suppression guard should fire at the settings-load level so any JSON-mode invocation benefits without per-surface patching.
822. **Unknown top-level subcommand falls through to REPL/provider startup instead of returning a `command_not_found` error** — dogfooded 2026-05-29 11:00 on `main` `69b59079`. `claw --output-format json foobar` does not return a structured `command_not_found` error; instead it falls through to the interactive/API path and hits `missing_credentials` (rc=1, stderr: `{"error_kind":"missing_credentials",...}`). Two gaps in one: (1) the unrecognized command word is silently treated as a prompt/text argument, not flagged as unknown, so the user gets a misleading "no credentials" error instead of "command not found"; (2) the resulting error goes to stderr. This makes automation scripts that probe for command availability impossible to distinguish from auth failures.
**Required fix shape.** Before falling through to the REPL/prompt path, check whether the first positional arg matches any known subcommand. If not, return a typed error: `{"error_kind":"command_not_found","message":"unknown command: foobar","hint":"Run `claw --help` for available commands.","status":"error"}` on stdout (JSON mode, rc=1) or stderr (text mode). This mirrors the behavior of `--bogus-flag` (which correctly returns `cli_parse`) but for unknown positional commands.
**Acceptance.** `claw --output-format json foobar` exits 1, stdout contains JSON with `error_kind:"command_not_found"`, stderr empty. Text mode prints the error to stderr. No provider startup attempted. [SCOPE: claw-code]
823. **`claw --output-format json prompt` with missing/empty prompt text routes JSON error to stderr (stdout empty)** — dogfooded 2026-05-29 11:30 on `main` `3a76c4f4`. `claw --output-format json prompt` (no text) and `claw --output-format json prompt ""` (empty string) both exit rc=1, stdout empty, and write `{"error_kind":"missing_prompt","action":"abort",...}` to stderr. The envelope is well-formed but channel-inconsistent: JSON mode machine consumers reading stdout for command results get empty stdout and must check stderr to detect the error. This is the same class as #819 (export session-not-found) and #820 (interactive_only / session subcommands), and the same root cause: the top-level abort handler writes to stderr regardless of output-format mode.
**Required fix shape.** In JSON mode, route `missing_prompt` abort errors to stdout (rc=1) and keep stderr empty. This is the same fix pattern as #817/#819/#820: detect JSON output mode in the abort handler and redirect the structured envelope to stdout. Add regression coverage for `claw --output-format json prompt` (no arg) and `claw --output-format json prompt ""` asserting rc=1, stdout parseable JSON with `error_kind:"missing_prompt"`, stderr empty.
**Acceptance.** Both invocations exit 1 with JSON envelope on stdout and empty stderr. Text mode still prints to stderr. [SCOPE: claw-code]
824. **Global settings-load deprecation warning still leaks to stderr in JSON mode for `status`, `sandbox`, `system-prompt`, `skills`, `mcp`, `agents` surfaces** — dogfooded 2026-05-29 13:30 on `main` `b4b1ba10`. After #816 and #821 (doc), the `enabledPlugins is deprecated` config warning still reaches stderr on every JSON-mode surface that loads settings: `claw --output-format json status`, `sandbox`, `system-prompt`, `mcp list`, `skills list`, `agents list` all emit `warning: /path/.claw/settings.json: field "enabledPlugins" is deprecated (line 2)...` to stderr. Root cause: `emit_config_warning_once()` in `runtime/src/config.rs` always uses `eprintln!` with no output-format awareness. The `config` surface avoids the duplicate by collecting warnings into a structured `warnings[]` field, but all other surfaces hit the raw `eprintln!` path.
**Required fix shape.** Add a global `SUPPRESS_CONFIG_WARNINGS_STDERR: AtomicBool` flag in `config.rs`. Set it to `true` immediately when `--output-format json` is detected in `main.rs` (before any settings load). Gate `emit_config_warning_once` on that flag. Text-mode invocations continue to print to stderr; JSON-mode invocations silently suppress the prose warning (warnings remain available via structured `config` output).
**Acceptance.** With deprecated `enabledPlugins` in `~/.claw/settings.json`, all JSON-mode surfaces (`status`, `sandbox`, `system-prompt`, `mcp list`, `skills list`, `agents list`, plus all `--resume /config*` forms) exit with empty stderr. Text-mode output is unchanged. [SCOPE: claw-code]
825. **Unknown single-word subcommand falls through to provider startup and surfaces `missing_credentials` instead of `command_not_found`** — dogfooded 2026-05-29 14:00 on `main` `de7edd5b`. `claw foobar` (and `claw --output-format json foobar`) hit the `looks_like_subcommand_typo` guard, which checked for close fuzzy matches but fell through silently when no suggestions matched. The fallthrough routed to `CliAction::Prompt`, triggering Anthropic provider startup and a misleading `missing_credentials` error (or burning API tokens if credentials were present). The `command_not_found` error kind existed in the registry but was never emitted by this path.
**Required fix shape.** When `looks_like_subcommand_typo` fires on a single-word positional arg with no close suggestions, emit `command_not_found:` rather than falling through. Add `command_not_found:` prefix classifier to `classify_error_kind`. Result: clean `{"error_kind":"command_not_found",...}` envelope on stdout (JSON mode), error on stderr (text mode), zero provider startup.
**Acceptance.** `claw --output-format json foobar` exits 1, stdout `error_kind:"command_not_found"`, stderr empty, no Anthropic call. Typo with suggestions (`claw statuz`) also gets `command_not_found` plus `hint` with suggestions. [SCOPE: claw-code]
826. **Multi-word unknown subcommand still falls through to `missing_credentials`** — dogfooded 2026-05-29 14:38 on `main` `70d64be0`. After #825 fixed single-word unknown subcommands, multi-word invocations (`claw foobar baz`) are still undetected: the `looks_like_subcommand_typo` guard only fires when `rest.len() == 1`. When there are two or more positional args, the first word is treated as a prompt and all args join into a prompt string → provider startup → `missing_credentials`. Same misleading-error class as #825 but for multi-word cases.
**Required fix shape.** Extend the command-not-found guard to also fire when `rest.len() > 1` and `rest[0]` passes `looks_like_subcommand_typo` but does not match any known subcommand. The multi-arg case should also emit `command_not_found` — with a note that if literal multi-word prompt was intended, use `claw prompt <text>` or `echo 'text' | claw`.
**Acceptance.** `claw --output-format json foobar baz` exits 1, stdout `error_kind:"command_not_found"`, stderr empty, no provider startup. `claw "write a haiku"` (valid prompt passthrough) is unaffected. [SCOPE: claw-code]
827. **`--resume <session> /unknown-slash-command` emits `error_kind:"unknown"` instead of a typed kind** — dogfooded 2026-05-29 14:58 on `main` `d47b0151`. `claw --resume latest --output-format json /bogus` exits rc=2 with `{"error_kind":"unknown",...}` on stdout (correct channel, correct rc) but the opaque `"unknown"` kind gives machine consumers no way to distinguish "unrecognized slash command" from other error classes. The error has a useful `hint` with suggestions, but the `error_kind` field is `"unknown"` across all unrecognized resume slash commands.
**Required fix shape.** Introduce a typed `error_kind` for unrecognized slash commands (e.g. `unknown_slash_command` or `command_not_found`). Update the JSON emit in the `--resume` unknown-command handler to use the typed kind. Add regression coverage asserting the typed kind.
**Acceptance.** `claw --resume latest --output-format json /bogus` exits rc=2, stdout `error_kind:"unknown_slash_command"` (or similar typed constant), stderr empty. [SCOPE: claw-code]
828. **`/approve` and `/deny` outside REPL emit `unknown_slash_command` instead of `interactive_only`** — dogfooded 2026-05-29 16:05 on `main` `9d05573f`. `claw --output-format json /approve` exited rc=1 with `error_kind:"unknown_slash_command"` — these are valid REPL-only slash commands but are not `SlashCommand` enum variants, so they fell through to `format_unknown_direct_slash_command`. Machine consumers saw the wrong error class.
**Fix applied.** `SlashCommand::Unknown` arm now special-cases `approve | yes | y | deny | no | n` and emits `interactive_only:` prefix before falling through to `format_unknown_direct_slash_command`. Both `error_kind` and hint are correct.
**Acceptance.** `claw --output-format json /approve` exits rc=1, stdout `error_kind:"interactive_only"`, stderr empty. [SCOPE: claw-code]
829. **`interactive_only` hint incorrectly suggests `--resume` for commands that are not resume-safe** — dogfooded 2026-05-29 16:40 on `main` `187aebd7`. `claw --output-format json /commit` hint says `"use claw --resume SESSION.jsonl /commit"` but `/commit` is not resume-safe (no `[resume]` marker in `/help`). Same for `/pr`, `/issue`, `/bughunter`, `/ultraplan`. The generic `interactive_only` hint template does not distinguish resume-safe from live-REPL-only commands, so it always suggests `--resume` regardless. Users who follow the hint will get `interactive_only` again.
**Required fix shape.** The generic `interactive_only:` message formatter (line ~1745 in `main.rs`) currently always appends `or use claw --resume SESSION.jsonl {command_name}`. This should be conditioned on whether the slash command appears in the resume-safe command list. Non-resume-safe interactive commands should only say `Start claw and run it there.`
**Acceptance.** `claw --output-format json /commit` hint does NOT mention `--resume`. `claw --output-format json /status` (resume-safe) hint still mentions `--resume`. [SCOPE: claw-code]
830. **`claw mcp show` (missing server name arg) emits `error_kind:"unknown_mcp_action"` instead of `missing_argument`** — dogfooded 2026-05-29 17:00 on `main` `ac5b19de`. `claw --output-format json mcp show` (no server name supplied) exits with `error_kind:"unknown_mcp_action"`. However `show` IS a known MCP action — the error is a missing required argument (server name), not an unknown action. Machine consumers inspecting `error_kind` cannot distinguish "I don't know this action" from "I know this action but a required arg is missing".
**Required fix shape.** The MCP subcommand parser should detect `show` with no following token and emit `missing_argument: mcp show requires a server name.\nUsage: claw mcp show <server>` with a distinct `error_kind`. Update the classifier arm to return `missing_argument` for this prefix.
**Acceptance.** `claw --output-format json mcp show` exits rc=1, stdout `error_kind:"missing_argument"`, stderr empty. Hint contains usage example. [SCOPE: claw-code]

View File

@@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use plugins::{PluginError, PluginLoadFailure, PluginManager, PluginSummary};
use runtime::{
compact_session, CompactionConfig, ConfigLoader, ConfigSource, McpOAuthConfig, McpServerConfig,
ScopedMcpServerConfig, Session,
RuntimeConfig, ScopedMcpServerConfig, Session,
};
use serde_json::{json, Value};
@@ -2342,7 +2342,7 @@ pub fn handle_plugins_slash_command(
reload_runtime: false,
}),
Some(other) => Err(PluginError::CommandFailed(format!(
"unknown_plugins_action: '{other}' is not a supported /plugins action. Use list, show, install, enable, disable, uninstall, or update."
"unknown_plugins_action: '{other}' is not a supported /plugins action.\nUse: list, show, install, enable, disable, uninstall, or update."
))),
}
}
@@ -2365,6 +2365,13 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
}
Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase();
// #803: reject flag-shaped tokens in text mode too (JSON guard was added in #792)
if filter.starts_with('-') {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown option for `agents list`: {filter}\nUsage: claw agents list [<filter>]\nFilters are name substrings, not flags."),
));
}
let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?;
let filtered: Vec<_> = agents
@@ -2383,22 +2390,30 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|| args.starts_with("info ")
|| args.starts_with("describe ") =>
{
let name = args
let name_raw = args
.split_once(' ')
.map(|(_, name)| name)
.unwrap_or_default()
.trim()
.to_lowercase();
// #804: detect extra positional args (parity with JSON-mode fix #796)
if name_raw.contains(' ') {
let extra = name_raw.split_once(' ').map(|(_, e)| e).unwrap_or("");
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unexpected extra arguments after agent name\nUsage: claw agents show <name>\nUnexpected extra: '{extra}'"),
));
}
let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?;
let matched: Vec<_> = agents
.into_iter()
.filter(|a| a.name.to_lowercase() == name)
.filter(|a| a.name.to_lowercase() == name_raw)
.collect();
if matched.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("agent not found: {name}"),
format!("agent not found: {name_raw}"),
));
}
Ok(render_agents_report(&matched))
@@ -2406,7 +2421,7 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
Some(args) if is_help_arg(args) => Ok(render_agents_usage(None)),
Some(args) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown agents subcommand: {args}. Supported: list, show, help"),
format!("unknown agents subcommand: {args}.\nSupported: list, show, help"),
)),
}
}
@@ -2429,6 +2444,18 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
}
Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase();
// #792: unknown flags (--something) silently became filter strings, returning
// empty success list instead of an error. Detect and reject flag-shaped tokens.
if filter.starts_with('-') {
return Ok(serde_json::json!({
"kind": "agents",
"action": "list",
"status": "error",
"error_kind": "unknown_option",
"unexpected": filter,
"hint": "Usage: claw agents list [<filter>]\nFilters are name substrings, not flags.",
}));
}
let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?;
let filtered: Vec<_> = agents
@@ -2447,12 +2474,29 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|| args.starts_with("info ")
|| args.starts_with("describe ") =>
{
let name = args
let name_raw = args
.split_once(' ')
.map(|(_, name)| name)
.unwrap_or_default()
.trim()
.to_lowercase();
// #796: extra positional args after the name (e.g. `agents show foo extra`)
// produced a confusing agent_not_found for "foo extra" instead of flagging
// the unexpected extra argument.
let (name, extra) = name_raw
.split_once(' ')
.map(|(n, e)| (n.to_string(), Some(e.to_string())))
.unwrap_or_else(|| (name_raw.clone(), None));
if let Some(extra_token) = extra {
return Ok(serde_json::json!({
"kind": "agents",
"action": "show",
"status": "error",
"error_kind": "unexpected_extra_args",
"unexpected": extra_token,
"hint": format!("Usage: claw agents show <name>\nUnexpected extra: '{extra_token}'"),
}));
}
let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?;
let matched: Vec<_> = agents
@@ -2477,7 +2521,7 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
Some(args) if is_help_arg(args) => Ok(render_agents_usage_json(None)),
Some(args) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown agents subcommand: {args}. Supported: list, show, help"),
format!("unknown agents subcommand: {args}.\nSupported: list, show, help"),
)),
}
}
@@ -2498,6 +2542,14 @@ pub fn handle_mcp_slash_command_json(
render_mcp_report_json_for(&loader, cwd, args)
}
fn load_runtime_config_without_stderr_warnings(
loader: &ConfigLoader,
) -> Result<RuntimeConfig, runtime::ConfigError> {
loader
.load_collecting_warnings()
.map(|(runtime_config, _warnings)| runtime_config)
}
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
if let Some(args) = normalize_optional_args(args) {
if let Some(help_path) = help_path_from_args(args) {
@@ -2517,6 +2569,13 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
}
Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase();
// #803: reject flag-shaped tokens in text mode too (JSON guard was added in #792)
if filter.starts_with('-') {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown option for `skills list`: {filter}\nUsage: claw skills list [<filter>]\nFilters are name substrings, not flags."),
));
}
let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?;
let filtered: Vec<_> = skills
@@ -2535,18 +2594,33 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|| args.starts_with("info ")
|| args.starts_with("describe ") =>
{
let name = args
let name_raw = args
.split_once(' ')
.map(|(_, name)| name)
.unwrap_or_default()
.trim()
.to_lowercase();
// #804: detect extra positional args (parity with JSON-mode fix #796)
if name_raw.contains(' ') {
let extra = name_raw.split_once(' ').map(|(_, e)| e).unwrap_or("");
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unexpected extra arguments after skill name\nUsage: claw skills show <name>\nUnexpected extra: '{extra}'"),
));
}
let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?;
let matched: Vec<_> = skills
.into_iter()
.filter(|s| s.name.to_lowercase() == name)
.filter(|s| s.name.to_lowercase() == name_raw)
.collect();
// #805: text-mode show must return an error when skill not found (parity with JSON)
if matched.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("skill '{name_raw}' not found\nRun `claw skills list` to see available skills."),
));
}
Ok(render_skills_report(&matched))
}
Some("install") => Ok(render_skills_usage(Some("install"))),
@@ -2582,6 +2656,18 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
}
Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase();
// #792: flag-shaped tokens silently became filter strings, returning
// empty success list instead of an error. Detect and reject them.
if filter.starts_with('-') {
return Ok(serde_json::json!({
"kind": "skills",
"action": "list",
"status": "error",
"error_kind": "unknown_option",
"unexpected": filter,
"hint": "Usage: claw skills list [<filter>]\nFilters are name substrings, not flags.",
}));
}
let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?;
let filtered: Vec<_> = skills
@@ -2600,12 +2686,29 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|| args.starts_with("info ")
|| args.starts_with("describe ") =>
{
let name = args
let name_raw = args
.split_once(' ')
.map(|(_, name)| name)
.unwrap_or_default()
.trim()
.to_lowercase();
// #796: extra positional args after the name (e.g. `skills show foo extra`)
// produced a confusing skill_not_found for "foo extra" instead of flagging
// the unexpected extra argument.
let (name, extra) = name_raw
.split_once(' ')
.map(|(n, e)| (n.to_string(), Some(e.to_string())))
.unwrap_or_else(|| (name_raw.clone(), None));
if let Some(extra_token) = extra {
return Ok(json!({
"kind": "skills",
"action": "show",
"status": "error",
"error_kind": "unexpected_extra_args",
"unexpected": extra_token,
"hint": format!("Usage: claw skills show <name>\nUnexpected extra: '{extra_token}'"),
}));
}
let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?;
let matched: Vec<_> = skills
@@ -2899,7 +3002,7 @@ fn render_mcp_report_json_for(
// failure, emit top-level `status: "degraded"` with
// `config_load_error`, empty servers[], and exit 0. On clean
// runs, the existing serializer adds `status: "ok"` below.
match loader.load() {
match load_runtime_config_without_stderr_warnings(loader) {
Ok(runtime_config) => {
let mut value =
render_mcp_summary_report_json(cwd, runtime_config.mcp().servers());
@@ -2924,7 +3027,19 @@ fn render_mcp_report_json_for(
}
}
Some(args) if is_help_arg(args) => Ok(render_mcp_usage_json(None)),
Some("show") => Ok(render_mcp_usage_json(Some("show"))),
// #830: `claw mcp show` with no server name is a missing required
// argument, not an unknown action. Emit a dedicated error_kind so
// machine consumers can distinguish "I know show, but need a name"
// from "I don't know this action".
Some("show") => Ok(serde_json::json!({
"kind": "mcp",
"action": "show",
"status": "error",
"ok": false,
"error_kind": "missing_argument",
"hint": "Usage: claw mcp show <server>\nRun `claw mcp list` to see available servers.",
"message": "missing required argument: mcp show requires a server name.",
})),
Some(args) if args.split_whitespace().next() == Some("show") => {
let mut parts = args.split_whitespace();
let _ = parts.next();
@@ -2935,7 +3050,7 @@ fn render_mcp_report_json_for(
return Ok(render_mcp_usage_json(Some(args)));
}
// #144: same degradation pattern for show action.
match loader.load() {
match load_runtime_config_without_stderr_warnings(loader) {
Ok(runtime_config) => {
let mut value = render_mcp_server_report_json(
cwd,
@@ -4173,12 +4288,20 @@ fn render_mcp_usage_json(unexpected: Option<&str>) -> Value {
} else {
Value::Null
};
// #774: add hint field so unknown_mcp_action errors have non-null hint parity
// with agents/plugins unknown-subcommand envelopes.
let hint: Value = if unexpected.is_some() {
json!("Use: list, show <server>, or help")
} else {
Value::Null
};
json!({
"kind": "mcp",
"action": "help",
"ok": unexpected.is_none(),
"status": if unexpected.is_some() { "error" } else { "ok" },
"error_kind": error_kind,
"hint": hint,
"usage": {
"slash_command": "/mcp [list|show <server>|help]",
"direct_cli": "claw mcp [list|show <server>|help]",

View File

@@ -10,7 +10,22 @@ use std::sync::Mutex;
static EMITTED_CONFIG_WARNINGS: std::sync::OnceLock<Mutex<HashSet<String>>> =
std::sync::OnceLock::new();
/// When set to `true`, `emit_config_warning_once` silently drops all prose
/// deprecation warnings instead of writing them to stderr. Set this flag
/// before any settings load when `--output-format json` is active so that
/// JSON-mode machine consumers see empty stderr on success. (#824)
static SUPPRESS_CONFIG_WARNINGS_STDERR: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
/// Call this once at startup when `--output-format json` is active.
pub fn suppress_config_warnings_for_json_mode() {
SUPPRESS_CONFIG_WARNINGS_STDERR.store(true, std::sync::atomic::Ordering::Relaxed);
}
fn emit_config_warning_once(warning: &str) {
if SUPPRESS_CONFIG_WARNINGS_STDERR.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
let set = EMITTED_CONFIG_WARNINGS.get_or_init(|| Mutex::new(HashSet::new()));
let mut guard = set.lock().unwrap_or_else(|e| e.into_inner());
if guard.insert(warning.to_string()) {
@@ -346,6 +361,64 @@ impl ConfigLoader {
feature_config,
})
}
/// Like [`load`] but also returns the list of validation warnings collected during
/// loading, without emitting them to stderr. Callers that want to surface warnings
/// through a structured channel (e.g. the JSON config envelope) should use this.
/// #773: enables JSON-mode callers to include `warnings` in their output envelope
/// instead of receiving unstructured text on stderr.
pub fn load_collecting_warnings(&self) -> Result<(RuntimeConfig, Vec<String>), ConfigError> {
let mut merged = BTreeMap::new();
let mut loaded_entries = Vec::new();
let mut mcp_servers = BTreeMap::new();
let mut all_warnings: Vec<String> = Vec::new();
for entry in self.discover() {
crate::config_validate::check_unsupported_format(&entry.path)?;
let Some(parsed) = read_optional_json_object(&entry.path)? else {
continue;
};
let validation = crate::config_validate::validate_config_file(
&parsed.object,
&parsed.source,
&entry.path,
);
if !validation.is_ok() {
let first_error = &validation.errors[0];
return Err(ConfigError::Parse(first_error.to_string()));
}
all_warnings.extend(validation.warnings.iter().map(|w| w.to_string()));
validate_optional_hooks_config(&parsed.object, &entry.path)?;
merge_mcp_servers(&mut mcp_servers, entry.source, &parsed.object, &entry.path)?;
deep_merge_objects(&mut merged, &parsed.object);
loaded_entries.push(entry);
}
let merged_value = JsonValue::Object(merged.clone());
let feature_config = RuntimeFeatureConfig {
hooks: parse_optional_hooks_config(&merged_value)?,
plugins: parse_optional_plugin_config(&merged_value)?,
mcp: McpConfigCollection {
servers: mcp_servers,
},
oauth: parse_optional_oauth_config(&merged_value, "merged settings.oauth")?,
model: parse_optional_model(&merged_value),
aliases: parse_optional_aliases(&merged_value)?,
permission_mode: parse_optional_permission_mode(&merged_value)?,
permission_rules: parse_optional_permission_rules(&merged_value)?,
sandbox: parse_optional_sandbox_config(&merged_value)?,
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
};
let config = RuntimeConfig {
merged,
loaded_entries,
feature_config,
};
Ok((config, all_warnings))
}
}
impl RuntimeConfig {

View File

@@ -65,12 +65,12 @@ pub use compact::{
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
};
pub use config::{
ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection,
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,
RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, ScopedMcpServerConfig,
CLAW_SETTINGS_SCHEMA_NAME,
suppress_config_warnings_for_json_mode, ConfigEntry, ConfigError, ConfigLoader, ConfigSource,
McpConfigCollection, McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig,
McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
McpWebSocketServerConfig, OAuthConfig, ProviderFallbackConfig, ResolvedPermissionMode,
RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig,
RuntimePluginConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
};
pub use config_validate::{
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,

File diff suppressed because it is too large Load Diff

View File

@@ -266,13 +266,15 @@ fn compact_subcommand_json_help_fails_fast_when_stdin_closed() {
!output.status.success(),
"compact json help should fail non-zero"
);
assert!(
output.stdout.is_empty(),
"compact json help should not start a prompt/spinner on stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
// #819/#820/#823: JSON abort envelopes route to stdout
let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8");
let parsed: Value = serde_json::from_str(stderr.trim()).expect("stderr should be JSON error");
assert!(
stderr.trim().is_empty() || !stderr.trim_start().starts_with('{'),
"compact json help should not emit JSON envelope to stderr (#819/#820/#823): {stderr}"
);
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
let parsed: Value =
serde_json::from_str(stdout.trim()).expect("stdout should be JSON error envelope");
assert_eq!(parsed["status"], "error");
assert_eq!(parsed["error_kind"], "interactive_only");
assert_eq!(parsed["action"], "abort");

File diff suppressed because it is too large Load Diff

View File

@@ -521,8 +521,9 @@ fn resumed_stub_command_emits_not_implemented_json() {
// Stub commands exit with code 2
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).expect("utf8");
let parsed: Value = serde_json::from_str(stderr.trim()).expect("should be json");
// #819/#820/#823: JSON abort envelopes route to stdout
let stdout = String::from_utf8(output.stdout).expect("utf8");
let parsed: Value = serde_json::from_str(stdout.trim()).expect("should be json");
assert_eq!(
parsed["status"], "error",
"stub command should emit status:error"

View File

@@ -16,6 +16,16 @@ def run(cmd: list[str], cwd: Path) -> int:
return subprocess.run(cmd, cwd=str(cwd)).returncode
def run_quiet_until_failure(cmd: list[str], cwd: Path) -> int:
result = subprocess.run(cmd, cwd=str(cwd), text=True, capture_output=True)
if result.returncode:
if result.stdout:
print(result.stdout, end="")
if result.stderr:
print(result.stderr, end="", file=sys.stderr)
return result.returncode
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=["generate", "validate"])
@@ -26,11 +36,13 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv)
repo_root = args.repo_root.resolve()
script_root = Path(__file__).resolve().parent
tool_root = script_root.parent
board_json = repo_root / args.board_json
board_md = repo_root / args.board_md
generator = repo_root / "scripts" / "generate_cc2_board.py"
validator = repo_root / "scripts" / "validate_cc2_board.py"
renderer = repo_root / ".omx" / "cc2" / "render_board_md.py"
generator = script_root / "generate_cc2_board.py"
validator = script_root / "validate_cc2_board.py"
renderer = tool_root / ".omx" / "cc2" / "render_board_md.py"
if args.command == "generate":
rc = run([sys.executable, str(generator), "--repo-root", str(repo_root), "--out-dir", str(board_json.parent)], repo_root)
@@ -43,7 +55,7 @@ def main(argv: list[str] | None = None) -> int:
[sys.executable, str(renderer), str(board_json), str(board_md), "--check"],
]
for cmd in checks:
rc = run(cmd, repo_root)
rc = run_quiet_until_failure(cmd, repo_root)
if rc:
return rc
print(f"CC2 board validation PASS: {board_json} and {board_md} are canonical and in sync")

View File

@@ -13,6 +13,24 @@
#
set -euo pipefail
usage() {
sed -n '2,12p' "$0" | sed 's/^# //; s/^#//'
}
if [[ $# -gt 0 ]]; then
case "$1" in
--help|-h)
usage
exit 0
;;
*)
echo "error: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
fi
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUST_DIR="$REPO_ROOT/rust"
BINARY="$RUST_DIR/target/debug/claw"
@@ -60,8 +78,8 @@ echo " export CLAW=$BINARY" >&2
echo "" >&2
echo " Dogfood with isolated config (no real user config on stderr):" >&2
echo " CLAW_ISOLATED=\$(mktemp -d)" >&2
echo " trap 'rm -rf \"\$CLAW_ISOLATED\"' EXIT" >&2
echo " CLAW_CONFIG_HOME=\$CLAW_ISOLATED \$CLAW plugins list --output-format json" >&2
echo " rm -rf \$CLAW_ISOLATED" >&2
echo "" >&2
echo " cargo run overhead: ~1s/invocation vs 7ms for pre-built binary." >&2
echo " Prefer pre-built binary (\$CLAW) for dogfood loops." >&2

View File

@@ -503,8 +503,12 @@ def main() -> int:
args = parser.parse_args()
repo_root = args.repo_root.resolve()
out_dir = args.out_dir or (repo_root / ".omx" / "cc2")
try:
board = build_board(repo_root)
except FileNotFoundError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
out_dir.mkdir(parents=True, exist_ok=True)
board = build_board(repo_root)
board_json = out_dir / "board.json"
board_md = out_dir / "board.md"
board_json.write_text(json.dumps(board, indent=2, sort_keys=True) + "\n", encoding="utf-8")

View File

@@ -11,6 +11,7 @@ set -euo pipefail
MIN_ID=723
ROADMAP="ROADMAP.md"
ROADMAP_PATH_SEEN=0
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -31,7 +32,12 @@ while [[ $# -gt 0 ]]; do
exit 2
;;
*)
if [[ "$ROADMAP_PATH_SEEN" -ne 0 ]]; then
echo "error: unexpected extra ROADMAP path: $1" >&2
exit 2
fi
ROADMAP="$1"
ROADMAP_PATH_SEEN=1
shift
;;
esac

View File

@@ -17,7 +17,31 @@
# and resolve any append collision at git-push time.
set -euo pipefail
ROADMAP="${1:-ROADMAP.md}"
ROADMAP="ROADMAP.md"
ROADMAP_PATH_SEEN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
sed -n '2,15p' "$0" | sed 's/^# //; s/^#//'
exit 0
;;
--*)
echo "error: unknown option: $1" >&2
exit 2
;;
*)
if [[ "$ROADMAP_PATH_SEEN" -ne 0 ]]; then
echo "error: unexpected extra ROADMAP path: $1" >&2
exit 2
fi
ROADMAP="$1"
ROADMAP_PATH_SEEN=1
shift
;;
esac
done
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
CHECKER="$SCRIPT_DIR/roadmap-check-ids.sh"

View File

@@ -44,7 +44,17 @@ def main() -> int:
args = parser.parse_args()
repo_root = args.repo_root.resolve()
board_path = args.board or (repo_root / ".omx" / "cc2" / "board.json")
board = json.loads(board_path.read_text(encoding="utf-8"))
try:
board = json.loads(board_path.read_text(encoding="utf-8"))
except FileNotFoundError:
print(f"error: board not found at {board_path}")
return 1
except IsADirectoryError:
print(f"error: board path is a directory: {board_path}")
return 1
except json.JSONDecodeError as exc:
print(f"error: invalid board JSON at {board_path}: {exc}")
return 1
errors: list[str] = []
ids = set()
for index, item in enumerate(board.get("items", []), 1):