mirror of
https://github.com/instructkr/claude-code.git
synced 2026-05-30 09:16:44 +00:00
Compare commits
62 Commits
2b0412043a
...
docs/roadm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6094254b2 | ||
|
|
19947545e2 | ||
|
|
f7b2d8d6fe | ||
|
|
6f92e54dc0 | ||
|
|
31d9198a02 | ||
|
|
5eb1d7d824 | ||
|
|
3b03375e69 | ||
|
|
0f9e8915be | ||
|
|
ab95b75fcd | ||
|
|
ee44ff984d | ||
|
|
2ab26df4bd | ||
|
|
a2a38df9b8 | ||
|
|
fd90c9fe67 | ||
|
|
cca6f6829c | ||
|
|
c77d1a87e1 | ||
|
|
ee41b266d3 | ||
|
|
ca92c695f4 | ||
|
|
c6c01beaca | ||
|
|
970cdc925e | ||
|
|
b2f7a3354f | ||
|
|
2a08b7a35c | ||
|
|
a510f73422 | ||
|
|
1283c6d532 | ||
|
|
a1bfcd4110 | ||
|
|
c49839bb1f | ||
|
|
f65b2b4f0e | ||
|
|
f4b74e89dd | ||
|
|
5856913104 | ||
|
|
d45a0d2f5b | ||
|
|
dc47482e40 | ||
|
|
9537c97231 | ||
|
|
f56a5afcf7 | ||
|
|
3efaf551ed | ||
|
|
30c9b438ef | ||
|
|
587bb18572 | ||
|
|
24ccb59bd2 | ||
|
|
0e8e75ef75 | ||
|
|
0f7578c064 | ||
|
|
213d406cbf | ||
|
|
ee85fed6ca | ||
|
|
3a34d83749 | ||
|
|
981aff7c8b | ||
|
|
c94940effa | ||
|
|
b90875fa8e | ||
|
|
2567cbcc78 | ||
|
|
d607ff3674 | ||
|
|
cdf6282965 | ||
|
|
e7074f47ee | ||
|
|
9468383b67 | ||
|
|
1da2781816 | ||
|
|
9037430d52 | ||
|
|
8e22f757d8 | ||
|
|
7676b376ae | ||
|
|
1011a83823 | ||
|
|
1376d92064 | ||
|
|
be53e04671 | ||
|
|
cb56dc12ab | ||
|
|
71686a20fc | ||
|
|
07992b8a1b | ||
|
|
74ea754d29 | ||
|
|
77afde768c | ||
|
|
6db68a2baa |
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- Frameworks: none detected from the supported starter markers.
|
||||
|
||||
## Verification
|
||||
- Run Rust verification from `rust/`: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`
|
||||
- Run Rust verification from repo root: `scripts/fmt.sh --check`; for formatting use `scripts/fmt.sh`. Run Rust clippy/tests from `rust/`: `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`
|
||||
- `src/` and `tests/` are both present; update both surfaces together when behavior changes.
|
||||
|
||||
## Repository shape
|
||||
|
||||
48
ROADMAP.md
48
ROADMAP.md
@@ -2128,7 +2128,7 @@ Original filing (2026-04-13): user requested a `-acp` parameter to support ACP p
|
||||
|
||||
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdJ` on main HEAD `b7539e6` in response to Clawhip pinpoint nudge at `1494744278423961742`. Adjacent to #85 (skill discovery ancestor walk) on the *discovery* side — #85 is "skills are discovered too broadly," #95 is "skills are *installed* too broadly." Together they bound the skill-surface trust problem from both the read and the write axes. Distinct sub-cluster from the permission-audit bundle (#50 / #87 / #91 / #94) and from the truth-audit cluster (#80–#87, #89): this is specifically about *scope asymmetry between install and settings* and the *missing uninstall verb*.
|
||||
|
||||
96. **`claw --help`'s "Resume-safe commands:" one-liner summary does not filter `STUB_COMMANDS` — 62 documented slash commands that are explicitly marked unimplemented still show up as valid resume-safe entries, contradicting the main Interactive slash commands list just above it (which *does* filter stubs per ROADMAP #39)** — dogfooded 2026-04-18 on main HEAD `8db8e49` from `/tmp/cdK`. The `render_help` output emits two separate enumerations of slash commands; only one of them applies the stub filter. The Resume-safe summary advertises `/budget`, `/rate-limit`, `/metrics`, `/diagnostics`, `/bookmarks`, `/workspace`, `/reasoning`, `/changelog`, `/vim`, `/summary`, `/brief`, `/advisor`, `/stickers`, `/insights`, `/thinkback`, `/keybindings`, `/privacy-settings`, `/output-style`, `/allowed-tools`, `/tool-details`, `/language`, `/max-tokens`, `/temperature`, `/system-prompt` — all of which are explicitly in `STUB_COMMANDS` with "Did you mean" guards and no parse arm.
|
||||
96. **`claw --help`'s "Resume-safe commands:" one-liner summary does not filter `STUB_COMMANDS` — 62 documented slash commands that are explicitly marked unimplemented still show up as valid resume-safe entries, contradicting the main Interactive slash commands list just above it (which *does* filter stubs per ROADMAP #39)** — **done (verified 2026-04-29):** the Resume-safe command summary now applies the same `STUB_COMMANDS` filter as the Interactive slash command block before rendering help, so unimplemented slash-command stubs no longer advertise as resume-safe. Added `stub_commands_absent_from_resume_safe_help` to lock the filtered one-liner contract alongside the existing REPL completion filter. Fresh proof: `cargo fmt --all --check`, `cargo test -p rusty-claude-cli stub_commands_absent_from_resume_safe_help -- --nocapture`, and `cargo test -p rusty-claude-cli parses_direct_cli_actions -- --nocapture` pass. Original filing below for traceability.
|
||||
|
||||
**Concrete repro.**
|
||||
```
|
||||
@@ -6158,7 +6158,8 @@ load_session('nonexistent') # raises FileNotFoundError with no structured error
|
||||
**Blocker.** None.
|
||||
|
||||
**Source.** Jobdori dogfood sweep 2026-04-22 08:46 KST — inspected `src/session_store.py` public API, confirmed only `save_session` + `load_session` present, no list/delete/exists surface.
|
||||
200. **Interactive MCP/tool permission prompts are invisible blockers** — dogfooded 2026-04-18 from `clawcode-human`. The session emitted `SessionStart hook (completed)` and `UserPromptSubmit hook (completed)`, then stalled on an interactive MCP permission gate (`Allow the omx_memory MCP server to run tool "project_memory_read"?`). From the outside this looks like a ready-but-quiet lane even though the real state is `blocked waiting for permission`. **Required fix shape:** (a) detect interactive MCP/tool permission prompts as a first-class blocked state instead of generic idle; (b) emit a typed event such as `blocked.mcp_permission` / `blocked.tool_permission` with tool/server name, prompt age, and whether the gate is session-only vs always-allow capable; (c) include this gate in startup/no-evidence evidence bundles and lane status surfaces so clawhip can say "blocked at MCP permission prompt" without pane scraping; (d) add a regression proving a prompt-gated session does not get misclassified as stale/idle/ready. **Why this matters:** prompt acceptance and startup telemetry are still incomplete if an interactive MCP gate can eat the first real action after hooks report success. Source: live dogfood session `clawcode-human` on 2026-04-18.
|
||||
200. **Interactive MCP/tool permission prompts are invisible blockers** — **done (verified 2026-04-27):** worker boot observation now detects interactive tool permission gates such as `Allow the omx_memory MCP server to run tool "project_memory_read"?` before generic readiness/idle handling, records `tool_permission_required` status, emits a structured `ToolPermissionPrompt` payload with server/tool identity, prompt age, allow-scope capability, and prompt preview, marks readiness snapshots as blocked, and carries `tool_permission_prompt_detected` through startup timeout evidence so the classifier returns `tool_permission_required` instead of a vague stale/idle/ready outcome. Regression coverage locks both the structured prompt-gate event metadata and startup-timeout classification paths. **Original filing below.**
|
||||
Original filing (2026-04-18): the session emitted `SessionStart hook (completed)` and `UserPromptSubmit hook (completed)`, then stalled on an interactive MCP permission gate (`Allow the omx_memory MCP server to run tool "project_memory_read"?`). From the outside this looks like a ready-but-quiet lane even though the real state is `blocked waiting for permission`. **Required fix shape:** (a) detect interactive MCP/tool permission prompts as a first-class blocked state instead of generic idle; (b) emit a typed event such as `blocked.mcp_permission` / `blocked.tool_permission` with tool/server name, prompt age, and whether the gate is session-only vs always-allow capable; (c) include this gate in startup/no-evidence evidence bundles and lane status surfaces so clawhip can say "blocked at MCP permission prompt" without pane scraping; (d) add a regression proving a prompt-gated session does not get misclassified as stale/idle/ready. **Why this matters:** prompt acceptance and startup telemetry are still incomplete if an interactive MCP gate can eat the first real action after hooks report success. Source: live dogfood session `clawcode-human` on 2026-04-18.
|
||||
|
||||
201. **`extract --model-payload` is not inspectable enough for deterministic dogfood: forced mode selection missing, and hybrid/no-snippet cases are opaque** — dogfooded 2026-04-19 from `dogfood-1776184671` against three real-repo files. `node dist/cli/index.js extract <file> --model-payload` succeeded and auto-selected `raw`, `raw`, and `hybrid`, but there is currently no CLI surface to force `raw` / `compressed` / `hybrid` for A/B comparison: `--mode raw` and `--mode compressed` both fail immediately with `Error: Unexpected extract argument: --mode`. That turns payload-shaping validation into guesswork because operators cannot ask the extractor to render the same file through each mode and compare the exact output. The opacity is worse in the observed hybrid case: the Formbricks checkbox file produced a hybrid payload with no snippets, leaving no visible explanation for why the extractor chose hybrid, what evidence it kept vs dropped, or whether the result is correct vs a silent fallback. **Required fix shape:** (a) add an explicit debug/inspection flag that forces extraction mode (`--mode raw|compressed|hybrid` or equivalent) without changing default auto-selection; (b) print/report the chosen mode and the decision reason in a machine-readable field when `--model-payload` is used; (c) when hybrid emits zero snippets, surface an explicit reason/count summary instead of making "no snippets" indistinguishable from silent loss; (d) add regression coverage on at least one real-world hybrid fixture so mode choice and snippet accounting stay stable. **Why this matters:** direct claw-code dogfood needs deterministic payload comparison to debug startup/context quality; without forced-mode inspection and snippet accounting, operators can see the outcome but not the extraction decision that produced it. Source: live dogfood session `dogfood-1776184671` on 2026-04-19.
|
||||
|
||||
@@ -6252,4 +6253,45 @@ load_session('nonexistent') # raises FileNotFoundError with no structured error
|
||||
|
||||
246. **Dogfood reminder cron can self-fail by timing out during active cycles, so the nudge loop itself is not trustworthy as an observability surface** — dogfooded 2026-04-21 in `#clawcode-building-in-public` after multiple consecutive alerts: `Cron job "clawcode-dogfood-cycle-reminder" failed: cron: job execution timed out` at 14:14, 14:24, 14:34, 14:44, 15:13, and 15:23 KST while the same dogfood cycle was actively producing reports and fixes. This is not just scheduler noise — it is a clawability gap in the reminder/control loop itself. A downstream claw seeing both repeated dogfood nudges and repeated cron timeouts cannot tell whether the reminder actually delivered, partially delivered, duplicated, or died after side effects. **Required fix shape:** (a) classify reminder execution outcome explicitly (`delivered`, `timed_out_after_send`, `timed_out_before_send`, `suppressed_as_duplicate`, `skipped_due_to_active_cycle`) instead of a single generic timeout; (b) attach the target message/report cycle id and whether a Discord post was already emitted before timeout; (c) add a fast-path/no-op path when the cycle state is unchanged or an active report is already in flight so the reminder job can exit cleanly instead of hanging; (d) add regression coverage proving repeated unchanged-state cycles do not stack timeouts or duplicate nudges. **Why this matters:** if the reminder loop itself is ambiguous, claws waste time responding to scheduler artifacts instead of real product state, and the dogfood surface stops being a reliable source of truth. Source: live clawhip/Jobdori dogfood cycle on 2026-04-21 with repeated timeout alerts in `#clawcode-building-in-public`.
|
||||
|
||||
247. **MCP memory permission prompts can recur after a transport failure, leaving an active worker blocked in a second consent loop instead of a typed degraded state** — dogfooded 2026-04-27 from live session `clawcode-human` while responding to the claw-code dogfood nudge. The session first asked permission for `omx_memory.project_memory_read`; after approval, the call failed with `Transport closed`, then the runtime immediately attempted `omx_memory.notepad_read` and blocked again on a fresh allow prompt. From the outside this looks like an automation-hostile MCP lifecycle gap: the worker is neither cleanly ready nor cleanly failed, and downstream claws must scrape the pane to learn that memory MCP is both consent-gated and transport-degraded. **Required fix shape:** (a) after an MCP transport closes, emit a typed degraded state such as `mcp_transport_closed` with server/tool identity; (b) suppress or batch follow-up permission prompts for the same failed MCP server until transport recovery is proven; (c) expose whether the task can continue without that MCP tool or is blocked on memory; (d) add regression coverage for `permission granted -> transport closed -> follow-up tool attempt` so it becomes one structured blocker instead of repeated interactive consent loops. **Why this matters:** MCP memory should either be available, explicitly degraded, or explicitly blocked; repeated permission prompts after a closed transport make prompt delivery and readiness ambiguous. Source: live `clawcode-human` pane on 2026-04-27 04:3x UTC.
|
||||
247. **MCP memory permission prompts can recur after a transport failure, leaving an active worker blocked in a second consent loop instead of a typed degraded state** — dogfooded 2026-04-27 from live session `clawcode-human` while responding to the claw-code dogfood nudge. The session first asked permission for `omx_memory.project_memory_read`; after approval, the call failed with `Transport closed`, then the runtime immediately attempted `omx_memory.notepad_read` and blocked again on a fresh allow prompt. From the outside this looks like an automation-hostile MCP lifecycle gap: the worker is neither cleanly ready nor cleanly failed, and downstream claws must scrape the pane to learn that memory MCP is both consent-gated and transport-degraded. **Required fix shape:** (a) after an MCP transport closes, emit a typed degraded state such as `mcp_transport_closed` with server/tool identity; (b) suppress or batch follow-up permission prompts for the same failed MCP server until transport recovery is proven; (c) expose whether the task can continue without that MCP tool or is blocked on memory; (d) add regression coverage for `permission granted -> transport closed -> follow-up tool attempt` so it becomes one structured blocker instead of repeated interactive consent loops. **Why this matters:** MCP memory should either be available, explicitly degraded, or explicitly blocked; repeated permission prompts after a closed transport make prompt delivery and readiness ambiguous. Source: live `clawcode-human` pane on 2026-04-27 04:3x UTC. **Fresh-run follow-up 2026-04-29:** owner-requested live session `claw-code-issue-247-human-fresh-run` used the actual `./rust/target/debug/claw` binary; `doctor` and `status` were green, so the remaining Phase-0 fresh-run evidence moved from MCP consent-loop reproduction to the non-interactive prompt silent-hang captured separately as #248.
|
||||
|
||||
248. **Non-interactive prompt mode can exceed caller timeouts with no in-band startup/API phase event or partial status artifact** — dogfooded 2026-04-29 from live tmux session `claw-code-issue-247-human-fresh-run` after the owner explicitly asked gaebal-gajae to make a fresh session and use `claw-code` directly. The actual `./rust/target/debug/claw` binary was launched via `clawhip tmux new` on current main. `claw doctor --output-format json` and `claw status --output-format json` both succeeded and reported auth/config/workspace ok, but minimal non-interactive prompt calls (`timeout 120 ./rust/target/debug/claw --output-format json --dangerously-skip-permissions "echo hello"` and `timeout 120 ./rust/target/debug/claw --output-format json prompt "Reply with just the word hello"`) both timed out from the outer harness after roughly 150s with only `Command exceeded timeout` visible. There was no machine-readable `api_request_started`, `waiting_for_first_token`, provider/model/base-url identity, retry count, or partial status file/event that would let clawhip distinguish slow provider, network stall, auth/OAuth drift, stream parser hang, or prompt-mode bug. **Required fix shape:** (a) emit structured non-interactive lifecycle events for `startup_ok`, `api_request_started`, `first_byte/first_token`, retry/backoff, and terminal `timeout_or_stall` states; (b) include provider/model/base URL source and auth source category without leaking secrets; (c) support a CLI/request timeout flag or env override that returns a typed JSON error before the outer orchestrator kills the process; (d) write/emit a final partial status artifact on timeout so lane monitors do not have to infer state from a dead process. **Why this matters:** non-interactive prompt mode is the automation path; if it can hang past the caller's timeout while doctor/status are green, claws lose the ability to tell whether startup, auth, transport, provider latency, or stream consumption failed. Source: live session `claw-code-issue-247-human-fresh-run` on 2026-04-29.
|
||||
|
||||
249. **`/issue` advertises GitHub issue creation but never reaches a GitHub/OAuth/auth preflight or creation path, and the non-interactive error suggests unusable resume forms** — dogfooded 2026-04-29 on current main `8e22f757` while chasing the remaining Phase-0 GitHub OAuth blocker. The visible help advertises `/issue [context]` as “Draft or create a GitHub issue from the conversation,” but the actual implementation path only renders a local `Issue` report (`format_issue_report`) and does not invoke `gh`, GitHub API, OAuth, token discovery, browser auth, or even a dry-run/auth-preflight surface. Direct non-interactive use (`./rust/target/debug/claw '/issue dogfood test'`) returns `slash command /issue dogfood test is interactive-only` and suggests `claw --resume SESSION.jsonl /issue ...` / `claw --resume latest /issue ...` “when the command is marked [resume]”, while `/help` does not mark `/issue` as resume-safe and resume dispatch rejects interactive-only commands. That leaves operators with a GitHub-labeled command whose real behavior is neither issue creation nor a clear GitHub OAuth blocker. **Required fix shape:** (a) split the contract explicitly: either rename/copy to “draft issue text” or implement a real `create` path with GitHub auth preflight; (b) surface a machine-readable GitHub auth state (`gh_cli_authenticated`, `github_token_present`, `oauth_required`, `creation_unavailable`) before any issue-create attempt; (c) make the direct-mode error avoid suggesting resume forms for commands not marked resume-safe; (d) add regression coverage proving `/issue` help, direct-mode rejection, resume support flags, and creation/draft behavior agree. **Why this matters:** Phase-0 GitHub OAuth verification cannot complete if the only GitHub issue surface stops at local prose while still advertising creation. Claws need to know whether they are missing GitHub auth, using a draft-only helper, or hitting an unimplemented creation path. Source: gaebal-gajae dogfood cycle in `#clawcode-building-in-public` on 2026-04-29.
|
||||
322. **Config deprecation warnings are emitted to stderr even under `--output-format json`, making JSON output unparseable from combined stdout+stderr capture** — dogfooded 2026-04-29 by Jobdori on current main (`8e22f75`). Running `cargo run --bin claw -- doctor --output-format json 2>&1 | python3 -c "import sys,json; json.loads(sys.stdin.read())"` fails with `Expecting value: line 1 column 1 (char 0)` because a `warning: /path/settings.json: field "enabledPlugins" is deprecated. Use "plugins.enabled" instead` line is emitted to stderr before the JSON body begins. When a caller captures combined output (the common automation pattern: `2>&1`, subprocess `STDOUT | STDERR`, PTY capture, or tmux pane scrape) the warning prefix breaks JSON parse for every downstream consumer. Root cause: `rust/crates/runtime/src/config.rs` line ~300 calls `eprintln!("warning: {warning}")` unconditionally during `ClawSettings::load_merged()` regardless of active output format. **Required fix shape:** (a) thread the active `CliOutputFormat` through the config loading path and suppress or defer human-readable warning strings when `json` mode is active; (b) instead, collect deprecation diagnostics and inject them into the JSON output as a top-level `"warnings": [...]` array (same field already used by `doctor`); (c) ensure the JSON body is always the first bytes on stdout and all prose warnings stay on stderr or are suppressed in json mode; (d) add regression coverage proving `claw <any-cmd> --output-format json` stdout is valid JSON regardless of config deprecation state. **Why this matters:** `--output-format json` is the automation/claw contract; if config warnings can silently corrupt the JSON stream, every orchestration layer that captures combined output gets broken parse-on-warning with no stable fallback. Source: Jobdori live dogfood on mengmotaHost, claw-code main `8e22f75`, 2026-04-29.
|
||||
|
||||
323. **`status --output-format json` reports `session.session = "live-repl"` while simultaneously reporting `session_lifecycle.kind = "saved_only"` — contradictory session identity in a single status snapshot** — dogfooded 2026-04-29 by Jobdori on current main (`804d96b`). Running `claw status --output-format json` from an active REPL-style invocation produced `"session": "live-repl"` in the `workspace` block and `"session_lifecycle": {"kind": "saved_only", "pane_id": null, ...}` in the same object. Those two fields carry contradictory claims: `"live-repl"` asserts there is an active interactive session, while `"saved_only"` asserts there is no live tmux pane hosting the session — the session exists only as a saved artifact. A downstream claw reading this snapshot cannot tell which claim to trust: is this a running session whose pane is undetectable, or a saved-only session that the `session` field is misclassifying? Root cause: `"live-repl"` is a fallback sentinel emitted by `main.rs:6070` when `context.session_path` is `None`, while `session_lifecycle` is computed independently by `classify_session_lifecycle_for()` from tmux pane discovery; the two fields share no common source and can diverge. **Required fix shape:** (a) derive both `session.session` and `session_lifecycle.kind` from the same lifecycle classification result so they cannot diverge; (b) replace the `"live-repl"` free-form sentinel with a structured `session_kind` field (`live_repl`, `saved`, `resume`, etc.) that carries the same type vocabulary as `session_lifecycle.kind`; (c) when `session_lifecycle.kind = "saved_only"`, never emit `"session": "live-repl"` (or vice versa); (d) add a regression test proving `status --output-format json` never emits `session.kind = "live_repl"` and `session_lifecycle.kind = "saved_only"` simultaneously. **Why this matters:** `status --output-format json` is the machine-readable truth surface for session state; if two fields in the same snapshot contradict each other, every lane, monitor, and orchestrator has to pick a winner instead of reading a coherent state. Source: Jobdori live dogfood on mengmotaHost, claw-code `804d96b`, 2026-04-29.
|
||||
|
||||
324. **Stale local debug binaries can impersonate the current workspace because version/status/doctor do not compare embedded build provenance to repo HEAD** — dogfooded 2026-04-29 on current `origin/main` / workspace HEAD `e7074f47` after PR #2838. The working tree was at `e7074f47`, but running `./rust/target/debug/claw version --output-format json` reported embedded `git_sha` `1f901988`. `status` and `doctor` remained green and exposed no warning that the executable under test was stale relative to the workspace HEAD, nor any structured build-provenance freshness signal that downstream claws could use to decide whether the observed behavior came from the checked-out code or an older debug artifact. This is a repo-identity opacity gap: the JSON truth surfaces can look authoritative while actually describing a different binary lineage than the source tree being dogfooded. **Required fix shape:** (a) compare the embedded build `git_sha` / build date with the current workspace git HEAD and dirty state when the binary can discover a containing worktree; (b) expose redaction-safe structured fields in `version --output-format json`, `status --output-format json`, and `doctor --output-format json`, including `binary_provenance`, `workspace_head`, and `stale_binary` (with enough reason/detail to distinguish clean match, dirty workspace, unknown workspace, and definite stale SHA mismatch); (c) warn in human/text mode when executing a stale local debug binary such as `./rust/target/debug/claw` so dogfooders do not trust old behavior as current-main evidence; (d) avoid leaking secrets or absolute sensitive paths beyond the existing workspace-identification policy; (e) add regression/fixture coverage for matching HEAD, dirty workspace, no-worktree/unknown provenance, and stale embedded SHA cases. **Why this matters:** status/doctor/version are supposed to be the machine-readable basis for dogfood truth. If a stale binary can report a different `git_sha` than the checked-out repo without any freshness warning, claws can file or verify bugs against the wrong code and waste cycles chasing already-fixed or not-yet-built behavior. Source: gaebal-gajae dogfood follow-up from current main `e7074f47` after PR #2838; observed `./rust/target/debug/claw version --output-format json` reporting `git_sha` `1f901988` with no stale-binary-vs-workspace-HEAD warning.
|
||||
|
||||
325. **`help --output-format json` returns valid JSON but hides the actual help schema inside one prose `message` string** — dogfooded 2026-04-29 on current `origin/main` / workspace HEAD `d607ff36`. Running `./rust/target/debug/claw help --output-format json` produces parseable JSON, but the object only exposes top-level keys like `kind` and `message`; all command names, global flags, slash-command metadata, aliases, resume-safety, output-format support, auth/preflight notes, and descriptions are flattened into one human-oriented prose blob. That technically satisfies “valid JSON” while still forcing automation to scrape the same help text humans read, making `/issue`, `/help`, and resume-safety contracts opaque to claws. **Required fix shape:** (a) keep `message` as the compact human-rendered help summary, but add a documented structured schema with `schema` / `schema_version` fields; (b) expose first-class arrays/objects such as `commands[]`, `options[]`, and `slash_commands[]` with stable fields including `name`, `aliases`, `description`, `args`, `output_formats_supported`, `resume_safe`, `interactive_only`, and `creates_external_side_effects`; (c) include auth and creation preflight metadata where relevant, especially for GitHub/issue flows (`auth_preflight`, `creation_unavailable`, `gh_cli_authenticated`, `github_token_present`, or equivalent non-secret state); (d) make `/issue`, `/help`, aliases, and resume-dispatch safety machine-readable from the JSON payload instead of recoverable only by parsing prose markers; (e) add regression coverage proving `help --output-format json` is valid JSON and that `/issue`, `/help`, resume-safe vs interactive-only slash commands, aliases, descriptions, supported output formats, and side-effect/auth-preflight fields are present and internally consistent. **Why this matters:** help JSON is the discoverability surface automation uses before invoking commands. If it is just prose wrapped in JSON, claws cannot safely decide whether a command can run non-interactively, resume from a saved session, create external GitHub side effects, or requires auth/preflight without brittle text scraping. Source: gaebal-gajae dogfood follow-up from current main `d607ff36`; observed `./rust/target/debug/claw help --output-format json` returning valid JSON with only `{kind,message}` at the top level while the actionable command schema remained buried in `message`.
|
||||
|
||||
326. **`status --output-format json` underreports active workspace pane inventory when one tmux session has multiple panes/processes in the same project** — dogfooded 2026-04-29 on current `origin/main` / workspace HEAD `b90875fa` while responding to the claw-code dogfood nudge. The active OMX session `claw-code-issue-326-dogfood-pinpoint` was running in `/mnt/offloading/Workspace/claw-code` with two panes: `%9384` (`cmd=node`, active pane) and `%9385` (`cmd=node`, inactive sidecar pane). `tmux list-panes -a -F '#{session_name}:#{window_index}.#{pane_index} #{pane_id} pid=#{pane_pid} cmd=#{pane_current_command} cwd=#{pane_current_path} active=#{pane_active}'` showed both panes in the same session/workspace, but `./rust/target/debug/claw status --output-format json` collapsed the workspace lifecycle to a single object: `session_lifecycle.kind = "running_process"`, `pane_id = "%9384"`, `pane_command = "node"`, with no `panes[]`, process count, sidecar/secondary-pane inventory, or ambiguity marker. A downstream claw reading only status JSON would believe there is exactly one live process for that workspace even though the control plane has multiple panes in the same task session. **Required fix shape:** (a) expose a structured active-session inventory in `status --output-format json`, including `panes[]` or `processes[]` with pane id, command, cwd, active flag, and session/window identity for all matching workspace panes; (b) keep the compact `session_lifecycle` summary, but add an explicit `pane_count` / `has_sidecar_panes` / `inventory_truncated` signal so summaries cannot masquerade as complete truth; (c) define how to classify primary vs sidecar/inactive panes without losing them, and make the chosen primary pane provenance visible; (d) add regression coverage for a tmux session with two panes in one workspace proving status JSON reports both panes or marks the inventory as partial. **Why this matters:** status JSON is the machine-readable lane truth surface. If it reports only the primary pane while hiding secondary panes, clawhip and other claws can miss sidecar workers, blocked helpers, stale subprocesses, or duplicated control-plane processes and make bad restart/cleanup/routing decisions from an undercounted session snapshot. Source: gaebal-gajae dogfood session `claw-code-issue-326-dogfood-pinpoint`; observed `claw status --output-format json` returning only `%9384` while `tmux list-panes` showed `%9384` and `%9385` in the same claw-code workspace.
|
||||
|
||||
327. **`claw mcp help` omits `.claw.json` from its documented config sources even though `claw mcp` still loads MCP servers from `.claw.json`** — dogfooded 2026-04-29 on current `origin/main` / workspace HEAD `981aff7c` after rebuilding the actual debug binary with `cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json` so `./rust/target/debug/claw version --output-format json` reported embedded `git_sha` `981aff7c` matching the workspace. Running `./rust/target/debug/claw mcp --help` printed `Sources .claw/settings.json, .claw/settings.local.json`, and `./rust/target/debug/claw mcp help --output-format json` returned `"sources": [".claw/settings.json", ".claw/settings.local.json"]`. In the same rebuilt binary, a temp workspace containing only a project `.claw.json` with `{"mcpServers":{"demo":{"command":"/bin/echo","args":["hi"]}}}` made `./rust/target/debug/claw mcp --output-format json` report `configured_servers: 1` and `servers[0].name: "demo"`. The MCP lifecycle surface therefore tells users and claws that `.claw.json` is not a source while actively accepting it as one. This is distinct from #322's JSON warning corruption, #323/#326's status lifecycle contradictions, #324's stale-binary provenance gap, and #325's top-level help schema flattening: the pinpoint is a concrete MCP subcommand source-of-truth mismatch in both text and JSON help. **Required fix shape:** (a) derive the `mcp help` source list from the same `ConfigLoader::discover`/settings-source registry that `mcp list` actually uses instead of hard-coding a partial list; (b) include all supported MCP config sources in stable order, including legacy/project `.claw.json`, user `~/.claw/settings.json`, project `.claw/settings.json`, and local `.claw/settings.local.json` as applicable; (c) add source metadata to `mcp --output-format json` entries so each server can be attributed to the file/layer that provided it; (d) add a regression proving a server loaded from `.claw.json` is accompanied by help/JSON source metadata that names `.claw.json`, and that help stays in sync when config source discovery changes. **Why this matters:** MCP setup is already a high-friction lifecycle path; if the command that diagnoses MCP servers omits a still-supported source, operators can move or delete the wrong config file, and automation cannot tell whether `.claw.json` support is intentional compatibility or accidental legacy behavior. Source: gaebal-gajae dogfood in `/home/bellman/Workspace/claw-code` on 2026-04-29 using the rebuilt actual `./rust/target/debug/claw`; temp-workspace proof showed `.claw.json` loads one MCP server while `mcp help` documents only `.claw/settings*.json` sources.
|
||||
|
||||
328. **`claw agents help` omits the `.codex/agents` roots that `claw agents` actually loads from, so native-agent discovery provenance is misleading** — dogfooded 2026-04-29 on current `origin/main` / workspace HEAD `ee85fed6` after rebuilding the actual debug binary with `cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json`; `./rust/target/debug/claw version --output-format json` then reported embedded `git_sha` `ee85fed6`, matching the workspace. Running `./rust/target/debug/claw agents help --output-format json` returned `usage.sources = [".claw/agents", "~/.claw/agents", "$CLAW_CONFIG_HOME/agents"]`, with no `.codex/agents` or `~/.codex/agents` entry. In the same environment, `./rust/target/debug/claw agents --output-format json` listed native agents such as `analyst` with source `{id: "user_claw", label: "User home roots"}` even though `/home/bellman/.claw/agents` does not exist and `/home/bellman/.codex/agents/analyst.toml` does exist. The agents lifecycle surface therefore documents one set of roots while loading from another, and the loaded-agent provenance collapses the real Codex root behind a generic `user_claw` label. This is distinct from #327's MCP source-list mismatch: the affected subsystem is native-agent discovery, where claws choose delegation/staffing lanes from `claw agents` and need to know which root supplied each agent. **Required fix shape:** (a) derive `agents help` source roots from the same registry/search path used by the agent loader instead of a hard-coded `.claw`-only list; (b) include all supported native-agent roots in stable order, including project/user `.codex/agents` roots alongside `.claw/agents` and `$CLAW_CONFIG_HOME/agents`; (c) make each `agents --output-format json` entry expose non-secret source provenance precise enough to distinguish `user_codex`, `project_codex`, `user_claw`, and `project_claw` (without leaking unnecessary absolute paths); (d) add a regression proving an agent loaded from `~/.codex/agents` is accompanied by help-source metadata naming that root and per-agent provenance that does not mislabel it as generic `user_claw`. **Why this matters:** agent selection is a control-plane decision. If help says only `.claw/agents` are searched while the runtime actually consumes `.codex/agents`, claws and operators can edit the wrong directory, misdiagnose missing/stale agents, or trust the wrong ownership boundary for delegated work. Source: gaebal-gajae dogfood in `/home/bellman/Workspace/claw-code` on 2026-04-29 using rebuilt `./rust/target/debug/claw`; proof commands showed `agents help` omitting `.codex/agents` while `agents` loaded `analyst` from the existing `/home/bellman/.codex/agents/analyst.toml` with no `/home/bellman/.claw/agents` directory present.
|
||||
329. **Resume-safe slash `/agents --output-format json` downgrades structured agent inventory into prose even though top-level `claw agents --output-format json` returns machine-readable entries** — dogfooded 2026-04-29 on current `origin/main` / workspace HEAD `0f7578c0` after rebuilding the actual debug binary with `cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json`; `./rust/target/debug/claw version --output-format json` reported embedded `git_sha` `0f7578c0`, matching the workspace. Running `./rust/target/debug/claw --resume latest /agents --output-format json` returned only `{"kind":"agents","text":"Agents\n 20 active agents..."}`: the agent names, source ids, models, reasoning effort, active/shadowed state, and working-directory context are all flattened into one human prose string. In the same rebuilt binary and same workspace, `./rust/target/debug/claw agents --output-format json` returned a structured object with top-level `agents[]`, `count`, `summary`, `working_directory`, and per-agent fields such as `name`, `description`, `model`, `reasoning_effort`, `active`, `shadowed_by`, and `source`. The resume-safe slash surface therefore looks JSON-shaped while throwing away exactly the structured inventory that automation needs, and it diverges from the already-existing top-level command schema. This is distinct from #325's broad help JSON opacity and #328's source-root mismatch: the pinpoint is the `/agents` slash command losing structured inventory in resume mode even though the non-slash agents command already has it. **Required fix shape:** (a) make resume-safe `/agents --output-format json` reuse the same serializer/schema as `claw agents --output-format json` instead of wrapping rendered text; (b) preserve per-agent source/provenance fields, model/reasoning metadata, active/shadowed state, count/summary, and working-directory context; (c) keep `text` or `message` as an optional human summary only, not the sole payload; (d) add regression coverage proving top-level `claw agents --output-format json` and resume-safe `/agents --output-format json` expose equivalent structured agent inventory for the same workspace. **Why this matters:** `/agents` is the in-session delegation/staffing truth surface. Claws operating through `--resume latest` need to choose agents without scraping prose; losing structure at the slash boundary makes automated staffing brittle and contradicts the top-level command contract. Source: gaebal-gajae dogfood in `/home/bellman/Workspace/claw-code` on 2026-04-29 using rebuilt `./rust/target/debug/claw`; proof commands showed slash `/agents` JSON had only `kind,text` while top-level `agents` JSON had `agents[]` and provenance metadata.
|
||||
337. **`status` / `/session list --output-format json` session lifecycle reports `workspace_dirty: true` and `abandoned: true` but omits dirty-file detail and abandonment cause, making automated GC unable to distinguish live work from crash leftovers** — restored from PR #2852 / Jobdori dogfood on current main (`0f7578c`). The evidence bundle listed 10 sessions and every listed session had `workspace_dirty: true` plus `abandoned: true`; each lifecycle object exposed `abandoned: true`, `kind: "saved_only"`, `pane_id: null`, and `workspace_dirty: true`, but did not include `dirty_file_count`, `dirty_file_paths` / summary, or `abandoned_reason`. That leaves cleanup policy with only a boolean dirty/abandoned pair: it cannot tell whether a saved-only session contains intentional uncommitted user work, a harmless stale pane artifact, or crash leftovers that are safe to collect. **Required fix shape:** (a) add `dirty_file_count: u32` to session lifecycle/status payloads whenever dirty state is evaluated; (b) add an `abandoned_reason` enum such as `pane_closed`, `process_killed`, `session_replaced`, `workspace_missing`, or `unknown` instead of a bare boolean-only abandonment signal; (c) optionally add summarized `dirty_file_paths` / `dirty_file_summary` with truncation metadata so automation can present useful evidence without leaking excessive path detail; (d) add regression coverage proving dirty abandoned saved-only sessions include file count, abandonment reason, and stable behavior when path summaries are omitted or truncated. **Why this matters:** session GC must not delete live user work, but it also cannot leave every crash leftover forever. A lifecycle object that says only `workspace_dirty: true` and `abandoned: true` forces cleanup tooling to guess instead of applying a safe policy from structured evidence. Source: PR #2852 / Jobdori dogfood; all 10 listed sessions shared the same dirty+abandoned shape, and the sample lifecycle object had `abandoned: true`, `kind: "saved_only"`, `pane_id: null`, `workspace_dirty: true`, with no dirty-file count, path summary, or abandonment reason.
|
||||
|
||||
338. **Top-level `help --output-format json` and resume-safe `/help --output-format json` use different payload fields for the same help surface (`message` vs `text`)** — dogfooded 2026-04-29 on current `origin/main` / workspace HEAD `24ccb59b` after rebuilding the actual debug binary with `cargo run --manifest-path rust/Cargo.toml --bin claw -- version --output-format json`; `./rust/target/debug/claw version --output-format json` reported embedded `git_sha` `24ccb59b`, matching the workspace. Running `./rust/target/debug/claw help --output-format json` returned a valid JSON object with keys `kind,message`, while `./rust/target/debug/claw --resume latest /help --output-format json` returned the same conceptual help surface with keys `kind,text`. Both are prose-only help payloads, but automation now has to special-case whether help was reached through the top-level command dispatcher or the resume-safe slash dispatcher before it can even locate the rendered help body. This is distinct from #325's broader structured-schema absence: the pinpoint here is a concrete JSON field-name contract drift between two help entrypoints that should be equivalent or explicitly versioned. **Required fix shape:** (a) define one canonical help JSON body field such as `message` or `text` and use it consistently across top-level `help`, slash `/help`, and resume-safe `/help`; (b) if backward compatibility requires both fields temporarily, emit both with identical contents plus a `schema_version` and deprecation metadata; (c) add regression coverage proving `claw help --output-format json` and `claw --resume latest /help --output-format json` expose the same top-level field contract and `kind=help`; (d) document whether slash-command JSON is intended to share schemas with top-level command JSON or carry its own explicit schema namespace. **Why this matters:** help JSON is the bootstrap discoverability surface for claws. If the same help concept moves its body between `message` and `text` depending on invocation path, every orchestrator needs brittle per-entrypoint parsers before it can inspect commands, flags, or resume safety. Source: gaebal-gajae dogfood in `/home/bellman/Workspace/claw-code` on 2026-04-29 using rebuilt `./rust/target/debug/claw`; proof commands showed top-level help JSON keys `kind,message` and resume-safe slash help JSON keys `kind,text` on the same rebuilt binary.
|
||||
|
||||
339. **`/session delete` is not resume-safe while `/session list` is, making session GC impossible from `--resume` mode automation** — dogfooded 2026-04-30 by Jobdori on `24ccb59`. Running `claw --output-format json --resume latest /session list` succeeds and returns the full session list (10 sessions, all `workspace_dirty: true, abandoned: true`). Running `claw --output-format json --resume latest /session delete <id>` returns `{"command":"...","error":"unsupported resumed slash command","type":"error"}` — again using `"type"` not `"kind"` (#336 vocab violation). An automation lane that discovers abandoned sessions via `--resume` cannot delete any of them via the same path; it must spawn an interactive REPL session just to issue delete, breaking the machine-readable JSON surface contract. **Required fix shape:** (a) mark `/session delete` as resume-safe; (b) return `{"kind":"session_deleted","session_id":"<id>","path":"<deleted_path>"}` on success; (c) require `--force` only for dirty/active sessions; (d) add regression coverage. Source: Jobdori live dogfood, mengmotaHost, `24ccb59`, 2026-04-30.
|
||||
|
||||
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.
|
||||
|
||||
370. **`--resume SESSION non-slash-arg` emits `"trailing arguments must be slash commands"` with `hint: null`; the constraint that `--resume` only accepts slash commands as trailing arguments is undocumented in `--help` and the error provides no hint about valid forms** — dogfooded 2026-04-30 by Jobdori on `19947545`. Running `./claw --output-format json --resume latest session list` returns: `{"error":"--resume trailing arguments must be slash commands","hint":null,"kind":"unknown","type":"error"}`. The `--help` output shows `claw --resume latest /status /diff /export notes.txt` as an example and describes `[/status] [/compact] [...]` as valid trailing args, but does not explicitly state that only slash commands (starting with `/`) are permitted — top-level subcommands like `session list`, `mcp list`, `doctor` are not valid trailing arguments even though they overlap functionally with their slash-command equivalents (`/session list`, `/mcp list`, `/doctor`). The error has `hint: null`, missing an actionable suggestion like "Did you mean `/session list`?". An automation author who tries `claw --resume latest doctor` (instead of `claw --resume latest /doctor`) receives an unhelpful null hint. **Required fix shape:** (a) populate `hint` with a concrete correction when the non-slash argument has a slash-command equivalent (e.g., `session list` → `Hint: use /session list instead`); (b) update `--help` to explicitly note that only slash commands (prefixed with `/`) are accepted as `--resume` trailing arguments; (c) optionally, auto-prefix bare subcommand names that have slash-command equivalents (e.g., treat `--resume latest doctor` as `--resume latest /doctor`); (d) add regression coverage confirming `hint` is non-null when a known subcommand is passed bare to `--resume`. Source: Jobdori live dogfood, mengmotaHost, `19947545`, 2026-04-30.
|
||||
|
||||
23
progress.txt
23
progress.txt
@@ -74,6 +74,18 @@ US-007 COMPLETE (Phase 5 - Plugin/MCP lifecycle maturity)
|
||||
- DegradedMode behavior
|
||||
- Tests: 11 unit tests passing
|
||||
|
||||
|
||||
Iteration 2026-04-27 - ROADMAP #200 COMPLETED
|
||||
------------------------------------------------
|
||||
- Selected next actionable backlog item because no active task was in progress.
|
||||
- ROADMAP #200: Interactive MCP/tool permission prompts are invisible blockers.
|
||||
- Files: rust/crates/runtime/src/worker_boot.rs, rust/crates/runtime/src/recovery_recipes.rs, ROADMAP.md, progress.txt.
|
||||
- Added tool_permission_required worker status and event classification for interactive MCP/tool permission gates.
|
||||
- Added structured ToolPermissionPrompt payload with server/tool identity and prompt preview.
|
||||
- Startup evidence now records tool_permission_prompt_detected and classifies timeout evidence as tool_permission_required.
|
||||
- Readiness snapshots now mark tool-permission-gated workers as blocked, not ready/idle.
|
||||
- Tests: targeted tool_permission regressions, full runtime test/clippy/fmt pending in Ralph verification loop.
|
||||
|
||||
VERIFICATION STATUS:
|
||||
------------------
|
||||
- cargo build --workspace: PASSED
|
||||
@@ -353,3 +365,14 @@ US-021 COMPLETED (Request body size pre-flight check - from dogfood findings)
|
||||
- Tests: 5 new tests for size estimation and limit checking
|
||||
|
||||
PROJECT STATUS: COMPLETE (21/21 stories)
|
||||
|
||||
Iteration 2026-04-29 - ROADMAP #96 COMPLETED
|
||||
------------------------------------------------
|
||||
- Pulled origin/main: already up to date.
|
||||
- Selected ROADMAP #96 as a small repo-local Immediate Backlog item: the `claw --help` Resume-safe command summary leaked slash-command stubs despite the main Interactive command listing filtering them.
|
||||
- Files: rust/crates/rusty-claude-cli/src/main.rs, ROADMAP.md, progress.txt.
|
||||
- Changed help rendering to filter `resume_supported_slash_commands()` through `STUB_COMMANDS` before building the Resume-safe one-liner.
|
||||
- Added `stub_commands_absent_from_resume_safe_help` regression coverage so future stub additions cannot leak into the Resume-safe summary.
|
||||
- Targeted verification: `cargo test -p rusty-claude-cli stub_commands_absent_from_resume_safe_help -- --nocapture` passed; `cargo test -p rusty-claude-cli parses_direct_cli_actions -- --nocapture` passed.
|
||||
- Format/check verification: `cargo fmt --all --check`, `git diff --check`, and `cargo check -p rusty-claude-cli` passed.
|
||||
- Broader clippy note: `cargo clippy -p rusty-claude-cli --all-targets -- -D warnings` is blocked by pre-existing `clippy::unnecessary_wraps` failures in `rust/crates/commands/src/lib.rs` (`render_mcp_report_for`, `render_mcp_report_json_for`), outside this diff.
|
||||
|
||||
@@ -7,7 +7,8 @@ This file provides guidance to Claw Code (clawcode.dev) when working with code i
|
||||
- Frameworks: none detected from the supported starter markers.
|
||||
|
||||
## Verification
|
||||
- Run Rust verification from the repo root: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`
|
||||
- From the repository root, run Rust formatting with `scripts/fmt.sh` (or `scripts/fmt.sh --check` for CI-style checks). From this `rust/` directory, the equivalent command is `../scripts/fmt.sh`. Root-level `cargo fmt --manifest-path rust/Cargo.toml` is not the supported formatting command.
|
||||
- From this `rust/` directory, run Rust verification with `cargo clippy --workspace --all-targets -- -D warnings` and `cargo test --workspace`.
|
||||
|
||||
## Working agreement
|
||||
- Prefer small, reviewable changes and keep generated bootstrap files aligned with actual repo workflows.
|
||||
|
||||
@@ -753,14 +753,14 @@ mod tests {
|
||||
#[test]
|
||||
fn returns_context_window_metadata_for_kimi_models() {
|
||||
// kimi-k2.5
|
||||
let k25_limit = model_token_limit("kimi-k2.5")
|
||||
.expect("kimi-k2.5 should have token limit metadata");
|
||||
let k25_limit =
|
||||
model_token_limit("kimi-k2.5").expect("kimi-k2.5 should have token limit metadata");
|
||||
assert_eq!(k25_limit.max_output_tokens, 16_384);
|
||||
assert_eq!(k25_limit.context_window_tokens, 256_000);
|
||||
|
||||
// kimi-k1.5
|
||||
let k15_limit = model_token_limit("kimi-k1.5")
|
||||
.expect("kimi-k1.5 should have token limit metadata");
|
||||
let k15_limit =
|
||||
model_token_limit("kimi-k1.5").expect("kimi-k1.5 should have token limit metadata");
|
||||
assert_eq!(k15_limit.max_output_tokens, 16_384);
|
||||
assert_eq!(k15_limit.context_window_tokens, 256_000);
|
||||
}
|
||||
@@ -768,11 +768,13 @@ mod tests {
|
||||
#[test]
|
||||
fn kimi_alias_resolves_to_kimi_k25_token_limits() {
|
||||
// The "kimi" alias resolves to "kimi-k2.5" via resolve_model_alias()
|
||||
let alias_limit = model_token_limit("kimi")
|
||||
.expect("kimi alias should resolve to kimi-k2.5 limits");
|
||||
let direct_limit = model_token_limit("kimi-k2.5")
|
||||
.expect("kimi-k2.5 should have limits");
|
||||
assert_eq!(alias_limit.max_output_tokens, direct_limit.max_output_tokens);
|
||||
let alias_limit =
|
||||
model_token_limit("kimi").expect("kimi alias should resolve to kimi-k2.5 limits");
|
||||
let direct_limit = model_token_limit("kimi-k2.5").expect("kimi-k2.5 should have limits");
|
||||
assert_eq!(
|
||||
alias_limit.max_output_tokens,
|
||||
direct_limit.max_output_tokens
|
||||
);
|
||||
assert_eq!(
|
||||
alias_limit.context_window_tokens,
|
||||
direct_limit.context_window_tokens
|
||||
|
||||
@@ -2195,9 +2195,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn provider_specific_size_limits_are_correct() {
|
||||
assert_eq!(OpenAiCompatConfig::dashscope().max_request_body_bytes, 6_291_456); // 6MB
|
||||
assert_eq!(OpenAiCompatConfig::openai().max_request_body_bytes, 104_857_600); // 100MB
|
||||
assert_eq!(OpenAiCompatConfig::xai().max_request_body_bytes, 52_428_800); // 50MB
|
||||
assert_eq!(
|
||||
OpenAiCompatConfig::dashscope().max_request_body_bytes,
|
||||
6_291_456
|
||||
); // 6MB
|
||||
assert_eq!(
|
||||
OpenAiCompatConfig::openai().max_request_body_bytes,
|
||||
104_857_600
|
||||
); // 100MB
|
||||
assert_eq!(OpenAiCompatConfig::xai().max_request_body_bytes, 52_428_800);
|
||||
// 50MB
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2623,10 +2623,8 @@ fn render_mcp_report_json_for(
|
||||
// runs, the existing serializer adds `status: "ok"` below.
|
||||
match loader.load() {
|
||||
Ok(runtime_config) => {
|
||||
let mut value = render_mcp_summary_report_json(
|
||||
cwd,
|
||||
runtime_config.mcp().servers(),
|
||||
);
|
||||
let mut value =
|
||||
render_mcp_summary_report_json(cwd, runtime_config.mcp().servers());
|
||||
if let Some(map) = value.as_object_mut() {
|
||||
map.insert("status".to_string(), Value::String("ok".to_string()));
|
||||
map.insert("config_load_error".to_string(), Value::Null);
|
||||
|
||||
@@ -45,7 +45,9 @@ impl FailureScenario {
|
||||
#[must_use]
|
||||
pub fn from_worker_failure_kind(kind: WorkerFailureKind) -> Self {
|
||||
match kind {
|
||||
WorkerFailureKind::TrustGate => Self::TrustPromptUnresolved,
|
||||
WorkerFailureKind::TrustGate | WorkerFailureKind::ToolPermissionGate => {
|
||||
Self::TrustPromptUnresolved
|
||||
}
|
||||
WorkerFailureKind::PromptDelivery => Self::PromptMisdelivery,
|
||||
WorkerFailureKind::Protocol => Self::McpHandshakeFailure,
|
||||
WorkerFailureKind::Provider | WorkerFailureKind::StartupNoEvidence => {
|
||||
|
||||
@@ -30,6 +30,7 @@ fn now_secs() -> u64 {
|
||||
pub enum WorkerStatus {
|
||||
Spawning,
|
||||
TrustRequired,
|
||||
ToolPermissionRequired,
|
||||
ReadyForPrompt,
|
||||
Running,
|
||||
Finished,
|
||||
@@ -41,6 +42,7 @@ impl std::fmt::Display for WorkerStatus {
|
||||
match self {
|
||||
Self::Spawning => write!(f, "spawning"),
|
||||
Self::TrustRequired => write!(f, "trust_required"),
|
||||
Self::ToolPermissionRequired => write!(f, "tool_permission_required"),
|
||||
Self::ReadyForPrompt => write!(f, "ready_for_prompt"),
|
||||
Self::Running => write!(f, "running"),
|
||||
Self::Finished => write!(f, "finished"),
|
||||
@@ -53,6 +55,7 @@ impl std::fmt::Display for WorkerStatus {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkerFailureKind {
|
||||
TrustGate,
|
||||
ToolPermissionGate,
|
||||
PromptDelivery,
|
||||
Protocol,
|
||||
Provider,
|
||||
@@ -71,6 +74,7 @@ pub struct WorkerFailure {
|
||||
pub enum WorkerEventKind {
|
||||
Spawning,
|
||||
TrustRequired,
|
||||
ToolPermissionRequired,
|
||||
TrustResolved,
|
||||
ReadyForPrompt,
|
||||
PromptMisdelivery,
|
||||
@@ -104,6 +108,8 @@ pub enum WorkerPromptTarget {
|
||||
pub enum StartupFailureClassification {
|
||||
/// Trust prompt is required but not detected/resolved
|
||||
TrustRequired,
|
||||
/// Tool permission prompt is required before startup can continue
|
||||
ToolPermissionRequired,
|
||||
/// Prompt was delivered to wrong target (shell misdelivery)
|
||||
PromptMisdelivery,
|
||||
/// Prompt was sent but acceptance timed out
|
||||
@@ -130,6 +136,14 @@ pub struct StartupEvidenceBundle {
|
||||
pub prompt_acceptance_state: bool,
|
||||
/// Result of trust prompt detection at timeout
|
||||
pub trust_prompt_detected: bool,
|
||||
/// Result of tool permission prompt detection at timeout
|
||||
pub tool_permission_prompt_detected: bool,
|
||||
/// Age in seconds of the latest tool permission prompt, when observed
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_permission_prompt_age_seconds: Option<u64>,
|
||||
/// Whether the prompt surface exposed only a session allow path or also an always-allow path
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_permission_allow_scope: Option<ToolPermissionAllowScope>,
|
||||
/// Transport health summary (true = healthy/responsive)
|
||||
pub transport_healthy: bool,
|
||||
/// MCP health summary (true = all servers healthy)
|
||||
@@ -146,6 +160,15 @@ pub enum WorkerEventPayload {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
resolution: Option<WorkerTrustResolution>,
|
||||
},
|
||||
ToolPermissionPrompt {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tool_name: Option<String>,
|
||||
prompt_age_seconds: u64,
|
||||
allow_scope: ToolPermissionAllowScope,
|
||||
prompt_preview: String,
|
||||
},
|
||||
PromptDelivery {
|
||||
prompt_preview: String,
|
||||
observed_target: WorkerPromptTarget,
|
||||
@@ -163,6 +186,14 @@ pub enum WorkerEventPayload {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ToolPermissionAllowScope {
|
||||
SessionOnly,
|
||||
SessionOrAlways,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WorkerTaskReceipt {
|
||||
pub repo: String,
|
||||
@@ -276,6 +307,29 @@ impl WorkerRegistry {
|
||||
.ok_or_else(|| format!("worker not found: {worker_id}"))?;
|
||||
let lowered = screen_text.to_ascii_lowercase();
|
||||
|
||||
if let Some(tool_prompt) = detect_tool_permission_prompt(screen_text, &lowered) {
|
||||
worker.status = WorkerStatus::ToolPermissionRequired;
|
||||
worker.last_error = Some(WorkerFailure {
|
||||
kind: WorkerFailureKind::ToolPermissionGate,
|
||||
message: tool_prompt.message(),
|
||||
created_at: now_secs(),
|
||||
});
|
||||
push_event(
|
||||
worker,
|
||||
WorkerEventKind::ToolPermissionRequired,
|
||||
WorkerStatus::ToolPermissionRequired,
|
||||
Some("tool permission prompt detected".to_string()),
|
||||
Some(WorkerEventPayload::ToolPermissionPrompt {
|
||||
server_name: tool_prompt.server_name,
|
||||
tool_name: tool_prompt.tool_name,
|
||||
prompt_age_seconds: 0,
|
||||
allow_scope: tool_prompt.allow_scope,
|
||||
prompt_preview: tool_prompt.prompt_preview,
|
||||
}),
|
||||
);
|
||||
return Ok(worker.clone());
|
||||
}
|
||||
|
||||
if !worker.trust_gate_cleared && detect_trust_prompt(&lowered) {
|
||||
worker.status = WorkerStatus::TrustRequired;
|
||||
worker.last_error = Some(WorkerFailure {
|
||||
@@ -503,7 +557,9 @@ impl WorkerRegistry {
|
||||
ready: worker.status == WorkerStatus::ReadyForPrompt,
|
||||
blocked: matches!(
|
||||
worker.status,
|
||||
WorkerStatus::TrustRequired | WorkerStatus::Failed
|
||||
WorkerStatus::TrustRequired
|
||||
| WorkerStatus::ToolPermissionRequired
|
||||
| WorkerStatus::Failed
|
||||
),
|
||||
replay_prompt_ready: worker.replay_prompt.is_some(),
|
||||
last_error: worker.last_error.clone(),
|
||||
@@ -624,6 +680,18 @@ impl WorkerRegistry {
|
||||
|
||||
let now = now_secs();
|
||||
let elapsed = now.saturating_sub(worker.created_at);
|
||||
let latest_tool_permission_event = worker
|
||||
.events
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|event| event.kind == WorkerEventKind::ToolPermissionRequired);
|
||||
let tool_permission_allow_scope =
|
||||
latest_tool_permission_event.and_then(|event| match &event.payload {
|
||||
Some(WorkerEventPayload::ToolPermissionPrompt { allow_scope, .. }) => {
|
||||
Some(*allow_scope)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Build evidence bundle
|
||||
let evidence = StartupEvidenceBundle {
|
||||
@@ -640,6 +708,13 @@ impl WorkerRegistry {
|
||||
.events
|
||||
.iter()
|
||||
.any(|e| e.kind == WorkerEventKind::TrustRequired),
|
||||
tool_permission_prompt_detected: worker
|
||||
.events
|
||||
.iter()
|
||||
.any(|e| e.kind == WorkerEventKind::ToolPermissionRequired),
|
||||
tool_permission_prompt_age_seconds: latest_tool_permission_event
|
||||
.map(|event| now.saturating_sub(event.timestamp)),
|
||||
tool_permission_allow_scope,
|
||||
transport_healthy,
|
||||
mcp_healthy,
|
||||
elapsed_seconds: elapsed,
|
||||
@@ -694,6 +769,13 @@ fn classify_startup_failure(evidence: &StartupEvidenceBundle) -> StartupFailureC
|
||||
return StartupFailureClassification::TrustRequired;
|
||||
}
|
||||
|
||||
// Check for tool permission prompts that were not resolved
|
||||
if evidence.tool_permission_prompt_detected
|
||||
&& evidence.last_lifecycle_state == WorkerStatus::ToolPermissionRequired
|
||||
{
|
||||
return StartupFailureClassification::ToolPermissionRequired;
|
||||
}
|
||||
|
||||
// Check for prompt acceptance timeout
|
||||
if evidence.prompt_sent_at.is_some()
|
||||
&& !evidence.prompt_acceptance_state
|
||||
@@ -815,6 +897,140 @@ fn normalize_path(path: &str) -> PathBuf {
|
||||
std::fs::canonicalize(path).unwrap_or_else(|_| Path::new(path).to_path_buf())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct ToolPermissionPromptObservation {
|
||||
server_name: Option<String>,
|
||||
tool_name: Option<String>,
|
||||
allow_scope: ToolPermissionAllowScope,
|
||||
prompt_preview: String,
|
||||
}
|
||||
|
||||
impl ToolPermissionPromptObservation {
|
||||
fn message(&self) -> String {
|
||||
match (&self.server_name, &self.tool_name) {
|
||||
(Some(server), Some(tool)) => {
|
||||
format!("worker boot blocked on tool permission prompt for {server}.{tool}")
|
||||
}
|
||||
(Some(server), None) => {
|
||||
format!("worker boot blocked on tool permission prompt for {server}")
|
||||
}
|
||||
(None, Some(tool)) => {
|
||||
format!("worker boot blocked on tool permission prompt for {tool}")
|
||||
}
|
||||
(None, None) => "worker boot blocked on tool permission prompt".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_tool_permission_prompt(
|
||||
screen_text: &str,
|
||||
lowered: &str,
|
||||
) -> Option<ToolPermissionPromptObservation> {
|
||||
let looks_like_prompt = lowered.contains("allow the")
|
||||
&& lowered.contains("server")
|
||||
&& lowered.contains("tool")
|
||||
&& lowered.contains("run");
|
||||
let looks_like_tool_gate = lowered.contains("allow tool") && lowered.contains("run");
|
||||
if !looks_like_prompt && !looks_like_tool_gate {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prompt_line = screen_text
|
||||
.lines()
|
||||
.rev()
|
||||
.find(|line| {
|
||||
let lowered_line = line.to_ascii_lowercase();
|
||||
lowered_line.contains("allow")
|
||||
&& lowered_line.contains("tool")
|
||||
&& (lowered_line.contains("run") || lowered_line.contains("server"))
|
||||
})
|
||||
.unwrap_or(screen_text)
|
||||
.trim();
|
||||
|
||||
let tool_name = extract_quoted_value(prompt_line)
|
||||
.or_else(|| extract_after(prompt_line, "tool ").map(|token| normalize_tool_token(&token)));
|
||||
let server_name = extract_between(prompt_line, "the ", " server")
|
||||
.map(|server| server.trim_end_matches(" MCP").to_string())
|
||||
.or_else(|| {
|
||||
tool_name
|
||||
.as_deref()
|
||||
.and_then(extract_server_from_qualified_tool)
|
||||
});
|
||||
|
||||
Some(ToolPermissionPromptObservation {
|
||||
server_name,
|
||||
tool_name,
|
||||
allow_scope: detect_tool_permission_allow_scope(lowered),
|
||||
prompt_preview: prompt_preview(prompt_line),
|
||||
})
|
||||
}
|
||||
|
||||
fn detect_tool_permission_allow_scope(lowered: &str) -> ToolPermissionAllowScope {
|
||||
let always_allow_capable = [
|
||||
"always allow",
|
||||
"allow always",
|
||||
"allow this tool always",
|
||||
"allow for all sessions",
|
||||
]
|
||||
.iter()
|
||||
.any(|needle| lowered.contains(needle));
|
||||
|
||||
if always_allow_capable {
|
||||
return ToolPermissionAllowScope::SessionOrAlways;
|
||||
}
|
||||
|
||||
let session_allow_capable = [
|
||||
"allow once",
|
||||
"allow for this session",
|
||||
"allow this session",
|
||||
"yes, allow",
|
||||
]
|
||||
.iter()
|
||||
.any(|needle| lowered.contains(needle));
|
||||
|
||||
if session_allow_capable {
|
||||
ToolPermissionAllowScope::SessionOnly
|
||||
} else {
|
||||
ToolPermissionAllowScope::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_quoted_value(text: &str) -> Option<String> {
|
||||
let start = text.find('"')? + 1;
|
||||
let rest = &text[start..];
|
||||
let end = rest.find('"')?;
|
||||
Some(rest[..end].to_string())
|
||||
}
|
||||
|
||||
fn extract_between(text: &str, prefix: &str, suffix: &str) -> Option<String> {
|
||||
let start = text.find(prefix)? + prefix.len();
|
||||
let rest = &text[start..];
|
||||
let end = rest.find(suffix)?;
|
||||
let value = rest[..end].trim();
|
||||
(!value.is_empty()).then(|| value.to_string())
|
||||
}
|
||||
|
||||
fn extract_after(text: &str, prefix: &str) -> Option<String> {
|
||||
let start = text.to_ascii_lowercase().find(prefix)? + prefix.len();
|
||||
let value = text[start..]
|
||||
.split_whitespace()
|
||||
.next()?
|
||||
.trim_matches(|ch: char| ch == '?' || ch == ':' || ch == '"' || ch == '\'');
|
||||
(!value.is_empty()).then(|| value.to_string())
|
||||
}
|
||||
|
||||
fn normalize_tool_token(token: &str) -> String {
|
||||
token
|
||||
.trim_matches(|ch: char| ch == '?' || ch == ':' || ch == '"' || ch == '\'')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn extract_server_from_qualified_tool(tool: &str) -> Option<String> {
|
||||
let rest = tool.strip_prefix("mcp__")?;
|
||||
let (server, _) = rest.split_once("__")?;
|
||||
(!server.is_empty()).then(|| server.to_string())
|
||||
}
|
||||
|
||||
fn detect_trust_prompt(lowered: &str) -> bool {
|
||||
[
|
||||
"do you trust the files in this folder",
|
||||
@@ -1134,6 +1350,96 @@ mod tests {
|
||||
assert!(detect_ready_for_prompt("│ >", "│ >"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_permission_prompt_blocks_worker_with_structured_event() {
|
||||
let registry = WorkerRegistry::new();
|
||||
let worker = registry.create("/tmp/repo-mcp", &[], true);
|
||||
|
||||
let blocked = registry
|
||||
.observe(
|
||||
&worker.worker_id,
|
||||
"Allow the omx_memory MCP server to run tool \"project_memory_read\"?\n\
|
||||
1. Yes, allow once\n\
|
||||
2. Always allow this tool",
|
||||
)
|
||||
.expect("tool permission observe should succeed");
|
||||
|
||||
assert_eq!(blocked.status, WorkerStatus::ToolPermissionRequired);
|
||||
assert_eq!(
|
||||
blocked
|
||||
.last_error
|
||||
.as_ref()
|
||||
.expect("tool permission error should exist")
|
||||
.kind,
|
||||
WorkerFailureKind::ToolPermissionGate
|
||||
);
|
||||
let event = blocked
|
||||
.events
|
||||
.iter()
|
||||
.find(|event| event.kind == WorkerEventKind::ToolPermissionRequired)
|
||||
.expect("tool permission event should exist");
|
||||
assert_eq!(
|
||||
event.payload,
|
||||
Some(WorkerEventPayload::ToolPermissionPrompt {
|
||||
server_name: Some("omx_memory".to_string()),
|
||||
tool_name: Some("project_memory_read".to_string()),
|
||||
prompt_age_seconds: 0,
|
||||
allow_scope: ToolPermissionAllowScope::SessionOrAlways,
|
||||
prompt_preview: prompt_preview(
|
||||
"Allow the omx_memory MCP server to run tool \"project_memory_read\"?",
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
let readiness = registry
|
||||
.await_ready(&worker.worker_id)
|
||||
.expect("ready snapshot should load");
|
||||
assert!(readiness.blocked);
|
||||
assert!(!readiness.ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_timeout_classifies_tool_permission_prompt() {
|
||||
let registry = WorkerRegistry::new();
|
||||
let worker = registry.create("/tmp/repo-mcp-timeout", &[], true);
|
||||
|
||||
registry
|
||||
.observe(
|
||||
&worker.worker_id,
|
||||
"Allow the omx_memory MCP server to run tool \"notepad_read\"?\n\
|
||||
1. Yes, allow once",
|
||||
)
|
||||
.expect("tool permission observe should succeed");
|
||||
|
||||
let timed_out = registry
|
||||
.observe_startup_timeout(&worker.worker_id, "claw prompt", true, true)
|
||||
.expect("startup timeout observe should succeed");
|
||||
let event = timed_out
|
||||
.events
|
||||
.iter()
|
||||
.find(|event| event.kind == WorkerEventKind::StartupNoEvidence)
|
||||
.expect("startup no evidence event should exist");
|
||||
|
||||
match event.payload.as_ref() {
|
||||
Some(WorkerEventPayload::StartupNoEvidence {
|
||||
classification,
|
||||
evidence,
|
||||
}) => {
|
||||
assert_eq!(
|
||||
*classification,
|
||||
StartupFailureClassification::ToolPermissionRequired
|
||||
);
|
||||
assert!(evidence.tool_permission_prompt_detected);
|
||||
assert_eq!(
|
||||
evidence.tool_permission_allow_scope,
|
||||
Some(ToolPermissionAllowScope::SessionOnly)
|
||||
);
|
||||
assert!(evidence.tool_permission_prompt_age_seconds.is_some());
|
||||
}
|
||||
_ => panic!("expected StartupNoEvidence payload"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_misdelivery_is_detected_and_replay_can_be_rearmed() {
|
||||
let registry = WorkerRegistry::new();
|
||||
@@ -1634,6 +1940,9 @@ mod tests {
|
||||
prompt_sent_at: Some(1_234_567_890),
|
||||
prompt_acceptance_state: false,
|
||||
trust_prompt_detected: true,
|
||||
tool_permission_prompt_detected: false,
|
||||
tool_permission_prompt_age_seconds: None,
|
||||
tool_permission_allow_scope: None,
|
||||
transport_healthy: true,
|
||||
mcp_healthy: false,
|
||||
elapsed_seconds: 60,
|
||||
@@ -1661,6 +1970,9 @@ mod tests {
|
||||
prompt_sent_at: None,
|
||||
prompt_acceptance_state: false,
|
||||
trust_prompt_detected: false,
|
||||
tool_permission_prompt_detected: false,
|
||||
tool_permission_prompt_age_seconds: None,
|
||||
tool_permission_allow_scope: None,
|
||||
transport_healthy: false,
|
||||
mcp_healthy: true,
|
||||
elapsed_seconds: 30,
|
||||
@@ -1678,6 +1990,9 @@ mod tests {
|
||||
prompt_sent_at: None,
|
||||
prompt_acceptance_state: false,
|
||||
trust_prompt_detected: false,
|
||||
tool_permission_prompt_detected: false,
|
||||
tool_permission_prompt_age_seconds: None,
|
||||
tool_permission_allow_scope: None,
|
||||
transport_healthy: true,
|
||||
mcp_healthy: true,
|
||||
elapsed_seconds: 10,
|
||||
@@ -1697,6 +2012,9 @@ mod tests {
|
||||
prompt_sent_at: None, // No prompt sent yet
|
||||
prompt_acceptance_state: false,
|
||||
trust_prompt_detected: false,
|
||||
tool_permission_prompt_detected: false,
|
||||
tool_permission_prompt_age_seconds: None,
|
||||
tool_permission_allow_scope: None,
|
||||
transport_healthy: true,
|
||||
mcp_healthy: false, // MCP unhealthy but transport healthy suggests crash
|
||||
elapsed_seconds: 45,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -172,7 +172,10 @@ stderr:
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||
let parsed: Value = serde_json::from_str(&stdout).expect("compact json stdout should parse");
|
||||
assert_eq!(parsed["message"], "Mock streaming says hello from the parity harness.");
|
||||
assert_eq!(
|
||||
parsed["message"],
|
||||
"Mock streaming says hello from the parity harness."
|
||||
);
|
||||
assert_eq!(parsed["compact"], true);
|
||||
assert_eq!(parsed["model"], "claude-sonnet-4-6");
|
||||
assert!(parsed["usage"].is_object());
|
||||
|
||||
@@ -240,6 +240,13 @@ impl GlobalToolRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
if allowed.is_empty() {
|
||||
return Err(format!(
|
||||
"--allowedTools was provided with no usable tool names (got `{}`). Omit the flag to allow all tools.",
|
||||
values.join(" ")
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(allowed))
|
||||
}
|
||||
|
||||
@@ -6883,6 +6890,21 @@ mod tests {
|
||||
assert!(empty_permission.contains("unsupported plugin permission: "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowed_tools_rejects_empty_token_lists() {
|
||||
let registry = GlobalToolRegistry::builtin();
|
||||
|
||||
for raw in ["", ",,", " "] {
|
||||
let err = registry
|
||||
.normalize_allowed_tools(&[raw.to_string()])
|
||||
.expect_err("empty allow-list input should be rejected");
|
||||
assert!(
|
||||
err.contains("--allowedTools was provided with no usable tool names"),
|
||||
"unexpected error for {raw:?}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_tools_extend_registry_definitions_permissions_and_search() {
|
||||
let registry = GlobalToolRegistry::builtin()
|
||||
|
||||
7
scripts/fmt.sh
Executable file
7
scripts/fmt.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$REPO_ROOT/rust"
|
||||
exec cargo fmt "$@"
|
||||
Reference in New Issue
Block a user