mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-11 17:39:30 +02:00
3ced68bf4a57a73ee9ee46a19648eb61b2bf66aa
943 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3ced68bf4a |
fix: #169 — command-graph and tool-pool now accept --output-format; diagnostic inventory JSON parity
Extends the diagnostic surface audit with the two inventory-structure
commands: command-graph (command family segmentation) and tool-pool
(assembled tool inventory). Both now expose their underlying rich
datastructures via JSON envelope.
Concrete additions:
- command-graph: --output-format {text,json}
- tool-pool: --output-format {text,json}
JSON envelope shapes:
command-graph:
{builtins_count, plugin_like_count, skill_like_count, total_count,
builtins: [{name, source_hint}],
plugin_like: [{name, source_hint}],
skill_like: [{name, source_hint}]}
tool-pool:
{simple_mode, include_mcp, tool_count,
tools: [{name, source_hint}]}
Backward compatibility:
- Default is 'text' (Markdown unchanged)
- Text output byte-identical to pre-#169
Tests (4 new, test_command_graph_tool_pool_output_format.py):
- TestCommandGraphOutputFormat (2): JSON structure + text compat
- TestToolPoolOutputFormat (2): JSON structure + text compat
Full suite: 137 → 141 passing, zero regression.
Closes ROADMAP #169.
Why this matters:
Claws auditing the codebase can now ask 'what commands exist' and
'what tools exist' and get structured, parseable answers instead of
regex-parsing Markdown headers and counting list items.
Related clusters:
- Diagnostic surfaces (#169 adds to #167/#168 work-verb parity)
- Inventory introspection (command-graph + tool-pool are the two
foundational 'what do we have?' queries)
|
||
|
|
3cad9864e6 |
fix: #168 — exec-command / exec-tool / route / bootstrap now accept --output-format; CLI family JSON parity COMPLETE
Extends the #167 inspect-surface parity fix to the four remaining CLI outliers: the commands claws actually invoke to DO work, not just inspect state. After this commit, the entire claw-code CLI family speaks a unified JSON envelope contract. Concrete additions: - exec-command: --output-format {text,json} - exec-tool: --output-format {text,json} - route: --output-format {text,json} - bootstrap: --output-format {text,json} JSON envelope shapes: exec-command (handled): {name, prompt, source_hint, handled: true, message} exec-command (not-found): {name, prompt, handled: false, error: {kind:'command_not_found', message, retryable: false}} exec-tool (handled): {name, payload, source_hint, handled: true, message} exec-tool (not-found): {name, payload, handled: false, error: {kind:'tool_not_found', message, retryable: false}} route: {prompt, limit, match_count, matches: [{kind, name, score, source_hint}]} bootstrap: {prompt, limit, setup: {python_version, implementation, platform_name, test_command}, routed_matches: [{kind, name, score, source_hint}], command_execution_messages: [str], tool_execution_messages: [str], turn: {prompt, output, stop_reason}, persisted_session_path} Exit codes (unchanged from pre-#168): 0 = success 1 = exec not-found (exec-command, exec-tool only) Backward compatibility: - Default (no --output-format) is 'text' - exec-command/exec-tool text output byte-identical - route text output: unchanged tab-separated kind/name/score/source_hint - bootstrap text output: unchanged Markdown runtime session report Tests (13 new, test_exec_route_bootstrap_output_format.py): - TestExecCommandOutputFormat (3): handled + not-found JSON; text compat - TestExecToolOutputFormat (3): handled + not-found JSON; text compat - TestRouteOutputFormat (3): JSON envelope; zero-matches case; text compat - TestBootstrapOutputFormat (2): JSON envelope; text-mode Markdown compat - TestFamilyWideJsonParity (2): parametrised over ALL 6 family commands (show-command, show-tool, exec-command, exec-tool, route, bootstrap) — every one accepts --output-format json and emits parseable JSON; every one defaults to text mode without a leading {. One future regression on any family member breaks this test. Full suite: 124 → 137 passing, zero regression. Closes ROADMAP #168. This completes the CLI-wide JSON parity sweep: - Session-lifecycle family: #160 (list/delete), #165 (load), #166 (flush) - Inspect family: #167 (show-command, show-tool) - Work-verb family: #168 (exec-command, exec-tool, route, bootstrap) ENTIRE CLI SURFACE is now machine-readable via --output-format json with typed errors, deterministic exit codes, and consistent envelope shape. Claws no longer need to regex-parse any CLI output. Related clusters: - Clawability principle: 'machine-readable in state and failure modes' (ROADMAP top-level). 9 pinpoints in this cluster; all now landed. - Typed-error envelope consistency: command_not_found / tool_not_found / session_not_found / session_load_failed all share {kind, message, retryable} shape. - Work-verb semantics: exec-* surfaces expose 'handled' boolean (not 'found') because 'not handled' is the operational signal — claws dispatch on whether the work was performed, not whether the entry exists in the inventory. |
||
|
|
b667558fb7 |
fix: #167 — show-command and show-tool now accept --output-format flag; CLI parity with session-lifecycle family
Closes the inspect-capability parity gap: show-command and show-tool were
the only discovery/inspection CLI commands lacking --output-format support,
making them outliers in the ecosystem that already had unified JSON
contracts across list-sessions, load-session, delete-session, and
flush-transcript (#160/#165/#166).
Concrete additions:
- show-command: --output-format {text,json}
- show-tool: --output-format {text,json}
JSON envelope shape (found case):
{name, found: true, source_hint, responsibility}
JSON envelope shape (not-found case):
{name, found: false, error: {kind:'command_not_found'|'tool_not_found',
message, retryable: false}}
Exit codes:
0 = success
1 = not found
Backward compatibility:
- Default (no --output-format) is 'text' (unchanged)
- Text output byte-identical to pre-#167 (three newline-separated lines)
Tests (10 new, test_show_command_tool_output_format.py):
- TestShowCommandOutputFormat (5): found + not-found in JSON; text mode
backward compat; text is default
- TestShowToolOutputFormat (3): found + not-found in JSON; text mode
backward compat
- TestShowCommandToolFormatParity (2): both accept same flag choices;
consistent JSON envelope shape
Full suite: 114 → 124 passing, zero regression.
Closes ROADMAP #167.
Why this matters:
Before: Claws calling show-command/show-tool had to parse human-readable
prose output via regex, with no structured error signal.
After: Same envelope contract as load-session and friends: JSON-first,
typed errors, machine-parseable.
Related clusters:
- Session-lifecycle CLI parity family (#160, #165, #166, #167)
- Machine-readable error contracts (same vein as #162 atomicity + #164
cancellation state-safety: structured boundaries for orchestration)
|
||
|
|
f0838395c0 |
fix: #164 Stage A — cooperative cancellation via cancel_event in submit_message
Closes the #161 follow-up gap identified in review: wall-clock timeout bounded caller-facing wait but did not cancel the underlying provider thread, which could silently mutate mutable_messages / transcript_store / permission_denials / total_usage after the caller had already observed stop_reason='timeout'. A ghost turn committed post-deadline would poison any session that got persisted afterwards. Stage A scope (this commit): runtime + engine layer cooperative cancel. Engine layer (src/query_engine.py): - submit_message now accepts cancel_event: threading.Event | None = None - Two safe checkpoints: 1. Entry (before max_turns / budget projection) — earliest possible return 2. Post-budget (after output synthesis, before mutation) — catches cancel that arrives while output was being computed - Both checkpoints return stop_reason='cancelled' with state UNCHANGED (mutable_messages, transcript_store, permission_denials, total_usage all preserved exactly as on entry) - cancel_event=None preserves legacy behaviour with zero overhead (no checkpoint checks at all) Runtime layer (src/runtime.py): - run_turn_loop creates one cancel_event per invocation when a deadline is in play (and None otherwise, preserving legacy fast path) - Passes the same event to every submit_message call across turns, so a late cancel on turn N-1 affects turn N - On timeout (either pre-call or mid-call), runtime explicitly calls cancel_event.set() before future.cancel() + synthesizing the timeout TurnResult. This upgrades #161's best-effort future.cancel() (which only cancels not-yet-started futures) to cooperative mid-flight cancel. Stop reason taxonomy after Stage A: 'completed' — turn committed, state mutated exactly once 'max_budget_reached' — overflow, state unchanged (#162) 'max_turns_reached' — capacity exceeded, state unchanged 'cancelled' — cancel_event observed, state unchanged (#164 Stage A) 'timeout' — synthesised by runtime, not engine (#161) The 'cancelled' vs 'timeout' split matters: - 'timeout' is the runtime's best-effort signal to the caller: deadline hit - 'cancelled' is the engine's confirmation: cancel was observed + honoured If the provider call wedges entirely (never reaches a checkpoint), the caller still sees 'timeout' and the thread is leaked — but any NEXT submit_message call on the same engine observes the event at entry and returns 'cancelled' immediately, preventing ghost-turn accumulation. This is the honest cooperative limit in Python threading land; true preemption requires async-native provider IO (future work, not Stage A). Tests (29 new tests, tests/test_submit_message_cancellation.py + tests/ test_run_turn_loop_cancellation.py): Engine-layer (12 tests): - TestCancellationBeforeCall (5): pre-set event returns 'cancelled' immediately; mutable_messages, transcript_store, usage, permission_denials all preserved - TestCancellationAfterBudgetCheck (1): cancel set mid-call (after projection, before commit) still honoured; output synthesised but state untouched - TestCancellationAfterCommit (2): post-commit cancel not observable (honest limit) BUT next call on same engine observes it + returns 'cancelled' - TestLegacyCallersUnchanged (3): cancel_event=None preserves #162 atomicity + max_turns contract with zero behaviour change - TestCancellationVsOtherStopReasons (2): cancel precedes max_turns check; cancel does not retroactively override a completed turn Runtime-layer (5 tests): - TestTimeoutPropagatesCancelEvent (3): submit_message receives a real Event object when deadline is set; None in legacy mode; timeout actually calls event.set() so in-flight threads observe at their next checkpoint - TestCancelEventSharedAcrossTurns (1): same event object passed to every turn (object identity check) — late cancel on turn N-1 must affect turn N Regression: 3 existing timeout test mocks updated to accept cancel_event kwarg (mocks that previously had signature (prompt, commands, tools, denials) now have (prompt, commands, tools, denials, cancel_event=None) since runtime passes cancel_event positionally on the timeout path). Full suite: 97 → 114 passing, zero regression. Closes ROADMAP #164 Stage A. What's explicitly NOT in Stage A: - Preemptive cancellation of wedged provider IO (requires asyncio-native provider path; larger refactor) - Timeout on the legacy unbounded run_turn_loop path (by design: legacy callers opt out of cancellation entirely) - CLI exposure of 'cancelled' as a distinct exit code (currently 'cancelled' maps to the same stop_reason != 'completed' break condition as others; CLI surface for cancel is a separate pinpoint if warranted) |
||
|
|
56bbe99f4e |
chore: gitignore .port_sessions/ to prevent dogfood-run pollution
Every 'claw flush-transcript' call without --directory writes to .port_sessions/<uuid>.json in CWD. Without a gitignore entry, every dogfood run leaves dozens of untracked files in the repo, masking real changes in 'git status' output. Now that #160/#166 ship structured session lifecycle commands and deterministic --session-id, this directory is purely transient by default — belongs in .gitignore. |
||
|
|
fd5950cf94 | fix: #166 — flush-transcript now accepts --directory / --output-format / --session-id; session-creation command parity with #160/#165 lifecycle triplet | ||
|
|
f8789316e7 |
fix: #159 — run_turn_loop no longer hardcodes empty denied_tools; permission denials now parity-match bootstrap_session
#159: multi-turn sessions had a silent security asymmetry: denied_tools were always empty in run_turn_loop, even though bootstrap_session inferred them from the routed matches. Result: any tool gated as 'destructive' (bash-family commands, rm, etc) would silently appear unblocked across all turns in multi-turn mode, giving a false 'clean' permission picture to any claw consuming TurnResult.permission_denials. Fix: compute denied_tools once at loop start via _infer_permission_denials, then pass the same denials to every submit_message call (both timeout and legacy unbounded paths). This mirrors the existing bootstrap_session pattern. Acceptance: run_turn_loop('run bash ls').permission_denials now matches what bootstrap_session returns — both infer the same denials from the routed matches. Multi-turn security posture is symmetric. Tests (tests/test_run_turn_loop_permissions.py, 2 tests): - test_turn_loop_surfaces_permission_denials_like_bootstrap: Symmetry check confirming both paths infer identical denials for destructive tools - test_turn_loop_with_continuation_preserves_denials: Denials inferred at loop start are passed consistently to all turns; captured via mock and verified non-empty Full suite: 82/82 passing, zero regression. Closes ROADMAP #159. |
||
|
|
5c9cede870 |
fix: #165 — load-session CLI now parity-matches list/delete (--directory, --output-format, typed JSON errors)
The #160 session-lifecycle CLI triplet was asymmetric: list-sessions and delete-session accepted --directory + --output-format and emitted typed JSON error envelopes, but load-session had neither flag and dumped a raw Python traceback (including the SessionNotFoundError class name) on a missing session. Three concrete impacts this fix closes: 1. Alternate session-store locations (e.g. /tmp/claw-run-XXX/.port_sessions) were unreachable via load-session; claws had to chdir or monkeypatch DEFAULT_SESSION_DIR to work around it. 2. Not-found emitted a multi-line Python stack, not a parseable envelope. Claws deciding retry/escalate/give-up had only exit code 1 to work with. 3. The traceback leaked 'src.session_store.SessionNotFoundError' verbatim, coupling version-pinned claws to our internal exception class name. Now all three triplet commands accept the same flag pair and emit the same JSON error shape: Success (json mode): {"session_id": "alpha", "loaded": true, "messages_count": 3, "input_tokens": 42, "output_tokens": 99} Not-found: {"session_id": "missing", "loaded": false, "error": {"kind": "session_not_found", "message": "session 'missing' not found in /path", "directory": "/path", "retryable": false}} Corrupted file: {"session_id": "broken", "loaded": false, "error": {"kind": "session_load_failed", "message": "...", "directory": "/path", "retryable": true}} Exit code contract: - 0 on successful load - 1 on not-found (preserves existing $?) - 1 on OSError/JSONDecodeError (distinct 'kind' in JSON) Backward compat: legacy 'claw load-session ID' text output unchanged byte-for-byte. Only new behaviour is the flags and structured error path. Tests (tests/test_load_session_cli.py, 13 tests): - TestDirectoryFlagParity (2): --directory works + fallback to CWD/.port_sessions - TestOutputFormatFlagParity (2): json schema + text-mode backward compat - TestNotFoundTypedError (2): JSON envelope on not-found; no traceback in either mode; no internal class name leak - TestLoadFailedDistinctFromNotFound (1): corrupted file = session_load_failed with retryable=true, distinct from session_not_found - TestTripletParityConsistency (6): parametrised over [list, delete, load] * [--directory, --output-format] — explicit parity guard for future regressions Full suite: 80/80 passing, zero regression. Discovered via Jobdori dogfood sweep 2026-04-22 17:44 KST — ran 'claw load-session nonexistent' expecting a clean error, got a Python traceback. Filed #165 + fixed in same commit. Closes ROADMAP #165. |
||
|
|
260ccd55d3 |
fix: #163 — remove [turn N] suffix pollution from run_turn_loop; file #164 timeout-cancellation followup
#163: run_turn_loop no longer injects f'{prompt} [turn N]' into follow-up prompts. The suffix was never defined or interpreted anywhere — not by the engine, not by the system prompt, not by any LLM. It looked like a real user-typed annotation in the transcript and made replay/analysis fragile. New behaviour: - turn 0 submits the original prompt (unchanged) - turn > 0 submits caller-supplied continuation_prompt if provided, else the loop stops cleanly — no fabricated user turn - added continuation_prompt: str | None = None parameter to run_turn_loop - added --continuation-prompt CLI flag for claws scripting multi-turn loops - zero '[turn' strings ever appear in mutable_messages or stdout now Behaviour change for existing callers: - Before: run_turn_loop(prompt, max_turns=3) submitted 3 turns ('prompt', 'prompt [turn 2]', 'prompt [turn 3]') - After: run_turn_loop(prompt, max_turns=3) submits 1 turn ('prompt') - To preserve old multi-turn behaviour, pass continuation_prompt='Continue.' or any structured follow-up text One existing timeout test (test_budget_is_cumulative_across_turns) updated to pass continuation_prompt so the cumulative-budget contract is actually exercised across turns instead of trivially satisfied by a one-turn loop. #164 filed: addresses reviewer feedback on #161. The wall-clock timeout bounds the caller-facing wait, but the underlying submit_message worker thread keeps running and can mutate engine state after the timeout TurnResult is returned. A cooperative cancel_event pattern is sketched in the pinpoint; real asyncio.Task.cancel() support will come once provider IO is async-native (larger refactor). Tests (tests/test_run_turn_loop_continuation.py, 8 tests): - TestNoTurnSuffixInjection (2): zero '[turn' strings in any submitted prompt, both default and explicit-continuation paths - TestContinuationDefaultStopsAfterTurnZero (2): default loops run exactly one turn; engine.submit_message called exactly once despite max_turns=10 - TestExplicitContinuationBehaviour (2): turn 0 = original, turn N = continuation verbatim; max_turns still respected - TestCLIContinuationFlag (2): CLI default emits only '## Turn 1'; --continuation-prompt wires through to multi-turn behaviour Full suite: 67/67 passing. Closes ROADMAP #163. Files #164. |
||
|
|
8d83a7e984 |
fix: #162 — budget-overflow no longer corrupts session state in submit_message
Previously, QueryEnginePort.submit_message() checked the token budget AFTER
appending the prompt to mutable_messages, transcript_store, and permission_denials,
and AFTER calling compact_messages_if_needed(). On overflow it set
stop_reason='max_budget_reached' but the overflow turn was already committed.
Any caller that persisted the session afterwards wrote the rejected prompt to
disk — the session was silently poisoned even though the TurnResult said the
turn never completed.
Fix:
- Restructure submit_message so the budget check early-returns BEFORE any
mutation of mutable_messages, transcript_store, permission_denials, or
total_usage.
- The returned TurnResult.usage reflects pre-call state (overflow never
advanced the usage counter).
- Normal (in-budget) path unchanged: mutation happens exactly once, at the
end, only on 'completed' results.
This closes the atomicity gap: submit_message is now either 'turn committed'
(stop_reason='completed') or 'turn rejected, state untouched'
(stop_reason in {'max_budget_reached', 'max_turns_reached'}). Callers can
safely retry with a fresh budget or a smaller prompt without worrying about
phantom committed turns from prior rejections.
Tests (tests/test_submit_message_budget.py, 10 tests):
- TestBudgetOverflowDoesNotMutate (5): mutable_messages / transcript /
permission_denials / total_usage / TurnResult.usage all pre-mutation after overflow
- TestOverflowPersistence (2): first-turn overflow persists empty session;
successful-turn-then-overflow persists only the successful turn
- TestEngineUsableAfterOverflow (2): subsequent in-budget call still works
with no residue; repeated overflows don't accumulate hidden state
- TestNormalPathStillCommits (1): regression guard — non-overflow path still
commits mutable_messages/transcript/usage as expected
Full suite: 59/59 passing, zero regression.
Blocker: none. Closes ROADMAP #162.
|
||
|
|
ef1bc6ba2d |
fix: #161 — wall-clock timeout for run_turn_loop; stalled turns now abort with stop_reason='timeout'
Previously, run_turn_loop was bounded only by max_turns (turn count). If engine.submit_message stalled — slow provider, hung network, infinite stream — the loop blocked indefinitely with no cancellation path. Claws calling run_turn_loop in CI or orchestration had no reliable way to enforce a deadline; the loop would hang until OS kill or human intervention. Fix: - Add timeout_seconds parameter to run_turn_loop (default None = legacy unbounded). - When set, each submit_message call runs inside a ThreadPoolExecutor and is bounded by the remaining wall-clock budget (total across all turns, not per-turn). - On timeout, synthesize a TurnResult with stop_reason='timeout' carrying the turn's prompt and routed matches so transcripts preserve orchestration context. - Exhausted/negative budget short-circuits before calling submit_message. - Legacy path (timeout_seconds=None) bypasses the executor entirely — zero overhead for callers that don't opt in. CLI: - Added --timeout-seconds flag to 'turn-loop' command. - Exit code 2 when the loop terminated on timeout (vs 0 for completed), so shell scripts can distinguish 'done' from 'budget exhausted'. Tests (tests/test_run_turn_loop_timeout.py, 6 tests): - Legacy unbounded path unchanged (timeout_seconds=None never emits 'timeout') - Hung submit_message aborted within budget (0.3s budget, 5s mock hang → exit <1.5s) - Budget is cumulative across turns (0.6s budget, 0.4s per turn, not per-turn) - timeout_seconds=0 short-circuits first turn without calling submit_message - Negative timeout treated as exhausted (guard against caller bugs) - Timeout TurnResult carries correct prompt, matches, UsageSummary shape Full suite: 49/49 passing, zero regression. Blocker: none. Closes ROADMAP #161. |
||
|
|
f97f29b4b2 |
feat(#160): wire claw list-sessions and delete-session CLI commands
Closes the last #160 gap: claws can now manage session lifecycle entirely through the CLI without filesystem hacks. New commands: - claw list-sessions [--directory DIR] [--output-format text|json] Enumerates stored session IDs. JSON mode emits {sessions, count}. Missing/empty directories return empty list (exit 0), not an error. - claw delete-session SESSION_ID [--directory DIR] [--output-format text|json] Idempotent: not-found is exit 0 with status='not_found' (no raise). Partial-failure: exit 1 with typed JSON error envelope: {session_id, deleted: false, error: {kind, message, retryable}} The 'session_delete_failed' kind is retryable=true so orchestrators know to retry vs escalate. Public API surface extended in src/__init__.py: - list_sessions, session_exists, delete_session - SessionNotFoundError, SessionDeleteError Tests added (tests/test_porting_workspace.py): - test_list_sessions_cli_runs: text + json modes against tempdir - test_delete_session_cli_idempotent: first call deleted=true, second call deleted=false (exit 0, status=not_found) - test_delete_session_cli_partial_failure_exit_1: permission error surfaces as exit 1 + typed JSON error with retryable=true All 43 tests pass. The session storage abstraction chapter is closed: - storage layer decoupled from claw code (#160 initial impl) - delete contract hardened + caller-audited (#160 hardening pass) - CLI wired with idempotency preserved at exit-code boundary (this commit) |
||
|
|
cd03f4df62 |
fix(#160): harden delete_session contract — idempotency, race-safety, typed partial-failure
Addresses review feedback on initial #160 implementation: 1. delete_session() contract now explicit: - Idempotent: delete(x); delete(x) is safe, second call returns False - Race-safe: TOCTOU between exists()/unlink() eliminated via unlink-then-catch - Partial-failure typed: permission/IO errors wrapped in SessionDeleteError (OSError subclass) so callers can distinguish 'not found' (return False) from 'could not delete' (raise) 2. New SessionDeleteError class for partial-failure surfacing. Distinct from SessionNotFoundError (KeyError subclass for missing loads). 3. Caller audit confirmed: no code outside session_store globs .port_sessions or imports DEFAULT_SESSION_DIR. Storage layout is fully encapsulated. 4. Added tests/test_session_store.py — 18 tests covering: - list_sessions: empty/missing/sorted/non-json filter - session_exists: true/false/missing-dir - load_session: SessionNotFoundError typing (KeyError subclass, not FileNotFoundError) - delete_session idempotency: first/second/never-existed calls - delete_session partial-failure: SessionDeleteError wraps OSError - delete_session race-safety: concurrent deletion returns False, not raise - Full save->list->exists->load->delete roundtrip All 18 tests pass. Merge-ready: contract documented, caller-audited, race-safe. |
||
|
|
b295043a0d |
fix: #160 — add list_sessions, session_exists, delete_session to session_store
- list_sessions(directory=None) -> list[str]: enumerate stored session IDs - session_exists(session_id, directory=None) -> bool: check existence without FileNotFoundError - delete_session(session_id, directory=None) -> bool: unlink a session file - load_session now raises typed SessionNotFoundError (subclass of KeyError) instead of FileNotFoundError - Claws can now manage session lifecycle without reaching past the module to glob filesystem Closes ROADMAP #160. Acceptance: claw can call list_sessions(), session_exists(id), delete_session(id) without importing Path or knowing .port_sessions/<id>.json layout. |
||
|
|
0cb16cf7cb | file: #161 — run_turn_loop has no wall-clock timeout, stalled turn blocks indefinitely | ||
|
|
1da2781816 |
Merge pull request #2835 from ultraworkers/docs/roadmap-249-issue-github-oauth-opacity
docs(roadmap): add #249 issue GitHub OAuth opacity pinpoint |
||
|
|
9037430d52 | docs(roadmap): add #249 issue github oauth opacity pinpoint | ||
|
|
8e22f757d8 |
Merge pull request #2834 from ultraworkers/docs/roadmap-248-prompt-mode-silent-hang
docs(roadmap): add #248 prompt-mode silent-hang pinpoint |
||
|
|
7676b376ae | docs(roadmap): add #248 prompt-mode silent-hang pinpoint | ||
|
|
1011a83823 |
Merge pull request #2829 from ultraworkers/fix/issue-320-session-lifecycle-classification
Fix session lifecycle classification for idle tmux shells |
||
|
|
1376d92064 |
Filter stub commands from resume-safe help
Keep claw --help's resume-safe slash command summary aligned with the interactive command list by filtering STUB_COMMANDS and adding regression coverage. |
||
|
|
be53e04671 |
Classify saved sessions by live work rather than pane existence
Operator status previously treated any tmux pane in a workspace as equivalent to active work. The new classifier uses tmux pane command/path metadata as a soft signal, treats plain shells as idle, and adds dirty-worktree abandoned markers to status and session-list output for clawhip consumers. Constraint: Keep issue #320 prototype minimal and additive without new dependencies Rejected: Screen-scraping pane output | fragile and broader than needed for lifecycle classification Confidence: high Scope-risk: narrow Tested: cargo test -p rusty-claude-cli Tested: cargo check -p rusty-claude-cli Not-tested: cargo clippy -p rusty-claude-cli --all-targets -- -D warnings is blocked by pre-existing commands crate clippy::unnecessary_wraps warnings |
||
|
|
cb56dc12ab |
Document Rust formatting wrapper
Make scripts/fmt.sh robust to caller cwd and document it as the supported repo-root formatting entrypoint for the Rust workspace. |
||
|
|
71686a20fc |
Resolve fmt wrapper path from its own directory
The formatting wrapper should remain safe when invoked through different current directories or shell contexts, so resolve the script directory before entering the Rust workspace and forwarding cargo fmt arguments. Constraint: Wrapper must be runnable from repo root while forwarding flags like --check Rejected: Leave relative dirname cd | less robust if invocation context changes Confidence: high Scope-risk: narrow Tested: scripts/fmt.sh --check Tested: git diff --check |
||
|
|
07992b8a1b |
Make Rust formatting guidance runnable from repo root
The Rust crate layout expects formatting to run from the rust directory, so add a root-level wrapper that preserves the working command while forwarding user flags like --check. Documentation now points contributors at the wrapper instead of the misleading virtual-workspace manifest invocation. Constraint: Root-level cargo fmt --manifest-path rust/Cargo.toml is misleading for this virtual workspace Rejected: Document cd rust && cargo fmt directly | a root wrapper gives one stable repo-root command Confidence: high Scope-risk: narrow Tested: scripts/fmt.sh --check Tested: git diff --check |
||
|
|
74ea754d29 |
Restore Rust formatting compliance
Run rustfmt from the Rust workspace so CI format checks pass without changing behavior. Constraint: Scope is formatting-only across tracked Rust files Confidence: high Scope-risk: narrow Tested: cd rust && cargo fmt --check Tested: git diff --check |
||
|
|
77afde768c |
Clarify allowed tool status handling
Reject empty --allowedTools inputs instead of treating them as an empty restriction, and surface status JSON metadata that distinguishes default unrestricted tools from flag-provided allow lists. Confidence: high Scope-risk: narrow Tested: cargo test -p rusty-claude-cli rejects_empty_allowed_tools_flag -- --nocapture Tested: cargo test -p tools allowed_tools_rejects_empty_token_lists -- --nocapture Tested: cargo check -p rusty-claude-cli -p tools Tested: cargo test -p rusty-claude-cli -p tools Not-tested: full workspace cargo fmt --check is blocked by pre-existing unrelated formatting drift |
||
|
|
6db68a2baa |
Expose tool permission gates as structured worker blockers
Worker boot could previously stall on an interactive MCP/tool permission prompt while readiness and startup-timeout surfaces only had generic idle/no-evidence shapes. This adds a first-class blocked lifecycle state, structured event payload, startup evidence fields, and regression coverage so callers can report the exact server/tool gate instead of pane-scraping. Constraint: ROADMAP #200 requires tool/server identity, prompt age, and session-only versus always-allow capability in status/evidence surfaces Rejected: Treat MCP/tool prompts as trust gates | conflates distinct prompts and loses tool identity Rejected: Leave allow-scope as pane text only | clawhip still could not classify the blocker without scraping Confidence: high Scope-risk: moderate Directive: Keep tool_permission_required distinct from trust_required; downstream claws rely on server/tool payload plus allow-scope metadata Tested: cargo test -p runtime tool_permission Tested: cargo fmt -p runtime -- --check && cargo clippy -p runtime --all-targets -- -D warnings && cargo test -p runtime Tested: cargo test --workspace Not-tested: live interactive MCP permission prompt in tmux |
||
|
|
5b910356a2 |
Preserve trust boundaries during pulled follow-up
The pull brought the branch current with origin/main while replaying local follow-up work. Conflict resolution kept the roadmap/progress additions and integrated the runtime event/trust changes with upstream's newer surfaces. The trust allowlist now treats worktree_pattern as an additional required predicate, including the missing-worktree case, so auto-trust cannot fall back to cwd-only matching when a worktree constraint was declared. The runtime formatting cleanup keeps clippy/fmt green after the merge. Constraint: Local branch was 109 commits behind origin/main with dirty tracked follow-up work. Rejected: Drop the autostash after conflict resolution | keeping it preserves a reversible safety backup for unrelated recovery. Confidence: high Scope-risk: moderate Directive: Do not relax worktree_pattern matching without preserving the missing-worktree regression. Tested: git diff --cached --check; cargo fmt -p runtime -- --check; cargo clippy -p runtime --all-targets -- -D warnings; cargo test -p runtime; cargo test --workspace; architect verification approved Not-tested: Live tmux/worker auto-trust behavior outside unit/integration tests |
||
|
|
a389f8dff1 | file: #160 — session_store missing list_sessions, delete_session, session_exists — claw cannot enumerate or clean up sessions without filesystem hacks | ||
|
|
7a014170ba | file: #159 — run_turn_loop hardcodes empty denied_tools, permission denials absent from multi-turn sessions | ||
|
|
986f8e89fd | file: #158 — compact_messages_if_needed drops turns silently, no structured compaction event | ||
|
|
ef1cfa1777 |
file: #157 — structured remediation registry for error hints (Phase 3 of #77)
## Gap #77 Phase 1 added machine-readable error kind discriminants and #156 extended them to text-mode output. However, the hint field is still prose derived from splitting existing error text — not a stable registry-backed remediation contract. Downstream claws inspecting the hint field still need to parse human wording to decide whether to retry, escalate, or terminate. ## Fix Shape 1. Remediation registry: remediation_for(kind, operation) -> Remediation struct with action (retry/escalate/terminate/configure), target, and stable message 2. Stable hint outputs per error class (no more prose splitting) 3. Golden fixture tests replacing split_error_hint() string hacks ## Source gaebal-gajae dogfood sweep 2026-04-22 05:30 KST |
||
|
|
f1e4ad7574 |
feat: #156 — error classification for text-mode output (Phase 2 of #77)
## Problem #77 Phase 1 added machine-readable error `kind` discriminants to JSON error payloads. Text-mode (stderr) errors still emit prose-only output with no structured classification. Observability tools (log aggregators, CI error parsers) parsing stderr can't distinguish error classes without regex-scraping the prose. ## Fix Added `[error-kind: <class>]` prefix line to all text-mode error output. The prefix appears before the error prose, making it immediately parseable by line-based log tools without any substring matching. **Examples:** ## Impact - Stderr observers (log aggregators, CI systems) can now parse error class from the first line without regex or substring scraping - Same classifier function used for JSON (#77 P1) and text modes - Text-mode output remains human-readable (error prose unchanged) - Prefix format follows syslog/structured-logging conventions ## Tests All 179 rusty-claude-cli tests pass. Verified on 3 different error classes. Closes ROADMAP #156. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |