Compare commits

...

49 Commits

Author SHA1 Message Date
YeonGyu-Kim
bac47d91bd style: cargo fmt after conflict resolution 2026-05-05 04:53:55 +09:00
YeonGyu-Kim
c956e46751 fix: block /plugins update in resume mode, fix comment
Address REQUEST_CHANGES from OMX review:
1. Add 'update' to the blocked mutation actions in resume mode
   (previously only install/uninstall/enable/disable were blocked)
2. Fix comment: 'Only list is supported' instead of 'Only list/help'
   since /plugins help doesn't actually parse as a valid action
2026-05-05 04:51:55 +09:00
YeonGyu-Kim
08fc38f51b style: cargo fmt line wrap in run_resume_command plugins handler 2026-05-05 04:51:55 +09:00
YeonGyu-Kim
4fc63e338d fix: support /plugins slash command in resume mode
Move SlashCommand::Plugins out of the 'unsupported resumed slash
command' catch-all and add a handler arm in run_resume_command that
calls handle_plugins_slash_command for list/help actions.

Mutation actions (install/uninstall/enable/disable) are rejected with
a clear error since there is no runtime to reload in resume mode.

Add /plugins coverage to resumed_inventory_commands test in
output_format_contract.rs: kind, action, reload_runtime, target.

Before: claw --resume session.jsonl /plugins --output-format json
-> {error: 'unsupported resumed slash command', type: 'error'}, exit 1

