Compare commits

...

27 Commits

Author SHA1 Message Date
YeonGyu-Kim
14c5ef1808 file: #156 — error classification for text-mode output (Phase 2 of #77)
ROADMAP entry for natural Phase 2 follow-up to #77 Phase 1 (JSON error kind
classification). Text-mode errors currently prose-only with no structured
class; observability tools parsing stderr need the kind token.

Two implementation options:
- Prefix line before error prose: [error-kind: missing_credentials]
- Suffix comment: # error_class=missing_credentials

Scope: ~20 lines. Non-breaking (adds classification, doesn't change error text).

Source: Cycle 11 dogfood probe at 23:18 KST — product surface clean after
today's batch, identified natural next step for error-classification symmetry.
2026-04-21 23:19:58 +09:00
YeonGyu-Kim
9362900b1b feat: #77 Phase 1 — machine-readable error classification in JSON error payloads
## Problem

All JSON error payloads had the same three-field envelope:
```json
{"type": "error", "error": "<prose with hint baked in>"}
```

Five distinct error classes were indistinguishable at the schema level:
- missing_credentials (no API key)
- missing_worker_state (no state file)
- session_not_found / session_load_failed
- cli_parse (unrecognized args)
- invalid_model_syntax

Downstream claws had to regex-scrape the prose to route failures.

## Fix

1. **Added `classify_error_kind()`** — prefix/keyword classifier that returns a
   snake_case discriminant token for 12 known error classes:
   `missing_credentials`, `missing_manifests`, `missing_worker_state`,
   `session_not_found`, `session_load_failed`, `no_managed_sessions`,
   `cli_parse`, `invalid_model_syntax`, `unsupported_command`,
   `unsupported_resumed_command`, `confirmation_required`, `api_http_error`,
   plus `unknown` fallback.

2. **Added `split_error_hint()`** — splits multi-line error messages into
   (short_reason, optional_hint) so the runbook prose stops being stuffed
   into the `error` field.

3. **Extended JSON envelope** at 4 emit sites:
   - Main error sink (line ~213)
   - Session load failure in resume_session
   - Stub command (unsupported_command)
   - Unknown resumed command (unsupported_resumed_command)

## New JSON shape

```json
{
  "type": "error",
  "error": "short reason (first line)",
  "kind": "missing_credentials",
  "hint": "Hint: export ANTHROPIC_API_KEY..."
}
```

`kind` is always present. `hint` is null when no runbook follows.
`error` now carries only the short reason, not the full multi-line prose.

## Tests

Added 2 new regression tests:
- `classify_error_kind_returns_correct_discriminants` — all 9 known classes + fallback
- `split_error_hint_separates_reason_from_runbook` — with and without hints

All 179 rusty-claude-cli tests pass. Full workspace green.

Closes ROADMAP #77 Phase 1.
2026-04-21 22:38:13 +09:00
YeonGyu-Kim
ff45e971aa fix: #80 — session-lookup error messages now show actual workspace-fingerprint directory
## Problem

Two session error messages advertised `.claw/sessions/` as the managed-session
location, but the actual on-disk layout is `.claw/sessions/<workspace_fingerprint>/`
where the fingerprint is a 16-char FNV-1a hash of the CWD path.

Users see error messages like:
```
no managed sessions found in .claw/sessions/
```

But the real directory is:
```
.claw/sessions/8497f4bcf995fc19/
```

The error copy was a direct lie — it made workspace-fingerprint partitioning
invisible and left users confused about whether sessions were lost or just in
a different partition.

## Fix

Updated two error formatters to accept the resolved `sessions_root` path
and extract the actual workspace-fingerprint directory:

1. **format_missing_session_reference**: now shows the actual fingerprint dir
   and explains that it's a workspace-specific partition

2. **format_no_managed_sessions**: now shows the actual fingerprint dir and
   includes a note that sessions from other CWDs are intentionally invisible

Updated all three call sites to pass `&self.sessions_root` to the formatters.

## Examples

**Before:**
```
no managed sessions found in .claw/sessions/
```

**After:**
```
no managed sessions found in .claw/sessions/8497f4bcf995fc19/
Start `claw` to create a session, then rerun with `--resume latest`.
Note: claw partitions sessions per workspace fingerprint; sessions from other CWDs are invisible.
```

```
session not found: nonexistent-id
Hint: managed sessions live in .claw/sessions/8497f4bcf995fc19/ (workspace-specific partition).
Try `latest` for the most recent session or `/session list` in the REPL.
```

## Impact

- Users can now tell from the error message that they're looking in the right
  directory (the one their current CWD maps to)
- The workspace-fingerprint partitioning stops being invisible
- Operators understand why sessions from adjacent CWDs don't appear
- Error copy matches the actual on-disk structure

## Tests

All 466 runtime tests pass. Verified on two real workspaces with actual
workspace-fingerprint directories.

Closes ROADMAP #80.
2026-04-21 22:18:12 +09:00
YeonGyu-Kim
4b53b97e36 docs: #155 — add USAGE.md documentation for /ultraplan, /teleport, /bughunter commands
## Problem

Three interactive slash commands are documented in `claw --help` but have no
corresponding section in USAGE.md:

- `/ultraplan [task]` — Run a deep planning prompt with multi-step reasoning
- `/teleport <symbol-or-path>` — Jump to a file or symbol by searching the workspace
- `/bughunter [scope]` — Inspect the codebase for likely bugs

New users see these commands in the help output but don't know:
- What each command does
- How to use it
- When to use it vs. other commands
- What kind of results to expect

## Fix

Added new section "Advanced slash commands (Interactive REPL only)" to USAGE.md
with documentation for all three commands:

1. **`/ultraplan`** — multi-step reasoning for complex tasks
   - Example: `/ultraplan refactor the auth module to use async/await`
   - Output: structured plan with numbered steps and reasoning

2. **`/teleport`** — navigate to a file or symbol
   - Example: `/teleport UserService`, `/teleport src/auth.rs`
   - Output: file content with the requested symbol highlighted

3. **`/bughunter`** — scan for likely bugs
   - Example: `/bughunter src/handlers`, `/bughunter` (all)
   - Output: list of suspicious patterns with explanations

## Impact

Users can now discover these commands and understand when to use them without
having to guess or search external sources. Bridges the gap between `--help`
output and full documentation.

Also filed ROADMAP #155 documenting the gap.

Closes ROADMAP #155.
2026-04-21 21:49:04 +09:00
YeonGyu-Kim
3cfe6e2b14 feat: #154 — hint provider prefix and env var when model name looks like different provider
## Problem

When a user types `claw --model gpt-4` or `--model qwen-plus`, they get:
```
error: invalid model syntax: 'gpt-4'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias
```

USAGE.md documents that "The error message now includes a hint that names the detected env var" — but this hint does not actually exist. The user has to re-read USAGE.md or guess the correct prefix.

## Fix

Enhance `validate_model_syntax` to detect when a model name looks like it belongs to a different provider:

1. **OpenAI models** (starts with `gpt-` or `gpt_`):
   ```
   Did you mean `openai/gpt-4`? (Requires OPENAI_API_KEY env var)
   ```

2. **Qwen/DashScope models** (starts with `qwen`):
   ```
   Did you mean `qwen/qwen-plus`? (Requires DASHSCOPE_API_KEY env var)
   ```

3. **Grok/xAI models** (starts with `grok`):
   ```
   Did you mean `xai/grok-3`? (Requires XAI_API_KEY env var)
   ```

Unrelated invalid models (e.g., `asdfgh`) do not get a spurious hint.

## Verification

- `claw --model gpt-4` → hints `openai/gpt-4` + `OPENAI_API_KEY`
- `claw --model qwen-plus` → hints `qwen/qwen-plus` + `DASHSCOPE_API_KEY`
- `claw --model grok-3` → hints `xai/grok-3` + `XAI_API_KEY`
- `claw --model asdfgh` → generic error (no hint)

## Tests

Added 3 new assertions in `parses_multiple_diagnostic_subcommands`:
- GPT model error hints openai/ prefix and OPENAI_API_KEY
- Qwen model error hints qwen/ prefix and DASHSCOPE_API_KEY
- Unrelated models don't get a spurious hint

All 177 rusty-claude-cli tests pass.

Closes ROADMAP #154.
2026-04-21 21:40:48 +09:00
YeonGyu-Kim
71f5f83adb feat: #153 — add post-build binary location and verification guide to README
## Problem

Users frequently ask after building:
- "Where is the claw binary?"
- "Did the build actually work?"
- "Why can't I run \`claw\` from anywhere?"

This happens because \`cargo build\` puts the binary in \`rust/target/debug/claw\`
(or \`rust/target/release/claw\`), and new users don't know:
1. Where to find it
2. How to test it
3. How to add it to PATH (optional but common follow-up)

## Fix

Added new section "Post-build: locate the binary and verify" to README covering:

1. **Binary location table:** debug vs. release, macOS/Linux vs. Windows paths
2. **Verification commands:** Test the binary with \`--help\` and \`doctor\`
3. **Three ways to add to PATH:**
   - Symlink (macOS/Linux): \`ln -s ... /usr/local/bin/claw\`
   - cargo install: \`cargo install --path . --force\`
   - Shell profile update: add rust/target/debug to \$PATH
4. **Troubleshooting:** Common errors ("command not found", "permission denied",
   debug vs. release build speed)

## Impact

New users can now:
- Find the binary immediately after build
- Run it and verify with \`claw doctor\`
- Know their options for system-wide access

Also filed ROADMAP #153 documenting the gap.

Closes ROADMAP #153.
2026-04-21 21:29:59 +09:00
YeonGyu-Kim
79352a2d20 feat: #152 — hint --output-format json when user types --json on diagnostic verbs
## Problem

Users commonly type `claw doctor --json`, `claw status --json`, or
`claw system-prompt --json` expecting JSON output. These fail with
`unrecognized argument \`--json\` for subcommand` with no hint that
`--output-format json` is the correct flag.

## Discovery

Filed as #152 during 21:17 dogfood nudge. The #127 worktree contained
a more comprehensive patch but conflicted with #141 (unified --help).
On re-investigation of main, Bugs 1 and 3 from #127 are already closed
(positional arg rejection works, no double "error:" prefix). Only
Bug 2 (the `--json` hint) remained.

## Fix

Two call sites add the hint:

1. `parse_single_word_command_alias`'s diagnostic-verb suffix path:
   when rest[1] == "--json", append "Did you mean \`--output-format json\`?"

2. `parse_system_prompt_options` unknown-option path: same hint when
   the option is exactly `--json`.

## Verification

Before:
  $ claw doctor --json
  error: unrecognized argument `--json` for subcommand `doctor`
  Run `claw --help` for usage.

After:
  $ claw doctor --json
  error: unrecognized argument `--json` for subcommand `doctor`
  Did you mean `--output-format json`?
  Run `claw --help` for usage.

Covers: `doctor --json`, `status --json`, `sandbox --json`,
`system-prompt --json`, and any other diagnostic verb that routes
through `parse_single_word_command_alias`.

Other unrecognized args (`claw doctor garbage`) correctly don't
trigger the hint.

## Tests

- 2 new assertions in `parses_multiple_diagnostic_subcommands`:
  - `claw doctor --json` produces hint
  - `claw doctor garbage` does NOT produce hint
- 177 rusty-claude-cli tests pass
- Workspace tests green

Closes ROADMAP #152.
2026-04-21 21:23:17 +09:00
YeonGyu-Kim
dddbd78dbd file: #152 — diagnostic verb suffixes allow arbitrary positional args, double error prefix
Filed from nudge directive at 21:17 KST. Implementation exists on worktree
`jobdori-127-verb-suffix` but needs rebase due to merge with #141.
Ready for Phase 1 implementation once conflicts resolved.
2026-04-21 21:19:51 +09:00
YeonGyu-Kim
7bc66e86e8 feat: #151 — canonicalize workspace path in SessionStore::from_cwd/data_dir
## Problem

`workspace_fingerprint(path)` hashes the raw path string without
canonicalization. Two equivalent paths (e.g. `/tmp/foo` vs
`/private/tmp/foo` on macOS) produce different fingerprints and
therefore different session stores. #150 fixed the test-side symptom;
this fixes the underlying product contract.

## Discovery path

#150 fix (canonicalize in test) was a workaround. Q's ack on #150
surfaced the deeper gap: the function itself is still fragile for
any caller passing a non-canonical path:

1. Embedded callers with a raw `--data-dir` path
2. Programmatic `SessionStore::from_cwd(user_path)` calls
3. NixOS store paths, Docker bind mounts, case-insensitive normalization

The REPL's default flow happens to work because `env::current_dir()`
returns canonical paths on macOS. But any caller passing a raw path
risks silent session-store divergence.

## Fix

Canonicalize inside `SessionStore::from_cwd()` and `from_data_dir()`
before computing the fingerprint. Kept `workspace_fingerprint()` itself
as a pure function for determinism — canonicalization is the entry
point's responsibility.

```rust
let canonical_cwd = fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
let sessions_root = canonical_cwd.join(".claw").join("sessions").join(workspace_fingerprint(&canonical_cwd));
```

Falls back to the raw path if canonicalize fails (directory doesn't
exist yet).

## Test-side updates

Three legacy-session tests expected the non-canonical base path to
match the store's workspace_root. Updated them to canonicalize
`base` after creation — same defensive pattern as #150, now
explicit across all three tests.

## Regression test

Added `session_store_from_cwd_canonicalizes_equivalent_paths` that
creates two stores from equivalent paths (raw vs canonical) and
asserts they resolve to the same sessions_dir.

## Verification

- `cargo test -p runtime session_store_` — 9/9 pass
- `cargo test --workspace` — all green, no FAILED markers
- No behavior change for existing users (REPL default flow already
  used canonical paths)

## Backward compatibility

Users on macOS who always went through `env::current_dir()`:
no hash change, sessions resume identically.

Users who ever called with a non-canonical path: hash would change,
but those sessions were already broken (couldn't be resumed from a
canonical-path cwd). Net improvement.

Closes ROADMAP #151.
2026-04-21 21:06:09 +09:00
YeonGyu-Kim
eaa077bf91 fix: #150 — eliminate symlink canonicalization flake in resume_latest test + file #246 (reminder outcome ambiguity)
## #150 Fix: resume_latest test flake

**Problem:** `resume_latest_restores_the_most_recent_managed_session` intermittently
fails when run in the workspace suite or multiple times in sequence, but passes in
isolation.

**Root cause:** `workspace_fingerprint(path)` hashes the path string without
canonicalization. On macOS, `/tmp` is a symlink to `/private/tmp`. The test
creates a temp dir via `std::env::temp_dir().join(...)` which returns
`/var/folders/...` (non-canonical). When the subprocess spawns,
`env::current_dir()` returns the canonical path `/private/var/folders/...`.
The two fingerprints differ, so the subprocess looks in
`.claw/sessions/<hash1>` while files are in `.claw/sessions/<hash2>`.
Session discovery fails.

**Fix:** Call `fs::canonicalize(&project_dir)` after creating the directory
to ensure test and subprocess use identical path representations.

**Verification:** 5 consecutive runs of the full test suite — all pass.
Previously: 5/5 failed when run in sequence.

## #246 Filing: Reminder cron outcome ambiguity (control-loop blocker)

The `clawcode-dogfood-cycle-reminder` cron times out repeatedly with no
structured feedback on whether the nudge was delivered, skipped, or died in-flight.

**Phase 1 outcome schema** — add explicit field to cron result:
- `delivered` — nudge posted to Discord
- `timed_out_before_send` — died before posting
- `timed_out_after_send` — posted but cleanup timed out
- `skipped_due_to_active_cycle` — previous cycle active
- `aborted_gateway_draining` — daemon shutdown

Assigned to gaebal-gajae (cron/orchestration domain). Unblocks trustworthy
dogfood cycle observability.

Closes ROADMAP #150. Filed ROADMAP #246.
2026-04-21 21:01:09 +09:00
YeonGyu-Kim
bc259ec6f9 fix: #149 — eliminate parallel-test flake in runtime::config tests
## Problem

`runtime::config::tests::validates_unknown_top_level_keys_with_line_and_field_name`
intermittently fails during `cargo test --workspace` (witnessed during
#147 and #148 workspace runs) but passes deterministically in isolation.

Example failure from workspace run:
  test result: FAILED. 464 passed; 1 failed

## Root cause

`runtime/src/config.rs::tests::temp_dir()` used nanosecond timestamp
alone for namespace isolation:

  std::env::temp_dir().join(format!("runtime-config-{nanos}"))

Under parallel test execution on fast machines with coarse clock
resolution, two tests start within the same nanosecond bucket and
collide on the same path. One test's `fs::remove_dir_all(root)` then
races another's in-flight `fs::create_dir_all()`.

Other crates already solved this pattern:
- plugins::tests::temp_dir(label) — label-parameterized
- runtime::git_context::tests::temp_dir(label) — label-parameterized

runtime/src/config.rs was missed.

## Fix

Added process id + monotonically-incrementing atomic counter to the
namespace, making every callsite provably unique regardless of clock
resolution or scheduling:

  static COUNTER: AtomicU64 = AtomicU64::new(0);
  let pid = std::process::id();
  let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
  std::env::temp_dir().join(format!("runtime-config-{pid}-{nanos}-{seq}"))

Chose counter+pid over the label-parameterized pattern to avoid
touching all 20 callsites in the same commit (mechanical noise with
no added safety — counter alone is sufficient).

## Verification

Before: one failure per workspace run (config test flake).
After: 5 consecutive `cargo test --workspace` runs — zero config
test failures. Only pre-existing `resume_latest` flake remains
(orthogonal, unrelated to this change).

  for i in 1 2 3 4 5; do cargo test --workspace; done
  # All 5 runs: config tests green. Only resume_latest flake appears.

  cargo test -p runtime
  # 465 passed; 0 failed

## ROADMAP.md

Added Pinpoint #149 documenting the gap, root cause, and fix.

Closes ROADMAP #149.
2026-04-21 20:54:12 +09:00
YeonGyu-Kim
f84c7c4ed5 feat: #148 + #128 closure — model provenance in claw status JSON/text
## Scope

Two deltas in one commit:

### #128 closure (docs)

Re-verified on main HEAD `4cb8fa0`: malformed `--model` strings already
rejected at parse time (`validate_model_syntax` in parse_args). All
historical repro cases now produce specific errors:

  claw --model ''                       → error: model string cannot be empty
  claw --model 'bad model'              → error: invalid model syntax: 'bad model' contains spaces
  claw --model 'sonet'                  → error: invalid model syntax: 'sonet'. Expected provider/model or known alias
  claw --model '@invalid'               → error: invalid model syntax: '@invalid'. Expected provider/model ...
  claw --model 'totally-not-real-xyz'   → error: invalid model syntax: ...
  claw --model sonnet                   → ok, resolves to claude-sonnet-4-6
  claw --model anthropic/claude-opus-4-6 → ok, passes through

Marked #128 CLOSED in ROADMAP with repro block. Residual provenance gap
split off as #148.

### #148 implementation

**Problem.** After #128 closure, `claw status --output-format json`
still surfaces only the resolved model string. No way for a claw to
distinguish whether `claude-sonnet-4-6` came from `--model sonnet`
(alias resolution) vs `--model claude-sonnet-4-6` (pass-through) vs
`ANTHROPIC_MODEL` env vs `.claw.json` config vs compiled-in default.

Debug forensics had to re-read argv instead of reading a structured
field. Clawhip orchestrators sending `--model` couldn't confirm the
flag was honored vs falling back to default.

**Fix.** Added two fields to status JSON envelope:
- `model_source`: "flag" | "env" | "config" | "default"
- `model_raw`: user's input before alias resolution (null on default)

Text mode appends a `Model source` line under `Model`, showing the
source and raw input (e.g. `Model source     flag (raw: sonnet)`).

**Resolution order** (mirrors resolve_repl_model but with source
attribution):
1. If `--model` / `--model=` flag supplied → source: flag, raw: flag value
2. Else if ANTHROPIC_MODEL set → source: env, raw: env value
3. Else if `.claw.json` model key set → source: config, raw: config value
4. Else → source: default, raw: null

## Changes

### rust/crates/rusty-claude-cli/src/main.rs

- Added `ModelSource` enum (Flag/Env/Config/Default) with `as_str()`.
- Added `ModelProvenance` struct (resolved, raw, source) with
  three constructors: `default_fallback()`, `from_flag(raw)`, and
  `from_env_or_config_or_default(cli_model)`.
- Added `model_flag_raw: Option<String>` field to `CliAction::Status`.
- Parse loop captures raw input in `--model` and `--model=` arms.
- Extended `parse_single_word_command_alias` to thread
  `model_flag_raw: Option<&str>` through.
- Extended `print_status_snapshot` signature to accept
  `model_flag_raw: Option<&str>`. Resolves provenance at dispatch time
  (flag provenance from arg; else probe env/config/default).
- Extended `status_json_value` signature with
  `provenance: Option<&ModelProvenance>`. On Some, adds `model_source`
  and `model_raw` fields; on None (legacy resume paths), omits them
  for backward compat.
- Extended `format_status_report` signature with optional provenance.
  On Some, renders `Model source` line after `Model`.
- Updated all existing callers (REPL /status, resume /status, tests)
  to pass None (legacy paths don't carry flag provenance).
- Added 2 regression assertions in parse_args test covering both
  `--model sonnet` and `--model=...` forms.

### ROADMAP.md

- Marked #128 CLOSED with re-verification block.
- Filed #148 documenting the provenance gap split, fix shape, and
  acceptance criteria.

## Live verification

$ claw --model sonnet --output-format json status | jq '{model,model_source,model_raw}'
{"model": "claude-sonnet-4-6", "model_source": "flag", "model_raw": "sonnet"}

$ claw --output-format json status | jq '{model,model_source,model_raw}'
{"model": "claude-opus-4-6", "model_source": "default", "model_raw": null}

$ ANTHROPIC_MODEL=haiku claw --output-format json status | jq '{model,model_source,model_raw}'
{"model": "claude-haiku-4-5-20251213", "model_source": "env", "model_raw": "haiku"}

$ echo '{"model":"claude-opus-4-7"}' > .claw.json && claw --output-format json status | jq '{model,model_source,model_raw}'
{"model": "claude-opus-4-7", "model_source": "config", "model_raw": "claude-opus-4-7"}

$ claw --model sonnet status
Status
  Model            claude-sonnet-4-6
  Model source     flag (raw: sonnet)
  Permission mode  danger-full-access
  ...

## Tests

- rusty-claude-cli bin: 177 tests pass (2 new assertions for #148)
- Full workspace green except pre-existing resume_latest flake (unrelated)

Closes ROADMAP #128, #148.
2026-04-21 20:48:46 +09:00
YeonGyu-Kim
4cb8fa059a feat: #147 — reject empty / whitespace-only prompts at CLI fallthrough
## Problem

The `"prompt"` subcommand arm enforced `if prompt.trim().is_empty()`
and returned a specific error. The fallthrough `other` arm in the same
match block — which routes any unrecognized first positional arg to
`CliAction::Prompt` — had no such guard. Result:

$ claw ""
error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN ...

$ claw "   "
error: missing Anthropic credentials; ...

$ claw "" ""
error: missing Anthropic credentials; ...

$ claw --output-format json ""
{"error":"missing Anthropic credentials; ...","type":"error"}

An empty prompt should never reach the credentials check. Worse: with
valid credentials, the literal empty string gets sent to Claude as a
user prompt, either burning tokens for nothing or triggering a model-
side refusal. Same prompt-misdelivery family as #145.

## Root cause

In `parse_subcommand()`, the final `other =>` arm in the top-level
match only guards against typos (#108 guard via `looks_like_subcommand_typo`)
and then unconditionally builds `CliAction::Prompt { prompt: rest.join(" ") }`.
An empty/whitespace-only join passes through.

## Changes

### rust/crates/rusty-claude-cli/src/main.rs

Added the same `if joined.trim().is_empty()` guard already used in the
`"prompt"` arm to the fallthrough path. Error message distinguishes it
from the `prompt` subcommand path:

  empty prompt: provide a subcommand (run `claw --help`) or a
  non-empty prompt string

Runs AFTER the typo guard (so `claw sttaus` still suggests `status`)
and BEFORE CliAction::Prompt construction (so no network call ever
happens for empty inputs).

### Regression tests

Added 4 assertions in the existing parse_args test:
- parse_args([""]) → Err("empty prompt: ...")
- parse_args(["   "]) → Err("empty prompt: ...")
- parse_args(["", ""]) → Err("empty prompt: ...")
- parse_args(["sttaus"]) → Err("unknown subcommand: ...") [verifies #108 typo guard still takes precedence]

### ROADMAP.md

Added Pinpoint #147 documenting the gap, verification, root cause,
fix shape, and acceptance. Joins the prompt-misdelivery cluster
alongside #145.

## Live verification

$ claw ""
error: empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string

$ claw "   "
error: empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string

$ claw --output-format json ""
{"error":"empty prompt: provide a subcommand ...","type":"error"}

$ claw prompt ""   # unchanged: subcommand-specific error preserved
error: prompt subcommand requires a prompt string

$ claw hello        # unchanged: typo guard still fires
error: unknown subcommand: hello.
  Did you mean     help

$ claw "real prompt here"   # unchanged: real prompts still reach API
error: api returned 401 Unauthorized (with dummy key, as expected)

All empty/whitespace-only paths exit 1. No network call. No misleading
credentials error.

## Tests

- rusty-claude-cli bin: 177 tests pass (4 new assertions)
- Full workspace green except pre-existing resume_latest flake (unrelated)

Closes ROADMAP #147.
2026-04-21 20:35:17 +09:00
YeonGyu-Kim
f877acacbf feat: #146 — wire claw config and claw diff as standalone subcommands
## Problem

`claw config` and `claw diff` are pure-local read-only introspection
commands (config merges .claw.json + .claw/settings.json from disk; diff
shells out to `git diff --cached` + `git diff`). Neither needs a session
context, yet both rejected direct CLI invocation:

$ claw config
error: `claw config` is a slash command. Use `claw --resume SESSION.jsonl /config` ...

$ claw diff
error: `claw diff` is a slash command. ...

This forced clawing operators to spin up a full session just to inspect
static disk state, and broke natural pipelines like
`claw config --output-format json | jq`.

## Root cause

Sibling of #145: `SlashCommand::Config { section }` and
`SlashCommand::Diff` had working renderers (`render_config_report`,
`render_config_json`, `render_diff_report`, `render_diff_json_for`)
exposed for resume sessions, but the top-level CLI parser in
`parse_subcommand()` had no arms for them. Zero-arg `config`/`diff`
hit `parse_single_word_command_alias`'s fallback to
`bare_slash_command_guidance`, producing the misleading guidance.

## Changes

### rust/crates/rusty-claude-cli/src/main.rs

- Added `CliAction::Config { section, output_format }` and
  `CliAction::Diff { output_format }` variants.
- Added `"config"` / `"diff"` arms to the top-level parser in
  `parse_subcommand()`. `config` accepts an optional section name
  (env|hooks|model|plugins) matching SlashCommand::Config semantics.
  `diff` takes no positional args. Both reject extra trailing args
  with a clear error.
- Added `"config" | "diff" => None` to
  `parse_single_word_command_alias` so bare invocations fall through
  to the new parser arms instead of the slash-guidance error.
- Added dispatch in run() that calls existing renderers: text mode uses
  `render_config_report` / `render_diff_report`; JSON mode uses
  `render_config_json` / `render_diff_json_for` with
  `serde_json::to_string_pretty`.
- Added 5 regression assertions in parse_args test covering:
  parse_args(["config"]), parse_args(["config", "env"]),
  parse_args(["config", "--output-format", "json"]),
  parse_args(["diff"]), parse_args(["diff", "--output-format", "json"]).

### ROADMAP.md

Added Pinpoint #146 documenting the gap, verification, root cause,
fix shape, and acceptance. Explicitly notes which other slash commands
(`hooks`, `usage`, `context`, etc.) are NOT candidates because they
are session-state-modifying.

## Live verification

$ claw config   # no config files
Config
  Working directory /private/tmp/cd-146-verify
  Loaded files      0
  Merged keys       0
Discovered files
  user    missing ...
  project missing ...
  local   missing ...
Exit 0.

$ claw config --output-format json
{
  "cwd": "...",
  "files": [...],
  ...
}

$ claw diff   # no git
Diff
  Result           no git repository
  Detail           ...
Exit 0.

$ claw diff --output-format json   # inside claw-code
{
  "kind": "diff",
  "result": "changes",
  "staged": "",
  "unstaged": "diff --git ..."
}
Exit 0.

## Tests

- rusty-claude-cli bin: 177 tests pass (5 new assertions in parse_args)
- Full workspace green except pre-existing resume_latest flake (unrelated)

## Not changed

`hooks`, `usage`, `context`, `tasks`, `theme`, `voice`, `rename`,
`copy`, `color`, `effort`, `branch`, `rewind`, `ide`, `tag`,
`output-style`, `add-dir` — all session-mutating or interactive-only;
correctly remain slash-only.

Closes ROADMAP #146.
2026-04-21 20:07:28 +09:00
YeonGyu-Kim
7d63699f9f feat: #145 — wire claw plugins subcommand to CLI parser (prompt misdelivery fix)
## Problem

`claw plugins` (and `claw plugins list`, `claw plugins --help`,
`claw plugins info <name>`, etc.) fell through the top-level subcommand
match and got routed into the prompt-execution path. Result: a purely
local introspection command triggered an Anthropic API call and surfaced
`missing Anthropic credentials` to the user. With valid credentials, it
would actually send the literal string "plugins" as a user prompt to
Claude, burning tokens for a local query.

$ claw plugins
error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API

$ ANTHROPIC_API_KEY=dummy claw plugins
⠋ 🦀 Thinking...
✘  Request failed
error: api returned 401 Unauthorized

Meanwhile siblings (`agents`, `mcp`, `skills`) all worked correctly:

$ claw agents
No agents found.
$ claw mcp
MCP
  Working directory ...
  Configured servers 0

## Root cause

`CliAction::Plugins` exists, has a working dispatcher
(`LiveCli::print_plugins`), and is produced inside the REPL via
`SlashCommand::Plugins`. But the top-level CLI parser in
`parse_subcommand()` had arms for `agents`, `mcp`, `skills`, `status`,
`doctor`, `init`, `export`, `prompt`, etc., and **no arm for
`plugins`**. The dispatch never ran from the CLI entry point.

## Changes

### rust/crates/rusty-claude-cli/src/main.rs

Added a `"plugins"` arm to the top-level match in `parse_subcommand()`
that produces `CliAction::Plugins { action, target, output_format }`,
following the same positional convention as `mcp` (`action` = first
positional, `target` = second). Rejects >2 positional args with a clear
error.

Added four regression assertions in the existing `parse_args` test:
- `plugins` alone → `CliAction::Plugins { action: None, target: None }`
- `plugins list` → action: Some("list"), target: None
- `plugins enable <name>` → action: Some("enable"), target: Some(...)
- `plugins --output-format json` → action: None, output_format: Json

### ROADMAP.md

Added Pinpoint #145 documenting the gap, verification, root cause,
fix shape, and acceptance.

## Live verification

$ claw plugins   # no credentials set
Plugins
  example-bundled      v0.1.0      disabled
  sample-hooks         v0.1.0      disabled

$ claw plugins --output-format json   # no credentials set
{
  "action": "list",
  "kind": "plugin",
  "message": "Plugins\n  example-bundled ...\n  sample-hooks ...",
  "reload_runtime": false,
  "target": null
}

Exit 0 in all modes. No network call. No "missing credentials" error.

## Tests

- rusty-claude-cli bin: 177 tests pass (new plugin assertions included)
- Full workspace green except pre-existing resume_latest flake (unrelated)

Closes ROADMAP #145.
2026-04-21 19:36:49 +09:00
YeonGyu-Kim
faeaa1d30c feat: #144 phase 1 + ROADMAP filing — claw mcp degrades gracefully on malformed config
Filing + Phase 1 fix in one commit (sibling of #143).

## Context

With #143 Phase 1 landed (`claw status` degrades), `claw mcp` was the
remaining diagnostic surface that hard-failed on a malformed `.claw.json`.
Same input, same parse error, same partial-success violation. Fresh
dogfood at 18:59 KST caught it on main HEAD `e2a43fc`.

## Changes

### ROADMAP.md
Added Pinpoint #144 documenting the gap and acceptance criteria. Joins
the partial-success / Principle #5 cluster with #143.

### rust/crates/commands/src/lib.rs
`render_mcp_report_for()` + `render_mcp_report_json_for()` now catch the
ConfigError at loader.load() instead of propagating:

- **Text mode** prepends a "Config load error" block (same shape as
  #143's status output) before the MCP listing. The listing still renders
  with empty servers so the output structure is preserved.
- **JSON mode** adds top-level `status: "ok" | "degraded"` +
  `config_load_error: string | null` fields alongside existing fields
  (`kind`, `action`, `working_directory`, `configured_servers`,
  `servers[]`). On clean runs, `status: "ok"` and
  `config_load_error: null`. On parse failure, `status: "degraded"`,
  `config_load_error: "..."`, `servers: []`, exit 0.
- Both list and show actions get the same treatment.

### Regression test
`commands::tests::mcp_degrades_gracefully_on_malformed_mcp_config_144`:
- Injects the same malformed .claw.json as #143 (one valid + one broken
  mcpServers entry).
- Asserts mcp list returns Ok (not Err).
- Asserts top-level status: "degraded" and config_load_error names the
  malformed field path.
- Asserts show action also degrades.
- Asserts clean path returns status: "ok" with config_load_error null.

## Live verification

$ claw mcp --output-format json
{
  "action": "list",
  "kind": "mcp",
  "status": "degraded",
  "config_load_error": ".../.claw.json: mcpServers.missing-command: missing string field command",
  "working_directory": "/Users/yeongyu/clawd",
  "configured_servers": 0,
  "servers": []
}
Exit 0.

## Contract alignment after this commit

All three diagnostic surfaces match now:
- `doctor` — degraded envelope with typed check entries 
- `status` — degraded envelope with config_load_error  (#143)
- `mcp` — degraded envelope with config_load_error  (this commit)

Phase 2 (typed-error object joining taxonomy §4.44) tracked separately
across all three surfaces.

Full workspace test green except pre-existing resume_latest flake (unrelated).

Closes ROADMAP #144 phase 1.
2026-04-21 19:07:17 +09:00
YeonGyu-Kim
e2a43fcd49 feat: #143 phase 1 — claw status degrades gracefully on malformed config
Previously `claw status` hard-failed on any config parse error, emitting
a bare error string and exiting 1. This took down the entire health
surface for a single malformed MCP entry, even though workspace, git,
model, permission, and sandbox state could all be reported independently.

`claw doctor` already degraded gracefully on the exact same input.
This commit matches `claw status` to that contract.

Changes:
- Add `StatusContext::config_load_error: Option<String>` to capture parse
  errors without aborting.
- Rewrite `status_context()` to match on `ConfigLoader::load()`: on Err,
  fall back to default `SandboxConfig` for sandbox resolution and record
  the parse error, then continue populating workspace/git/memory fields.
- JSON output gains top-level `status: "ok" | "degraded"` marker and a
  `config_load_error` string (null on clean runs). All other existing
  fields preserved for backward compat.
- Text output prepends a "Config load error" block with Details + Hint
  when config failed to parse, then a "Status (degraded)" header on the
  main block. Clean runs show the usual "Status" header.
- Doctor path updated to pass the config load error through StatusContext.

Regression test `status_degrades_gracefully_on_malformed_mcp_config_143`:
- Injects a .claw.json with one valid + one malformed mcpServers entry
- Asserts status_context() returns Ok (not Err)
- Asserts config_load_error names the malformed field path
- Asserts workspace/sandbox fields still populated in JSON
- Asserts top-level status is 'degraded'
- Asserts clean config path still returns status: 'ok'

Verified live on /Users/yeongyu/clawd (contains deliberately broken MCP entries):
  $ claw status --output-format json
  { "status": "degraded",
    "config_load_error": ".../mcpServers.missing-command: missing string field command",
    "model": "claude-opus-4-6",
    "workspace": {...},
    "sandbox": {...},
    ... }

Phase 2 (typed error object joining #4.44 taxonomy) tracked separately.

Full workspace test green except pre-existing resume_latest flake (unrelated).

Closes ROADMAP #143 phase 1.
2026-04-21 18:37:42 +09:00
YeonGyu-Kim
fcd5b49428 ROADMAP #143: claw status hard-fails on malformed MCP config while doctor degrades gracefully 2026-04-21 18:32:09 +09:00
YeonGyu-Kim
e73b6a2364 docs: USAGE.md sections for claw init (#142) and claw state (#139)
Add two missing sections documenting the recently-fixed commands:

- **Initialize a repository**: Shows both text and JSON output modes for
  `claw init`. Explains that structured JSON fields (created[], updated[],
  skipped[], artifacts[]) allow claws to detect per-artifact state without
  substring-matching prose. Documents idempotency.

- **Inspect worker state**: Documents `claw state` and the prerequisite
  that a worker must have executed at least once. Includes the helpful error
  message and remediation hints (claw or claw prompt <text>) so users
  discovering the command for the first time see actionable guidance.

These sections complement the product fixes in #142 (init JSON structure)
and #139 (state error actionability) by documenting the contract from a
user perspective.

Related: ROADMAP #142 (structured init output), #139 (worker-state discoverability).
2026-04-21 18:28:21 +09:00
YeonGyu-Kim
541c5bb95d feat: #139 actionable worker-state guidance in claw state error + help
Previously `claw state` errored with "no worker state file found ... — run a
worker first" but there is no `claw worker` subcommand, so claws had no
discoverable path from the error to a fix.

Changes:
- Rewrite the missing-state error to name the two concrete commands that
  produce .claw/worker-state.json:
    * `claw` (interactive REPL, writes state on first turn)
    * `claw prompt <text>` (one non-interactive turn)
  Also tell the user what to rerun: `claw state [--output-format json]`.
- Expand the State --help topic with "Produces state", "Observes state",
  and "Exit codes" lines so the worker-state contract is discoverable
  before the user hits the error.
- Add regression test state_error_surfaces_actionable_worker_commands_139
  asserting the error contains `claw prompt`, REPL mention, and the
  rerun path, plus that the help topic documents the producer contract.

Verified live:
  $ claw state
  error: no worker state file found at .claw/worker-state.json
    Hint: worker state is written by the interactive REPL or a non-interactive prompt.
    Run:   claw               # start the REPL (writes state on first turn)
    Or:    claw prompt <text> # run one non-interactive turn
    Then rerun: claw state [--output-format json]

JSON mode preserves the full hint inside the error envelope so CI/claws
can match on `claw prompt` without losing the canonical prefix.

Full workspace test green except pre-existing resume_latest flake (unrelated).

Closes ROADMAP #139.
2026-04-21 18:04:04 +09:00
YeonGyu-Kim
611eed1537 feat: #142 structured fields in claw init --output-format json
Previously `claw init --output-format json` emitted a valid JSON envelope but
packed the entire human-formatted output into a single `message` string. Claw
scripts had to substring-match human language to tell `created` from `skipped`.

Changes:
- Add InitStatus::json_tag() returning machine-stable "created"|"updated"|"skipped"
  (unlike label() which includes the human " (already exists)" suffix).
- Add InitReport::NEXT_STEP constant so claws can read the next-step hint
  without grepping the message string.
- Add InitReport::artifacts_with_status() to partition artifacts by state.
- Add InitReport::artifact_json_entries() for the structured artifacts[] array.
- Rewrite run_init + init_json_value to emit first-class fields alongside the
  legacy message string (kept for text consumers): project_path, created[],
  updated[], skipped[], artifacts[], next_step, message.
- Update the slash-command Init dispatch to use the same structured JSON.
- Add regression test artifacts_with_status_partitions_fresh_and_idempotent_runs
  asserting both fresh + idempotent runs produce the right partitioning and
  that the machine-stable tag is bare 'skipped' not label()'s phrasing.

Verified output:
- Fresh dir: created[] has 4 entries, skipped[] empty
- Idempotent call: created[] empty, skipped[] has 4 entries
- project_path, next_step as first-class keys
- message preserved verbatim for backward compat

Full workspace test green except pre-existing resume_latest flake (unrelated).

Closes ROADMAP #142.
2026-04-21 17:42:00 +09:00
YeonGyu-Kim
7763ca3260 feat: #141 unify claw <subcommand> --help contract across all 14 subcommands
Previously, `claw <subcommand> --help` had 5 different behaviors:
- 7 subcommands returned subcommand-specific help (correct)
- init/export/state/version silently fell back to global `claw --help`
- system-prompt/dump-manifests errored with `unknown <cmd> option: --help`
- bootstrap-plan printed its phase list instead of help text

Changes:
- Extend LocalHelpTopic enum with Init, State, Export, Version, SystemPrompt,
  DumpManifests, BootstrapPlan variants.
- Extend parse_local_help_action() to resolve those 7 subcommands to their
  local help topic instead of falling through to the main dispatch.
- Remove init/state/export/version from the explicit wants_help=true matcher
  so they reach parse_local_help_action() before being routed to global help.
- Add render_help_topic() entries for the 7 new topics with consistent
  Usage/Purpose/Output/Formats/Related structure.
- Add regression test subcommand_help_flag_has_one_contract_across_all_subcommands_141
  asserting every documented subcommand + both --help and -h variants resolve
  to a HelpTopic with non-empty text that contains a Usage line.

Verification:
- All 14 subcommands now return subcommand-specific help (live dogfood).
- Full workspace test green except pre-existing resume_latest flake.

Closes ROADMAP #141.
2026-04-21 17:36:48 +09:00
YeonGyu-Kim
2665ada94e ROADMAP #142: claw init --output-format json emits unstructured message string instead of created/skipped fields 2026-04-21 17:31:11 +09:00
YeonGyu-Kim
21b377d9c0 ROADMAP #141: claw <subcommand> --help has 5 different behaviors — inconsistent help surface 2026-04-21 17:01:46 +09:00
YeonGyu-Kim
27ffd75f03 fix: #140 isolate test cwd + env in punctuation_bearing_single_token test
Previously this test inherited the cargo test runner's CWD, which could contain
a stale .claw/settings.json with "permissionMode": "acceptEdits" written by
another test. The deprecated-field resolver then silently downgraded the
default permission mode to WorkspaceWrite, breaking the test's assertion.

Fix: wrap the assertion in with_current_dir() + env_lock() so the test runs in
an isolated temp directory with no stale config.

Full workspace test now passes except for pre-existing resume_latest flake
(unrelated to #140, environment-dependent, tracked separately).

Closes ROADMAP #140.
2026-04-21 16:34:58 +09:00
YeonGyu-Kim
0cf8241978 ROADMAP #140: deprecated permissionMode migration silently downgrades DangerFullAccess to WorkspaceWrite — 1 test failure on main HEAD 36b3a09 2026-04-21 16:23:00 +09:00
YeonGyu-Kim
36b3a09818 ROADMAP #139: claw state error references undocumented 'worker' concept (unactionable for claws) 2026-04-21 16:01:54 +09:00
9 changed files with 2342 additions and 83 deletions

View File

@@ -98,10 +98,87 @@ export ANTHROPIC_API_KEY="sk-ant-..."
**Git Bash / WSL** are optional alternatives, not requirements. If you prefer bash-style paths (`/c/Users/you/...` instead of `C:\Users\you\...`), Git Bash (ships with Git for Windows) works well. In Git Bash, the `MINGW64` prompt is expected and normal — not a broken install.
## Post-build: locate the binary and verify
After running `cargo build --workspace`, the `claw` binary is built but **not** automatically installed to your system. Here's where to find it and how to verify the build succeeded.
### Binary location
After `cargo build --workspace` in `claw-code/rust/`:
**Debug build (default, faster compile):**
- **macOS/Linux:** `rust/target/debug/claw`
- **Windows:** `rust/target/debug/claw.exe`
**Release build (optimized, slower compile):**
- **macOS/Linux:** `rust/target/release/claw`
- **Windows:** `rust/target/release/claw.exe`
If you ran `cargo build` without `--release`, the binary is in the `debug/` folder.
### Verify the build succeeded
Test the binary directly using its path:
```bash
# macOS/Linux (debug build)
./rust/target/debug/claw --help
./rust/target/debug/claw doctor
# Windows PowerShell (debug build)
.\rust\target\debug\claw.exe --help
.\rust\target\debug\claw.exe doctor
```
If these commands succeed, the build is working. `claw doctor` is your first health check — it validates your API key, model access, and tool configuration.
### Optional: Add to PATH
If you want to run `claw` from any directory without the full path, choose one of these approaches:
**Option 1: Symlink (macOS/Linux)**
```bash
ln -s $(pwd)/rust/target/debug/claw /usr/local/bin/claw
```
Then reload your shell and test:
```bash
claw --help
```
**Option 2: Use `cargo install` (all platforms)**
Build and install to Cargo's default location (`~/.cargo/bin/`, which is usually on PATH):
```bash
# From the claw-code/rust/ directory
cargo install --path . --force
# Then from anywhere
claw --help
```
**Option 3: Update shell profile (bash/zsh)**
Add this line to `~/.bashrc` or `~/.zshrc`:
```bash
export PATH="$(pwd)/rust/target/debug:$PATH"
```
Reload your shell:
```bash
source ~/.bashrc # or source ~/.zshrc
claw --help
```
### Troubleshooting
- **"command not found: claw"** — The binary is in `rust/target/debug/claw`, but it's not on your PATH. Use the full path `./rust/target/debug/claw` or symlink/install as above.
- **"permission denied"** — On macOS/Linux, you may need `chmod +x rust/target/debug/claw` if the executable bit isn't set (rare).
- **Debug vs. release** — If the build is slow, you're in debug mode (default). Add `--release` to `cargo build` for faster runtime, but the build itself will take 510 minutes.
> [!NOTE]
> **Auth:** claw requires an **API key** (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) — Claude subscription login is not a supported auth path.
Run the workspace test suite:
Run the workspace test suite after verifying the binary works:
```bash
cd rust

View File

@@ -830,6 +830,30 @@ Acceptance:
- channel status updates stay short and machine-grounded
- claws stop inferring state from raw build spam
### 140. Deprecated `permissionMode` migration silently downgrades `DangerFullAccess` to `WorkspaceWrite`
**Filed:** 2026-04-21 from dogfood cycle — `cargo test --workspace` on `main` HEAD `36b3a09` shows 1 deterministic failure.
**Problem:** `tests::punctuation_bearing_single_token_still_dispatches_to_prompt` fails with:
```
assert left == right failed
left: ... permission_mode: WorkspaceWrite ...
right: ... permission_mode: DangerFullAccess ...
warning: .claw/settings.json: field "permissionMode" is deprecated (line 1). Use "permissions.defaultMode" instead
```
The test fixture writes a `.claw/settings.json` with the deprecated `permissionMode: "dangerFullAccess"` key. The migration/deprecation shim reads it but resolves to `WorkspaceWrite` instead of `DangerFullAccess`. Result: `cargo test --workspace` is red on `main` with 172 passing, 1 failing.
**Root cause hypothesis:** The deprecated field reader in `parse_args` or `ConfigLoader` applies the `permissionMode` value through a permission-mode resolver that does not map `"dangerFullAccess"` to `PermissionMode::DangerFullAccess`, likely defaulting or falling back to `WorkspaceWrite`.
**Fix shape:**
- Ensure the deprecated-key migration path correctly maps `permissionMode: "dangerFullAccess"``PermissionMode::DangerFullAccess` (same as `permissions.defaultMode: "dangerFullAccess"`).
- Alternatively, update the test fixture to use the canonical `permissions.defaultMode` key so it exercises the migration shim rather than depending on it.
- Verify `cargo test --workspace` returns 0 failures.
**Acceptance:**
- `cargo test --workspace` passes with 0 failures on `main`.
- Deprecated `permissionMode: "dangerFullAccess"` migrates cleanly to `DangerFullAccess` without downgrading to `WorkspaceWrite`.
### 137. Model-alias shorthand regression in test suite — bare alias parsing broken on `feat/134-135-session-identity` branch
**Filed:** 2026-04-21 from dogfood cycle — `cargo test --workspace` on `feat/134-135-session-identity` HEAD (`91ba54d`) shows 3 failing tests.
@@ -4791,7 +4815,19 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label],
**Source.** Jobdori dogfood 2026-04-20 against `/tmp/claw-dogfood` (env-cleaned, no git, no config) on main HEAD `7370546` in response to Clawhip pinpoint nudge at `1495620050424434758`. Joins **Silent-flag / documented-but-unenforced** (#96#101, #104, #108, #111, #115, #116, #117, #118, #119, #121, #122, #123, #124, #126) as 18th — `--json` silently swallowed into Prompt dispatch instead of being recognized or rejected. Joins **Parser-level trust gap quintet** (#108, #117, #119, #122, **#127**) as 5th — same `_other => Prompt` fall-through arm, fifth distinct entry case (#108 = typoed verb, #117 = `-p` greedy, #119 = bare slash + arg, #122 = `--base-commit` greedy, **#127 = valid verb + unrecognized suffix arg**). Joins **Cred-error misdirection / failure-classification gaps** as a sibling of #99 (system-prompt unvalidated) — same family of "local diagnostic verb pretends to need API creds." Joins **Truth-audit / diagnostic-integrity** (#80#87, #89, #100, #102, #103, #105, #107, #109, #110, #112, #114, #115, #125) — `claw --help` lies about per-verb accepted flags. Joins **Parallel-entry-point asymmetry** (#91, #101, #104, #105, #108, #114, #117, #122, #123, #124) as 11th — three working forms and one broken form for the same logical intent (`--json` doctor output). Joins **Claude Code migration parity** (#103, #109, #116) as 4th — Claude Code's `--json` convention shorthand is unrecognized in claw-code's verb-suffix position; users migrating get cred errors instead. Cross-cluster with **README/USAGE doc-vs-implementation gap** — README explicitly recommends `claw doctor` as the first health check; the natural JSON form of that exact command is broken. Natural bundle: **#108 + #117 + #119 + #122 + #127** — parser-level trust gap quintet: complete `_other => Prompt` fall-through audit (typoed verb + greedy `-p` + bare slash-verb + greedy `--base-commit` + valid verb + unrecognized suffix). Also **#99 + #127** — local-diagnostic cred-error misdirection pair: `system-prompt` and verb-suffix `--json` both pretend to need creds for pure-local operations. Also **#126 + #127** — diagnostic-verb surface integrity pair: `/config` section args ignored (#126) + verb-suffix args silently mis-dispatched (#127). Session tally: ROADMAP #127.
128. **`claw --model <malformed>` (spaces, empty string, special chars, invalid provider/model syntax) silently falls through to API-layer cred error instead of rejecting at parse time** — dogfooded 2026-04-20 on main HEAD `d284ef7` from a fresh environment (no config, no auth). The `--model` flag accepts any string without syntactic validation: spaces (`claw --model "bad model"`), empty strings (`claw --model ""`), special characters (`claw --model "@invalid"`), non-existent provider/model combinations all parse successfully. The malformed model string then flows into the runtime's provider-detection layer, which silently accepts it as Anthropic fallback or passes it to an API layer that fails with `missing Anthropic credentials` (misdirection) rather than a clear "invalid model syntax" error at parse time. With API credentials configured, a malformed model string gets sent to the API, billing tokens against a request that should have failed client-side.
128. **[CLOSED 2026-04-21]** **`claw --model <malformed>` (spaces, empty string, special chars, invalid provider/model syntax) silently falls through to API-layer cred error instead of rejecting at parse time** — dogfooded 2026-04-20 on main HEAD `d284ef7` from a fresh environment (no config, no auth). The `--model` flag accepts any string without syntactic validation: spaces (`claw --model "bad model"`), empty strings (`claw --model ""`), special characters (`claw --model "@invalid"`), non-existent provider/model combinations all parse successfully. The malformed model string then flows into the runtime's provider-detection layer, which silently accepts it as Anthropic fallback or passes it to an API layer that fails with `missing Anthropic credentials` (misdirection) rather than a clear "invalid model syntax" error at parse time. With API credentials configured, a malformed model string gets sent to the API, billing tokens against a request that should have failed client-side.
**Closure (2026-04-21):** Re-verified on main HEAD `4cb8fa0`. All cases now rejected at parse time:
```
$ claw --model '' status → error: model string cannot be empty
$ claw --model 'bad model' status → error: invalid model syntax: 'bad model' contains spaces
$ claw --model 'sonet' status → error: invalid model syntax: 'sonet'. Expected provider/model ...
$ claw --model '@invalid' status → error: invalid model syntax: '@invalid'. Expected provider/model ...
$ claw --model 'totally-not-real-xyz' status → error: invalid model syntax ...
$ claw --model sonnet status → ok, resolves to claude-sonnet-4-6
$ claw --model anthropic/claude-opus-4-6 status → ok, passes through
```
Validation happens in `validate_model_syntax()` before `resolve_model_alias_with_config()`. All `--model` and `--model=` parse paths call it. No API call ever reached with malformed input. Residual gap (model provenance in status JSON — raw input vs resolved value) was split off as #148 (see below).
129. **MCP server startup blocks credential validation — `claw <prompt>` with any `.claw.json` `mcpServers` entry awaits the MCP server's stdio handshake BEFORE checking whether the operator has Anthropic credentials. With no `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY` set and `mcpServers.everything = { command: "npx", args: ["-y", "@modelcontextprotocol/server-everything"] }` configured, the CLI hangs forever (verified via `timeout 30s` — still in MCP startup at 30s with three repeated `"Starting default (STDIO) server..."` lines), instead of fail-fasting with the same `missing Anthropic credentials` error that fires in milliseconds when no MCP is configured. A misconfigured-but-running MCP server (one that spawns successfully but never completes its `initialize` handshake) wedges every `claw <prompt>` invocation permanently. A misconfigured MCP server with a slow-but-eventually-succeeding init (npx download, container pull, network roundtrip) burns startup latency on every Prompt invocation regardless of whether the LLM call would even succeed. This is the runtime-side companion to #102's config-time MCP diagnostic gap: #102 says doctor doesn't surface MCP reachability; #129 says the Prompt path's reachability check is implicit, blocking, retried, and runs *before* the cheaper auth precondition that should run first** — dogfooded 2026-04-20 on main HEAD `d284ef7` from `/tmp/claw-mcp-test` with `env -i PATH=$PATH HOME=$HOME` (all auth env vars unset).
@@ -5171,3 +5207,810 @@ Nudge surface should report explicit state + timestamp: `"feat/134-135 state=pus
- Means closure-state tracking must live inside the repo (ROADMAP) or in an external surface (Discord message edits, `.dogfood-closure.json`)
**Filed:** 2026-04-21 15:05 KST as evidence for #138 by Jobdori dogfood loop.
## Pinpoint #139. `claw state` error message refers to "worker" concept that is not discoverable via `--help` or any documented command — error is unactionable for claws and CI
**Gap.** `claw state` (both text and JSON output modes) returns this error when no worker-state.json exists:
```
error: no worker state file found at /private/tmp/cd-16/.claw/worker-state.json — run a worker first
```
**The problem:** "worker" is a concept that has **zero discoverability path** from the CLI surface:
1. `claw --help` has no mention of workers, `claw worker`, or worker state
2. There is no `claw worker` subcommand (not listed in help, not in the 16 known subcommands)
3. No hint in the error itself about what command triggers worker state creation
4. A claw, CI pipeline, or first-time user hitting this error has no actionable next step
**Verified on main HEAD `f3f6643` (2026-04-21 15:58 KST):**
```
$ claw state --output-format json
{"error":"no worker state file found at /private/tmp/cd-16/.claw/worker-state.json — run a worker first","type":"error"}
```
**Trace path.**
- `rust/crates/rusty-claude-cli/src/main.rs` — `handle_state()` or equivalent returns this error when `.claw/worker-state.json` is missing.
- No internal documentation on what produces `worker-state.json` (likely background worker session, but not surfaced)
- `claw bootstrap-plan` mentions phases like `DaemonWorkerFastPath` and `BackgroundSessionFastPath` — suggesting workers are part of daemon/background execution — but this is internal architecture jargon, not user-facing
**Why this is a clawability gap.**
1. **Error references concept that is not discoverable.** Product Principle violation: "Errors must be actionable." Current error is descriptive but unactionable.
2. **Claws can't self-heal.** A claw orchestrator that gets this error cannot construct a follow-up command because the remediation is not in the error or in `--help`.
3. **Dogfood blocker.** Automated test setups that include `claw state` as a health check will fail silently for users who haven't triggered the worker path.
4. **Internal architecture leaks into user surface.** The `worker` / `daemon` / `background session` distinction is internal runtime nomenclature, not user-facing workflow.
**Fix shape (~20-40 lines).**
1. **Error message should include remediation.** Change error to:
```json
{
"error": "no worker state file found at <path> — run `claw` (interactive REPL) or `claw prompt <text>` to produce worker state",
"type": "error",
"hint": "Worker state is created when claw executes a prompt (REPL or one-shot). If you have run claw but still see this, check that your session wrote to .claw/worker-state.json.",
"next_action": "claw prompt \"hello\""
}
```
2. **Add `claw --help` reference.** Document under `Flags` or `Subcommand overview` that `claw state` requires prior execution.
3. **Consistency with typed-error envelope** (ROADMAP §4.44): include `operation: "state-read"`, `target: "<path>"`, `retryable: false` fields for machine consumers.
**Acceptance.**
- `claw state` error text explicitly names the command(s) that produce worker state
- `--help` has at least one line documenting the state/worker relationship
- A claw reading the JSON error gets a structured `next_action` field
**Blocker.** None. Pure error-text + doc fix. ~30 lines.
**Source.** Jobdori dogfood 2026-04-21 16:00 KST on main HEAD `f3f6643`. Joins **error-message-quality** cluster (related to §4.44 typed error taxonomy and §5 failure class enumeration). Joins **CLI discoverability** cluster (#108 did-you-mean for typos, #127 --json on diagnostic verbs). Session tally: ROADMAP #139.
## Pinpoint #141. `claw <subcommand> --help` has 5 different behaviors — inconsistent help surface breaks discoverability
**Gap.** Running `<subcommand> --help` has five different behaviors depending on which subcommand you pick. This breaks the expected CLI contract that `<subcommand> --help` returns subcommand-specific help.
**Matrix (verified on main HEAD `27ffd75` 2026-04-21 16:59 KST):**
| Subcommand | Behavior | Status |
|---|---|---|
| `status`, `sandbox`, `doctor`, `skills`, `agents`, `mcp`, `acp` | Subcommand-specific help | ✅ correct |
| `version` | Global `claw --help` | ⚠️ inconsistent |
| `init`, `export`, `state` | Global `claw --help` | ⚠️ inconsistent |
| `dump-manifests`, `system-prompt` | `error: unknown <cmd> option: --help` | ❌ broken |
| `bootstrap-plan` | Prints phases JSON (not help at all) | ❌ broken |
**Concrete repro:**
```
$ claw system-prompt --help
error: unknown system-prompt option: --help
$ claw dump-manifests --help
error: unknown dump-manifests option: --help
$ claw bootstrap-plan --help
- CliEntry
- FastPathVersion
...
$ claw init --help
claw v0.1.0
Usage:
claw [--model MODEL] ... # this is global help, not init-specific
```
**Why this is a clawability gap.**
1. **Product principle violation**: every CLI subcommand should have a consistent `<cmd> --help` contract that returns subcommand-specific help.
2. **CI/orchestration hazard**: a claw script that tries `<cmd> --help | grep <option>` gets structural behavior differences — some return 0, some return 1 with "unknown option", some return global help that doesn't mention the subcommand at all.
3. **Discoverability asymmetry**: 7 subcommands have good help, 4 have global-help fallback, 2 error out, 1 produces irrelevant output. No documented reason for the split.
4. **Follow-on from #108**: #108 fixed subcommand typos at the dispatch layer. #141 is the next layer up — even valid subcommands have inconsistent `--help` dispatch.
**Fix shape (~50 lines).**
1. For subcommands that return a structured help block (`status`, `sandbox`, `doctor`, `skills`, `agents`, `mcp`, `acp`): this is the model. Use the same pattern.
2. For `init`, `export`, `state`, `version`: add subcommand-specific help block or explicitly dispatch `--help` to `claw --help` (consistent fallback is OK; returning global help that doesn't mention the subcommand is not).
3. For `dump-manifests`, `system-prompt`: fix the parser to recognize `--help` as a dispatch rather than unknown flag. Add subcommand-specific help.
4. For `bootstrap-plan`: add `--help` dispatch to explain what the subcommand does (currently prints phases, which is the primary output but not help text).
5. Add a consistency test: `for cmd in <list>: assert exitcode_of("claw $cmd --help") == 0 and contains help text`.
**Acceptance.**
- All 14 subcommands have `<cmd> --help` exit 0 with relevant help text
- No "unknown option" errors from `<cmd> --help`
- Consistency test in the regression suite
**Blocker.** None. Scoped to CLI parser + help text. ~50 lines + test.
**Source.** Jobdori dogfood 2026-04-21 16:59 KST on main HEAD `27ffd75`. Joins **CLI/REPL parity** cluster (§7.1) and **discoverability** cluster (#108 did-you-mean, #127 --json on diagnostic verbs, #139 worker concept unactionable). Session tally: ROADMAP #141.
## Pinpoint #142. `claw init --output-format json` dumps human text into `message` — no structured fields for created/skipped files
**Gap.** `claw init --output-format json` emits a valid JSON envelope, but the payload is entirely a human-formatted multi-line text block packed into `message`. There are no structured fields to tell a claw script which files were created, which were skipped, or what the project path was.
**Verified on main HEAD `21b377d` 2026-04-21 17:34 KST.**
**Actual output (fresh directory, everything created):**
```json
{
"kind": "init",
"message": "Init\n Project /private/tmp/cd-1730b\n .claw/ created\n .claw.json created\n .gitignore created\n CLAUDE.md created\n Next step Review and tailor the generated guidance"
}
```
**Idempotent second call (everything skipped):**
```json
{
"kind": "init",
"message": "Init\n Project /private/tmp/cd-1730b\n .claw/ skipped (already exists)\n .claw.json skipped (already exists)\n .gitignore skipped (already exists)\n CLAUDE.md skipped (already exists)\n Next step Review and tailor the generated guidance"
}
```
**Compare `claw status --output-format json` (the model):**
```json
{
"kind": "status",
"model": "claude-opus-4-6",
"permission_mode": "danger-full-access",
"sandbox": { "active": false, "enabled": true, "fallback_reason": "...", ... },
"usage": { "cumulative_input": 0, "messages": 0, "turns": 0, ... },
"workspace": { "changed_files": 0, ... }
}
```
**Why this is a clawability gap.**
1. **Substring matching required**: to tell whether `.claw/` was created vs skipped, a claw has to grep the `message` string for `"created"` or `"skipped (already exists)"`. Not a contract — human-language fragility.
2. **No programmatic idempotency signal**: CI/orchestration cannot easily tell "first run produced new files" from "second run was no-op". Both paths end up with `kind: init` and a free-form message.
3. **Inconsistent with `status`/`sandbox`/`doctor`**: those subcommands have first-class structured JSON. `init` does not. Product contract asymmetry.
4. **Path isn't a field**: the project path is embedded in the same string. No `project_path` key.
5. **Joins JSON-output cluster** (#90, #91, #92, #127, #130, #136): every one of those was a JSON contract shortfall where the command technically emitted JSON but did not emit *useful* JSON.
**Fix shape (~40 lines).**
Add structured fields alongside `message` (keep `message` for backward compat):
```json
{
"kind": "init",
"project_path": "/private/tmp/cd-1730b",
"created": [".claw", ".claw.json", ".gitignore", "CLAUDE.md"],
"skipped": [],
"next_step": "Review and tailor the generated guidance",
"message": "Init\n Project..."
}
```
On idempotent call: `created: []`, `skipped: [".claw", ".claw.json", ...]`.
**Acceptance.**
- `claw init --output-format json` has `created`, `skipped`, `project_path`, `next_step` top-level fields
- `created.len() + skipped.len() == 4` on standard init
- Idempotent call has empty `created`
- Existing `message` field preserved for text consumers (deprecation path only if needed)
- Regression test: JSON schema assertions for both fresh + idempotent cases
**Blocker.** None. Scoped to `init` subcommand JSON serializer. ~40 lines.
**Source.** Jobdori dogfood 2026-04-21 17:34 KST on main HEAD `21b377d`. Joins **JSON output completeness** cluster (#90/#91/#92/#127/#130/#136). Session tally: ROADMAP #142.
## Pinpoint #143. `claw status` hard-fails on malformed MCP config; `claw doctor` degrades gracefully — inconsistent contract around partial config breakage
**Gap.** Running `claw status` against a workspace with a malformed `.claw.json` (e.g., one `mcpServers.*` entry missing the required `command` field) crashes out at parse time with a terse error, even when the rest of the config is valid and most status fields could still be reported. `claw doctor` handles the exact same file correctly, embedding the parse error inside the typed envelope as `status: "fail"` on the `config` check while still reporting `auth`, `install source`, `workspace`, etc.
This is both an inconsistency (two diagnostic surfaces behave differently on identical input) and a violation of Product Principle #5 (*Partial success is first-class*).
**Verified on main HEAD `e73b6a2` (2026-04-21 18:30 KST):**
Given a `.claw.json` with one valid server and one malformed entry:
```json
{
"mcpServers": {
"everything": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"] },
"missing-command": { "args": ["arg-only-no-command"] }
}
}
```
`claw status` (both text and JSON modes):
```
$ claw status
error: /Users/.../.claw.json: mcpServers.missing-command: missing string field command
Run `claw --help` for usage.
$ claw status --output-format json
{"error":"/Users/.../.claw.json: mcpServers.missing-command: missing string field command","type":"error"}
```
`claw doctor --output-format json` on the *same* file:
```json
{
"checks": [
{"name":"auth", "status":"warn", ...},
{
"name":"config",
"status":"fail",
"load_error":"/Users/.../.claw.json: mcpServers.missing-command: missing string field command",
"discovered_files":["..."],
"discovered_files_count":5,
"summary":"runtime config failed to load: ..."
},
{"name":"install_source", "status":"ok", ...},
...
]
}
```
Doctor keeps going and produces a full typed report. Status refuses to produce any fields at all.
**Why this is a clawability gap.**
1. **Two surfaces, one config, two behaviors.** A claw cannot rely on a stable contract: `doctor` treats malformed MCP as a classifiable condition; `status` treats it as a fatal parse error. Same input, opposite response.
2. **Partial-success violation (Principle #5).** The malformed field is scoped to one MCP server entry. Workspace state, current model, permission mode, session info, and git state are all independently resolvable and would be useful to report even when one MCP server entry is unparseable. A claw debugging a misconfig needs to see which fields *do* work.
3. **No per-field error surface.** Even the bare error string lacks structure (`mcpServers.missing-command: missing string field command` is a parse trace, not a typed error object). No `error_kind`, no `retryable`, no `affected_field`, no `hint`. Claws can't route on this.
4. **Clawhip health checks.** Clawhip uses `claw status --output-format json` as a liveness probe on managed lanes. A single broken MCP entry takes down the probe entirely, not just the MCP subsystem, making "is the workspace usable?" impossible to answer without also running `doctor`.
5. **Onboarding friction.** A user who copy-pastes an MCP config and mistypes one field discovers this only when `status` stops working. Doctor tells them what's wrong; status does not. First-run users are more likely to reach for `status`.
**Fix shape (~60-100 lines, two-phase).**
**Phase 1 (immediate, small):** Make `claw status` degrade gracefully like `doctor` does. When config load fails:
- Report `config_load_error` as a first-class field with the parse-error string.
- Still report what can be resolved without config: effective model (from env + CLI args), permission mode, sandbox posture, git state, workspace metadata.
- Set top-level `status: "degraded"` in the envelope so claws can distinguish "status ran but config is broken" from "status ran cleanly".
- Keep the existing error text as a `config_load_error` string for humans, but do not abort.
**Phase 2 (medium, joins typed-error taxonomy #4.44):** Typed error object for config-parse failures:
```json
"config_load_error": {
"kind": "config_parse",
"retryable": false,
"file": "/Users/.../.claw.json",
"field_path": "mcpServers.missing-command",
"message": "missing string field command",
"hint": "each mcpServers entry requires a `command` string; see USAGE.md#mcp"
}
```
**Acceptance.**
- `claw status` on a workspace with one malformed MCP entry returns exit code 0 with a top-level `status: "degraded"` (or equivalent typed marker) and populated workspace/git/model/permission fields.
- The malformed MCP error surfaces as a structured `config_load_error` field, not as a bare string at the envelope root.
- `claw status --output-format json` contract matches `claw doctor --output-format json` on the same input: both must report the config parse error, neither may hard-fail.
- Regression test: inject malformed MCP config, assert `status` returns 0 with degraded marker and `config_load_error.field_path == "mcpServers.missing-command"`.
**Blocker.** None for Phase 1. Phase 2 depends on the typed-error taxonomy landing (ROADMAP §4.44), but Phase 1 can ship independently and be tightened later.
**Source.** Jobdori dogfood 2026-04-21 18:30 KST on main HEAD `e73b6a2`, surfaced by running `claw status` in `/Users/yeongyu/clawd` which contains a `.claw.json` with deliberately broken MCP entries. Joins **partial-success / degraded-mode** cluster (Principle #5, Phase 6) and **surface consistency** cluster (#141 help-contract unification, #108 typo guard). Session tally: ROADMAP #143.
## Pinpoint #144. `claw mcp` hard-fails on malformed MCP config — same surface inconsistency as #143, one command over
**Gap.** With `claw status` fixed in #143 Phase 1, `claw mcp` is now the remaining diagnostic surface that hard-fails on a malformed `.claw.json`. Same input, same parse error, same partial-success violation.
**Verified on main HEAD `e2a43fc` (2026-04-21 18:59 KST):**
Same `.claw.json` used for #143 repro (one valid `everything` server + one malformed `missing-command` entry).
`claw mcp`:
```
error: /Users/.../.claw.json: mcpServers.missing-command: missing string field command
Run `claw --help` for usage.
```
Exit 1. No list. The well-formed `everything` server is invisible.
`claw mcp --output-format json`:
```json
{"error":"/Users/.../.claw.json: mcpServers.missing-command: missing string field command","type":"error"}
```
Exit 1. Same story.
`claw status --output-format json` on the same file (post-#143):
```json
{"kind":"status","status":"degraded","config_load_error":"...","workspace":{...},"sandbox":{...},...}
```
Exit 0. Full envelope with error surfaced.
**Why this is a clawability gap (same family as #143).**
1. **Principle #5 violation**: partial success is first-class. One malformed entry shouldn't make the entire MCP subsystem invisible.
2. **Surface inconsistency (cluster of 3)**: after #143 Phase 1, the behavior matrix is:
- `doctor` — degraded envelope ✅
- `status` — degraded envelope ✅ (#143)
- `mcp` — hard-fail ❌ (this pinpoint)
3. **Clawhip impact**: `claw mcp --output-format json` is used by orchestrators to detect which MCP servers are available before invoking tools. A broken probe forces clawhip to fall back to doctor parse, which is suboptimal.
**Fix shape (~40 lines, mirrors #143 Phase 1).**
1. Make `render_mcp_report_json_for()` and `render_mcp_report_for()` catch the `ConfigError` at `loader.load()?`.
2. On parse failure, emit a degraded envelope:
```json
{
"kind": "mcp",
"action": "list",
"status": "degraded",
"config_load_error": "...",
"working_directory": "...",
"configured_servers": 0,
"servers": []
}
```
3. Text mode: prepend a "Config load error" block (same shape as #143) before the "MCP" block.
4. Exit 0 so downstream probes don't treat a parse error as process death.
**Acceptance.**
- `claw mcp` and `claw mcp --output-format json` on a workspace with malformed config exit 0.
- JSON mode includes `status: "degraded"` and `config_load_error` field.
- Text mode shows the parse error in a separate block, not as the only output.
- Clean path (no config errors) still returns `status: "ok"` (or equivalent — align with #143 serializer).
- Regression test: inject malformed config, assert mcp returns degraded envelope.
**Blocker.** None. Mirrors #143 Phase 1 shape exactly.
**Future phase (joins #143 Phase 2).** When typed-error taxonomy lands (§4.44), promote `config_load_error` from string to typed object across `doctor`, `status`, and `mcp` in one pass.
**Source.** Jobdori dogfood 2026-04-21 18:59 KST on main HEAD `e2a43fc`. Joins **partial-success** cluster (#143, Principle #5) and **surface consistency** cluster. Session tally: ROADMAP #144.
## Pinpoint #145. `claw plugins` subcommand not wired to CLI parser — word gets treated as a prompt, hits Anthropic API
**Gap.** `claw plugins` (and `claw plugins list`, `claw plugins --help`, `claw plugins info <name>`, etc.) fall through the top-level subcommand match and get routed into the prompt-execution path. Result: a purely local introspection command triggers an Anthropic API call and surfaces `missing Anthropic credentials` to the user. With valid credentials, it would actually send the string `"plugins"` as a prompt to Claude, burning tokens for a local query.
**Verified on main HEAD `faeaa1d` (2026-04-21 19:32 KST):**
```
$ claw plugins
error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API
$ claw plugins --output-format json
{"error":"missing Anthropic credentials; ...","type":"error"}
$ claw plugins --help
error: missing Anthropic credentials; ...
$ claw plugins list
error: missing Anthropic credentials; ...
$ ANTHROPIC_API_KEY=dummy claw plugins
⠋ 🦀 Thinking...
✘ ❌ Request failed
error: api returned 401 Unauthorized (authentication_error)
```
Compare `agents`, `mcp`, `skills` — all recognized, all local, all exit 0:
```
$ claw agents
No agents found.
$ claw mcp
MCP
Working directory ...
Configured servers 0
```
**Root cause.** In `rusty-claude-cli/src/main.rs`, the top-level `match rest[0].as_str()` parser has arms for `agents`, `mcp`, `skills`, `status`, `doctor`, `init`, `export`, `prompt`, etc., but **no arm for `plugins`**. The `CliAction::Plugins` variant exists, has a dispatcher (`print_plugins`), and is produced by `SlashCommand::Plugins` inside the REPL — but the top-level CLI path was never wired. Result: `plugins` matches neither a known subcommand nor a slash path, so it falls through to the default "run as prompt" behavior.
**Why this is a clawability gap.**
1. **Prompt misdelivery (explicit Clawhip category)**: the command string is sent to the LLM instead of dispatched locally. Real risk: without the credentials guard, `claw plugins` would send `"plugins"` as a user prompt to Claude, burning tokens.
2. **Surface asymmetry**: `plugins` is the only diagnostic-adjacent command that isn't wired. Documentation, slash command, and dispatcher all exist; parser wiring was missed.
3. **`--help` should never hit the network**. Anywhere.
4. **Misleading error**: user running `claw plugins` sees an Anthropic credential error. No hint that `plugins` wasn't a recognized subcommand.
**Fix shape (~20 lines).** Add a `"plugins"` arm to the top-level parser in `main.rs` that produces `CliAction::Plugins { action, target, output_format }`, following the same positional convention as `mcp` (`action` = first positional, `target` = second). The existing `CliAction::Plugins` handler (`LiveCli::print_plugins`) already covers text and JSON.
**Acceptance.**
- `claw plugins` exits 0 with plugins list (empty in a clean workspace, which is the honest state).
- `claw plugins --output-format json` emits `{"kind":"plugin","action":"list",...}` with exit 0.
- `claw plugins list` exits 0 and matches `claw plugins`.
- `claw plugins info <name>` resolves through the existing handler.
- No Anthropic network call occurs for any `plugins` invocation.
- Regression test: parse `["claw", "plugins"]`, assert `CliAction::Plugins { action: None, target: None, .. }`.
**Blocker.** None. `CliAction::Plugins` already exists with a working dispatcher.
**Source.** Jobdori dogfood 2026-04-21 19:30 KST on main HEAD `faeaa1d` in response to Clawhip nudge. Joins **prompt misdelivery** cluster. Session tally: ROADMAP #145.
## Pinpoint #146. `claw config` and `claw diff` are pure-local introspection commands but require `--resume SESSION.jsonl` wrapping
**Gap.** Running `claw config` or `claw diff` directly exits with an error pointing to `claw --resume SESSION.jsonl /config` as the only path. Both commands are pure, read-only introspection: `config` reads files from disk and merges them; `diff` shells out to `git diff --cached` + `git diff`. Neither needs a session context to produce correct output.
**Verified on main HEAD `7d63699` (2026-04-21 20:03 KST):**
```
$ claw config
error: `claw config` is a slash command. Use `claw --resume SESSION.jsonl /config` or start `claw` and run `/config`.
$ claw config --output-format json
{"error":"`claw config` is a slash command. ...","type":"error"}
$ claw diff
error: `claw diff` is a slash command. Use `claw --resume SESSION.jsonl /diff` or start `claw` and run `/diff`.
```
Meanwhile `agents`, `mcp`, `skills`, `status`, `doctor`, `sandbox`, `plugins` (after #145) all work standalone.
**Why this is a clawability gap.**
1. **Synthetic friction**: requires a session file to inspect static disk state. A claw probing configuration has to spin up a session it doesn't need.
2. **Surface asymmetry**: all other read-only diagnostics are standalone. `config` and `diff` are the remaining holdouts.
3. **Pipeline-unfriendly**: `claw config --output-format json | jq` and `claw diff | less` are natural operator workflows; both are currently broken.
4. **Both already have working JSON renderers** (`render_config_json`, `render_diff_json_for`) — infrastructure for top-level wiring exists.
**Fix shape (~30 lines).** Add `"config"` and `"diff"` arms to the top-level parser in `main.rs` (mirroring #145's `plugins` wiring). Each dispatches to a new `CliAction` variant or to existing resume-supported renderers directly. Text mode uses `render_config_report` / `render_diff_report`; JSON mode uses `render_config_json` / `render_diff_json_for`. Remove `config` from `bare_slash_command_guidance`'s fallback allowlist only if explicitly gating (parser arm already short-circuits).
**Acceptance.**
- `claw config` exits 0 with discovered-file listing + merged-keys count.
- `claw config --output-format json` emits typed envelope with discovered files and merged JSON.
- `claw config env` / `claw config plugins` surface specific sections (matches `SlashCommand::Config { section }` semantics).
- `claw diff` exits 0 with clean-tree message or staged/unstaged summary.
- `claw diff --output-format json` emits typed envelope.
- Regression tests: `parse_args(["config"])` → `CliAction::Config`; `parse_args(["diff"])` → `CliAction::Diff`.
**Blocker.** None. Renderers exist and are resume-supported (proving they're pure-local).
**Not applying to.** `hooks` (session-state-modifying, explicitly flagged "unsupported resumed slash command" in main.rs), `usage`, `context`, `tasks`, `theme`, `voice`, `rename`, `copy`, `color`, `effort`, `branch`, `rewind`, `ide`, `tag`, `output-style`, `add-dir` — all session-mutating or interactive-only.
**Source.** Jobdori dogfood 2026-04-21 20:03 KST on main HEAD `7d63699` in response to Clawhip nudge. Joins **surface asymmetry** cluster (#145 sibling). Session tally: ROADMAP #146.
## Pinpoint #147. `claw ""` / `claw " "` silently fall through to prompt-execution path; empty-prompt guard is subcommand-only
**Gap.** The explicit `claw prompt ""` path rejects empty/whitespace-only prompts with a clear error (`prompt subcommand requires a prompt string`, exit 1, no network call). The implicit fallthrough path — where any unrecognized first positional arg is treated as a prompt — has no such guard. Result: `claw ""`, `claw " "`, and `claw "" ""` all get routed to the Anthropic call with an empty prompt string, which surfaces the misleading `missing Anthropic credentials` error.
**Verified on main HEAD `f877aca` (2026-04-21 20:32 KST):**
```
$ claw prompt ""
error: prompt subcommand requires a prompt string
$ claw ""
error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY ...
$ claw " "
error: missing Anthropic credentials; ...
$ claw "" ""
error: missing Anthropic credentials; ...
$ claw --output-format json ""
{"error":"missing Anthropic credentials; ...","type":"error"}
```
With valid credentials, the empty string would be sent to Claude as a user prompt — burning tokens for nothing, or getting a model-side refusal for empty input.
**Why this is a clawability gap.**
1. **Inconsistent guard**: the `"prompt"` subcommand arm enforces `if prompt.trim().is_empty() { Err(...) }`, but the fallthrough `other` arm in the same match block does not. Same contract should apply to both paths.
2. **Prompt misdelivery (Clawhip category)**: same root pattern as #145 (wrong thing gets treated as a prompt). Different manifestation — here it's an empty string, not a typo'd subcommand.
3. **Misleading error surface**: user sees `missing Anthropic credentials` for a request that should never have reached the API layer at all.
4. **Clawhip risk**: a misconfigured orchestrator passing `""` or `" "` as a positional arg ends up paying API costs for empty prompts instead of getting fast feedback.
**Fix shape (~5 lines).** In `parse_subcommand()`'s fallthrough `other` arm, add the same trim-based empty check already used in the `"prompt"` arm, with a message that distinguishes it from the `prompt` subcommand path (e.g. `"empty prompt: provide a command or non-empty prompt text"`). Happens before `looks_like_subcommand_typo` since typos aren't empty.
**Acceptance.**
- `claw ""` exits 1 with a clear "empty prompt" error, no credential check.
- `claw " "` exits 1 with the same error.
- `claw "" ""` exits 1 with the same error.
- `claw --output-format json ""` emits the error in typed envelope, exit 1.
- `claw hello` still reaches the typo guard (#108), not the empty guard.
- `claw prompt ""` still emits its own specific error.
- Regression test: `parse_args([""])` → Err, `parse_args([" "])` → Err.
**Blocker.** None. 5-line change in `parse_subcommand()`.
**Source.** Jobdori dogfood 2026-04-21 20:32 KST on main HEAD `f877aca` in response to Clawhip nudge. Joins **prompt misdelivery** cluster (#145 sibling). Session tally: ROADMAP #147.
## Pinpoint #148. `claw status` JSON shows resolved model but not raw input or source — post-hoc "why did my --model flag behave this way?" requires re-reading argv
**Gap.** After #128 closed (malformed model strings now rejected at parse time), the residual provenance gap from the original #124 pinpoint remains: `claw status --output-format json` surfaces only the resolved model string. No trace of whether the user passed `--model sonnet` (alias → resolved), `--model anthropic/claude-opus-4-6` (pass-through), or relied on env/config default. A claw debugging "which model actually runs if I invoke this?" has to inspect argv instead of reading a structured field.
**Verified on main HEAD `4cb8fa0` (2026-04-21 20:40 KST):**
```
$ claw --model sonnet --output-format json status | jq '{model}'
{"model": "claude-sonnet-4-6"}
$ claw --model anthropic/claude-opus-4-6 --output-format json status | jq '{model}'
{"model": "anthropic/claude-opus-4-6"}
# Same resolved value can come from three different sources;
# JSON envelope gives no way to distinguish.
```
**Why this is a clawability gap.**
1. **Loss of origin information**: alias resolution collapses `sonnet` and `claude-sonnet-4-6` and `{"aliases":{"x":"claude-sonnet-4-6"}}` + `--model x` into one string. Debug forensics has to read argv.
2. **Clawhip orchestration**: a clawhip dispatcher sending `--model` wants to confirm its flag was honored, not that the default kicked in (#105 model-resolution-source disagreement is adjacent).
3. **Truth-audit / diagnostic-integrity**: the status envelope is supposed to be the single source of truth for "what would this process run as". Missing provenance weakens the contract.
**Fix shape (~50 lines).** Add two fields to status JSON:
- `model_source`: `"flag" | "env" | "config" | "default"` — where the model string came from.
- `model_raw`: the user's original input (pre-alias-resolution). Null when source is `default`.
Text mode appends a line: `Model source flag (raw: sonnet)` or `Model source default`.
Threading: parser already knows the source (it's the arm that sets `model`). Propagate `(model, model_raw, model_source)` tuple through `CliAction::Status` and into `StatusContext`. Env/default resolution paths are in `resolve_repl_model*` helpers.
**Acceptance.**
- `claw --model sonnet --output-format json status` → `model: "claude-sonnet-4-6"`, `model_raw: "sonnet"`, `model_source: "flag"`.
- `claw --model anthropic/claude-opus-4-6 --output-format json status` → `model_raw: "anthropic/claude-opus-4-6"`, `model_source: "flag"`.
- `claw --output-format json status` (no flag) → `model_raw: null`, `model_source: "default"` (or `"env"` if `ANTHROPIC_MODEL` set; or `"config"` if `.claw.json` set `model`).
- Text mode shows same provenance.
- Regression test: parse_args + status_json_value roundtrip asserts each source value.
**Blocker.** None. All resolution sites already exist; only plumbing + one serialization addition.
**Not a regression of #128.** #128 was about rejecting malformed strings (now closed). #148 is about labeling the valid ones after resolution.
**Source.** Jobdori dogfood 2026-04-21 20:40 KST on main HEAD `4cb8fa0` in response to Q's bundle hint. Split from historical #124 residual. Joins **truth-audit / diagnostic-integrity** cluster. Session tally: ROADMAP #148.
## Pinpoint #149. `runtime::config::tests::validates_unknown_top_level_keys_with_line_and_field_name` flakes under parallel workspace test runs
**Gap.** When `cargo test --workspace` runs with normal parallel test execution (default), `runtime::config::tests::validates_unknown_top_level_keys_with_line_and_field_name` intermittently fails. In isolation (`cargo test -p runtime validates_unknown_top_level_keys_with_line_and_field_name`), it passes deterministically. The same pattern affects other tests in `runtime/src/config.rs` and sibling test modules that share the `temp_dir()` naming strategy.
**Verified on main HEAD `f84c7c4` (2026-04-21 20:50 KST):** witnessed during `cargo test --workspace` runs for #147 and #148 — one workspace run produced:
```
test config::tests::validates_unknown_top_level_keys_with_line_and_field_name ... FAILED
test result: FAILED. 464 passed; 1 failed; 0 ignored; 0 measured
```
Same test passed on the next workspace run. Same test passes in isolation every time.
**Root cause.** `runtime/src/config.rs` tests share this helper:
```rust
fn temp_dir() -> std::path::PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("runtime-config-{nanos}"))
}
```
Two weaknesses:
1. **Timestamp-only namespacing**: on fast machines with coarse-grained clocks (or with tests starting within the same nanosecond bucket), two tests pick the same path. One races `fs::create_dir_all()` with another's `fs::remove_dir_all()`.
2. **No label differentiation**: every test in the file calls `temp_dir()` and constructs sub-paths inside the shared prefix. A `fs::remove_dir_all(root)` in one test's cleanup may clobber a live sibling.
Other crates in the workspace (`plugins::tests::temp_dir`, `runtime::git_context::tests::temp_dir`) already use the **labeled** form `temp_dir(label)` to segregate namespaces per-test. `runtime/src/config.rs` was missed in that sweep.
**Fix shape (~30 lines).** Convert `temp_dir()` in `runtime/src/config.rs` to `temp_dir(label: &str)` mirroring the plugins/git_context pattern, plus add a PID + atomic counter suffix for double-strength collision resistance:
```rust
fn temp_dir(label: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).expect("...").as_nanos();
let pid = std::process::id();
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("runtime-config-{label}-{pid}-{nanos}-{seq}"))
}
```
Update each `temp_dir()` callsite in the file to pass a unique label (test function name usually works).
**Acceptance.**
- `cargo test --workspace` 10x consecutive runs all green (excluding pre-existing `resume_latest` flake which is orthogonal).
- `cargo test -p runtime` 10x consecutive runs all green.
- Cleanup `fs::remove_dir_all(root)` never races because `root` is guaranteed unique per-test.
- No behavior change for tests already passing in isolation.
**Blocker.** None. Mechanical rename + label addition.
**Not applying to.** `plugins::tests::temp_dir` and `runtime::git_context::tests::temp_dir` already use the labeled form. The label pattern is the established workspace convention; this just applies it to the one holdout.
**Source.** Jobdori dogfood 2026-04-21 20:50 KST, flagged during #147 and #148 workspace-test runs. Joins **test brittleness / flake** cluster. Session tally: ROADMAP #149.
## Pinpoint #150. `resume_latest_restores_the_most_recent_managed_session` flakes due to symlink/canonicalization mismatch
**Gap.** Test `resume_latest_restores_the_most_recent_managed_session` in `rusty-claude-cli/tests/resume_slash_commands.rs` intermittently fails when run as part of the workspace suite or in parallel.
**Root cause.** `workspace_fingerprint(path)` hashes the workspace path string directly without canonicalization. On macOS, `/tmp` is a symlink to `/private/tmp`. The test creates a temp dir via `std::env::temp_dir().join(...)` which may return `/var/folders/...` (non-canonical). The test uses this non-canonical path to create sessions. When the subprocess spawns, `env::current_dir()` returns the canonical path `/private/var/folders/...`. The two fingerprints differ, so the subprocess looks in `.claw/sessions/<hash1>` while files are in `.claw/sessions/<hash2>`. Session discovery fails.
**Verified on main HEAD `bc259ec` (2026-04-21 21:00 KST):** Test failed intermittently during workspace runs and consistently failed when run 5x in sequence before the fix.
**Fix shape (~5 lines).** Call `fs::canonicalize(&project_dir)` after creating the directory but before passing it to `SessionStore::from_cwd()`. This ensures the test and subprocess use identical path representations when computing the fingerprint.
```rust
fs::create_dir_all(&project_dir).expect("project dir should exist");
let project_dir = fs::canonicalize(&project_dir).unwrap_or(project_dir);
let store = runtime::SessionStore::from_cwd(&project_dir).expect(...);
```
**Acceptance.**
- `cargo test -p rusty-claude-cli --test resume_slash_commands` passes.
- 5 consecutive runs all green (previously: 5/5 failed).
- No behavior change; test now correctly isolates temp paths.
**Blocker.** None.
**Note.** This is the last known pre-existing test flake in the workspace. `resume_latest` was the only survivor from earlier sessions.
**Source.** Jobdori dogfood 2026-04-21 21:00 KST, Q's "clean up remaining flake" hint led to root-cause analysis and fix. Session tally: ROADMAP #150.
## Pinpoint #246. Reminder cron outcome ambiguity — no structured feedback on nudge delivery/skip/timeout
**Gap (control-loop blocker).** The `clawcode-dogfood-cycle-reminder` cron triggers dogfood cycles every 10 minutes. When it times out (witnessed multiple times during 2026-04-21 sweep), there is no structured answer to: Was the nudge delivered? Did it fail before send? After send? Was it skipped due to an active cycle? Did the gateway drain and abort?
**Impact.** Repeated timeouts produce scheduler fog instead of trustworthy dogfood pressure. Team cannot distinguish:
- Silent delivery (nudge went out, cycle ran)
- Delivery followed by subprocess crash (nudge reached Discord, but cycle had issues)
- Timeout before send (cron died early)
- Timeout after send (cron sent nudge, died before cleanup)
- Deduplication (active cycle still running, nudge skipped)
- Gateway draining (request in-flight when daemon shutdown)
**Phase 1 spec (outcome schema).** Extend cron task results to include a `reminder_outcome` field with explicit values:
- `"delivered"` — nudge successfully posted to Discord; next cycle can proceed
- `"timed_out_before_send"` — cron died before posting; retry on next interval
- `"timed_out_after_send"` — nudge posted (or should assume posted), but cleanup/logging timed out
- `"skipped_due_to_active_cycle"` — previous cycle still running; no nudge issued
- `"aborted_gateway_draining"` — reminding stopped because o p e n c l a w gateway is draining
Deliverable: Update `clawcode-dogfood-cycle-reminder` task to emit this field on completion/timeout/skip.
**Phase 2 (observability).** Log all five outcomes to Agentika and surface via `clawhip status` or similar monitoring surface so Q/gaebal-gajae can see nudge history.
**Blocker.** Assigned to gaebal-gajae's domain (cron scheduling / o p e n c l a w orchestration). Not a claw-code CLI blocker; purely infrastructure/monitoring.
**Source.** Q's direct observation during 2026-04-21 20:5021:00 dogfood cycles: repeated timeouts with no way to diagnose. Session tally: ROADMAP #246.
## Pinpoint #151. `workspace_fingerprint` path-equivalence contract gap (product, not just test)
**Gap.** `workspace_fingerprint(path)` hashes the raw path string without canonicalization. Two callers passing equivalent paths (e.g. `/tmp/foo` vs `/private/tmp/foo` on macOS where `/tmp` is a symlink to `/private/tmp`) get different fingerprints and therefore different session stores. #150 was the test-side symptom; the product contract itself is still fragile.
**Discovery path.** #150 fix (canonicalize in test) was a workaround. Real users hit this whenever:
1. Embedded callers pass a raw `--data-dir` path that differs from canonical `env::current_dir()`
2. Programmatic use of `SessionStore::from_cwd(some_path)` with a non-canonical input
3. Symlinks elsewhere in the filesystem (not just macOS `/tmp`): NixOS store paths, Docker bind mounts, network mounts with case-insensitive normalization, etc.
The REPL's default flow happens to work because `env::current_dir()` returns canonicalized paths on macOS. But anyone calling `SessionStore::from_cwd()` with a user-supplied path risks silent session-store divergence.
**Root cause.** The function treats path-string equality and path-equivalence as the same thing:
```rust
pub fn workspace_fingerprint(workspace_root: &Path) -> String {
let input = workspace_root.to_string_lossy(); // ← raw bytes
// ... FNV-1a hash ...
}
```
**Fix shape (~10 lines).** Canonicalize inside `SessionStore::from_cwd()` (and `from_data_dir`) before computing the fingerprint. Keep `workspace_fingerprint()` itself as a pure function of its input for determinism — the canonicalization is the caller's responsibility, but the two production entry points should always canonicalize.
```rust
pub fn from_cwd(cwd: impl AsRef<Path>) -> Result<Self, SessionControlError> {
let cwd = cwd.as_ref();
// #151: canonicalize so that equivalent paths (symlinks, ./foo vs /abs/foo)
// produce the same workspace_fingerprint. Falls back to the raw path when
// canonicalize() fails (e.g. directory doesn't exist yet — callers that
// haven't materialized the workspace).
let canonical_cwd = fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
let sessions_root = canonical_cwd
.join(".claw")
.join("sessions")
.join(workspace_fingerprint(&canonical_cwd));
fs::create_dir_all(&sessions_root)?;
Ok(Self {
sessions_root,
workspace_root: canonical_cwd,
})
}
```
**Backward compatibility.** Existing users on macOS where `env::current_dir()` already returns canonical paths: no change in hash. Users who ever called with a non-canonical path: hash would change, but those sessions were already broken (couldn't be resumed from a canonical-path cwd). Net improvement.
**Acceptance.**
- Revert the test-side workaround from #150; test still passes.
- Add regression test: `SessionStore::from_cwd("/tmp/foo")` and `SessionStore::from_cwd("/private/tmp/foo")` return stores with identical `sessions_dir()` on macOS.
- Workspace tests green.
**Blocker.** None.
**Source.** Q's ack on #150 surfaced the deeper gap: "#150 closed is real value" but the product function still has the brittleness. Session tally: ROADMAP #151.
## Pinpoint #152. Diagnostic verb suffixes allow arbitrary positional args, emit double "error:" prefix
**Gap.** Verbs like `claw doctor garbage` and `claw status foo bar` parse successfully instead of failing at parse time. The positional arguments fall through to the prompt-execution path, or in some cases the verb parser doesn't have a flag-only guard. Additionally, the error formatter doubles the "error:" prefix and doesn't hint at `--output-format json` for verbs that don't recognize `--json` as an alias.
**Example failures:**
- `claw doctor garbage` → silently treats "garbage" as a prompt instead of rejecting "doctor" as a verb with unexpected args
- `claw system-prompt --json` → errors with "error: unknown option" but doesn't suggest `--output-format json`
- Error messages show `error: error: <message>` (double prefix)
**Fix shape (~30 lines).** Three improvements:
1. Wire parse_verb_suffix to reject positional args after verbs (except multi-word prompts like "help me debug")
2. Special-case `--json` in the verb-option error path to suggest `--output-format json`
3. Remove the "error:" prefix from format_unknown_verb_option (already added by top-level handler)
**Acceptance:** `claw doctor garbage` exits 1 with "unexpected positional argument"; `claw system-prompt --json` hints at `--output-format json`; error messages have single "error:" prefix.
**Blocker.** None. Implementation exists on worktree `jobdori-127-verb-suffix` but needs rebase against main (conflicts with #141 which already shipped).
**Source.** Clawhip nudge 2026-04-21 21:17 KST — "no excuses, always find something to ship" directive. Session tally: ROADMAP #152.
## Pinpoint #153. README/USAGE missing "add binary to PATH" and "verify install" bridge
**Gap.** After `cargo build --workspace`, new users don't know:
1. Where the binary actually ends up (e.g., `rust/target/debug/claw` vs. expecting it in `/usr/local/bin`)
2. How to verify the build succeeded (e.g., `claw --help`, `which claw`, `claw doctor`)
3. How to add it to PATH for shell integration (optional but common follow-up)
This creates a confusing gap: users build successfully but then get "command not found: claw" and assume the build failed, or they immediately ask "how do I install this properly?"
**Real examples from #claw-code:**
- "claw not found — did the build fail?"
- "do I need to `cargo install` this?"
- "why is the binary at `rust/target/debug/claw` and not just `claw`?"
**Fix shape (~50 lines).** Add a new "Post-build verification and PATH" section in README (after Quick start) covering:
1. **Where the binary lives:** `rust/target/debug/claw` (debug build) or `rust/target/release/claw` (release)
2. **Verify it works:** Run `./rust/target/debug/claw --help` and `./rust/target/debug/claw doctor`
3. **Optional: Add to PATH** — three approaches:
- symlink: `ln -s $(pwd)/rust/target/debug/claw /usr/local/bin/claw`
- `cargo install --path ./rust` (builds and installs to `~/.cargo/bin/`)
- update shell profile to export PATH
4. **Windows equivalent:** Point to `rust\target\debug\claw.exe` and `cargo install --path .\rust`
**Acceptance:** New users can find the binary location, run it directly, and know their first verification step is `claw doctor`.
**Blocker:** None. Pure documentation.
**Source:** Clawhip nudge 2026-04-21 21:27 KST — onboarding gap from #claw-code observations earlier this month.
## Pinpoint #154. Model syntax error doesn't hint at env var when multiple credentials present
**Gap.** When a user types `claw --model gpt-4` but only has `ANTHROPIC_API_KEY` set (no `OPENAI_API_KEY`), the error is:
```
error: invalid model syntax: 'gpt-4'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku)
```
But USAGE.md documents that "The error message now includes a hint that names the detected env var" — **this hint is not actually emitted.** The user gets a generic syntax error and has to re-read USAGE.md to discover they should type `openai/gpt-4` instead.
**Expected behavior (from USAGE.md):** When the user has multiple providers' env vars set, or when a model name looks like it belongs to a different provider (e.g., `gpt-4` looks like OpenAI), the error should hint:
- "Did you mean `openai/gpt-4`? (but `OPENAI_API_KEY` is not set)"
- or "You have `ANTHROPIC_API_KEY` set but `gpt-4` looks like an OpenAI model. Try `openai/gpt-4` with `OPENAI_API_KEY` exported"
**Current behavior:** Generic syntax error, user has to infer the fix from USAGE.md or guess.
**Fix shape (~20 lines).** Enhance `FormatError::InvalidModelSyntax` or the model-parsing validation to:
1. Detect if the model name looks like it belongs to a known provider (prefix `gpt-`, `openai/`, `qwen`, etc.)
2. If it does, check if that provider's env var is missing
3. Append a hint: "Did you mean \`{inferred_prefix}/{model}\`? (requires `{PROVIDER_KEY}` env var)"
**Acceptance:** `claw --model gpt-4` produces a hint about OpenAI prefix and missing `OPENAI_API_KEY`. Same for `qwen-plus` → hint about `DASHSCOPE_API_KEY`, etc.
**Blocker:** None. Pure error-message UX improvement.
**Source:** Clawhip nudge 2026-04-21 21:37 KST — discovered during dogfood probing of model validation.
## Pinpoint #155. USAGE.md missing docs for `/ultraplan`, `/teleport`, `/bughunter` commands
**Gap.** The `claw --help` output lists three interactive slash commands that are not documented in USAGE.md:
- `/ultraplan [task]` — Run a deep planning prompt with multi-step reasoning
- `/teleport <symbol-or-path>` — Jump to a file or symbol by searching the workspace
- `/bughunter [scope]` — Inspect the codebase for likely bugs
New users see these commands in the help output but have no explanation of:
1. What each does
2. How to use it
3. What kind of input it expects
4. When to use it (vs. other commands)
5. Any limitations or prerequisites
**Impact.** Users run `/ultraplan` or `/teleport` out of curiosity, or they skip these commands because they don't understand them. Documentation should lower the barrier to discovery.
**Fix shape (~100 lines).** Add a new section to USAGE.md after "Interactive slash commands" covering:
1. **Planning & Reasoning**`/ultraplan [task]`
- Purpose: extended multi-step reasoning over a task
- Input: a task description or problem statement
- Output: a structured plan with steps and reasoning
- Example: `/ultraplan refactor this module to use async/await`
2. **Navigation**`/teleport <symbol-or-path>`
- Purpose: quickly jump to a file or function by name
- Input: a symbol name (function, class, struct) or file path
- Output: the file content with that symbol highlighted
- Example: `/teleport UserService`, `/teleport src/auth.rs`
3. **Code Analysis**`/bughunter [scope]`
- Purpose: scan the codebase for likely bugs or issues
- Input: optional scope (e.g., "src/handlers", "lib.rs")
- Output: list of suspicious patterns with explanations
- Example: `/bughunter src`, `/bughunter` (entire workspace)
**Acceptance:** Each command has a one-line description, a practical example, and expected behavior documented.
**Blocker:** None. Pure documentation.
**Source:** Clawhip nudge 2026-04-21 21:47 KST — discovered discrepancy between `claw --help` and USAGE.md coverage.
## Pinpoint #156. Error classification for text-mode output (Phase 2 of #77)
**Gap.** #77 Phase 1 added machine-readable `kind` discriminants to JSON error payloads. Text-mode errors still emit prose-only output with no structured classification.
**Impact.** Observability tools that parse stderr (e.g., log aggregators, CI error parsers) can't distinguish error classes without regex or substring matching. Phase 1 solves it for JSON consumers; Phase 2 should extend the classification to text mode.
**Fix shape (~20 lines).** Option A: Emit a `[error-kind: missing_credentials]` prefix line before the prose error so text parsers can quickly identify the class. Option B: Structured comment format like `# error_class=missing_credentials` at the end. Either way, the `kind` token should appear in text output as well.
**Acceptance.** A stderr observer can distinguish `missing_credentials` from `session_not_found` from `cli_parse` without regex-scraping the full error prose.
**Blocker.** None. Scope is small and non-breaking (adds a prefix or suffix, doesn't change existing error text).
**Source.** Clayhip nudge 2026-04-21 23:18 — dogfood surface clean, Phase 1 proven solid, natural next step is symmetry across output formats.

View File

@@ -52,6 +52,26 @@ cd rust
**Note:** Diagnostic verbs (`doctor`, `status`, `sandbox`, `version`) support `--output-format json` for machine-readable output. Invalid suffix arguments (e.g., `--json`) are now rejected at parse time rather than falling through to prompt dispatch.
### Initialize a repository
Set up a new repository with `.claw` config, `.claw.json`, `.gitignore` entries, and a `CLAUDE.md` guidance file:
```bash
cd /path/to/your/repo
./target/debug/claw init
```
Text mode (human-readable) shows artifact creation summary with project path and next steps. Idempotent — running multiple times in the same repo marks already-created files as "skipped".
JSON mode for scripting:
```bash
./target/debug/claw init --output-format json
```
Returns structured output with `project_path`, `created[]`, `updated[]`, `skipped[]` arrays (one per artifact), and `artifacts[]` carrying each file's `name` and machine-stable `status` tag. The legacy `message` field preserves backward compatibility.
**Why structured fields matter:** Claws can detect per-artifact state (`created` vs `updated` vs `skipped`) without substring-matching human prose. Use the `created[]`, `updated[]`, and `skipped[]` arrays for conditional follow-up logic (e.g., only commit if files were actually created, not just updated).
### Interactive REPL
```bash
@@ -80,6 +100,85 @@ cd rust
./target/debug/claw --output-format json prompt "status"
```
### Inspect worker state
The `claw state` command reads `.claw/worker-state.json`, which is written by the interactive REPL or a one-shot prompt when a worker executes a task. This file contains the worker ID, session reference, model, and permission mode.
Prerequisite: You must run `claw` (interactive REPL) or `claw prompt <text>` at least once in the repository to produce the worker state file.
```bash
cd rust
./target/debug/claw state
```
JSON mode:
```bash
./target/debug/claw state --output-format json
```
If you run `claw state` before any worker has executed, you will see a helpful error:
```
error: no worker state file found at .claw/worker-state.json
Hint: worker state is written by the interactive REPL or a non-interactive prompt.
Run: claw # start the REPL (writes state on first turn)
Or: claw prompt <text> # run one non-interactive turn
Then rerun: claw state [--output-format json]
```
## Advanced slash commands (Interactive REPL only)
These commands are available inside the interactive REPL (`claw` with no args). They extend the assistant with workspace analysis, planning, and navigation features.
### `/ultraplan` — Deep planning with multi-step reasoning
**Purpose:** Break down a complex task into steps using extended reasoning.
```bash
# Start the REPL
claw
# Inside the REPL
/ultraplan refactor the auth module to use async/await
/ultraplan design a caching layer for database queries
/ultraplan analyze this module for performance bottlenecks
```
Output: A structured plan with numbered steps, reasoning for each step, and expected outcomes. Use this when you want the assistant to think through a problem in detail before coding.
### `/teleport` — Jump to a file or symbol
**Purpose:** Quickly navigate to a file, function, class, or struct by name.
```bash
# Jump to a symbol
/teleport UserService
/teleport authenticate_user
/teleport RequestHandler
# Jump to a file
/teleport src/auth.rs
/teleport crates/runtime/lib.rs
/teleport ./ARCHITECTURE.md
```
Output: The file content, with the requested symbol highlighted or the file fully loaded. Useful for exploring the codebase without manually navigating directories. If multiple matches exist, the assistant shows the top candidates.
### `/bughunter` — Scan for likely bugs and issues
**Purpose:** Analyze code for common pitfalls, anti-patterns, and potential bugs.
```bash
# Scan the entire workspace
/bughunter
# Scan a specific directory or file
/bughunter src/handlers
/bughunter rust/crates/runtime
/bughunter src/auth.rs
```
Output: A list of suspicious patterns with explanations (e.g., "unchecked unwrap()", "potential race condition", "missing error handling"). Each finding includes the file, line number, and suggested fix. Use this as a first pass before a full code review.
## Model and permission controls
```bash

View File

@@ -2554,11 +2554,22 @@ fn render_mcp_report_for(
match normalize_optional_args(args) {
None | Some("list") => {
let runtime_config = loader.load()?;
Ok(render_mcp_summary_report(
cwd,
runtime_config.mcp().servers(),
))
// #144: degrade gracefully on config parse failure (same contract
// as #143 for `status`). Text mode prepends a "Config load error"
// block before the MCP list; the list falls back to empty.
match loader.load() {
Ok(runtime_config) => Ok(render_mcp_summary_report(
cwd,
runtime_config.mcp().servers(),
)),
Err(err) => {
let empty = std::collections::BTreeMap::new();
Ok(format!(
"Config load error\n Status fail\n Summary runtime config failed to load; reporting partial MCP view\n Details {err}\n Hint `claw doctor` classifies config parse errors; fix the listed field and rerun\n\n{}",
render_mcp_summary_report(cwd, &empty)
))
}
}
}
Some(args) if is_help_arg(args) => Ok(render_mcp_usage(None)),
Some("show") => Ok(render_mcp_usage(Some("show"))),
@@ -2571,12 +2582,19 @@ fn render_mcp_report_for(
if parts.next().is_some() {
return Ok(render_mcp_usage(Some(args)));
}
let runtime_config = loader.load()?;
Ok(render_mcp_server_report(
cwd,
server_name,
runtime_config.mcp().get(server_name),
))
// #144: same degradation for `mcp show`; if config won't parse,
// the specific server lookup can't succeed, so report the parse
// error with context.
match loader.load() {
Ok(runtime_config) => Ok(render_mcp_server_report(
cwd,
server_name,
runtime_config.mcp().get(server_name),
)),
Err(err) => Ok(format!(
"Config load error\n Status fail\n Summary runtime config failed to load; cannot resolve `{server_name}`\n Details {err}\n Hint `claw doctor` classifies config parse errors; fix the listed field and rerun"
)),
}
}
Some(args) => Ok(render_mcp_usage(Some(args))),
}
@@ -2599,11 +2617,35 @@ fn render_mcp_report_json_for(
match normalize_optional_args(args) {
None | Some("list") => {
let runtime_config = loader.load()?;
Ok(render_mcp_summary_report_json(
cwd,
runtime_config.mcp().servers(),
))
// #144: match #143's degraded envelope contract. On config parse
// failure, emit top-level `status: "degraded"` with
// `config_load_error`, empty servers[], and exit 0. On clean
// runs, the existing serializer adds `status: "ok"` below.
match loader.load() {
Ok(runtime_config) => {
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);
}
Ok(value)
}
Err(err) => {
let empty = std::collections::BTreeMap::new();
let mut value = render_mcp_summary_report_json(cwd, &empty);
if let Some(map) = value.as_object_mut() {
map.insert("status".to_string(), Value::String("degraded".to_string()));
map.insert(
"config_load_error".to_string(),
Value::String(err.to_string()),
);
}
Ok(value)
}
}
}
Some(args) if is_help_arg(args) => Ok(render_mcp_usage_json(None)),
Some("show") => Ok(render_mcp_usage_json(Some("show"))),
@@ -2616,12 +2658,29 @@ fn render_mcp_report_json_for(
if parts.next().is_some() {
return Ok(render_mcp_usage_json(Some(args)));
}
let runtime_config = loader.load()?;
Ok(render_mcp_server_report_json(
cwd,
server_name,
runtime_config.mcp().get(server_name),
))
// #144: same degradation pattern for show action.
match loader.load() {
Ok(runtime_config) => {
let mut value = render_mcp_server_report_json(
cwd,
server_name,
runtime_config.mcp().get(server_name),
);
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);
}
Ok(value)
}
Err(err) => Ok(serde_json::json!({
"kind": "mcp",
"action": "show",
"server": server_name,
"status": "degraded",
"config_load_error": err.to_string(),
"working_directory": cwd.display().to_string(),
})),
}
}
Some(args) => Ok(render_mcp_usage_json(Some(args))),
}
@@ -5479,6 +5538,82 @@ mod tests {
let _ = fs::remove_dir_all(config_home);
}
#[test]
fn mcp_degrades_gracefully_on_malformed_mcp_config_144() {
// #144: mirror of #143's partial-success contract for `claw mcp`.
// Previously `mcp` hard-failed on any config parse error, hiding
// well-formed servers and forcing claws to fall back to `doctor`.
// Now `mcp` emits a degraded envelope instead: exit 0, status:
// "degraded", config_load_error populated, servers[] empty.
let _guard = env_guard();
let workspace = temp_dir("mcp-degrades-144");
let config_home = temp_dir("mcp-degrades-144-cfg");
fs::create_dir_all(workspace.join(".claw")).expect("create workspace .claw dir");
fs::create_dir_all(&config_home).expect("create config home");
// One valid server + one malformed entry missing `command`.
fs::write(
workspace.join(".claw.json"),
r#"{
"mcpServers": {
"everything": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"]},
"missing-command": {"args": ["arg-only-no-command"]}
}
}
"#,
)
.expect("write malformed .claw.json");
let loader = ConfigLoader::new(&workspace, &config_home);
// list action: must return Ok (not Err) with degraded envelope.
let list = render_mcp_report_json_for(&loader, &workspace, None)
.expect("mcp list should not hard-fail on config parse errors (#144)");
assert_eq!(list["kind"], "mcp");
assert_eq!(list["action"], "list");
assert_eq!(
list["status"].as_str(),
Some("degraded"),
"top-level status should be 'degraded': {list}"
);
let err = list["config_load_error"]
.as_str()
.expect("config_load_error must be a string on degraded runs");
assert!(
err.contains("mcpServers.missing-command"),
"config_load_error should name the malformed field path: {err}"
);
assert_eq!(list["configured_servers"], 0);
assert!(list["servers"].as_array().unwrap().is_empty());
// show action: should also degrade (not hard-fail).
let show = render_mcp_report_json_for(&loader, &workspace, Some("show everything"))
.expect("mcp show should not hard-fail on config parse errors (#144)");
assert_eq!(show["kind"], "mcp");
assert_eq!(show["action"], "show");
assert_eq!(
show["status"].as_str(),
Some("degraded"),
"show action should also report status: 'degraded': {show}"
);
assert!(show["config_load_error"].is_string());
// Clean path: status: "ok", config_load_error: null.
let clean_ws = temp_dir("mcp-degrades-144-clean");
fs::create_dir_all(&clean_ws).expect("clean ws");
let clean_loader = ConfigLoader::new(&clean_ws, &config_home);
let clean_list = render_mcp_report_json_for(&clean_loader, &clean_ws, None)
.expect("clean mcp list should succeed");
assert_eq!(
clean_list["status"].as_str(),
Some("ok"),
"clean run should report status: 'ok'"
);
assert!(clean_list["config_load_error"].is_null());
let _ = fs::remove_dir_all(workspace);
let _ = fs::remove_dir_all(config_home);
let _ = fs::remove_dir_all(clean_ws);
}
#[test]
fn parses_quoted_skill_frontmatter_values() {
let contents = "---\nname: \"hud\"\ndescription: 'Quoted description'\n---\n";

View File

@@ -1254,11 +1254,21 @@ mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_dir() -> std::path::PathBuf {
// #149: previously used `runtime-config-{nanos}` which collided
// under parallel `cargo test --workspace` when multiple tests
// started within the same nanosecond bucket on fast machines.
// Add process id + a monotonically-incrementing atomic counter
// so every callsite gets a provably-unique directory regardless
// of clock resolution or scheduling.
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("runtime-config-{nanos}"))
let pid = std::process::id();
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("runtime-config-{pid}-{nanos}-{seq}"))
}
#[test]

View File

@@ -31,14 +31,19 @@ impl SessionStore {
/// The on-disk layout becomes `<cwd>/.claw/sessions/<workspace_hash>/`.
pub fn from_cwd(cwd: impl AsRef<Path>) -> Result<Self, SessionControlError> {
let cwd = cwd.as_ref();
let sessions_root = cwd
// #151: canonicalize so equivalent paths (symlinks, relative vs
// absolute, /tmp vs /private/tmp on macOS) produce the same
// workspace_fingerprint. Falls back to the raw path if canonicalize
// fails (e.g. the directory doesn't exist yet).
let canonical_cwd = fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
let sessions_root = canonical_cwd
.join(".claw")
.join("sessions")
.join(workspace_fingerprint(cwd));
.join(workspace_fingerprint(&canonical_cwd));
fs::create_dir_all(&sessions_root)?;
Ok(Self {
sessions_root,
workspace_root: cwd.to_path_buf(),
workspace_root: canonical_cwd,
})
}
@@ -51,14 +56,18 @@ impl SessionStore {
workspace_root: impl AsRef<Path>,
) -> Result<Self, SessionControlError> {
let workspace_root = workspace_root.as_ref();
// #151: canonicalize workspace_root for consistent fingerprinting
// across equivalent path representations.
let canonical_workspace = fs::canonicalize(workspace_root)
.unwrap_or_else(|_| workspace_root.to_path_buf());
let sessions_root = data_dir
.as_ref()
.join("sessions")
.join(workspace_fingerprint(workspace_root));
.join(workspace_fingerprint(&canonical_workspace));
fs::create_dir_all(&sessions_root)?;
Ok(Self {
sessions_root,
workspace_root: workspace_root.to_path_buf(),
workspace_root: canonical_workspace,
})
}
@@ -103,7 +112,7 @@ impl SessionStore {
candidate
} else if looks_like_path {
return Err(SessionControlError::Format(
format_missing_session_reference(reference),
format_missing_session_reference(reference, &self.sessions_root),
));
} else {
self.resolve_managed_path(reference)?
@@ -134,7 +143,7 @@ impl SessionStore {
}
}
Err(SessionControlError::Format(
format_missing_session_reference(session_id),
format_missing_session_reference(session_id, &self.sessions_root),
))
}
@@ -152,7 +161,7 @@ impl SessionStore {
self.list_sessions()?
.into_iter()
.next()
.ok_or_else(|| SessionControlError::Format(format_no_managed_sessions()))
.ok_or_else(|| SessionControlError::Format(format_no_managed_sessions(&self.sessions_root)))
}
pub fn load_session(
@@ -513,15 +522,25 @@ fn session_id_from_path(path: &Path) -> Option<String> {
.map(ToOwned::to_owned)
}
fn format_missing_session_reference(reference: &str) -> String {
fn format_missing_session_reference(reference: &str, sessions_root: &Path) -> String {
// #80: show the actual workspace-fingerprint directory instead of lying about .claw/sessions/
let fingerprint_dir = sessions_root
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("<unknown>");
format!(
"session not found: {reference}\nHint: managed sessions live in .claw/sessions/. Try `{LATEST_SESSION_REFERENCE}` for the most recent session or `/session list` in the REPL."
"session not found: {reference}\nHint: managed sessions live in .claw/sessions/{fingerprint_dir}/ (workspace-specific partition).\nTry `{LATEST_SESSION_REFERENCE}` for the most recent session or `/session list` in the REPL."
)
}
fn format_no_managed_sessions() -> String {
fn format_no_managed_sessions(sessions_root: &Path) -> String {
// #80: show the actual workspace-fingerprint directory instead of lying about .claw/sessions/
let fingerprint_dir = sessions_root
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("<unknown>");
format!(
"no managed sessions found in .claw/sessions/\nStart `claw` to create a session, then rerun with `--resume {LATEST_SESSION_REFERENCE}`."
"no managed sessions found in .claw/sessions/{fingerprint_dir}/\nStart `claw` to create a session, then rerun with `--resume {LATEST_SESSION_REFERENCE}`.\nNote: claw partitions sessions per workspace fingerprint; sessions from other CWDs are invisible."
)
}
@@ -744,6 +763,40 @@ mod tests {
assert_eq!(fp_a1.len(), 16, "fingerprint must be a 16-char hex string");
}
/// #151 regression: equivalent paths (e.g. `/tmp/foo` vs `/private/tmp/foo`
/// on macOS where `/tmp` is a symlink to `/private/tmp`) must resolve to
/// the same session store. Previously they diverged because
/// `workspace_fingerprint()` hashed the raw path string. Now
/// `SessionStore::from_cwd()` canonicalizes first.
#[test]
fn session_store_from_cwd_canonicalizes_equivalent_paths() {
let base = temp_dir();
let real_dir = base.join("real-workspace");
fs::create_dir_all(&real_dir).expect("real workspace should exist");
// Build two stores via different but equivalent path representations:
// the raw path and the canonicalized path.
let raw_path = real_dir.clone();
let canonical_path = fs::canonicalize(&real_dir).expect("canonicalize ok");
let store_from_raw =
SessionStore::from_cwd(&raw_path).expect("store from raw should build");
let store_from_canonical =
SessionStore::from_cwd(&canonical_path).expect("store from canonical should build");
assert_eq!(
store_from_raw.sessions_dir(),
store_from_canonical.sessions_dir(),
"equivalent paths must produce the same sessions dir (raw={} canonical={})",
raw_path.display(),
canonical_path.display()
);
if base.exists() {
fs::remove_dir_all(base).expect("cleanup ok");
}
}
#[test]
fn session_store_from_cwd_isolates_sessions_by_workspace() {
// given
@@ -832,6 +885,11 @@ mod tests {
let workspace_b = base.join("repo-beta");
fs::create_dir_all(&workspace_a).expect("workspace a should exist");
fs::create_dir_all(&workspace_b).expect("workspace b should exist");
// #151: canonicalize so test expectations match the store's canonical
// workspace_root. Without this, the test builds sessions with a raw
// path but the store resolves to the canonical form.
let workspace_a = fs::canonicalize(&workspace_a).unwrap_or(workspace_a);
let workspace_b = fs::canonicalize(&workspace_b).unwrap_or(workspace_b);
let store_b = SessionStore::from_cwd(&workspace_b).expect("store b should build");
let legacy_root = workspace_b.join(".claw").join("sessions");
@@ -865,6 +923,8 @@ mod tests {
// given
let base = temp_dir();
fs::create_dir_all(&base).expect("base dir should exist");
// #151: canonicalize for path-representation consistency with store.
let base = fs::canonicalize(&base).unwrap_or(base);
let store = SessionStore::from_cwd(&base).expect("store should build");
let legacy_root = base.join(".claw").join("sessions");
let legacy_path = legacy_root.join("legacy-safe.jsonl");
@@ -893,6 +953,8 @@ mod tests {
// given
let base = temp_dir();
fs::create_dir_all(&base).expect("base dir should exist");
// #151: canonicalize for path-representation consistency with store.
let base = fs::canonicalize(&base).unwrap_or(base);
let store = SessionStore::from_cwd(&base).expect("store should build");
let legacy_root = base.join(".claw").join("sessions");
let legacy_path = legacy_root.join("legacy-unbound.json");

View File

@@ -27,6 +27,18 @@ impl InitStatus {
Self::Skipped => "skipped (already exists)",
}
}
/// Machine-stable identifier for structured output (#142).
/// Unlike `label()`, this never changes wording: claws can switch on
/// these values without brittle substring matching.
#[must_use]
pub(crate) fn json_tag(self) -> &'static str {
match self {
Self::Created => "created",
Self::Updated => "updated",
Self::Skipped => "skipped",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -58,6 +70,36 @@ impl InitReport {
lines.push(" Next step Review and tailor the generated guidance".to_string());
lines.join("\n")
}
/// Summary constant that claws can embed in JSON output without having
/// to read it out of the human-formatted `message` string (#142).
pub(crate) const NEXT_STEP: &'static str = "Review and tailor the generated guidance";
/// Artifact names that ended in the given status. Used to build the
/// structured `created[]`/`updated[]`/`skipped[]` arrays for #142.
#[must_use]
pub(crate) fn artifacts_with_status(&self, status: InitStatus) -> Vec<String> {
self.artifacts
.iter()
.filter(|artifact| artifact.status == status)
.map(|artifact| artifact.name.to_string())
.collect()
}
/// Structured artifact list for JSON output (#142). Each entry carries
/// `name` and machine-stable `status` tag.
#[must_use]
pub(crate) fn artifact_json_entries(&self) -> Vec<serde_json::Value> {
self.artifacts
.iter()
.map(|artifact| {
serde_json::json!({
"name": artifact.name,
"status": artifact.status.json_tag(),
})
})
.collect()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
@@ -333,7 +375,7 @@ fn framework_notes(detection: &RepoDetection) -> Vec<String> {
#[cfg(test)]
mod tests {
use super::{initialize_repo, render_init_claude_md};
use super::{initialize_repo, render_init_claude_md, InitStatus};
use std::fs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -413,6 +455,63 @@ mod tests {
fs::remove_dir_all(root).expect("cleanup temp dir");
}
#[test]
fn artifacts_with_status_partitions_fresh_and_idempotent_runs() {
// #142: the structured JSON output needs to be able to partition
// artifacts into created/updated/skipped without substring matching
// the human-formatted `message` string.
let root = temp_dir();
fs::create_dir_all(&root).expect("create root");
let fresh = initialize_repo(&root).expect("fresh init should succeed");
let created_names = fresh.artifacts_with_status(InitStatus::Created);
assert_eq!(
created_names,
vec![
".claw/".to_string(),
".claw.json".to_string(),
".gitignore".to_string(),
"CLAUDE.md".to_string(),
],
"fresh init should place all four artifacts in created[]"
);
assert!(
fresh.artifacts_with_status(InitStatus::Skipped).is_empty(),
"fresh init should have no skipped artifacts"
);
let second = initialize_repo(&root).expect("second init should succeed");
let skipped_names = second.artifacts_with_status(InitStatus::Skipped);
assert_eq!(
skipped_names,
vec![
".claw/".to_string(),
".claw.json".to_string(),
".gitignore".to_string(),
"CLAUDE.md".to_string(),
],
"idempotent init should place all four artifacts in skipped[]"
);
assert!(
second.artifacts_with_status(InitStatus::Created).is_empty(),
"idempotent init should have no created artifacts"
);
// artifact_json_entries() uses the machine-stable `json_tag()` which
// never changes wording (unlike `label()` which says "skipped (already exists)").
let entries = second.artifact_json_entries();
assert_eq!(entries.len(), 4);
for entry in &entries {
let status = entry.get("status").and_then(|v| v.as_str()).unwrap();
assert_eq!(
status, "skipped",
"machine status tag should be the bare word 'skipped', not label()'s 'skipped (already exists)'"
);
}
fs::remove_dir_all(root).expect("cleanup temp dir");
}
#[test]
fn render_init_template_mentions_detected_python_and_nextjs_markers() {
let root = temp_dir();

File diff suppressed because it is too large Load Diff

View File

@@ -180,6 +180,8 @@ fn resume_latest_restores_the_most_recent_managed_session() {
// given
let temp_dir = unique_temp_dir("resume-latest");
let project_dir = temp_dir.join("project");
fs::create_dir_all(&project_dir).expect("project dir should exist");
let project_dir = fs::canonicalize(&project_dir).unwrap_or(project_dir);
let store = runtime::SessionStore::from_cwd(&project_dir).expect("session store should build");
let older_path = store.create_handle("session-older").path;
let newer_path = store.create_handle("session-newer").path;