mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-06 12:16:44 +00:00
Compare commits
40 Commits
cb56dc12ab
...
docs/roadm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d89b4b0f5 | ||
|
|
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 |
37
ROADMAP.md
37
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.**
|
||||
```
|
||||
@@ -6253,4 +6253,37 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
||||
|
||||
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`.
|
||||
|
||||
354. **`/cwd` slash command is not implemented but the fuzzy-match "Did you mean" suggestion returns `/cwd` itself, creating a self-referential error loop** — dogfooded 2026-04-30 by Jobdori on `c6c01bea`. Running `/cwd --output-format json` returns `{"command":"/cwd","error":"Unknown slash command: /cwd\n Did you mean /cwd, /chat, /copy\n Help...","type":"error"}`. The command reports itself as unknown and then suggests itself as the correction. A user who follows the suggestion types `/cwd` again, gets the same error, and is stuck in an infinite loop with no way to reach the current working directory via a slash command. The correct working directory information is available via `status --output-format json` as `workspace.cwd`, but there is no dedicated `/cwd` command. **Required fix shape:** (a) either implement `/cwd` to return `{"kind":"cwd","cwd":"/current/path"}` (consistent with `/status`'s `workspace.cwd`), or (b) remove `/cwd` from the fuzzy-match candidate list entirely so it does not appear in "Did you mean" suggestions when it cannot be executed. As a secondary fix, the fuzzy-match algorithm should filter out commands whose `status` is `unsupported` or `not-yet-implemented` so they are never surfaced as suggestions. Source: Jobdori live dogfood, mengmotaHost, `c6c01bea`, 2026-04-30.
|
||||
|
||||
11
progress.txt
11
progress.txt
@@ -365,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.
|
||||
|
||||
@@ -1961,6 +1961,7 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
|
||||
project_root,
|
||||
git_branch,
|
||||
git_summary,
|
||||
session_lifecycle: classify_session_lifecycle_for(&cwd),
|
||||
sandbox_status: resolve_sandbox_status(sandbox_config.sandbox(), &cwd),
|
||||
// Doctor path has its own config check; StatusContext here is only
|
||||
// fed into health renderers that don't read config_load_error.
|
||||
@@ -2805,6 +2806,7 @@ struct StatusContext {
|
||||
project_root: Option<PathBuf>,
|
||||
git_branch: Option<String>,
|
||||
git_summary: GitWorkspaceSummary,
|
||||
session_lifecycle: SessionLifecycleSummary,
|
||||
sandbox_status: runtime::SandboxStatus,
|
||||
/// #143: when `.claw.json` (or another loaded config file) fails to parse,
|
||||
/// we capture the parse error here and still populate every field that
|
||||
@@ -2834,6 +2836,75 @@ struct GitWorkspaceSummary {
|
||||
conflicted_files: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SessionLifecycleKind {
|
||||
RunningProcess,
|
||||
IdleShell,
|
||||
SavedOnly,
|
||||
}
|
||||
|
||||
impl SessionLifecycleKind {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::RunningProcess => "running_process",
|
||||
Self::IdleShell => "idle_shell",
|
||||
Self::SavedOnly => "saved_only",
|
||||
}
|
||||
}
|
||||
|
||||
fn human_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::RunningProcess => "running process",
|
||||
Self::IdleShell => "idle shell",
|
||||
Self::SavedOnly => "saved only",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct SessionLifecycleSummary {
|
||||
kind: SessionLifecycleKind,
|
||||
pane_id: Option<String>,
|
||||
pane_command: Option<String>,
|
||||
pane_path: Option<PathBuf>,
|
||||
workspace_dirty: bool,
|
||||
abandoned: bool,
|
||||
}
|
||||
|
||||
impl SessionLifecycleSummary {
|
||||
fn signal(&self) -> String {
|
||||
let mut parts = vec![self.kind.human_label().to_string()];
|
||||
if self.workspace_dirty {
|
||||
parts.push("dirty worktree".to_string());
|
||||
}
|
||||
if self.abandoned {
|
||||
parts.push("abandoned?".to_string());
|
||||
}
|
||||
if let Some(command) = self.pane_command.as_deref() {
|
||||
parts.push(format!("cmd={command}"));
|
||||
}
|
||||
parts.join(" · ")
|
||||
}
|
||||
|
||||
fn json_value(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"kind": self.kind.as_str(),
|
||||
"pane_id": self.pane_id,
|
||||
"pane_command": self.pane_command,
|
||||
"pane_path": self.pane_path.as_ref().map(|path| path.display().to_string()),
|
||||
"workspace_dirty": self.workspace_dirty,
|
||||
"abandoned": self.abandoned,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct TmuxPaneSnapshot {
|
||||
pane_id: String,
|
||||
current_command: String,
|
||||
current_path: PathBuf,
|
||||
}
|
||||
|
||||
impl GitWorkspaceSummary {
|
||||
fn is_clean(self) -> bool {
|
||||
self.changed_files == 0
|
||||
@@ -2865,6 +2936,120 @@ impl GitWorkspaceSummary {
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_session_lifecycle_for(workspace: &Path) -> SessionLifecycleSummary {
|
||||
classify_session_lifecycle_from_panes(workspace, discover_tmux_panes())
|
||||
}
|
||||
|
||||
fn classify_session_lifecycle_from_panes(
|
||||
workspace: &Path,
|
||||
panes: Vec<TmuxPaneSnapshot>,
|
||||
) -> SessionLifecycleSummary {
|
||||
let workspace_dirty = git_worktree_is_dirty(workspace);
|
||||
let mut idle_shell = None;
|
||||
for pane in panes {
|
||||
if !pane_path_matches_workspace(&pane.current_path, workspace) {
|
||||
continue;
|
||||
}
|
||||
if is_idle_shell_command(&pane.current_command) {
|
||||
idle_shell.get_or_insert(pane);
|
||||
} else {
|
||||
return SessionLifecycleSummary {
|
||||
kind: SessionLifecycleKind::RunningProcess,
|
||||
pane_id: Some(pane.pane_id),
|
||||
pane_command: Some(pane.current_command),
|
||||
pane_path: Some(pane.current_path),
|
||||
workspace_dirty,
|
||||
abandoned: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pane) = idle_shell {
|
||||
SessionLifecycleSummary {
|
||||
kind: SessionLifecycleKind::IdleShell,
|
||||
pane_id: Some(pane.pane_id),
|
||||
pane_command: Some(pane.current_command),
|
||||
pane_path: Some(pane.current_path),
|
||||
workspace_dirty,
|
||||
abandoned: workspace_dirty,
|
||||
}
|
||||
} else {
|
||||
SessionLifecycleSummary {
|
||||
kind: SessionLifecycleKind::SavedOnly,
|
||||
pane_id: None,
|
||||
pane_command: None,
|
||||
pane_path: None,
|
||||
workspace_dirty,
|
||||
abandoned: workspace_dirty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_tmux_panes() -> Vec<TmuxPaneSnapshot> {
|
||||
let output = Command::new("tmux")
|
||||
.args([
|
||||
"list-panes",
|
||||
"-a",
|
||||
"-F",
|
||||
"#{pane_id}\t#{pane_current_command}\t#{pane_current_path}",
|
||||
])
|
||||
.output();
|
||||
let Ok(output) = output else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !output.status.success() {
|
||||
return Vec::new();
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
parse_tmux_pane_snapshots(&stdout)
|
||||
}
|
||||
|
||||
fn parse_tmux_pane_snapshots(output: &str) -> Vec<TmuxPaneSnapshot> {
|
||||
output
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let mut fields = line.splitn(3, '\t');
|
||||
let pane_id = fields.next()?.trim();
|
||||
let current_command = fields.next()?.trim();
|
||||
let current_path = fields.next()?.trim();
|
||||
if pane_id.is_empty() || current_path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(TmuxPaneSnapshot {
|
||||
pane_id: pane_id.to_string(),
|
||||
current_command: current_command.to_string(),
|
||||
current_path: PathBuf::from(current_path),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pane_path_matches_workspace(pane_path: &Path, workspace: &Path) -> bool {
|
||||
let pane_path = fs::canonicalize(pane_path).unwrap_or_else(|_| pane_path.to_path_buf());
|
||||
let workspace = fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf());
|
||||
pane_path == workspace || pane_path.starts_with(&workspace)
|
||||
}
|
||||
|
||||
fn is_idle_shell_command(command: &str) -> bool {
|
||||
let command = command.rsplit('/').next().unwrap_or(command);
|
||||
matches!(
|
||||
command,
|
||||
"bash" | "zsh" | "sh" | "fish" | "nu" | "pwsh" | "powershell" | "cmd"
|
||||
)
|
||||
}
|
||||
|
||||
fn git_worktree_is_dirty(workspace: &Path) -> bool {
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(workspace)
|
||||
.args(["status", "--porcelain"])
|
||||
.output();
|
||||
output
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
.is_some_and(|output| !output.stdout.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn format_unknown_slash_command_message(name: &str) -> String {
|
||||
let suggestions = suggest_slash_commands(name);
|
||||
@@ -3407,6 +3592,18 @@ fn run_resume_command(
|
||||
} if act == "list" => {
|
||||
let sessions = list_managed_sessions().unwrap_or_default();
|
||||
let session_ids: Vec<String> = sessions.iter().map(|s| s.id.clone()).collect();
|
||||
let session_details: Vec<serde_json::Value> = sessions
|
||||
.iter()
|
||||
.map(|session| {
|
||||
serde_json::json!({
|
||||
"id": session.id,
|
||||
"path": session.path.display().to_string(),
|
||||
"message_count": session.message_count,
|
||||
"updated_at_ms": session.updated_at_ms,
|
||||
"lifecycle": session.lifecycle.json_value(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let active_id = session.session_id.clone();
|
||||
let text = render_session_list(&active_id).unwrap_or_else(|e| format!("error: {e}"));
|
||||
Ok(ResumeCommandOutcome {
|
||||
@@ -3415,6 +3612,7 @@ fn run_resume_command(
|
||||
json: Some(serde_json::json!({
|
||||
"kind": "session_list",
|
||||
"sessions": session_ids,
|
||||
"session_details": session_details,
|
||||
"active": active_id,
|
||||
})),
|
||||
})
|
||||
@@ -3646,6 +3844,7 @@ struct ManagedSessionSummary {
|
||||
message_count: usize,
|
||||
parent_session_id: Option<String>,
|
||||
branch_name: Option<String>,
|
||||
lifecycle: SessionLifecycleSummary,
|
||||
}
|
||||
|
||||
struct LiveCli {
|
||||
@@ -5251,7 +5450,9 @@ fn resolve_managed_session_path(session_id: &str) -> Result<PathBuf, Box<dyn std
|
||||
}
|
||||
|
||||
fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::error::Error>> {
|
||||
Ok(current_session_store()?
|
||||
let store = current_session_store()?;
|
||||
let lifecycle = classify_session_lifecycle_for(store.workspace_root());
|
||||
Ok(store
|
||||
.list_sessions()
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?
|
||||
.into_iter()
|
||||
@@ -5263,12 +5464,15 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
|
||||
message_count: session.message_count,
|
||||
parent_session_id: session.parent_session_id,
|
||||
branch_name: session.branch_name,
|
||||
lifecycle: lifecycle.clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn latest_managed_session() -> Result<ManagedSessionSummary, Box<dyn std::error::Error>> {
|
||||
let session = current_session_store()?
|
||||
let store = current_session_store()?;
|
||||
let lifecycle = classify_session_lifecycle_for(store.workspace_root());
|
||||
let session = store
|
||||
.latest_session()
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
||||
Ok(ManagedSessionSummary {
|
||||
@@ -5279,6 +5483,7 @@ fn latest_managed_session() -> Result<ManagedSessionSummary, Box<dyn std::error:
|
||||
message_count: session.message_count,
|
||||
parent_session_id: session.parent_session_id,
|
||||
branch_name: session.branch_name,
|
||||
lifecycle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5343,8 +5548,9 @@ fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::e
|
||||
(None, None) => String::new(),
|
||||
};
|
||||
lines.push(format!(
|
||||
" {id:<20} {marker:<10} msgs={msgs:<4} modified={modified}{lineage} path={path}",
|
||||
" {id:<20} {marker:<10} lifecycle={lifecycle} msgs={msgs:<4} modified={modified}{lineage} path={path}",
|
||||
id = session.id,
|
||||
lifecycle = session.lifecycle.signal(),
|
||||
msgs = session.message_count,
|
||||
modified = format_session_modified_age(session.modified_epoch_millis),
|
||||
lineage = lineage,
|
||||
@@ -5527,6 +5733,7 @@ fn status_json_value(
|
||||
// .claw/sessions/. Extract the stem (drop the .jsonl extension).
|
||||
path.file_stem().map(|n| n.to_string_lossy().into_owned())
|
||||
}),
|
||||
"session_lifecycle": context.session_lifecycle.json_value(),
|
||||
"loaded_config_files": context.loaded_config_files,
|
||||
"discovered_config_files": context.discovered_config_files,
|
||||
"memory_file_count": context.memory_file_count,
|
||||
@@ -5582,7 +5789,7 @@ fn status_context(
|
||||
parse_git_status_metadata(project_context.git_status.as_deref());
|
||||
let git_summary = parse_git_workspace_summary(project_context.git_status.as_deref());
|
||||
Ok(StatusContext {
|
||||
cwd,
|
||||
cwd: cwd.clone(),
|
||||
session_path: session_path.map(Path::to_path_buf),
|
||||
loaded_config_files,
|
||||
discovered_config_files,
|
||||
@@ -5590,6 +5797,7 @@ fn status_context(
|
||||
project_root,
|
||||
git_branch,
|
||||
git_summary,
|
||||
session_lifecycle: classify_session_lifecycle_for(&cwd),
|
||||
sandbox_status,
|
||||
config_load_error,
|
||||
})
|
||||
@@ -5663,6 +5871,7 @@ fn format_status_report(
|
||||
Unstaged {}
|
||||
Untracked {}
|
||||
Session {}
|
||||
Lifecycle {}
|
||||
Config files loaded {}/{}
|
||||
Memory files {}
|
||||
Suggested flow /status → /diff → /commit",
|
||||
@@ -5681,6 +5890,7 @@ fn format_status_report(
|
||||
|| "live-repl".to_string(),
|
||||
|path| path.display().to_string()
|
||||
),
|
||||
context.session_lifecycle.signal(),
|
||||
context.loaded_config_files,
|
||||
context.discovered_config_files,
|
||||
context.memory_file_count,
|
||||
@@ -8952,6 +9162,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out)?;
|
||||
let resume_commands = resume_supported_slash_commands()
|
||||
.into_iter()
|
||||
.filter(|spec| !STUB_COMMANDS.contains(&spec.name))
|
||||
.map(|spec| match spec.argument_hint {
|
||||
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
||||
None => format!("/{}", spec.name),
|
||||
@@ -9025,10 +9236,10 @@ fn print_help(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::
|
||||
mod tests {
|
||||
use super::{
|
||||
build_runtime_plugin_state_with_loader, build_runtime_with_plugin_state,
|
||||
classify_error_kind, collect_session_prompt_history, create_managed_session_handle,
|
||||
describe_tool_progress, filter_tool_specs, format_bughunter_report,
|
||||
format_commit_preflight_report, format_commit_skipped_report, format_compact_report,
|
||||
format_connected_line, format_cost_report, format_history_timestamp,
|
||||
classify_error_kind, classify_session_lifecycle_from_panes, collect_session_prompt_history,
|
||||
create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
||||
format_bughunter_report, format_commit_preflight_report, format_commit_skipped_report,
|
||||
format_compact_report, format_connected_line, format_cost_report, format_history_timestamp,
|
||||
format_internal_prompt_progress_line, format_issue_report, format_model_report,
|
||||
format_model_switch_report, format_permissions_report, format_permissions_switch_report,
|
||||
format_pr_report, format_resume_report, format_status_report, format_tool_call_start,
|
||||
@@ -9039,14 +9250,15 @@ mod tests {
|
||||
parse_history_count, permission_policy, print_help_to, push_output_block,
|
||||
render_config_report, render_diff_report, render_diff_report_for, render_help_topic,
|
||||
render_memory_report, render_prompt_history_report, render_repl_help, render_resume_usage,
|
||||
render_session_markdown, resolve_model_alias, resolve_model_alias_with_config,
|
||||
resolve_repl_model, resolve_session_reference, response_to_events,
|
||||
resume_supported_slash_commands, run_resume_command, short_tool_id,
|
||||
render_session_list, render_session_markdown, resolve_model_alias,
|
||||
resolve_model_alias_with_config, resolve_repl_model, resolve_session_reference,
|
||||
response_to_events, resume_supported_slash_commands, run_resume_command, short_tool_id,
|
||||
slash_command_completion_candidates_with_sessions, split_error_hint, status_context,
|
||||
summarize_tool_payload_for_markdown, try_resolve_bare_skill_prompt, validate_no_args,
|
||||
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
||||
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
|
||||
PromptHistoryEntry, SlashCommand, StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
||||
status_json_value, summarize_tool_payload_for_markdown, try_resolve_bare_skill_prompt,
|
||||
validate_no_args, write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor,
|
||||
GitWorkspaceSummary, InternalPromptProgressEvent, InternalPromptProgressState, LiveCli,
|
||||
LocalHelpTopic, PromptHistoryEntry, SessionLifecycleKind, SessionLifecycleSummary,
|
||||
SlashCommand, StatusUsage, TmuxPaneSnapshot, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
||||
STUB_COMMANDS,
|
||||
};
|
||||
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
||||
@@ -11637,6 +11849,14 @@ mod tests {
|
||||
untracked_files: 1,
|
||||
conflicted_files: 0,
|
||||
},
|
||||
session_lifecycle: SessionLifecycleSummary {
|
||||
kind: SessionLifecycleKind::IdleShell,
|
||||
pane_id: Some("%7".to_string()),
|
||||
pane_command: Some("zsh".to_string()),
|
||||
pane_path: Some(PathBuf::from("/tmp/project")),
|
||||
workspace_dirty: true,
|
||||
abandoned: true,
|
||||
},
|
||||
sandbox_status: runtime::SandboxStatus::default(),
|
||||
config_load_error: None,
|
||||
},
|
||||
@@ -11659,11 +11879,149 @@ mod tests {
|
||||
assert!(status.contains("Unstaged 1"));
|
||||
assert!(status.contains("Untracked 1"));
|
||||
assert!(status.contains("Session session.jsonl"));
|
||||
assert!(
|
||||
status.contains("Lifecycle idle shell · dirty worktree · abandoned? · cmd=zsh")
|
||||
);
|
||||
assert!(status.contains("Config files loaded 2/3"));
|
||||
assert!(status.contains("Memory files 4"));
|
||||
assert!(status.contains("Suggested flow /status → /diff → /commit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_lifecycle_prefers_running_process_over_idle_shell() {
|
||||
let workspace = PathBuf::from("/tmp/project");
|
||||
let lifecycle = classify_session_lifecycle_from_panes(
|
||||
&workspace,
|
||||
vec![
|
||||
TmuxPaneSnapshot {
|
||||
pane_id: "%1".to_string(),
|
||||
current_command: "zsh".to_string(),
|
||||
current_path: workspace.clone(),
|
||||
},
|
||||
TmuxPaneSnapshot {
|
||||
pane_id: "%2".to_string(),
|
||||
current_command: "claw".to_string(),
|
||||
current_path: workspace.join("rust"),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(lifecycle.kind, SessionLifecycleKind::RunningProcess);
|
||||
assert_eq!(lifecycle.pane_id.as_deref(), Some("%2"));
|
||||
assert_eq!(lifecycle.pane_command.as_deref(), Some("claw"));
|
||||
assert!(!lifecycle.abandoned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_lifecycle_marks_dirty_idle_shell_as_abandoned() {
|
||||
let _guard = env_lock();
|
||||
let workspace = temp_workspace("dirty-idle-shell");
|
||||
fs::create_dir_all(&workspace).expect("workspace should create");
|
||||
git(&["init", "--quiet"], &workspace);
|
||||
git(&["config", "user.email", "tests@example.com"], &workspace);
|
||||
git(&["config", "user.name", "Rusty Claude Tests"], &workspace);
|
||||
fs::write(workspace.join("tracked.txt"), "hello\n").expect("write tracked");
|
||||
git(&["add", "tracked.txt"], &workspace);
|
||||
git(&["commit", "-m", "init", "--quiet"], &workspace);
|
||||
fs::write(workspace.join("tracked.txt"), "hello\nchanged\n").expect("dirty tracked");
|
||||
|
||||
let lifecycle = classify_session_lifecycle_from_panes(
|
||||
&workspace,
|
||||
vec![TmuxPaneSnapshot {
|
||||
pane_id: "%3".to_string(),
|
||||
current_command: "bash".to_string(),
|
||||
current_path: workspace.clone(),
|
||||
}],
|
||||
);
|
||||
|
||||
assert_eq!(lifecycle.kind, SessionLifecycleKind::IdleShell);
|
||||
assert!(lifecycle.workspace_dirty);
|
||||
assert!(lifecycle.abandoned);
|
||||
|
||||
fs::remove_dir_all(workspace).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_list_surfaces_saved_dirty_abandoned_lifecycle() {
|
||||
let _guard = cwd_guard();
|
||||
let workspace = temp_workspace("session-list-lifecycle");
|
||||
fs::create_dir_all(&workspace).expect("workspace should create");
|
||||
git(&["init", "--quiet"], &workspace);
|
||||
git(&["config", "user.email", "tests@example.com"], &workspace);
|
||||
git(&["config", "user.name", "Rusty Claude Tests"], &workspace);
|
||||
fs::write(workspace.join(".gitignore"), ".claw/\n").expect("write gitignore");
|
||||
fs::write(workspace.join("tracked.txt"), "hello\n").expect("write tracked");
|
||||
git(&["add", ".gitignore", "tracked.txt"], &workspace);
|
||||
git(&["commit", "-m", "init", "--quiet"], &workspace);
|
||||
|
||||
let previous = std::env::current_dir().expect("cwd");
|
||||
std::env::set_current_dir(&workspace).expect("switch cwd");
|
||||
let handle = create_managed_session_handle("session-alpha").expect("session handle");
|
||||
Session::new()
|
||||
.with_workspace_root(workspace.clone())
|
||||
.with_persistence_path(handle.path.clone())
|
||||
.save_to_path(&handle.path)
|
||||
.expect("session should save");
|
||||
fs::write(workspace.join("tracked.txt"), "hello\nchanged\n").expect("dirty tracked");
|
||||
|
||||
let report = render_session_list("session-alpha").expect("session list should render");
|
||||
|
||||
assert!(report.contains("session-alpha"));
|
||||
assert!(report.contains("lifecycle=saved only · dirty worktree · abandoned?"));
|
||||
|
||||
std::env::set_current_dir(previous).expect("restore cwd");
|
||||
fs::remove_dir_all(workspace).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_json_surfaces_session_lifecycle_for_clawhip() {
|
||||
let context = super::StatusContext {
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
session_path: None,
|
||||
loaded_config_files: 0,
|
||||
discovered_config_files: 0,
|
||||
memory_file_count: 0,
|
||||
project_root: Some(PathBuf::from("/tmp/project")),
|
||||
git_branch: Some("feature/session-lifecycle".to_string()),
|
||||
git_summary: GitWorkspaceSummary::default(),
|
||||
session_lifecycle: SessionLifecycleSummary {
|
||||
kind: SessionLifecycleKind::RunningProcess,
|
||||
pane_id: Some("%9".to_string()),
|
||||
pane_command: Some("claw".to_string()),
|
||||
pane_path: Some(PathBuf::from("/tmp/project")),
|
||||
workspace_dirty: false,
|
||||
abandoned: false,
|
||||
},
|
||||
sandbox_status: runtime::SandboxStatus::default(),
|
||||
config_load_error: None,
|
||||
};
|
||||
|
||||
let value = status_json_value(
|
||||
Some("claude-sonnet"),
|
||||
StatusUsage {
|
||||
message_count: 0,
|
||||
turns: 0,
|
||||
latest: runtime::TokenUsage::default(),
|
||||
cumulative: runtime::TokenUsage::default(),
|
||||
estimated_tokens: 0,
|
||||
},
|
||||
"workspace-write",
|
||||
&context,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
value["workspace"]["session_lifecycle"]["kind"],
|
||||
"running_process"
|
||||
);
|
||||
assert_eq!(
|
||||
value["workspace"]["session_lifecycle"]["pane_command"],
|
||||
"claw"
|
||||
);
|
||||
assert_eq!(value["workspace"]["session_lifecycle"]["abandoned"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_reports_surface_workspace_context() {
|
||||
let summary = GitWorkspaceSummary {
|
||||
@@ -13000,6 +13358,32 @@ UU conflicted.rs",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stub_commands_absent_from_resume_safe_help() {
|
||||
let mut help = Vec::new();
|
||||
print_help_to(&mut help).expect("help should render");
|
||||
let help = String::from_utf8(help).expect("help should be utf8");
|
||||
let resume_line = help
|
||||
.lines()
|
||||
.find(|line| line.starts_with("Resume-safe commands:"))
|
||||
.expect("resume-safe command line should exist");
|
||||
let resume_roots = resume_line
|
||||
.trim_start_matches("Resume-safe commands:")
|
||||
.split(',')
|
||||
.filter_map(|entry| entry.trim().strip_prefix('/'))
|
||||
.filter_map(|entry| entry.split_whitespace().next())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for stub in STUB_COMMANDS {
|
||||
assert!(
|
||||
!resume_roots.contains(stub),
|
||||
"stub command /{stub} should not appear in resume-safe command list"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(resume_roots.contains(&"status"));
|
||||
}
|
||||
}
|
||||
|
||||
fn write_mcp_server_fixture(script_path: &Path) {
|
||||
|
||||
Reference in New Issue
Block a user