After: claw --resume session.jsonl /plugins --output-format json
-> {kind: 'plugin', action: 'list', ...}, exit 0
2026-05-05 04:51:55 +09:00
YeonGyu-Kim
ac8a24b30b fix(config): emit section and section_value in JSON output for config subcommands (#2990)
`claw config model --output-format json` and all other section subcommands
(`env`, `hooks`, `plugins`) returned identical output with no section field
— the section arg was parsed but discarded (_section parameter).

Fix: render_config_json now:
- Passes section through to handler
- Looks up the section value via runtime_config.get(), converting the
  internal JsonValue to serde_json::Value via render()+parse
- Emits `section` (string) and `section_value` (JSON value or null)
  in the response envelope
- Returns ok:false + error for unsupported section tokens

Test: config_section_json_emits_section_and_value asserts:
- No section field when no section arg
- section + section_value fields present for all known sections
- ok:false + error for unknown section

Pinpoint: ROADMAP #126
2026-05-05 04:50:33 +09:00
YeonGyu-Kim
94b80a05d3 fix(skills): route show/info/list-filter to local, not model invoke (#2988)
`claw skills show <name>`, `claw skills info <name>`, and
`claw skills list <filter>` were all falling through to
SkillSlashDispatch::Invoke, which spawned a real model session,
consumed tokens, and created session files.

Root cause: classify_skills_slash_command had no guards for
these discovery prefixes; every non-reserved arg became Invoke.

Fix:
- Add "show", "info" as Local-only bare tokens
- Add starts_with guards for "show ", "info ", "list " args
- handle_skills_slash_command: filter skill list by name/substring
  for show/info/list-filter paths (no model call, no session)
- handle_skills_slash_command_json: same structured filtering

Test: skills_show_and_list_filter_do_not_invoke_model asserts
  classify_skills_slash_command returns Local for all discovery
  patterns and still returns Invoke for bare skill names.

Pinpoint: ROADMAP #502
2026-05-05 04:50:30 +09:00
YeonGyu-Kim
9b97c4d832 fix(tests): isolate CLAW_CONFIG_HOME in resumed_status JSON test (#2992)
resumed_status_command_emits_structured_json_when_requested was reading
the real ~/.claw/settings.json, causing loaded_config_files to be 1
instead of the expected 0 on machines with user config present.

Root cause: unlike other tests (e.g. resumed_config_command_loads_settings_files),
this test did not pass an isolated CLAW_CONFIG_HOME env var to run_claw,
so claw fell back to the real HOME and loaded the developer's settings file.

Fix: create a temp config-home dir and pass it as CLAW_CONFIG_HOME via
run_claw_with_env. This gives the assertion a clean 0-file baseline.

Unblocks PRs #2973, #2988, #2990 which all failed this same test on main.

Ref: ROADMAP #65
2026-05-05 04:49:46 +09:00
YeonGyu-Kim
1206f4131d fix(resume): emit structured JSON for /agents --output-format json (#2987)
Resumed /agents --output-format json was returning a human-readable
text render wrapped in a JSON envelope field instead of the actual
structured agent list. The run_resume_command handler was calling
handle_agents_slash_command (text) for the json field instead of
handle_agents_slash_command_json.

Fix: use handle_agents_slash_command_json for the json outcome field,
matching the pattern already used by /skills and /plugins.

Test: extended resumed_inventory_commands_emit_structured_json_when_requested
to cover /agents, asserting kind=="agents", action=="list",
agents is an array, and count is a number (not a text render).
2026-05-05 04:20:52 +09:00
YeonGyu-Kim
c99330372c fix(version): add build_date and executable_path to version JSON output
`claw version --output-format json` was missing build_date and
executable_path, making it impossible to identify which binary is
running or correlate it with a specific build/commit.

Fix: version_json_value() now includes:
- build_date: compile-time BUILD_DATE env (already in text output)
- executable_path: std::env::current_exe() at runtime

Test: version_emits_json_when_requested extended to assert both fields
are strings in the JSON envelope.

Pinpoint: ROADMAP #507
2026-05-05 04:20:12 +09:00
YeonGyu-Kim
8e45f1850c test(output_format_contract): add plugins json coverage to inventory_commands test (#2972)
Add four assertions to inventory_commands_emit_structured_json_when_requested:
- kind == "plugin"
- action == "list"
- reload_runtime is boolean
- target is null when no plugin is targeted

Closes the only major --output-format json surface with zero contract
coverage. All other surfaces (agents, mcp, skills, status, sandbox,
doctor, help, version, acp, bootstrap-plan, system-prompt, init, diff,
config) already had test assertions.
2026-05-01 06:03:31 +09:00
YeonGyu-Kim
57096b0a1a docs(roadmap): add no-session kind drift item
Adds ROADMAP #422 documenting the concrete export/resume no-session ErrorKind drift.
2026-05-01 02:46:12 +09:00
Bellman
e939777f92 Merge pull request #2921 from ultraworkers/docs/roadmap-381-cache-help-json-hangs
docs(roadmap): add #381 — cache help json hangs
2026-04-30 12:09:06 +09:00
Yeachan-Heo
1093e26792 Document cache help JSON hang
Constraint: ROADMAP-only dogfood follow-up for 03:00 nudge on rebuilt claw git_sha d95b230c

Rejected: implementation change to cache help; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus version JSON responsiveness sanity check
Scope-risk: narrow
Directive: Continue preflight help hang coverage with distinct cache surface pinpoint
Tested: repeated timeout --kill-after=1s 8s ./rust/target/debug/claw cache --help --output-format json; timeout --kill-after=1s 8s ./rust/target/debug/claw version --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-30 03:06:16 +00:00
Bellman
44cca2054d Merge pull request #2918 from ultraworkers/docs/roadmap-380-tokens-help-json-hangs
docs(roadmap): add #380 — tokens help json hangs
2026-04-30 11:38:50 +09:00
Yeachan-Heo
6dc7b26d82 Document tokens help JSON hang
Constraint: ROADMAP-only dogfood follow-up for 02:30 nudge on rebuilt claw git_sha d95b230c

Rejected: implementation change to tokens help; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus version JSON responsiveness sanity check
Scope-risk: narrow
Directive: Continue cost/token preflight help coverage with a distinct tokens surface pinpoint
Tested: repeated timeout 8 ./rust/target/debug/claw tokens --help --output-format json; timeout 8 ./rust/target/debug/claw version --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-30 02:35:19 +00:00
Bellman
a0bd406c8f Merge pull request #2915 from ultraworkers/docs/roadmap-358-cost-help-json-hangs
docs(roadmap): add #358 — cost help json hangs
2026-04-30 11:32:32 +09:00
Yeachan-Heo
b62646edfe Document cost help JSON hang
Constraint: ROADMAP-only dogfood follow-up for 02:00 nudge on rebuilt claw git_sha d95b230c

Rejected: implementation change to cost help; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus version JSON responsiveness sanity check
Scope-risk: narrow
Directive: Continue help surface coverage after #356/#357 but preserve exact evidence only
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw cost --help --output-format json; timeout 8 ./rust/target/debug/claw version --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-30 02:03:20 +00:00
Bellman
d95b230cae Merge pull request #2912 from ultraworkers/docs/roadmap-357-doctor-help-json-plain-text
docs(roadmap): add #357 — doctor help json emits plain text
2026-04-30 11:01:19 +09:00
Yeachan-Heo
f48f156754 Document doctor help JSON plain-text fallback
Constraint: ROADMAP-only dogfood follow-up for 01:30 nudge on rebuilt claw git_sha 52a909ce

Rejected: implementation change to doctor help; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus status help plain-text sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2911 with verified help JSON-format fallback gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw doctor --help --output-format json; timeout 8 ./rust/target/debug/claw status --help --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-30 01:31:48 +00:00
Bellman
52a909cebe Merge pull request #2908 from ultraworkers/docs/roadmap-356-status-help-json-plain-text
docs(roadmap): add #356 — status help json emits plain text
2026-04-30 10:30:47 +09:00
Yeachan-Heo
c4c618e476 Document status help JSON plain-text fallback
Constraint: ROADMAP-only dogfood follow-up for 01:00 nudge on rebuilt claw git_sha 74338dc6

Rejected: implementation change to status help; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus version JSON sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2907 with verified help JSON-format fallback gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw status --help --output-format json; timeout 8 ./rust/target/debug/claw version --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-30 01:02:15 +00:00
Bellman
74338dc635 Merge pull request #2904 from ultraworkers/docs/roadmap-355-session-json-help-list-hangs
docs(roadmap): add #355 — session json help/list hangs
2026-04-30 10:01:09 +09:00
Yeachan-Heo
c092cf7fef Document session JSON help/list hang
Constraint: ROADMAP-only dogfood follow-up for 00:30 nudge on rebuilt claw git_sha 8e24f304

Rejected: implementation change to session command dispatch; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded session list samples plus session help bounded probe also hung/no bytes
Scope-risk: narrow
Directive: Switch from memory command introspection to session command introspection clawability
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw session list --output-format json; timeout 8 ./rust/target/debug/claw session help --output-format json/killed after no bytes; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-30 00:32:26 +00:00
Bellman
8e24f3049e Merge pull request #2901 from ultraworkers/docs/roadmap-354-memory-list-hangs
docs(roadmap): add #354 — memory json help/list hangs
2026-04-30 09:30:57 +09:00
Yeachan-Heo
71d8e7b925 Document memory JSON help/list hang
Constraint: ROADMAP-only dogfood follow-up for 00:00 nudge on rebuilt claw git_sha 19947545

Rejected: implementation change to memory command dispatch; request was one concrete follow-up if no backlog item
Confidence: high after bounded memory list samples plus memory help bounded sanity also hung
Scope-risk: narrow
Directive: Switch from plugin lifecycle repetition to memory command introspection clawability
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; timeout 8 ./rust/target/debug/claw memory list --output-format json samples; timeout 8 ./rust/target/debug/claw memory help --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-30 00:02:58 +00:00
Bellman
19947545e2 Merge pull request #2899 from ultraworkers/docs/roadmap-353-plugins-uninstall-stderr-only
docs(roadmap): add #353 — plugins uninstall json error is stderr-only
2026-04-30 09:01:02 +09:00
Yeachan-Heo
f7b2d8d6fe Document plugins uninstall JSON stderr-only not-found
Constraint: ROADMAP-only dogfood follow-up for 23:30 nudge on rebuilt claw git_sha 6f92e54d

Rejected: implementation change to plugin lifecycle uninstall; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus prompt list sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2897 with verified stderr-only JSON-mode gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins uninstall does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 23:31:56 +00:00
Bellman
6f92e54dc0 Merge pull request #2895 from ultraworkers/docs/roadmap-352-plugins-update-stderr-only
docs(roadmap): add #352 — plugins update json error is stderr-only
2026-04-30 08:30:58 +09:00
Yeachan-Heo
31d9198a02 Document plugins update JSON stderr-only not-found
Constraint: ROADMAP-only dogfood follow-up for 23:00 nudge on rebuilt claw git_sha 5eb1d7d8

Rejected: implementation change to plugin lifecycle update; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus prompt list sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2894 with verified stderr-only JSON-mode gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins update does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 23:02:03 +00:00
Bellman
5eb1d7d824 Merge pull request #2892 from ultraworkers/docs/roadmap-351-plugins-disable-stderr-only
docs(roadmap): add #351 — plugins disable json error is stderr-only
2026-04-30 08:00:56 +09:00
Yeachan-Heo
3b03375e69 Document plugins disable JSON stderr-only not-found
Constraint: ROADMAP-only dogfood follow-up for 22:30 nudge on rebuilt claw git_sha 0f9e8915

Rejected: implementation change to plugin lifecycle mutation; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus prompt list sanity check
Scope-risk: narrow
Directive: Replaces invalid hang PR #2891 with verified stderr-only JSON-mode gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins disable does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 22:32:14 +00:00
Bellman
0f9e8915be Merge pull request #2887 from ultraworkers/docs/roadmap-350-plugins-enable-missing-hangs
docs(roadmap): add #350 — plugins enable missing target hangs
2026-04-30 07:30:51 +09:00
Yeachan-Heo
ab95b75fcd Document plugins enable missing-target hang
Constraint: ROADMAP-only dogfood follow-up for 22:00 nudge on rebuilt claw git_sha ee44ff98

Rejected: implementation change to plugin lifecycle mutation; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples plus prompt list sanity check
Scope-risk: narrow
Directive: Keep supported lifecycle missing-target hang distinct from #348 list schema and #349 unsupported show action
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins enable does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 22:02:04 +00:00
Bellman
ee44ff984d Merge pull request #2886 from ultraworkers/docs/roadmap-349-plugins-show-unsupported-success
docs(roadmap): add #349 — plugins unsupported action returns success-shaped json
2026-04-30 07:00:56 +09:00
Yeachan-Heo
2ab26df4bd Document plugins unsupported action success-shaped JSON
Constraint: ROADMAP-only dogfood follow-up for 21:30 nudge on rebuilt claw git_sha a2a38df9

Rejected: implementation change to plugin action dispatch; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples
Scope-risk: narrow
Directive: Replaces invalid hang PR #2885 with verified unsupported-action classification gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins show does-not-exist --output-format json; timeout 8 ./rust/target/debug/claw plugins list --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 21:32:19 +00:00
Bellman
a2a38df9b8 Merge pull request #2883 from ultraworkers/docs/roadmap-348-plugins-list-prose-only
docs(roadmap): add #348 — plugins list json is prose-only
2026-04-30 06:31:11 +09:00
Yeachan-Heo
fd90c9fe67 Document plugins list prose-only JSON inventory
Constraint: ROADMAP-only dogfood follow-up for 21:00 nudge on rebuilt claw git_sha cca6f682

Rejected: implementation change to plugin list serializer; request was one concrete follow-up if no backlog item
Confidence: high after repeated bounded samples
Scope-risk: narrow
Directive: Keep plugin inventory schema issue distinct from broad help JSON opacity
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; repeated timeout 8 ./rust/target/debug/claw plugins list --output-format json; ./rust/target/debug/claw plugins help --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 21:02:04 +00:00
Bellman
cca6f6829c Merge pull request #2881 from ultraworkers/docs/roadmap-347-mcp-show-missing-status-ok
docs(roadmap): add #347 — mcp show missing server reports status ok
2026-04-30 06:01:08 +09:00
Yeachan-Heo
c77d1a87e1 Document mcp show missing status contract gap
Constraint: ROADMAP-only dogfood follow-up for 20:30 nudge on rebuilt claw git_sha ee41b266

Rejected: implementation change to MCP show status schema; request was one concrete follow-up if no backlog item
Confidence: high after bounded successful repro
Scope-risk: narrow
Directive: Replaces invalid hang/nondeterminism PRs with verified status contract gap
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; ./rust/target/debug/claw mcp show does-not-exist --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 20:33:42 +00:00
Bellman
ee41b266d3 Merge pull request #2877 from ultraworkers/docs/roadmap-346-agents-show-help-fallback
docs(roadmap): add #346 — agents show falls back to help json
2026-04-30 05:30:50 +09:00
Yeachan-Heo
ca92c695f4 Document agents show help fallback gap
Constraint: ROADMAP-only dogfood follow-up for 20:00 nudge on rebuilt claw git_sha c6c01bea

Rejected: implementation change to native-agent detail dispatch; request was one concrete follow-up if no backlog item
Confidence: high
Scope-risk: narrow
Directive: Keep agent detail fallback distinct from #328/#329 native-agent source/schema issues; closed invalid hang hypotheses first
Tested: cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json; ./rust/target/debug/claw agents list --output-format json; ./rust/target/debug/claw agents show analyst --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 20:01:42 +00:00
Bellman
c6c01beaca Merge pull request #2871 from ultraworkers/docs/roadmap-345-config-sections-identical-json
docs(roadmap): add #345 — config sections return identical json
2026-04-30 04:41:58 +09:00
Yeachan-Heo
970cdc925e Document config sections identical JSON gap
Constraint: ROADMAP-only dogfood follow-up for 19:00 nudge on rebuilt claw git_sha a510f734

Rejected: implementation change to config section serialization; request was one concrete follow-up if no backlog item
Confidence: high
Scope-risk: narrow
Directive: Keep section-payload issue distinct from #344 section discovery/help
Tested: ./rust/target/debug/claw --resume latest /config env --output-format json; /config hooks; /config model; /config plugins; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 19:31:25 +00:00
Bellman
b2f7a3354f Merge pull request #2870 from ultraworkers/docs/roadmap-344-config-help-section-discovery
docs(roadmap): add #344 — config help lacks structured section discovery
2026-04-30 04:31:05 +09:00
Yeachan-Heo
2a08b7a35c Document config section discovery gap
Constraint: ROADMAP-only dogfood follow-up for 18:30 nudge on rebuilt claw git_sha a510f734

Rejected: implementation change to config slash dispatcher; request was one concrete follow-up if no backlog item
Confidence: high
Scope-risk: narrow
Directive: Keep /config section discovery issue distinct from #342 /commands and #343 /models correction issues
Tested: ./rust/target/debug/claw --resume latest /config help --output-format json; /config list; /config show; bare /config; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 19:00:29 +00:00
Bellman
a510f73422 Merge pull request #2866 from ultraworkers/docs/roadmap-343-models-dead-end-suggestion
docs(roadmap): add #343 — models suggestion dead-ends under resume json
2026-04-30 03:31:05 +09:00
Yeachan-Heo
1283c6d532 Document resume model suggestion dead-end
Constraint: ROADMAP-only dogfood follow-up for 17:00 nudge on rebuilt claw git_sha a1bfcd41

Rejected: implementation change to slash suggestion/resume-safety logic; request was one concrete follow-up if no backlog item
Confidence: high
Scope-risk: narrow
Directive: Keep /models suggestion issue distinct from #342 /commands discovery alias
Tested: ./rust/target/debug/claw --resume latest /models --output-format json; ./rust/target/debug/claw --resume latest /model --output-format json; ./rust/target/debug/claw --resume latest /tokens --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 17:02:18 +00:00
Bellman
a1bfcd4110 Merge pull request #2863 from ultraworkers/docs/roadmap-342-commands-discovery-alias
docs(roadmap): add #342 — commands discovery alias has no structured fallback
2026-04-30 02:01:43 +09:00
Yeachan-Heo
c49839bb1f Document slash command discovery alias gap
Constraint: ROADMAP-only dogfood follow-up for 16:30 nudge on rebuilt claw git_sha f65b2b4f

Rejected: implementation change to slash dispatcher; request was one concrete follow-up if no backlog item
Confidence: high
Scope-risk: narrow
Directive: Keep /commands discovery issue distinct from #340/#341 stderr-only envelope items
Tested: ./rust/target/debug/claw --resume latest /commands --output-format json; ./rust/target/debug/claw --resume latest /help --output-format json; git diff --check; scripts/fmt.sh --check
Not-tested: runtime behavior change, because this commit only documents the gap
2026-04-29 16:31:03 +00:00
5 changed files with 327 additions and 10 deletions

View File

@@ -6281,3 +6281,24 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
340. **Resume-safe `/session help --output-format json` writes its primary JSON error envelope to stderr and uses `type` instead of the session JSON `kind` vocabulary** — dogfooded 2026-04-29 on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `dc47482e`. Running `./rust/target/debug/claw --resume latest /session help --output-format json` wrote no stdout bytes, but wrote a JSON error object to stderr: `{"command":"/session help","error":"Unknown /session action ...","type":"error"}`. Meanwhile `/session list --output-format json` wrote valid stdout JSON with `kind=session_list`. The JSON output contract is therefore split across stderr for an error/help-ish action and switches vocabulary from `kind` to `type`; automation that reads stdout sees empty/non-JSON output and cannot handle errors consistently with successful session JSON responses. **Required fix shape:** (a) all `--output-format json` command responses, including resumed slash errors, should emit the primary JSON envelope on stdout; (b) use `kind:"error"` or a documented error schema consistently instead of an ad hoc `type` field; (c) reserve stderr prose for text mode or optional non-primary diagnostics, not the machine-readable envelope; (d) add a regression for `/session help` or an unsupported `/session` action under `--resume` proving stdout contains the structured JSON error envelope and stderr does not carry the only parseable payload. **Why this matters:** claws need one stdout JSON contract for both success and failure. If a help-ish session error is silently moved to stderr and shaped differently from `session_list`, orchestration lanes cannot distinguish an unsupported action from transport corruption or an empty response without bespoke stderr parsing. Source: gaebal-gajae dogfood follow-up for the 15:30 nudge on rebuilt `./rust/target/debug/claw` `dc47482e`.
341. **Resume-safe `/tasks --output-format json` emits an unsupported-command JSON error only on stderr and mixes `kind` with `type` classification vocabularies** — dogfooded 2026-04-29 for the 16:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `58569131`. Running `./rust/target/debug/claw --resume latest /tasks --output-format json` wrote no stdout bytes, but wrote a JSON error object to stderr: `{"command":"/tasks","error":"/tasks is not yet implemented in this build","kind":"unsupported_command","type":"error"}`. The unsupported command envelope therefore has two separate top-level classification vocabularies (`kind=unsupported_command` and `type=error`) and places the only parseable payload on stderr, while successful JSON commands use stdout and a `kind`-only classification. This is distinct from #340 because it is not session help; it shows implemented-but-unsupported command stubs can emit a dual-vocabulary error envelope. **Required fix shape:** (a) in `--output-format json` mode, emit the primary JSON envelope on stdout for unsupported resumed slash commands such as `/tasks`; (b) document and use one error discriminator, preferably `kind:"error"` plus `code:"unsupported_command"`, or `kind:"unsupported_command"` plus `status:"error"`, but not `type`; (c) reserve stderr for non-primary diagnostics or text-mode prose, never as the sole JSON payload; (d) add regression coverage for `/tasks` under `--resume` with JSON output proving stdout contains the structured error envelope, stderr is not the only parseable stream, and the envelope uses the documented single-vocabulary discriminator. **Why this matters:** claws need the same stdout JSON contract for implemented successes and implemented-but-unsupported stubs. If `/tasks` errors can silently move to stderr and advertise both `kind` and `type`, automation must special-case command stubs instead of applying one JSON error parser. Source: gaebal-gajae dogfood follow-up for the 16:00 nudge on rebuilt `./rust/target/debug/claw` `58569131`.
342. **Resume-safe `/commands --output-format json` is rejected as an unknown slash command even though the error points users at `/help` for slash-command discovery, leaving no structured command-index alias** — dogfooded 2026-04-29 for the 16:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `f65b2b4f`. Running `./rust/target/debug/claw --resume latest /commands --output-format json` wrote no stdout bytes and emitted only stderr JSON: `{"command":"/commands","error":"Unknown slash command: /commands\n Help /help lists available slash commands","type":"error"}`. In the same rebuilt binary, `./rust/target/debug/claw --resume latest /help --output-format json` succeeded on stdout but exposed only prose keys `kind,text`. The discoverability path therefore has two gaps at once: the intuitive `/commands` index/alias is unavailable, and the fallback suggestion is buried inside an error string rather than surfaced as structured `suggested_command` / `discovery_command` metadata. This is distinct from #340 and #341: the pinpoint is not merely stderr-only JSON error placement, but the absence of a machine-readable slash-command discovery alias/index and typed correction guidance when users or claws try the natural `/commands` form. **Required fix shape:** (a) either implement `/commands` as a resume-safe alias for slash-command discovery or return a typed `unknown_command` JSON envelope with `suggested_command:"/help"` and `discovery_command:"/help"` fields; (b) make the primary JSON error envelope follow the stdout JSON contract and single-discriminator schema from #340/#341; (c) expose structured slash-command inventory from the discovery surface rather than requiring callers to scrape `text`; (d) add regression coverage proving `/commands --output-format json` either returns the structured command inventory or returns a structured correction that automation can follow without parsing prose. **Why this matters:** claws need a predictable way to discover valid slash commands before invoking them. If the natural command-index spelling fails with stderr-only JSON and a human-formatted hint, orchestration has to guess, parse prose, and special-case command discovery before it can even learn the supported command surface. Source: gaebal-gajae dogfood follow-up for the 16:30 nudge on rebuilt `./rust/target/debug/claw` `f65b2b4f`.
343. **Resume-safe `/models --output-format json` suggests `/model` as a correction even though `/model` is itself unsupported in the same resume-safe JSON path** — dogfooded 2026-04-29 for the 17:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `a1bfcd41`. Running `./rust/target/debug/claw --resume latest /models --output-format json` wrote no stdout bytes and emitted stderr JSON: `{"command":"/models","error":"Unknown slash command: /models\n Did you mean /model, /tokens\n Help /help lists available slash commands","type":"error"}`. Immediately following the suggested correction with `./rust/target/debug/claw --resume latest /model --output-format json` also wrote no stdout bytes and returned `{"command":"/model","error":"unsupported resumed slash command","type":"error"}`. The correction path therefore points automation from an unknown plural form to a command that cannot run in the same resume-safe noninteractive mode, while `/tokens --output-format json` succeeds and exposes only token counters. This is distinct from #342's missing `/commands` discovery alias: the pinpoint here is dead-end suggestion quality and resume-safety awareness in `Did you mean` guidance. **Required fix shape:** (a) make unknown-command suggestions context-aware so resume-mode JSON only suggests commands that are actually resume-safe for the current invocation, or labels non-resume-safe suggestions with `resume_safe:false`; (b) expose suggestions as structured `suggestions[]` objects with `command`, `resume_safe`, `reason`, and optional `replacement_for` fields instead of burying them in the `error` string; (c) if `/model` remains interactive-only, suggest a machine-readable status/config/model inspection command that works under `--resume`, or return a typed `interactive_only` blocker; (d) add regression coverage proving `/models --output-format json` does not recommend an unusable `/model` command without structured resume-safety metadata. **Why this matters:** claws follow correction hints automatically. A suggestion that leads straight into another unsupported resumed slash command turns error recovery into a loop and makes command discovery less trustworthy than no suggestion at all. Source: gaebal-gajae dogfood follow-up for the 17:00 nudge on rebuilt `./rust/target/debug/claw` `a1bfcd41`.
344. **Resume-safe `/config help --output-format json` is treated as an unsupported config section instead of a structured config-section discovery surface** — dogfooded 2026-04-29 for the 18:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `a510f734`. Running `./rust/target/debug/claw --resume latest /config help --output-format json` wrote no stdout bytes and emitted stderr JSON: `{"command":"/config help","error":"Unsupported /config section 'help'. Use env, hooks, model, or plugins.\n Usage /config [env|hooks|model|plugins]\n\n/config\n Summary Inspect Claude config files or merged sections\n Usage /config [env|hooks|model|plugins]\n Category Config\n Resume Supported with --resume SESSION.jsonl","type":"error"}`. The same shape appears for natural discovery forms such as `/config list` and `/config show`, while bare `/config --output-format json` succeeds and returns config-file data. The config surface is therefore resume-supported, but its section discovery/help path is only available as a human-formatted error string on stderr, with no structured `sections[]`, no `help` alias, and no typed `unsupported_section` metadata. This is distinct from #342's missing slash-command index and #343's dead-end suggestion: the pinpoint is a command-specific subcommand/section discovery contract for an otherwise working resume-safe command. **Required fix shape:** (a) make `/config help` or `/config sections` resume-safe and return stdout JSON containing supported sections such as `env`, `hooks`, `model`, and `plugins`; (b) for unsupported config sections, emit a typed JSON envelope with `kind:"error"` or equivalent plus `code:"unsupported_config_section"`, `section`, and structured `supported_sections[]`; (c) keep human usage text optional, not the only machine-readable recovery path; (d) add regression coverage proving `/config help --output-format json` or its canonical replacement exposes structured section metadata and that `/config list`/`show` errors include structured supported-section guidance. **Why this matters:** config inspection is a control-plane surface. Claws should not have to intentionally trigger an error and scrape prose to learn which config sections can be inspected under `--resume`; section discovery needs the same machine-readable contract as the config payload itself. Source: gaebal-gajae dogfood follow-up for the 18:30 nudge on rebuilt `./rust/target/debug/claw` `a510f734`.
345. **Resume-safe `/config env|hooks|model|plugins --output-format json` accepts different section names but returns the same generic config-file summary for every section** — dogfooded 2026-04-29 for the 19:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `a510f734`. Running `./rust/target/debug/claw --resume latest /config env --output-format json`, `/config hooks`, `/config model`, and `/config plugins` all wrote stdout JSON successfully and no stderr, but each response had the same top-level shape and values: `kind:"config"`, `cwd`, `files[]`, `loaded_files:1`, and `merged_keys:1`. None of the outputs included the requested `section`, section-specific keys, hook/model/plugin/env data, `section_missing`, `section_empty`, or truncation metadata; the `env`, `hooks`, `model`, and `plugins` arguments appear to be accepted while producing an indistinguishable generic config summary. This is distinct from #344's missing config-section discovery/help path: the pinpoint here is that the advertised section-specific entrypoints do not produce section-specific machine-readable payloads once invoked. **Required fix shape:** (a) include a `section` field in `/config <section> --output-format json` responses; (b) return section-specific structured payloads for `env`, `hooks`, `model`, and `plugins`, with explicit empty/missing states when applicable; (c) preserve the config-file provenance summary separately from the requested section content so callers can tell what was inspected; (d) add regression coverage proving the four supported sections produce distinguishable JSON contracts and do not silently collapse to the bare `/config` summary. **Why this matters:** config inspection is used to diagnose model, hook, plugin, and env lifecycle issues. If every supported section returns the same generic file list, claws cannot tell whether a section is empty, unsupported, redacted, or simply ignored, and config troubleshooting remains prose/error archaeology instead of structured state inspection. Source: gaebal-gajae dogfood follow-up for the 19:00 nudge on rebuilt `./rust/target/debug/claw` `a510f734`.
346. **Top-level `agents show <name> --output-format json` accepts a natural agent-detail request but falls back to generic help JSON instead of returning the selected agent or a typed unsupported-detail error** — dogfooded 2026-04-29 for the 20:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `c6c01bea`. Running `./rust/target/debug/claw agents list --output-format json` returned a valid stdout JSON inventory with `kind:"agents"`, `action:"list"`, and an `agents[]` entry named `analyst`. Immediately running `./rust/target/debug/claw agents show analyst --output-format json` returned success on stdout but did not return the `analyst` detail object; instead it returned generic help-shaped JSON: `{"action":"help","kind":"agents","unexpected":"show analyst","usage":{"direct_cli":"claw agents [list|help]","slash_command":"/agents [list|help]",...}}`. Both stderr streams were empty. The command therefore accepts a natural detail-inspection spelling, recognizes it only as `unexpected`, and hides the absence of an agent-detail surface behind a successful help fallback rather than a typed `unsupported_agents_action` / `agent_detail_unavailable` error. This is distinct from #328 and #329: those cover source/provenance mismatch and slash `/agents` inventory flattening, while this pinpoint is the missing top-level agent detail/inspection contract after inventory discovery succeeds. **Required fix shape:** (a) either implement `agents show <name> --output-format json` returning the selected agent's structured fields and provenance, or return a non-success typed JSON error with `code:"unsupported_agents_action"`, `requested_action:"show"`, and `supported_actions:["list","help"]`; (b) include `agent_name` and whether the name exists in the current inventory when rejecting detail inspection; (c) avoid `action:"help"` success envelopes for unsupported subcommands because they make failed detail inspection look like intentional help output; (d) add regression coverage proving `agents show analyst --output-format json` does not silently collapse to generic help when `analyst` exists in `agents list`. **Why this matters:** claws discover agents first, then need to inspect a chosen agent before delegation. If the natural detail command returns successful generic help instead of a selected-agent payload or typed unsupported-action error, automation cannot distinguish typo, unsupported detail view, missing agent, or successful help request without comparing unrelated inventory output. Source: gaebal-gajae dogfood follow-up for the 20:00 nudge on rebuilt `./rust/target/debug/claw` `c6c01bea`; earlier false hang hypotheses for `mcp help` and `agents list` were closed after bounded repros succeeded.
347. **Top-level `mcp show <missing-server> --output-format json` reports a missing server as `status:"ok"` instead of a typed not-found/error status** — dogfooded 2026-04-29 for the 20:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `ee41b266`. After rebuilding and verifying the binary provenance, running `./rust/target/debug/claw mcp show does-not-exist --output-format json` returned stdout JSON with `{"action":"show","config_load_error":null,"found":false,"kind":"mcp","message":"server `does-not-exist` is not configured","server_name":"does-not-exist","status":"ok"}` and no stderr. `found:false` is useful, but pairing it with `status:"ok"` makes the command-level outcome ambiguous: a missing requested server is not an OK inspection result for automation that needs to distinguish successful detail retrieval from a not-found lookup. This is distinct from #327's MCP source-list mismatch and the invalid #2874/#2879/#2880 hang/nondeterminism hypotheses that were closed after bounded repros. **Required fix shape:** (a) return a typed not-found status such as `status:"not_found"` or `kind:"error"` plus `code:"mcp_server_not_found"` while preserving `server_name` and optional `available_servers[]`; (b) document whether `found:false` objects are considered success or error and keep that convention consistent across text and JSON modes; (c) ensure process exit semantics match the JSON status contract or expose a separate `exit_ok`/`lookup_status` field; (d) add regression coverage proving missing-server lookup is distinguishable from successful server detail retrieval without parsing the human `message`. **Why this matters:** MCP inspection is a control-plane diagnostic. If a missing server returns `status:"ok"`, claws can silently treat a failed lookup as healthy MCP state unless they special-case `found:false`, which defeats the purpose of a clear machine-readable status field. Source: gaebal-gajae dogfood follow-up for the 20:30 nudge on rebuilt `./rust/target/debug/claw` `ee41b266`.
348. **Top-level `plugins list --output-format json` returns plugin inventory only as a prose `message` string instead of structured `plugins[]` entries** — dogfooded 2026-04-29 for the 21:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `cca6f682`. Running `./rust/target/debug/claw plugins list --output-format json` repeatedly returned valid stdout JSON with `{"action":"list","kind":"plugin","message":"Plugins\n example-bundled v0.1.0 disabled\n sample-hooks v0.1.0 disabled","reload_runtime":false,"target":null}` and no stderr. The actual plugin names, versions, and enabled/disabled states are present only inside the human-formatted `message` table; there is no `plugins[]` array, no per-plugin `name`, `version`, `enabled`, `source`, `load_error`, or lifecycle/action metadata. This is distinct from #325's broad help JSON opacity and the config/MCP/agent items: the affected surface is plugin lifecycle inventory, where automation needs a structured list before enabling, disabling, updating, or uninstalling plugins. **Required fix shape:** (a) add `plugins[]` with stable per-plugin fields such as `name`, `version`, `enabled`, `source`, `configured`, `load_status`, and optional `error`; (b) keep `message` only as a human summary, not the sole inventory payload; (c) expose counts and truncation metadata if the list can be large; (d) add regression coverage proving `plugins list --output-format json` can be parsed without scraping the prose message and that disabled/enabled state survives as booleans/enums. **Why this matters:** plugin lifecycle management is a control-plane path. If the JSON inventory is just a text table, claws must scrape spacing-sensitive prose before deciding whether a plugin is installed, disabled, broken, or safe to mutate. Source: gaebal-gajae dogfood follow-up for the 21:00 nudge on rebuilt `./rust/target/debug/claw` `cca6f682`.
349. **Top-level `plugins show <name> --output-format json` returns success-shaped JSON for an unsupported plugin action instead of a typed unsupported-action error** — dogfooded 2026-04-29 for the 21:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `a2a38df9`. After rebuilding and verifying the binary provenance, repeated bounded runs of `./rust/target/debug/claw plugins show does-not-exist --output-format json` returned stdout JSON with `{"action":"show","kind":"plugin","message":"Unknown /plugins action 'show'. Use list, install, enable, disable, uninstall, or update.","reload_runtime":false,"target":"does-not-exist"}` and no stderr. The command therefore reports the requested unsupported action as the top-level `action:"show"` and exits successfully while hiding the failure class inside a human `message`; it does not provide `status:"unsupported_action"`, `code:"plugin_action_unsupported"`, or structured `supported_actions[]`. This is distinct from #348's prose-only plugin inventory schema: #348 covers `plugins list` payload shape, while this pinpoint covers unsupported plugin action classification and recovery metadata. **Required fix shape:** (a) return a typed stdout JSON error or explicit non-ok status for unsupported plugin actions, with `requested_action`, `supported_actions`, and `target` fields; (b) do not label the primary `action` as the unsupported requested verb unless a separate `status`/`code` makes the failure unambiguous; (c) keep the human message optional and avoid making it the only way to detect the unsupported action; (d) add regression coverage proving `plugins show foo --output-format json` is machine-classifiable as unsupported without scraping prose. **Why this matters:** plugin lifecycle automation follows action/status fields. If an unsupported mutation/inspection verb returns success-shaped JSON and only says "Unknown" in prose, claws can treat a failed preflight as a valid plugin show result and continue toward unsafe lifecycle actions. Source: gaebal-gajae dogfood follow-up for the 21:30 nudge on rebuilt `./rust/target/debug/claw` `a2a38df9`; invalid hang PR #2885 was closed after repeated bounded repros returned stdout JSON.
350. **Top-level `plugins enable <missing-plugin> --output-format json` hangs with zero stdout/stderr instead of returning a typed plugin-not-found or unsupported-target response** — dogfooded 2026-04-29 for the 22:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `ee44ff98`. After rebuilding and verifying the binary provenance, repeated bounded runs of `timeout 8 ./rust/target/debug/claw plugins enable does-not-exist --output-format json` exited `124` with `stdout=0` and `stderr=0`; a third sample was still stuck until killed. In the same rebuilt binary, `plugins list --output-format json` returned promptly with the known plugin inventory payload, proving the plugin top-level surface is reachable and narrowing the hang to missing-plugin lifecycle mutation. This is distinct from #348's prose-only list inventory and #349's unsupported `plugins show` success-shaped JSON: #350 covers a supported lifecycle verb (`enable`) against an absent target, where the CLI should be able to fail fast before any plugin runtime work. **Required fix shape:** (a) validate the target plugin against the discovered/configured inventory before invoking enable-side effects; (b) return bounded stdout JSON such as `kind:"plugin"`, `action:"enable"`, `status:"not_found"` or `kind:"error"`, `code:"plugin_not_found"`, `plugin`, and optional `available_plugins[]`; (c) add internal timeout/diagnostic metadata for plugin lifecycle operations so registry or hook stalls do not produce silent zero-byte hangs; (d) add regression coverage proving `plugins enable does-not-exist --output-format json` returns a typed JSON outcome within a deterministic budget and does not mutate plugin state. **Why this matters:** enable/disable/update/uninstall are destructive control-plane actions. A missing or stale plugin name must fail safely and machine-readably; otherwise claws cannot preflight plugin lifecycle operations, distinguish typo from loader deadlock, or recover without killing a hung process. Source: gaebal-gajae dogfood follow-up for the 22:00 nudge on rebuilt `./rust/target/debug/claw` `ee44ff98`.
351. **Top-level `plugins disable <missing-plugin> --output-format json` sends the JSON error envelope to stderr only, leaving stdout empty** — dogfooded 2026-04-29 for the 22:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `0f9e8915`. After rebuilding and verifying the binary provenance, repeated bounded runs of `timeout 8 ./rust/target/debug/claw plugins disable does-not-exist --output-format json` exited `1` with `stdout=0` and `stderr=113`; stderr contained JSON (`{"error":"plugin `does-not-exist` is not installed or discoverable","hint":null,"kind":"unknown","type":"error"}`), but stdout was empty. In the same rebuilt binary, `plugins list --output-format json` returned stdout JSON promptly with the known plugin inventory payload, proving the plugin command surface is reachable. This is distinct from #350's missing-target `plugins enable` zero-byte timeout: the disable path fails fast, but its JSON-mode error envelope is routed to stderr and uses generic `kind:"unknown"`/`type:"error"` instead of a plugin-specific stdout outcome. **Required fix shape:** (a) define and consistently document whether JSON mode emits machine-readable envelopes on stdout, stderr, or both for nonzero exits; (b) return a plugin-specific typed error with `kind:"plugin"` or `domain:"plugin"`, `action:"disable"`, `status:"not_found"` or `code:"plugin_not_found"`, `plugin`, and optional `available_plugins[]`; (c) keep stdout/stderr placement consistent across plugin lifecycle verbs so callers do not need per-action stream heuristics; (d) add regression coverage proving `plugins disable does-not-exist --output-format json` produces a typed plugin-not-found JSON contract on the documented stream. **Why this matters:** disable is a recovery/control-plane operation. A stale plugin name should be a structured, domain-specific not-found result on a predictable stream; otherwise claws that read stdout JSON for normal responses and stderr for human diagnostics must special-case this lifecycle failure. Source: gaebal-gajae dogfood follow-up for the 22:30 nudge on rebuilt `./rust/target/debug/claw` `0f9e8915`; invalid hang PR #2891 was closed after repeated bounded repros returned exit 1 with JSON on stderr.
352. **Top-level `plugins update <missing-plugin> --output-format json` sends a generic JSON error envelope to stderr only, leaving stdout empty** — dogfooded 2026-04-29 for the 23:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `5eb1d7d8`. After rebuilding and verifying the binary provenance, repeated bounded runs of `timeout 8 ./rust/target/debug/claw plugins update does-not-exist --output-format json` exited `1` with `stdout=0` and `stderr=97`; stderr contained JSON (`{"error":"plugin `does-not-exist` is not installed","hint":null,"kind":"unknown","type":"error"}`), but stdout was empty. In the same rebuilt binary, `plugins list --output-format json` returned stdout JSON promptly with the known plugin inventory payload. This is distinct from #350's missing-target `plugins enable` zero-byte timeout and parallel to #351's `plugins disable` stderr-only JSON envelope: update fails fast, but the JSON-mode error lives on stderr only and uses generic `kind:"unknown"`/`type:"error"` instead of a plugin-specific not-found contract. **Required fix shape:** (a) define and consistently document stdout/stderr placement for JSON-mode lifecycle errors; (b) return a plugin-specific typed error with `kind:"plugin"` or `domain:"plugin"`, `action:"update"`, `status:"not_found"` or `code:"plugin_not_found"`, `plugin`, and optional `available_plugins[]`; (c) share missing-target error-envelope behavior across disable/update/uninstall and reconcile it with enable's timeout path; (d) add regression coverage proving `plugins update does-not-exist --output-format json` produces a typed plugin-not-found JSON contract on the documented stream. **Why this matters:** update is a maintenance/control-plane operation often run in automation. A stale plugin name should produce a predictable, domain-specific not-found result, not require callers to special-case stderr-only generic error envelopes after explicitly requesting JSON. Source: gaebal-gajae dogfood follow-up for the 23:00 nudge on rebuilt `./rust/target/debug/claw` `5eb1d7d8`; invalid hang PR #2894 was closed after repeated bounded repros returned exit 1 with JSON on stderr.
353. **Top-level `plugins uninstall <missing-plugin> --output-format json` sends a generic JSON error envelope to stderr only, leaving stdout empty** — dogfooded 2026-04-29 for the 23:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `6f92e54d`. After rebuilding and verifying the binary provenance, repeated bounded runs of `timeout 8 ./rust/target/debug/claw plugins uninstall does-not-exist --output-format json` exited `1` with `stdout=0` and `stderr=97`; stderr contained JSON (`{"error":"plugin `does-not-exist` is not installed","hint":null,"kind":"unknown","type":"error"}`), but stdout was empty. In the same rebuilt binary, `plugins list --output-format json` returned stdout JSON promptly with the known plugin inventory payload. This is distinct from #350's missing-target `plugins enable` zero-byte timeout and parallel to #351/#352 for disable/update: uninstall fails fast, but the JSON-mode error lives on stderr only and uses generic `kind:"unknown"`/`type:"error"` instead of a plugin-specific not-found contract. **Required fix shape:** (a) define and consistently document stdout/stderr placement for JSON-mode lifecycle errors; (b) return a plugin-specific typed error with `kind:"plugin"` or `domain:"plugin"`, `action:"uninstall"`, `status:"not_found"` or `code:"plugin_not_found"`, `plugin`, and optional `available_plugins[]`; (c) share missing-target error-envelope behavior across disable/update/uninstall and reconcile it with enable's timeout path; (d) add regression coverage proving `plugins uninstall does-not-exist --output-format json` produces a typed plugin-not-found JSON contract on the documented stream. **Why this matters:** uninstall is the most destructive plugin lifecycle action. A stale plugin name should produce a predictable, domain-specific not-found result before cleanup hooks or loader work, not require callers to special-case stderr-only generic error envelopes after explicitly requesting JSON. Source: gaebal-gajae dogfood follow-up for the 23:30 nudge on rebuilt `./rust/target/debug/claw` `6f92e54d`; invalid hang PR #2897 was closed after repeated bounded repros returned exit 1 with JSON on stderr.
354. **Top-level `memory list` and `memory help` with `--output-format json` hang with zero stdout/stderr instead of returning bounded memory inventory/help or a typed unavailable response** — dogfooded 2026-04-30 for the 00:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `19947545`. After rebuilding and verifying the binary provenance, bounded runs of `timeout 8 ./rust/target/debug/claw memory list --output-format json` produced `stdout=0` and `stderr=0`; the first sample exited `124` and the second sample was still stuck until killed. A follow-up sanity check of `timeout 8 ./rust/target/debug/claw memory help --output-format json` also exited `124` with `stdout=0` and `stderr=0`, so the issue is broader than list inventory: even the memory help path can hang silently in JSON mode. This is distinct from prior plugin lifecycle stream/status items: the affected surface is memory command introspection, where claws need safe local help/inventory before reading or mutating memory. **Required fix shape:** (a) make `memory help` and `memory list --output-format json` return bounded local JSON without requiring external/authenticated backing store availability; (b) return stdout JSON with `kind:"memory"`, `action:"help"|"list"`, `status`, usage or `entries[]`, source/provenance, counts, and truncation metadata; (c) if credentials/config/backing store are missing or slow, return a typed JSON unavailable/config/timeout error instead of hanging; (d) add regression coverage proving both `memory help --output-format json` and `memory list --output-format json` return machine-readable outcomes within a deterministic budget. **Why this matters:** memory is a core clawability surface. If even help/list can hang silently with no bytes, agents cannot tell whether memory is empty, unavailable, remote-auth blocked, or deadlocked, and any higher-level recall/debug flow stalls at the first introspection step. Source: gaebal-gajae dogfood follow-up for the 00:00 nudge on rebuilt `./rust/target/debug/claw` `19947545`.
355. **Top-level `session list` and `session help` with `--output-format json` hang with zero stdout/stderr instead of returning bounded session inventory/help or a typed unavailable response** — dogfooded 2026-04-30 for the 00:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `8e24f304`. After rebuilding and verifying the binary provenance, repeated bounded runs of `timeout 8 ./rust/target/debug/claw session list --output-format json` exited `124` with `stdout=0` and `stderr=0`. A follow-up bounded `session help --output-format json` probe also produced no stdout/stderr before it had to be killed, so the issue is broader than inventory: even the session help path can silently hang in JSON mode. This is distinct from #354's memory help/list hang: the affected surface is session command introspection, where claws need a safe local way to enumerate resumable sessions or at least read usage before deciding whether to resume, inspect, or clean them up. **Required fix shape:** (a) make `session help` and `session list --output-format json` return bounded local JSON without waiting indefinitely on remote API/auth/session-store availability; (b) return stdout JSON with `kind:"session"`, `action:"help"|"list"`, `status`, usage or `sessions[]`, source/provenance, counts, and truncation metadata, or typed `status:"unavailable"`/`code` when backing state cannot be reached; (c) add explicit timeout diagnostics if a remote/authenticated session source is consulted; (d) add regression coverage proving both `session help --output-format json` and `session list --output-format json` return machine-readable outcomes within a deterministic budget. **Why this matters:** session inventory/help is a core recovery/control-plane path. If even help/list can hang silently with no bytes, claws cannot distinguish no sessions, missing credentials, remote API stall, corrupted local store, or dispatch deadlock, and resume/cleanup automation blocks before it can choose a safe next action. Source: gaebal-gajae dogfood follow-up for the 00:30 nudge on rebuilt `./rust/target/debug/claw` `8e24f304`.
356. **Top-level `status --help --output-format json` exits successfully but emits plain text help instead of JSON** — dogfooded 2026-04-30 for the 01:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `74338dc6`. After rebuilding and verifying the binary provenance, repeated bounded runs of `./rust/target/debug/claw status --help --output-format json` exited `0` with `stdout=326` and `stderr=0`, but stdout was plain text (`Status`, `Usage`, `Purpose`, `Output`, `Formats`, `Related`) rather than a JSON object. In the same rebuilt binary, `version --output-format json` returned proper stdout JSON with version/build metadata, proving the JSON output path itself is reachable. This is distinct from #354/#355 memory/session JSON help/list hangs: the status help path returns promptly, but ignores the requested JSON format. **Required fix shape:** (a) make `status --help --output-format json` emit valid stdout JSON with `kind:"help"` or `kind:"status"`, `action:"help"`, usage, options, examples, supported output formats, and related slash/direct commands; (b) preserve text help for default/text mode only; (c) add a `format:"json"` or equivalent field so callers can assert the contract without parsing prose; (d) add regression coverage proving status help with JSON format parses as JSON and does not silently fall back to plain text. **Why this matters:** help is the discovery surface automation uses before invoking status. If `--output-format json` is accepted but help remains plain text, claws must scrape formatting-sensitive prose or special-case help output, defeating the point of machine-readable CLI contracts. Source: gaebal-gajae dogfood follow-up for the 01:00 nudge on rebuilt `./rust/target/debug/claw` `74338dc6`; invalid hang PR #2907 was closed after repeated bounded repros returned promptly.
357. **Top-level `doctor --help --output-format json` exits successfully but emits plain text help instead of JSON** — dogfooded 2026-04-30 for the 01:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `52a909ce`. After rebuilding and verifying the binary provenance, repeated bounded runs of `./rust/target/debug/claw doctor --help --output-format json` exited `0` with `stdout=343` and `stderr=0`, but stdout was plain text (`Doctor`, `Usage`, `Purpose`, `Output`, `Formats`, `Related`) rather than a JSON object. In the same rebuilt binary, `status --help --output-format json` also returned promptly as plain text (#356), confirming a broader help-format fallback class while keeping this pinpoint on the doctor surface. This is distinct from #354/#355 memory/session JSON help/list hangs: doctor help returns promptly, but ignores the requested JSON format. **Required fix shape:** (a) make `doctor --help --output-format json` emit valid stdout JSON with `kind:"help"` or `kind:"doctor"`, `action:"help"`, usage, checks, options, examples, supported output formats, and related slash/direct commands; (b) preserve text help for default/text mode only; (c) add a `format:"json"` or equivalent field so callers can assert the contract without parsing prose; (d) add regression coverage proving doctor help with JSON format parses as JSON and does not silently fall back to plain text. **Why this matters:** doctor is the diagnostic entrypoint users reach for when things are broken. If JSON help falls back to prose, claws cannot discover diagnostic semantics or present structured recovery instructions without scraping formatting-sensitive text. Source: gaebal-gajae dogfood follow-up for the 01:30 nudge on rebuilt `./rust/target/debug/claw` `52a909ce`; invalid hang PR #2911 was closed after repeated bounded repros returned promptly.
358. **Top-level `cost --help --output-format json` hangs with zero stdout/stderr instead of returning bounded command help JSON** — dogfooded 2026-04-30 for the 02:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `d95b230c`. After rebuilding and verifying the binary provenance, repeated bounded runs of `timeout 8 ./rust/target/debug/claw cost --help --output-format json` exited `124` with `stdout=0` and `stderr=0`. In the same rebuilt binary, `version --output-format json` returned promptly with version/build metadata, proving the binary itself and the JSON output path are reachable; the hang is specific to the cost help path, though other help surfaces have separate known JSON contract issues (#356/#357). **Required fix shape:** (a) make `cost --help --output-format json` return static/bounded stdout JSON with `kind:"help"` or `kind:"cost"`, `action:"help"`, usage, options, examples, supported output formats, and related slash/direct commands; (b) ensure help rendering does not initialize slow cost/session/accounting providers; (c) if any dynamic provider is accidentally consulted, return a typed JSON timeout/unavailable error instead of hanging; (d) add regression coverage proving cost help in JSON mode returns within a deterministic budget. **Why this matters:** cost/tokens surfaces are commonly consumed by automation for budgeting. If even cost help can hang silently, claws cannot discover cost command semantics or present safe budget diagnostics before running potentially slow accounting paths. Source: gaebal-gajae dogfood follow-up for the 02:00 nudge on rebuilt `./rust/target/debug/claw` `d95b230c`.
380. **Top-level `tokens --help --output-format json` hangs with zero stdout/stderr instead of returning bounded command help JSON** — dogfooded 2026-04-30 for the 02:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `d95b230c`. After verifying #358 covered `cost --help`, a fresh adjacent probe on the token-budget surface showed the same silent failure class: repeated bounded runs of `timeout 8 ./rust/target/debug/claw tokens --help --output-format json` exited `124` with `stdout=0` and `stderr=0`. In the same rebuilt binary, `version --output-format json` returned promptly with version/build metadata, proving the binary itself and JSON output path are reachable. This is distinct from #358's cost help hang: the affected surface is the sibling `tokens` command help, which agents use before estimating prompt/session token budgets. **Required fix shape:** (a) make `tokens --help --output-format json` return static/bounded stdout JSON with `kind:"help"` or `kind:"tokens"`, `action:"help"`, usage, options, examples, supported output formats, and related slash/direct commands; (b) ensure help rendering does not initialize slow token accounting, session, or provider state; (c) if any dynamic provider is consulted, return a typed JSON timeout/unavailable error instead of hanging; (d) add regression coverage proving tokens help in JSON mode returns within a deterministic budget. **Why this matters:** token budgeting is a preflight clawability surface. If help hangs silently, automation cannot safely discover how to inspect or constrain token usage before running expensive prompts, and budget-aware wrappers stall at the discovery step. Source: gaebal-gajae dogfood follow-up for the 02:30 nudge on rebuilt `./rust/target/debug/claw` `d95b230c`.
381. **Top-level `cache --help --output-format json` hangs with zero stdout/stderr instead of returning bounded command help JSON** — dogfooded 2026-04-30 for the 03:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `d95b230c`. After #358 and #380 landed for the cost/tokens preflight help hangs, a fresh adjacent probe on the cache-control surface showed the same silent failure class: repeated bounded runs of `timeout --kill-after=1s 8s ./rust/target/debug/claw cache --help --output-format json` exited `124` with `stdout=0` and `stderr=0`. In the same rebuilt binary, `version --output-format json` returned promptly with version/build metadata, proving the binary itself and JSON output path are reachable. This is distinct from the separate `/cache` slash-command envelope mismatch class: the affected surface here is top-level `cache` command help, where agents need bounded local discovery before deciding whether to inspect, clear, or summarize cache state. **Required fix shape:** (a) make `cache --help --output-format json` return static/bounded stdout JSON with `kind:"help"` or `kind:"cache"`, `action:"help"`, usage, options, examples, supported output formats, and related slash/direct commands; (b) ensure help rendering does not initialize slow cache/session/provider state; (c) if any dynamic provider is consulted, return a typed JSON timeout/unavailable error instead of hanging; (d) add regression coverage proving cache help in JSON mode returns within a deterministic budget. **Why this matters:** cache inspection and cleanup are recovery/control-plane operations. If cache help hangs silently, claws cannot safely discover cache semantics before attempting cleanup, and automation stalls before it can choose a non-destructive cache action. Source: gaebal-gajae dogfood follow-up for the 03:00 nudge on rebuilt `./rust/target/debug/claw` `d95b230c`.
422. **`export --output-format json` and `--resume latest` report the same "no managed sessions" scenario using two different `kind` codes — `no_managed_sessions` vs `session_load_failed` — making "no session found" undetectable by a single kind-code check** — dogfooded 2026-04-30 KST (UTC+9) by Jobdori on `e939777f`. Running `claw export --output-format json` with no session present returns (on stderr, exit 1): `{"error":"no managed sessions found in .claw/sessions/<fingerprint>/","hint":"Start \`claw\` to create a session, then rerun with \`--resume latest\`.\nNote: claw partitions sessions per workspace fingerprint; sessions from other CWDs are invisible.","kind":"no_managed_sessions","type":"error"}`. Running `claw --resume latest /status --output-format json` with no session present returns (on stderr, exit 1): `{"error":"failed to restore session: no managed sessions found in .claw/sessions/<fingerprint>/","hint":"Start \`claw\` to create a session, then rerun with \`--resume latest\`.\nNote: claw partitions sessions per workspace fingerprint; sessions from other CWDs are invisible.","kind":"session_load_failed","type":"error"}`. Both describe the same root condition — there are no sessions to operate on — but they expose it via different `kind` discriminants. Automation that checks `kind == "no_managed_sessions"` to detect a cold workspace will miss the `--resume` path's `session_load_failed`, and vice versa. A wrapper that guards "run with --resume only if a session exists" must special-case both codes. The hint text is identical between them, suggesting the messages are logically equivalent. Additionally neither code matches the proposed canonical names `session_not_found` / `session_load_failed` as stable `ErrorKind` discriminants described in ROADMAP #77's fix shape, which explicitly proposes typed error-kind codes for session lifecycle failures. **Required fix shape:** (a) unify "no sessions found for this workspace fingerprint" under a single canonical `kind` code — either `no_managed_sessions` or `session_not_found` — used consistently by every command path that encounters an empty session registry; (b) if `session_load_failed` is a more general category (covering e.g. corrupt session files, IO errors, schema version mismatches), it should nest a concrete `reason:"no_managed_sessions"` or `reason:"session_not_found"` sub-field so callers can distinguish "empty registry" from "found but unreadable"; (c) align with the canonical error-kind contract proposed in #77; (d) add regression coverage proving `export` and `--resume latest` in an empty workspace both return an error with the same top-level `kind` code. **Why this matters:** session guard-rails in orchestration need a single stable `kind` to detect cold workspaces without enumerating all possible no-session synonyms. Two divergent codes for the same condition make defensive automation brittle and contradict the promise of machine-readable error envelopes. Source: Jobdori live dogfood, `e939777f`, 2026-04-30 KST (UTC+9).

View File

@@ -2371,6 +2371,40 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
let skills = load_skills_from_roots(&roots)?;
Ok(render_skills_report(&skills))
}
Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase();
let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?;
let filtered: Vec<_> = skills
.into_iter()
.filter(|s| s.name.to_lowercase().contains(&filter))
.collect();
Ok(render_skills_report(&filtered))
}
Some("show" | "info" | "describe") => {
let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?;
Ok(render_skills_report(&skills))
}
Some(args)
if args.starts_with("show ")
|| args.starts_with("info ")
|| args.starts_with("describe ") =>
{
let name = args
.splitn(2, ' ')
.nth(1)
.unwrap_or_default()
.trim()
.to_lowercase();
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)
.collect();
Ok(render_skills_report(&matched))
}
Some("install") => Ok(render_skills_usage(Some("install"))),
Some(args) if args.starts_with("install ") => {
let target = args["install ".len()..].trim();
@@ -2402,6 +2436,40 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
let skills = load_skills_from_roots(&roots)?;
Ok(render_skills_report_json(&skills))
}
Some(args) if args.starts_with("list ") => {
let filter = args["list ".len()..].trim().to_lowercase();
let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?;
let filtered: Vec<_> = skills
.into_iter()
.filter(|s| s.name.to_lowercase().contains(&filter))
.collect();
Ok(render_skills_report_json(&filtered))
}
Some("show" | "info" | "describe") => {
let roots = discover_skill_roots(cwd);
let skills = load_skills_from_roots(&roots)?;
Ok(render_skills_report_json(&skills))
}
Some(args)
if args.starts_with("show ")
|| args.starts_with("info ")
|| args.starts_with("describe ") =>
{
let name = args
.splitn(2, ' ')
.nth(1)
.unwrap_or_default()
.trim()
.to_lowercase();
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)
.collect();
Ok(render_skills_report_json(&matched))
}
Some("install") => Ok(render_skills_usage_json(Some("install"))),
Some(args) if args.starts_with("install ") => {
let target = args["install ".len()..].trim();
@@ -2419,10 +2487,20 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
#[must_use]
pub fn classify_skills_slash_command(args: Option<&str>) -> SkillSlashDispatch {
match normalize_optional_args(args) {
None | Some("list" | "help" | "-h" | "--help") => SkillSlashDispatch::Local,
None | Some("list" | "help" | "-h" | "--help" | "show" | "info" | "describe") => {
SkillSlashDispatch::Local
}
Some(args) if args == "install" || args.starts_with("install ") => {
SkillSlashDispatch::Local
}
Some(args)
if args.starts_with("list ")
|| args.starts_with("show ")
|| args.starts_with("info ")
|| args.starts_with("describe ") =>
{
SkillSlashDispatch::Local
}
Some(args) => SkillSlashDispatch::Invoke(format!("${}", args.trim_start_matches('/'))),
}
}
@@ -4619,6 +4697,32 @@ mod tests {
assert!(agents_error.contains(" Usage /agents [list|help]"));
}
#[test]
fn skills_show_and_list_filter_do_not_invoke_model() {
// `show`, `info`, `list <filter>` must route to Local, not Invoke.
// Regression for: `claw skills show plan` unexpectedly spawned a model session.
for token in &["show", "info", "describe"] {
assert_eq!(
classify_skills_slash_command(Some(token)),
SkillSlashDispatch::Local,
"`skills {token}` alone must be Local"
);
}
for prefix in &["show ", "info ", "list ", "describe "] {
let arg = format!("{prefix}plan");
assert_eq!(
classify_skills_slash_command(Some(&arg)),
SkillSlashDispatch::Local,
"`skills {arg}` must be Local, not Invoke"
);
}
// Bare invocable tokens still dispatch to Invoke.
assert_eq!(
classify_skills_slash_command(Some("plan")),
SkillSlashDispatch::Invoke("$plan".to_string()),
);
}
#[test]
fn accepts_skills_invocation_arguments_for_prompt_dispatch() {
assert_eq!(

View File

@@ -2627,12 +2627,15 @@ fn print_version(output_format: CliOutputFormat) -> Result<(), Box<dyn std::erro
}
fn version_json_value() -> serde_json::Value {
let executable_path = env::current_exe().ok().map(|p| p.display().to_string());
json!({
"kind": "version",
"message": render_version_report(),
"version": VERSION,
"git_sha": GIT_SHA,
"target": BUILD_TARGET,
"build_date": DEFAULT_DATE,
"executable_path": executable_path,
})
}
@@ -3523,10 +3526,10 @@ fn run_resume_command(
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(handle_agents_slash_command(args.as_deref(), &cwd)?),
json: Some(serde_json::json!({
"kind": "agents",
"text": handle_agents_slash_command(args.as_deref(), &cwd)?,
})),
json: Some(
serde_json::to_value(handle_agents_slash_command_json(args.as_deref(), &cwd)?)
.unwrap_or_else(|_| serde_json::json!(null)),
),
})
}
SlashCommand::Skills { args } => {
@@ -3542,6 +3545,37 @@ fn run_resume_command(
json: Some(handle_skills_slash_command_json(args.as_deref(), &cwd)?),
})
}
SlashCommand::Plugins { action, target } => {
// Only list is supported in resume mode (no runtime to reload)
match action.as_deref() {
Some("install") | Some("uninstall") | Some("enable") | Some("disable")
| Some("update") => {
return Err(
"resumed /plugins mutations are interactive-only; start `claw` and run `/plugins` in the REPL".into(),
);
}
_ => {}
}
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
let runtime_config = loader.load()?;
let mut manager = build_plugin_manager(&cwd, &loader, &runtime_config);
let result =
handle_plugins_slash_command(action.as_deref(), target.as_deref(), &mut manager)?;
let action_str = action.as_deref().unwrap_or("list");
let json = serde_json::json!({
"kind": "plugin",
"action": action_str,
"target": target,
"message": &result.message,
"reload_runtime": result.reload_runtime,
});
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(result.message),
json: Some(json),
})
}
SlashCommand::Doctor => {
let report = render_doctor_report()?;
Ok(ResumeCommandOutcome {
@@ -3628,7 +3662,6 @@ fn run_resume_command(
| SlashCommand::Model { .. }
| SlashCommand::Permissions { .. }
| SlashCommand::Session { .. }
| SlashCommand::Plugins { .. }
| SlashCommand::Login
| SlashCommand::Logout
| SlashCommand::Vim
@@ -6207,7 +6240,7 @@ fn render_config_report(section: Option<&str>) -> Result<String, Box<dyn std::er
}
fn render_config_json(
_section: Option<&str>,
section: Option<&str>,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
@@ -6240,13 +6273,52 @@ fn render_config_json(
})
.collect();
Ok(serde_json::json!({
let base = serde_json::json!({
"kind": "config",
"cwd": cwd.display().to_string(),
"loaded_files": loaded_paths.len(),
"merged_keys": runtime_config.merged().len(),
"files": files,
}))
});
if let Some(section) = section {
let section_rendered: Option<String> = match section {
"env" => runtime_config.get("env").map(|v| v.render()),
"hooks" => runtime_config.get("hooks").map(|v| v.render()),
"model" => runtime_config.get("model").map(|v| v.render()),
"plugins" => runtime_config
.get("plugins")
.or_else(|| runtime_config.get("enabledPlugins"))
.map(|v| v.render()),
other => {
return Ok(serde_json::json!({
"kind": "config",
"section": other,
"ok": false,
"error": format!("Unsupported config section '{other}'. Use env, hooks, model, or plugins."),
"cwd": cwd.display().to_string(),
"loaded_files": loaded_paths.len(),
"files": files,
}));
}
};
// Parse the rendered JSON string back into serde_json::Value so that
// section_value is a real JSON object/array in the envelope, not a quoted string.
let section_value: serde_json::Value = section_rendered
.as_deref()
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or(serde_json::Value::Null);
let mut obj = base;
let map = obj.as_object_mut().expect("base is object");
map.insert(
"section".to_string(),
serde_json::Value::String(section.to_string()),
);
map.insert("section_value".to_string(), section_value);
return Ok(obj);
}
Ok(base)
}
fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {

View File

@@ -30,6 +30,15 @@ fn version_emits_json_when_requested() {
let parsed = assert_json_command(&root, &["--output-format", "json", "version"]);
assert_eq!(parsed["kind"], "version");
assert_eq!(parsed["version"], env!("CARGO_PKG_VERSION"));
// Provenance fields must be present for binary identification (#507).
assert!(
parsed["build_date"].is_string(),
"build_date must be a string in version JSON"
);
assert!(
parsed["executable_path"].is_string(),
"executable_path must be a string in version JSON so callers can identify which binary is running"
);
}
#[test]
@@ -105,6 +114,18 @@ fn inventory_commands_emit_structured_json_when_requested() {
let skills = assert_json_command(&root, &["--output-format", "json", "skills"]);
assert_eq!(skills["kind"], "skills");
assert_eq!(skills["action"], "list");
let plugins = assert_json_command(&root, &["--output-format", "json", "plugins"]);
assert_eq!(plugins["kind"], "plugin");
assert_eq!(plugins["action"], "list");
assert!(
plugins["reload_runtime"].is_boolean(),
"plugins reload_runtime should be a boolean"
);
assert!(
plugins["target"].is_null(),
"plugins target should be null when no plugin is targeted"
);
}
#[test]
@@ -348,6 +369,62 @@ fn resumed_inventory_commands_emit_structured_json_when_requested() {
assert_eq!(skills["action"], "list");
assert!(skills["summary"]["total"].is_number());
assert!(skills["skills"].is_array());
let agents = assert_json_command_with_env(
&root,
&[
"--output-format",
"json",
"--resume",
session_path.to_str().expect("utf8 session path"),
"/agents",
],
&[
(
"CLAW_CONFIG_HOME",
config_home.to_str().expect("utf8 config home"),
),
("HOME", home.to_str().expect("utf8 home")),
],
);
assert_eq!(agents["kind"], "agents");
assert_eq!(agents["action"], "list");
assert!(
agents["agents"].is_array(),
"agents field must be a JSON array"
);
assert!(
agents["count"].is_number(),
"count must be a number, not a text render"
);
let plugins = assert_json_command_with_env(
&root,
&[
"--output-format",
"json",
"--resume",
session_path.to_str().expect("utf8 session path"),
"/plugins",
],
&[
(
"CLAW_CONFIG_HOME",
config_home.to_str().expect("utf8 config home"),
),
("HOME", home.to_str().expect("utf8 home")),
],
);
assert_eq!(plugins["kind"], "plugin");
assert_eq!(plugins["action"], "list");
assert!(
plugins["reload_runtime"].is_boolean(),
"plugins reload_runtime should be a boolean"
);
assert!(
plugins["target"].is_null(),
"plugins target should be null when no plugin is targeted"
);
}
#[test]
@@ -384,6 +461,44 @@ fn resumed_version_and_init_emit_structured_json_when_requested() {
assert!(root.join("CLAUDE.md").exists());
}
#[test]
fn config_section_json_emits_section_and_value() {
let root = unique_temp_dir("config-section-json");
fs::create_dir_all(&root).expect("temp dir should exist");
// Without a section: should return base envelope (no section field).
let base = assert_json_command(&root, &["--output-format", "json", "config"]);
assert_eq!(base["kind"], "config");
assert!(base["loaded_files"].is_number());
assert!(base["merged_keys"].is_number());
assert!(
base.get("section").is_none(),
"no section field without section arg"
);
// With a known section: should add section + section_value fields.
for section in &["model", "env", "hooks", "plugins"] {
let result = assert_json_command(&root, &["--output-format", "json", "config", section]);
assert_eq!(result["kind"], "config", "section={section}");
assert_eq!(
result["section"].as_str(),
Some(*section),
"section field must match requested section, got {result:?}"
);
assert!(
result.get("section_value").is_some(),
"section_value field must be present for section={section}"
);
}
// With an unsupported section: should return ok:false + error field.
let bad = assert_json_command(&root, &["--output-format", "json", "config", "unknown"]);
assert_eq!(bad["kind"], "config");
assert_eq!(bad["ok"], false);
assert!(bad["error"].as_str().is_some());
assert!(bad["section"].as_str().is_some());
}
fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value {
assert_json_command_with_env(current_dir, args, &[])
}

View File

@@ -227,6 +227,8 @@ fn resumed_status_command_emits_structured_json_when_requested() {
// given
let temp_dir = unique_temp_dir("resume-status-json");
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
let config_home = temp_dir.join("config-home");
fs::create_dir_all(&config_home).expect("isolated config home should exist");
let session_path = temp_dir.join("session.jsonl");
let mut session = workspace_session(&temp_dir);
@@ -238,7 +240,9 @@ fn resumed_status_command_emits_structured_json_when_requested() {
.expect("session should persist");
// when
let output = run_claw(
// Use an isolated CLAW_CONFIG_HOME so ~/.claw/settings.json is not loaded,
// which would cause loaded_config_files to be non-zero (#65).
let output = run_claw_with_env(
&temp_dir,
&[
"--output-format",
@@ -247,6 +251,7 @@ fn resumed_status_command_emits_structured_json_when_requested() {
session_path.to_str().expect("utf8 path"),
"/status",
],
&[("CLAW_CONFIG_HOME", config_home.to_str().expect("utf8 path"))],
);
// then