- Remove retry_after: None from ApiError::Api structs in openai_compat.rs (field was removed)
- Remove SlashCommand::Team parse arm (variant was removed from enum)
- Add config_load_error_kind: None to doctor path StatusContext initializer
- Add Thinking arm to all ContentBlock match blocks in trident.rs
- Remove cargo fmt drift across commands, config, compact, tools, trident
Adds aspect-rs AOP-style logging to execute_tool_with_enforcer, providing cross-cutting arg/result logging for all tool dispatches. Introduces aspect-core, aspect-macros, and aspect-std dependencies.
Fixes bundled_root() to resolve the bundled plugins directory relative to the executable path at runtime instead of using a compile-time CARGO_MANIFEST_DIR path that may be root-owned. Resolution order: standard FHS layout, adjacent layout, then dev/source-tree fallback. Includes proper tests for override, nonexistent, and auto-detection scenarios.
Instead of re-nesting prior highlights under '- Previously compacted context:', flatten them directly into the top-level list with '- ' prefix. This prevents each compaction cycle from adding a nesting layer, which inflated the summary by ~depth * overhead per turn.
Adds early return in wire_model_for_base_url for Gemini/Gemma/XAI/Kimi/Grok model prefixes to ensure the provider prefix is preserved correctly when routing through the OpenAI-compatible provider path.
Threads reasoning_content back into Thinking blocks for DeepSeek V4 multi-turn calls. Adds pending_thinking accumulator to capture thinking/signature delta events during streaming, and converts ContentBlock::Thinking to InputContentBlock::Thinking in convert_messages to preserve reasoning between turns, fixing the 'reasoning_content must be passed back' error.
Fixes alias resolution ordering: aliases (opus/sonnet/haiku) are now resolved to their full provider/model form BEFORE syntax validation. Previously, aliases bypassed validation via an early-return check. Also adds the 'log' crate for debug tracing of alias resolution and wraps PermissionsExt import in #[cfg(unix)] for portability.
Adds MAX_GIT_DIFF_CHARS (50_000) limit and truncate_diff() function to prevent oversized git diffs from blowing up the system prompt. Truncation respects UTF-8 character boundaries and appends a clear truncation notice. Includes unit tests.
Changes the catch-all arm in model_token_limit() from None to conservative defaults (max_output_tokens: 16_384, context_window_tokens: 131_072) to prevent crashes when an unknown model is used.
Adds OPENAI_API_KEY detection to check_auth_health() alongside existing api_key and auth_token checks, creating a combined any_auth_present variable. Also displays openai_key presence in the environment details.
Sets user_agent on both build_http_client_or_default() and build_http_client_with() to 'clawd-rust-tools/0.1' for consistent HTTP client identification.
Resolve the G012 evidence gate by fixing permission-mode regressions, platform-sensitive tests, and the clippy surface that blocked an all-targets verification run.
Constraint: G012 final gate required docs, board, full workspace tests, and clippy -D warnings evidence before checkpointing.
Rejected: documenting the worker-2 gate failure as an accepted gap | the failing tests and lints were locally reproducible and fixable.
Confidence: high
Scope-risk: moderate
Directive: Preserve read-only permission requirements for read/glob/grep tools; write/edit remain workspace-write or danger-full-access when outside the workspace.
Tested: python3 .github/scripts/check_doc_source_of_truth.py; python3 .github/scripts/check_release_readiness.py; python3 scripts/validate_cc2_board.py --board .omx/cc2/board.json; python3 .omx/cc2/validate_issue_parity_intake.py .omx/cc2/issue-parity-intake.json; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml --workspace; cargo test --manifest-path rust/Cargo.toml --workspace -- --nocapture; cargo clippy --manifest-path rust/Cargo.toml --workspace --all-targets -- -D warnings
Not-tested: live network provider smoke tests and remote PR/issue mutations.
The resumed session command matcher now owns every /session variant in one arm so adding supported actions does not leave a redundant fallback that triggers unreachable-pattern warnings.\n\nConstraint: G010 session hygiene verification requires clean focused tests around resume/session recovery UX.\nRejected: Leave the warning in place | it obscures real regressions in the focused recovery checks.\nConfidence: high\nScope-risk: narrow\nDirective: Keep new /session resume-mode actions inside run_resumed_session_command rather than duplicating match arms.\nTested: cargo test -p rusty-claude-cli context_window_preflight_errors_render_recovery_steps\nNot-tested: full workspace test suite
Co-authored-by: OmX <omx@local>
Resolve an overlap introduced while adding G010 session command UX so resumed /session handling remains exhaustive without an unreachable fallback arm.\n\nConstraint: G010 leader verification found rusty-claude-cli failed to compile because SlashCommand::Session was both handled and still listed in the unsupported command union.\nRejected: leaving worker lanes to rediscover the compile failure | leader had direct verification evidence and a one-line localized fix.\nConfidence: high\nScope-risk: narrow\nDirective: Keep resumed /session subcommands routed through run_resumed_session_command; do not re-add Session to the unsupported slash-command union.\nTested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test resume_slash_commands -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test compact_output -- --nocapture; git diff --check\nNot-tested: full cargo test --workspace not rerun for this one-line compile fix; G010 leader verification continues separately.
Keep the G008 capability and diagnostic helpers compile-ready by restoring the public report/support/severity types that team integrations referenced after merge reconciliation.
Constraint: Final G008 verification failed on missing provider capability and diagnostic type definitions.
Confidence: high
Scope-risk: narrow
Directive: Keep provider diagnostics exported as typed API surfaces; do not replace them with ad-hoc JSON-only status fields.
Tested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; git diff --check; cargo test --manifest-path rust/Cargo.toml -p api providers:: -- --nocapture --test-threads=1; cargo test --manifest-path rust/Cargo.toml -p api --test openai_compat_integration -- --nocapture --test-threads=1
Not-tested: full workspace clippy; known unrelated runtime policy_engine struct_excessive_bools remains outside G008.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Keep the mock HTTP/SSE/proxy coverage deterministic under strict linting while preserving provider request behavior.\n\nConstraint: Task 4 scope is limited to OpenAI-compatible HTTP/SSE/proxy coverage and provider compatibility surfaces.\nRejected: Environment-variable proxy testing | It races with parallel integration tests and can route unrelated localhost mocks through a single proxy fixture.\nConfidence: high\nScope-risk: narrow\nDirective: Prefer explicit injected reqwest clients for proxy integration tests instead of mutating process proxy environment.\nTested: cargo fmt --check; cargo check -p api; cargo test -p api --test openai_compat_integration -- --nocapture; cargo test -p api\nNot-tested: cargo clippy --no-deps -p api --all-targets -- -D warnings fails on pre-existing anthropic.rs/providers/mod.rs lints outside task scope.\n\nCo-authored-by: OmX <omx@local>
Keep integrated G008 provider changes formatted and compile-ready so worker follow-up commits can merge against a clean leader baseline.
Constraint: G008 provider verification must pass before ultragoal checkpointing.
Confidence: high
Scope-risk: narrow
Directive: Keep provider compatibility follow-ups rebased on this formatted baseline before retrying failed cherry-picks.
Tested: cargo test --manifest-path rust/Cargo.toml -p api providers:: -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p api --test openai_compat_integration -- --nocapture --test-threads=1
Not-tested: full workspace clippy; known pre-existing runtime policy_engine LaneContext clippy warning remains outside this change.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Expose MCP server requiredness through config parsing, inventory reports, config hashes, and degraded startup failure context so orchestrators can distinguish optional degradation from required startup breakage.
Constraint: G007-plugin-mcp Task 3 requires required vs optional MCP behavior and must not mutate .omx/ultragoal.
Rejected: Treating all MCP failures as equivalent | it preserves the existing opacity that prevents required-server failures from being escalated differently.
Confidence: high
Scope-risk: moderate
Directive: Preserve required=false as the backward-compatible default; keep required surfaced in JSON/text inventory and degraded failure context when extending MCP lifecycle states.
Tested: cargo test -p runtime parses_typed_mcp_and_oauth_config -- --nocapture; cargo test -p runtime manager_discovery_report_keeps_healthy_servers_when_one_server_fails -- --nocapture; cargo test -p runtime manager_records_unsupported_non_stdio_servers_without_panicking -- --nocapture; cargo test -p commands renders_mcp_reports -- --nocapture; cargo check --workspace; cargo fmt --all -- --check
Not-tested: cargo clippy -p runtime -p commands -- -D warnings is blocked by pre-existing runtime/src/policy_engine.rs LaneContext clippy::struct_excessive_bools.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Lock the plugin inventory JSON contract so lifecycle state and lifecycle summary fields stay visible to orchestrators while allowing bundled plugins to coexist in isolated inventories.
Constraint: G007-plugin-mcp Task 1 requires plugin/MCP lifecycle contract evidence without mutating .omx/ultragoal.
Rejected: Assuming an empty plugin inventory in tests | bundled plugins are auto-synced and should not make lifecycle contract verification brittle.
Confidence: high
Scope-risk: narrow
Directive: Keep plugin inventory JSON machine-readable for lifecycle_state, lifecycle, status, and load_failures; do not collapse it back to message-only JSON.
Tested: cargo test -p plugins plugin_registry_report_collects_load_failures_without_dropping_valid_plugins -- --nocapture; cargo test -p commands renders_plugins_report -- --nocapture; cargo test -p rusty-claude-cli --test output_format_contract plugins_json_surfaces_lifecycle_contract_when_plugin_is_installed -- --nocapture; cargo test -p rusty-claude-cli --test output_format_contract inventory_commands_emit_structured_json_when_requested -- --nocapture; cargo check --workspace; cargo fmt --all -- --check
Not-tested: cargo clippy -p rusty-claude-cli --test output_format_contract -- -D warnings is blocked by pre-existing runtime::policy_engine::LaneContext clippy::struct_excessive_bools.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Route plugin command rendering through the same degraded config envelope used by status and MCP, falling back to empty runtime config when config loading fails so local plugin listing remains inspectable.
Constraint: Task 4 requires malformed MCP config consistency across status, doctor, mcp, and plugins surfaces.
Rejected: Hard-failing plugins on ConfigLoader errors | inconsistent with status/mcp degraded-mode contract and hides local plugin diagnostics.
Confidence: high
Scope-risk: narrow
Directive: Keep config_load_error/status fields aligned across local introspection commands when adding new config-dependent surfaces.
Tested: cargo test -p rusty-claude-cli malformed_mcp_config -- --nocapture; cargo test -p commands mcp_degrades_gracefully_on_malformed_mcp_config_144 -- --nocapture; cargo check -p rusty-claude-cli; cargo fmt --all -- --check; claw plugins --output-format json malformed-MCP smoke.
Not-tested: full workspace clippy remains blocked by pre-existing clippy warnings in runtime and rusty-claude-cli unrelated to this change.
Constraint: G007 worker integrations added plugin command surfaces but left the REPL handler referencing a pre-refactor variable.\nRejected: Revert the worker plugin-command surface | the parser/degraded-config changes are part of the G007 scope and only needed a narrow compile repair.\nConfidence: high\nScope-risk: narrow\nDirective: Keep plugin CLI and REPL command paths routed through plugins_command_payload_for so malformed config can degrade consistently.\nTested: cargo check --manifest-path rust/Cargo.toml -p runtime -p tools -p rusty-claude-cli -p commands -p plugins; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli parse_args_plugins -- --nocapture\nNot-tested: full G007 team suite pending worker completion\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
Promote merge-ready green contracts from a level-only check to explicit provenance requirements for test commands, base freshness, recovery-attempt context, and known blocking flakes. This preserves simple level contracts while giving policy code a single satisfied-contract signal to require before merge decisions.\n\nConstraint: Task scope was limited to green_contract.rs, policy_engine.rs if needed, and narrow tests; stale_* and recovery_recipes.rs were not edited.\nRejected: Adding more boolean fields to GreenContract | clippy flagged the shape and a requirement list is more explicit.\nConfidence: high\nScope-risk: narrow\nDirective: Treat raw test level as insufficient for merge readiness unless green contract evidence is satisfied.\nTested: cargo check --manifest-path rust/Cargo.toml -p runtime; cargo test --manifest-path rust/Cargo.toml -p runtime; cargo clippy --manifest-path rust/Cargo.toml -p runtime -- -D warnings; focused green_contract, policy_engine, and integration tests.\nNot-tested: full workspace cargo test due pre-existing rusty-claude-cli session_lifecycle_prefers_running_process_over_idle_shell failure observed before this slice.
Add a runtime approval-token ledger so policy-blocked actions can require scoped owner grants, consume one-time tokens, reject replay, and retain delegation traceability.\n\nConstraint: Task 3 scope is the G004 approval-token chain for runtime event/report contract families.\nRejected: Extending the existing permission prompt path directly | the token contract can be tested independently without changing live tool authorization behavior.\nConfidence: high\nScope-risk: narrow\nDirective: Keep approval grants scoped to policy/action/repo/branch before wiring them into external execution paths.\nTested: cargo check --manifest-path rust/Cargo.toml --workspace; cargo test --manifest-path rust/crates/runtime/Cargo.toml; cargo test --manifest-path rust/crates/runtime/Cargo.toml approval_token -- --nocapture\nNot-tested: cargo clippy --manifest-path rust/crates/runtime/Cargo.toml --all-targets -- -D warnings is blocked by pre-existing warnings in compact.rs, file_ops.rs, policy_engine.rs, and sandbox.rs.
Constraint: G003 boot/session work adds a structured doctor boot-preflight check that must be visible in JSON output.
Rejected: reducing the doctor check count back to six | boot preflight is an explicit G003 acceptance surface.
Confidence: high
Scope-risk: narrow
Directive: Keep doctor/status JSON contract tests aligned with boot_preflight schema fields when extending preflight diagnostics.
Tested: git diff --check; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p runtime trusted_roots -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime startup -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime worker_boot -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools path_scope -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract -- --nocapture; cargo check --manifest-path rust/Cargo.toml --workspace
Not-tested: full cargo test --workspace remains deferred during active G003 team reconciliation.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Canonicalize absolute shell path operands before comparing them with the workspace root so symlink-expanded reads cannot be downgraded under workspace-write enforcement. Also resolves local clippy findings in the touched tools crate so targeted linting can run cleanly.\n\nConstraint: Task 1 scope is workspace/path scope enforcement only; do not mutate .omx/ultragoal.\nRejected: Editing shared path-scope regression tests | worker-3 owns that test coverage and the current tests already prove the contract.\nConfidence: high\nScope-risk: narrow\nDirective: Keep shell/file permission classification canonical-path based before permitting workspace-write execution.\nTested: ../scripts/fmt.sh --check; cargo test -p tools --test path_scope_enforcement -- --nocapture; cargo test -p tools given_workspace_write_enforcer_when_bash -- --nocapture; cargo check -p tools; cargo clippy -p tools --all-targets --no-deps -- -D warnings\nNot-tested: Full workspace clippy still has known unrelated runtime crate warnings outside this task scope.
Constraint: G003 worker outputs added config and startup evidence fields that must compile under focused runtime validation before leader push.
Rejected: pushing auto-checkpoints without leader validation | integrated tests initially failed to compile due missing imports and stale StartupEvidenceBundle fixtures.
Confidence: high
Scope-risk: narrow
Directive: When extending StartupEvidenceBundle, update all in-crate fixtures in the same change.
Tested: git diff --check; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p runtime trusted_roots -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime startup -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime worker_boot -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools path_scope -- --nocapture; cargo check --manifest-path rust/Cargo.toml --workspace
Not-tested: full cargo test --workspace remains deferred during active G003 team work.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Worker-3's path-scope regression showed outside read_file paths were blocked by the workspace wrapper after dispatch instead of by the permission enforcer. File, glob, and grep tools now classify path scope before dispatch and require danger-full-access for paths that resolve outside the current workspace.
Constraint: G002-alpha-security requires permission-mode event/status visibility for blocked file and shell paths
Rejected: relying only on runtime wrapper errors | it hides the active permission-mode denial contract from callers
Confidence: high
Scope-risk: narrow
Directive: keep path-sensitive tool permission classification aligned with workspace wrapper resolution
Tested: cargo test -p tools --test path_scope_enforcement --manifest-path rust/Cargo.toml --quiet; cargo test -p tools given_workspace_write_enforcer_when_bash --manifest-path rust/Cargo.toml --quiet; cargo check --manifest-path rust/Cargo.toml --workspace; cargo fmt --all --manifest-path rust/Cargo.toml -- --check
Not-tested: full workspace test suite after this small permission-classification follow-up
Co-authored-by: OmX <omx@oh-my-codex.dev>
Constraint: G002-alpha-security requires direct file-tool escapes to fail before reads while accepting the canonical runtime error text.
Rejected: weakening the test to accept successful reads | the verified behavior denies the escape and only the assertion vocabulary was stale.
Confidence: high
Scope-risk: narrow
Directive: Keep path-scope tests asserting denial semantics, not a single legacy wording.
Tested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p tools path_scope -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools --test path_scope_enforcement -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime workspace_ -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract -- --nocapture; python3 -m pytest tests/test_security_scope.py -q; cargo check --manifest-path rust/Cargo.toml --workspace; git diff --check
Not-tested: full cargo test --workspace due known unrelated session_lifecycle_prefers_running_process_over_idle_shell failure.
Co-authored-by: OmX <omx@oh-my-codex.dev>
The G002 security gate caught that PowerShell path classification still treated Windows absolute paths as workspace-relative on POSIX, so workspace scope now rejects those tokens before permission downgrades.
Constraint: G002-alpha-security requires workspace/path scope across Windows path cases as well as direct paths, symlinks, globbing, shell expansion, and worktrees.
Rejected: Relying on PathBuf::is_absolute for Windows syntax on POSIX | it treats C:\ and UNC-like tokens as relative and weakens permission classification.
Confidence: high
Scope-risk: narrow
Directive: Keep bash and PowerShell path classifiers aligned whenever new shell syntax is admitted.
Tested: cargo test --manifest-path rust/Cargo.toml -p tools path_scope -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools --test path_scope_enforcement -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime workspace_ -- --nocapture; python3 -m pytest tests/test_security_scope.py -q; cargo check --manifest-path rust/Cargo.toml --workspace.
Not-tested: Full cargo test --workspace still has existing unrelated rusty-claude-cli session lifecycle failure reported by workers.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Cherry-pick from Yeachan-Heo's #2945 with manual conflict resolution:
- classify_skills_slash_command now catches -h/--help anywhere in args
- Restored pending_thinking parameter in push_output_block test calls
Co-authored-by: Yeachan-Heo <bellman@ultraworkers.dev>
Five interrelated fixes from parallel Hephaestus sessions:
1. fix(repl): display assistant text after spinner (#2981, #2982, #2937)
- Added final_assistant_text() call after run_turn spinner completes
- REPL now shows response text like run_prompt_json does
2. fix(compact): handle Thinking content blocks (#2985)
- Added ContentBlock::Thinking variant throughout compact summarizer
- Prevents panic when /compact encounters thinking blocks
3. fix(prompt): provider-aware model identity (#2822)
- New ModelFamilyIdentity enum (Claude vs Generic)
- Non-Anthropic models no longer say 'I am Claude'
- model_family_identity_for() detects provider and sets identity
4. fix(openai): preserve DeepSeek reasoning_content (#2821)
- Stream parser now captures reasoning_content from OpenAI-compat
- Emits ThinkingDelta/SignatureDelta events for reasoning models
- Thinking blocks included in conversation history for re-send
5. feat(runtime): Thinking block support across codebase
- AssistantEvent::Thinking variant in conversation.rs
- ContentBlock::Thinking in session serialization
- Thinking-aware compact summarization
- Tests for thinking block ordering and content
Closes#2981, #2982, #2937, #2985, #2822, #2821
* fix(mcp): exit 1 when JSON envelope contains ok:false
mcp info, mcp describe, and mcp list-filter all return
{"action":"error","ok":false,...} but previously exited 0,
requiring automation callers to inspect the envelope field.
After this fix: print_mcp detects ok:false in the rendered JSON
value and calls process::exit(1) after printing, so the exit code
reflects the semantic error in the envelope.
Unaffected: mcp list, mcp show, mcp help all have no ok field and
continue to exit 0 (they are not error paths).
Closes ROADMAP #68 (partial — agents bogus/mcp show nonexistent
found:false remain exit:0 as they use different envelope shapes).
* feat(scripts): add dogfood-build.sh — build from checkout and verify provenance
Builds claw from the current HEAD, then checks that the binary's
git_sha matches git rev-parse --short HEAD. Exits non-zero if the
binary is stale or provenance is opaque (git_sha: null).
Usage:
CLAW=$(bash scripts/dogfood-build.sh) # fail-fast if stale
$CLAW version --output-format json # provenance confirmed
Addresses ROADMAP #69: dogfooders using a stale installed binary
cannot attribute behavior to specific commits. This script makes
dogfood round zero unambiguous.
Also documents the safe workaround for contributors who have a
stale system-installed binary.
claw permissions list / claw permissions allow <tool> / claw permissions deny <tool>
all fell through to the prompt/LLM path because parse_subcommand had no
arm for "permissions". The single-word bare form was already intercepted
by bare_slash_command_guidance, but any form with rest.len() > 1 bypassed
the single-word guard and landed in the _other => CliAction::Prompt branch.
Fix: add a "permissions" arm in parse_subcommand that returns a structured
guidance Err so all multi-word forms get the same exit:1 + JSON error as
the bare single-word form, without any LLM call or session creation.
Verified: all invocation forms (bare, list, read-only, workspace-write,
allow/deny <tool>) exit 1 with kind:unknown guidance JSON. Zero sessions.
claw plugin list / claw marketplace / claw marketplace list all fell
through to the prompt/LLM path because parse_subcommand only matched
"plugins" (the primary name) while the canonical spec aliases
"plugin" and "marketplace" were unhandled.
This manifested as auth errors and session creation on direct
invocation — dogfood confirmed Gaebal's binary created one session
via plugin prompt fallback.
Fix: extend the plugins arm in parse_subcommand to also match
"plugin" | "marketplace" so all three forms route to the same
CliAction::Plugins without network calls or session creation.
Verified: all six forms (bare + list subcommand for each name) return
kind:plugin JSON, exit 0, and create zero sessions.
Closes ROADMAP #55 partial (plugins/marketplace bypass complete).
* fix: support /plugins slash command in resume mode
Move SlashCommand::Plugins out of the 'unsupported resumed slash
command' catch-all and add a handler arm in run_resume_command that
calls handle_plugins_slash_command for list/help actions.
Mutation actions (install/uninstall/enable/disable) are rejected with
a clear error since there is no runtime to reload in resume mode.
Add /plugins coverage to resumed_inventory_commands test in
output_format_contract.rs: kind, action, reload_runtime, target.
Before: claw --resume session.jsonl /plugins --output-format json
-> {error: 'unsupported resumed slash command', type: 'error'}, exit 1
After: claw --resume session.jsonl /plugins --output-format json
-> {kind: 'plugin', action: 'list', ...}, exit 0
* style: cargo fmt line wrap in run_resume_command plugins handler
* fix: block /plugins update in resume mode, fix comment
Address REQUEST_CHANGES from OMX review:
1. Add 'update' to the blocked mutation actions in resume mode
(previously only install/uninstall/enable/disable were blocked)
2. Fix comment: 'Only list is supported' instead of 'Only list/help'
since /plugins help doesn't actually parse as a valid action
* style: cargo fmt after conflict resolution
`claw config model --output-format json` and all other section subcommands
(`env`, `hooks`, `plugins`) returned identical output with no section field
— the section arg was parsed but discarded (_section parameter).
Fix: render_config_json now:
- Passes section through to handler
- Looks up the section value via runtime_config.get(), converting the
internal JsonValue to serde_json::Value via render()+parse
- Emits `section` (string) and `section_value` (JSON value or null)
in the response envelope
- Returns ok:false + error for unsupported section tokens
Test: config_section_json_emits_section_and_value asserts:
- No section field when no section arg
- section + section_value fields present for all known sections
- ok:false + error for unknown section
Pinpoint: ROADMAP #126
`claw skills show <name>`, `claw skills info <name>`, and
`claw skills list <filter>` were all falling through to
SkillSlashDispatch::Invoke, which spawned a real model session,
consumed tokens, and created session files.
Root cause: classify_skills_slash_command had no guards for
these discovery prefixes; every non-reserved arg became Invoke.
Fix:
- Add "show", "info" as Local-only bare tokens
- Add starts_with guards for "show ", "info ", "list " args
- handle_skills_slash_command: filter skill list by name/substring
for show/info/list-filter paths (no model call, no session)
- handle_skills_slash_command_json: same structured filtering
Test: skills_show_and_list_filter_do_not_invoke_model asserts
classify_skills_slash_command returns Local for all discovery
patterns and still returns Invoke for bare skill names.
Pinpoint: ROADMAP #502
resumed_status_command_emits_structured_json_when_requested was reading
the real ~/.claw/settings.json, causing loaded_config_files to be 1
instead of the expected 0 on machines with user config present.
Root cause: unlike other tests (e.g. resumed_config_command_loads_settings_files),
this test did not pass an isolated CLAW_CONFIG_HOME env var to run_claw,
so claw fell back to the real HOME and loaded the developer's settings file.
Fix: create a temp config-home dir and pass it as CLAW_CONFIG_HOME via
run_claw_with_env. This gives the assertion a clean 0-file baseline.
Unblocks PRs #2973, #2988, #2990 which all failed this same test on main.
Ref: ROADMAP #65
Resumed /agents --output-format json was returning a human-readable
text render wrapped in a JSON envelope field instead of the actual
structured agent list. The run_resume_command handler was calling
handle_agents_slash_command (text) for the json field instead of
handle_agents_slash_command_json.
Fix: use handle_agents_slash_command_json for the json outcome field,
matching the pattern already used by /skills and /plugins.
Test: extended resumed_inventory_commands_emit_structured_json_when_requested
to cover /agents, asserting kind=="agents", action=="list",
agents is an array, and count is a number (not a text render).
`claw version --output-format json` was missing build_date and
executable_path, making it impossible to identify which binary is
running or correlate it with a specific build/commit.
Fix: version_json_value() now includes:
- build_date: compile-time BUILD_DATE env (already in text output)
- executable_path: std::env::current_exe() at runtime
Test: version_emits_json_when_requested extended to assert both fields
are strings in the JSON envelope.
Pinpoint: ROADMAP #507
Add four assertions to inventory_commands_emit_structured_json_when_requested:
- kind == "plugin"
- action == "list"
- reload_runtime is boolean
- target is null when no plugin is targeted
Closes the only major --output-format json surface with zero contract
coverage. All other surfaces (agents, mcp, skills, status, sandbox,
doctor, help, version, acp, bootstrap-plan, system-prompt, init, diff,
config) already had test assertions.
Keep claw --help's resume-safe slash command summary aligned with the interactive command list by filtering STUB_COMMANDS and adding regression coverage.
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
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
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
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
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
## 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.
## 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.
## 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.
## 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.
## 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.
## 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.
## #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.
## 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.
## 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.
## 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.
## 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.
## 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.
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.
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.
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.
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.
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.
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.
Updated LocalHelpTopic help strings to surface --output-format support:
- Status, Sandbox, Doctor, Acp all now show [--output-format <format>]
- Added 'Formats: text (default), json' line to each
Diagnostic verbs support JSON output but help text didn't advertise it.
Post-#127 fix: help text now matches actual CLI surface.
Verified: cargo build passes, claw doctor --help shows output-format.
Refs: #127
Diagnostic verbs (help, version, status, sandbox, doctor, state) now
reject unrecognized suffix arguments at parse time instead of silently
falling through to Prompt dispatch.
Fixes: claw doctor --json (and similar) no longer accepts --json silently
and attempts to send it to the LLM as a prompt. Now properly emits:
'unrecognized argument `--json` for subcommand `doctor`'
Joined parser-level trust gap quintet #108 + #117 + #119 + #122 + #127.
Prevents token burn on rejected arguments.
Verified: cargo build --workspace passes, claw doctor --json errors cleanly.
Refs: #127, ROADMAP
Adds ship provenance detection and emission in execute_bash_async():
- Detects git push to main/master commands
- Captures current branch, HEAD commit, git user as actor
- Emits ship.prepared event with ShipProvenance payload
- Logs to stderr as interim routing (event stream integration pending)
This is the first wired provenance event — schema (§4.44.5) now has
runtime emission at actual git operation boundary.
Verified: cargo build --workspace passes.
Next: wire ship.commits_selected, ship.merged, ship.pushed_main events.
Refs: §4.44.5.1, ROADMAP #4.44.5
#122: doctor invocation now checks stale-base condition
- Calls run_stale_base_preflight(None) in render_doctor_report()
- Emits stale-base warnings to stderr when branch is behind main
- Fixes inconsistency: doctor 'ok' vs prompt 'stale base' warning
#125: git_state field reflects non-git directories
- When !in_git_repo, git_state = 'not in git repo' instead of 'clean'
- Fixes contradiction: in_git_repo: false but git_state: 'clean'
- Applied in both doctor text output and status JSON
Verified: cargo build --workspace passes.
Refs: ROADMAP #122 (dd73962), #125 (debbcbe)
Adds run_stale_base_preflight(None) call to render_doctor_report() so that
claw doctor emits stale-base warnings to stderr when the current branch is
behind main. Previously doctor reported 'ok' even when branch was stale,
creating inconsistency with prompt path warnings.
Fixes silent-state inventory gap: doctor now consistent with prompt/repl
stale-base checking. No behavior change for non-stale branches.
Verified: cargo build --workspace passes, no test failures.
Ref: ROADMAP #122 dogfood filing @ dd73962
Adds structured ship provenance surface to eliminate delivery-path opacity:
New lane events:
- ship.prepared — intent to ship established
- ship.commits_selected — commit range locked
- ship.merged — merge completed with provenance
- ship.pushed_main — delivery to main confirmed
ShipProvenance struct carries:
- source_branch, base_commit
- commit_count, commit_range
- merge_method (direct_push/fast_forward/merge_commit/squash_merge/rebase_merge)
- actor, pr_number
Constructor methods added to LaneEvent for all four ship events.
Tests:
- Wire value serialization for ship events
- Round-trip deserialization
- Canonical event name coverage
Runtime: 465 tests pass
ROADMAP updated with IMPLEMENTED status
This closes the gap where 56 commits pushed to main had no structured
provenance trail — now emits first-class events for clawhip consumption.
Added structured delivery-path contract to surface branch → merge → main-push
provenance as first-class events. Filed from the 56-commit 2026-04-20 push
that exposed the gap.
Also fixes: ApiError test compilation — add suggested_action: None to 4 sites
- Line ~8414: opaque_provider_wrapper_surfaces_failure_class_session_and_trace
- Line ~8436: retry_exhaustion_uses_retry_failure_class_for_generic_provider_wrapper
- Line ~8499: provider_context_window_errors_are_reframed_with_same_guidance
- Line ~8533: retry_wrapped_context_window_errors_keep_recovery_guidance