Compare commits

...

284 Commits

Author SHA1 Message Date
Yeachan-Heo
8382e1ec51 docs(roadmap): add tee read-only bypass 2026-05-20 12:31:57 +00:00
Yeachan-Heo
916bf5f24d docs(roadmap): add git availability hang 2026-05-20 12:01:19 +00:00
Yeachan-Heo
4d185f0dee docs(roadmap): add tmux availability hang 2026-05-20 11:31:07 +00:00
Yeachan-Heo
2bf6924e01 docs(roadmap): add gitcontext system prompt hang 2026-05-20 11:01:37 +00:00
Yeachan-Heo
56555a3ad6 docs(roadmap): add dirty diff git hang 2026-05-20 10:31:18 +00:00
Yeachan-Heo
d5aa815b39 docs(roadmap): add status doctor git metadata hang 2026-05-20 10:01:41 +00:00
Yeachan-Heo
edcf5bf33a docs(roadmap): add stale branch freshness ref gap 2026-05-20 09:30:51 +00:00
Yeachan-Heo
d541130121 docs(roadmap): add status dirty path inventory gap 2026-05-20 09:01:30 +00:00
Yeachan-Heo
92e97e0b63 docs(roadmap): add prompt post-flag hang 2026-05-20 08:31:34 +00:00
Yeachan-Heo
32961cfc5b docs(roadmap): add prompt clean-home hang 2026-05-20 08:01:25 +00:00
Yeachan-Heo
8a2e133f66 docs(roadmap): add planning alias argument hang 2026-05-20 07:31:23 +00:00
Yeachan-Heo
51a450e473 docs(roadmap): add debug alias argument hang 2026-05-20 07:01:25 +00:00
Yeachan-Heo
dbd04ad334 docs(roadmap): add interactive alias help hang 2026-05-20 06:31:24 +00:00
Yeachan-Heo
625b8b06d8 docs(roadmap): add telemetry alias help hang 2026-05-20 06:01:16 +00:00
Yeachan-Heo
111e7e853c docs(roadmap): add slash alias help hang 2026-05-20 05:31:22 +00:00
Yeachan-Heo
51e6040b23 docs(roadmap): add session direct command hang 2026-05-20 05:01:09 +00:00
Yeachan-Heo
93f20dfd25 docs(roadmap): add unknown skill invocation hang 2026-05-20 04:32:31 +00:00
Yeachan-Heo
4d52703ca9 docs(roadmap): add export option hang 2026-05-20 03:31:35 +00:00
Yeachan-Heo
90a0d38d52 docs(roadmap): add system-prompt modifier hang 2026-05-20 03:01:45 +00:00
Yeachan-Heo
8afdb9448a docs(roadmap): add inventory json parity gap 2026-05-20 02:31:20 +00:00
Yeachan-Heo
bb2cf3f448 docs(roadmap): add local diagnostics json gap 2026-05-20 02:01:25 +00:00
Yeachan-Heo
9495dbee30 docs(roadmap): add local verb help arity hangs 2026-05-20 01:31:56 +00:00
Yeachan-Heo
1208b9a034 docs(roadmap): add compact slash-only help hang 2026-05-20 01:01:25 +00:00
Yeachan-Heo
e9db12d98b docs(roadmap): add silent malformed config gap 2026-05-20 00:30:56 +00:00
Yeachan-Heo
a666fa6f10 docs(roadmap): add version json alias gap 2026-05-20 00:01:10 +00:00
Yeachan-Heo
ac3665fe2f docs(roadmap): add help alias arity hang 2026-05-19 23:31:17 +00:00
Yeachan-Heo
7cbb6e7fa5 docs(roadmap): add unexpected positional hang 2026-05-19 22:32:00 +00:00
Yeachan-Heo
674cec191f docs(roadmap): add lifecycle explicit action hangs 2026-05-19 22:01:43 +00:00
Yeachan-Heo
59df684a17 docs(roadmap): add explicit plugin list hang 2026-05-19 21:31:23 +00:00
Yeachan-Heo
31d5db7453 docs(roadmap): add mixed PATH mcp reachability proof 2026-05-19 21:00:48 +00:00
Yeachan-Heo
164589b8e6 docs(roadmap): add mcp executable reachability gap 2026-05-19 20:30:59 +00:00
Yeachan-Heo
b4a732f33d docs(roadmap): add compact skills output gap 2026-05-19 20:01:01 +00:00
Yeachan-Heo
2980f6de6e docs(roadmap): add output-format flag placement gap 2026-05-19 19:31:20 +00:00
Yeachan-Heo
2cca298f7a docs(roadmap): add direct slash invocation contract 2026-05-19 18:31:28 +00:00
Yeachan-Heo
6b90f83b9d docs(roadmap): add text command help hang 2026-05-19 18:02:27 +00:00
Yeachan-Heo
8303af0898 docs(roadmap): add command-specific json help hang 2026-05-19 17:32:04 +00:00
Yeachan-Heo
3e2d902271 docs(roadmap): add structured root help schema 2026-05-19 17:01:00 +00:00
Yeachan-Heo
78d334c4e2 docs(roadmap): add dogfood timeout retry evidence 2026-05-19 16:31:00 +00:00
Yeachan-Heo
0063c0d698 docs(roadmap): add global flag order and clean version hang 2026-05-19 16:01:15 +00:00
Yeachan-Heo
429671ec12 docs(roadmap): add root help json hang 2026-05-19 15:31:27 +00:00
Yeachan-Heo
ed9d387e9a docs(roadmap): add shared lifecycle help hang 2026-05-19 15:01:46 +00:00
Yeachan-Heo
8d02077cfd docs(roadmap): add plugin help hang contract 2026-05-19 14:32:04 +00:00
Yeachan-Heo
44bd2b54f5 docs(roadmap): add plugin lifecycle help contract 2026-05-19 14:01:01 +00:00
Yeachan-Heo
e2c310dc04 docs(roadmap): add stale bundled plugin source provenance 2026-05-19 13:32:37 +00:00
Yeachan-Heo
25d663d140 docs(roadmap): add missing dogfood binary preflight 2026-05-19 13:00:59 +00:00
Yeachan-Heo
6183d958ba docs(roadmap): add dogfood workdir provenance guard 2026-05-19 12:31:56 +00:00
YeonGyu-Kim
f8e1bb7262 docs(roadmap): add #450 — prompt JSON error routed to stderr not stdout; doctor missing prompt_ready field 2026-05-16 23:06:07 +09:00
YeonGyu-Kim
a35ee9a002 docs(roadmap): add #449 — session list routes through ResumeSession and hits auth gate despite being a local-only filesystem read 2026-05-16 23:02:25 +09:00
bellman
63ce483c27 Merge commit '17260f69f14d28d0f22ce46e330e98c8d9ff9fd5' 2026-05-15 13:40:50 +09:00
bellman
c910063161 Close the ultragoal ledger after final gate
Record G012 as complete so the durable OMX audit trail matches the finished Claw Code 2.0 delivery.

Constraint: OMX required a completed Codex goal snapshot plus aiSlopCleaner, verification, and clean codeReview evidence before accepting the final checkpoint.

Rejected: leaving G012 pending after code/test completion | the user requested roadmap backlog completion and durable ledger reconciliation.

Confidence: high

Scope-risk: narrow

Directive: Treat .omx/ultragoal/ledger.jsonl as the authoritative completion audit for G001-G012.

Tested: omx ultragoal checkpoint --goal-id G012-final-gate --status complete; omx ultragoal status => 12/12 complete

Not-tested: remote GitHub PR merges or issue closures, because G012 evidence classified them as unsafe without maintainer approval/fresh green checks/conflict-free branches.
2026-05-15 13:39:38 +09:00
bellman
04c2abb412 Stabilize final gate before release checkpoint
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.
2026-05-15 13:34:57 +09:00
bellman
33df16b6dd Record PR gate evidence to avoid unsafe final merges
G012 requires a fresh PR reconciliation snapshot before Claw Code 2.0 can close, and the live GitHub state does not provide enough safety evidence for worker-side merges.

Constraint: Worker-3 must not mutate .omx/ultragoal and may merge only correct, safe, non-conflicting, resolvable PRs with evidence.

Rejected: Blindly merging GitHub-mergeable PRs | GitHub reported UNSTABLE/no fresh check evidence for mergeable PRs and DIRTY conflicts for the rest.

Confidence: high

Scope-risk: narrow

Directive: Keep PR merge decisions gated by fresh CI, conflict-free merge state, and content/source-of-truth review.

Tested: python3 -m json.tool docs/pr-triage-g012-final-gate.json; python3 .github/scripts/check_doc_source_of_truth.py; (cd rust && cargo check --workspace); (cd rust && cargo fmt --check --all)

Not-tested: (cd rust && cargo test --workspace) failed in unrelated rusty-claude-cli tests tests::resume_usage_mentions_latest_shortcut and tests::session_lifecycle_prefers_running_process_over_idle_shell; no Rust files changed.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 12:34:00 +09:00
bellman
17260f69f1 Preserve final-gate evidence for release arbitration
Constraint: G012 worker boundary prohibits mutating .omx/ultragoal and W1 must avoid W2/W3/W4 action lanes except to reference evidence.
Rejected: Remote PR or issue actions from W1 | W3 and W4 own reconciliation, and current roadmap PRs are mostly conflicting or product-fit gated.
Confidence: high
Scope-risk: narrow
Directive: Treat docs/g012-final-release-readiness-report.md as an evidence map, not release approval by itself.
Tested: git diff --check; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml --workspace; python3 .github/scripts/check_doc_source_of_truth.py; python3 .github/scripts/check_release_readiness.py; python3 scripts/validate_cc2_board.py; python3 .omx/cc2/validate_issue_parity_intake.py; gh pr/issue list snapshots.
Not-tested: full cargo test --workspace; W2 owns final quality gate.
Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 12:04:32 +09:00
bellman
6f73103bf1 Record why issue reconciliation is evidence-gated
Capture the fresh G012 issue snapshot and classify open issues without mutating remote state so the final gate has durable evidence despite the team claim-token mismatch.

Constraint: Task 5 remains lifecycle-blocked because task metadata assigns the W4 lane in text but keeps owner=worker-1, so worker-4 cannot obtain a claim token.

Rejected: Closing or labeling issues from this worker lane | remote issue mutation requires maintainer-owned approval and a valid task claim.

Confidence: medium

Scope-risk: narrow

Directive: Do not mark G012 issue reconciliation complete until leader repairs the task claim conflict or explicitly reconciles this evidence-only commit.

Tested: python3 .github/scripts/check_doc_source_of_truth.py; python3 .github/scripts/check_release_readiness.py; git diff --check

Not-tested: full Rust test/typecheck suite was not run because this commit changes only the docs evidence artifact.

Co-authored-by: OmX <omx@users.noreply.github.com>
2026-05-15 12:03:44 +09:00
bellman
a92e5b2892 omx(team): auto-checkpoint worker-3 [unknown] 2026-05-15 12:01:32 +09:00
bellman
0fb1c2d39e omx(team): merge worker-1 2026-05-15 12:00:26 +09:00
bellman
0eddcca702 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 12:00:26 +09:00
bellman
2e93264919 Record G011 ultragoal completion
The ultragoal ledger must reflect that the G011 ecosystem ops UX team finished and passed the leader quality gate before final G012 release reconciliation starts.

Constraint: OMX ultragoal checkpoint requires fresh Codex goal reconciliation and durable .omx/ultragoal goals/ledger updates.

Rejected: leaving G011 only in team state | G012 needs the aggregate ledger to show 11/12 goals complete.

Confidence: high

Scope-risk: narrow

Directive: start G012 only from this 11/12-complete ledger state and keep final PR/issue reconciliation evidence fresh.

Tested: omx ultragoal checkpoint --goal-id G011-ecosystem-ops-ux --status complete; omx ultragoal status shows 11/12 complete and G012 pending.

Not-tested: G012 final gate not run yet.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 11:56:37 +09:00
bellman
1ac8ce8882 Keep G011 artifacts reviewable
G011 needs final evidence without committing generated local port-session scratch files from worker verification.

Constraint: .port_sessions is a local Python session-store scratch directory and worker-4 auto-checkpoint added transient test artifacts.

Rejected: keeping generated .port_sessions files | they are not required by tests and obscure the G011 acceptance diff.

Confidence: high

Scope-risk: narrow

Directive: keep .port_sessions ignored unless a future test intentionally needs checked-in fixtures under a dedicated fixture path.

Tested: omx team status g011-ecosystem-ops-ux-e61d2271; git diff --check; python3 .github/scripts/check_doc_source_of_truth.py; python3 .github/scripts/check_release_readiness.py; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p commands -p rusty-claude-cli; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract acp_guidance_emits_json_when_requested; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli parses_acp_command_surfaces; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli acp_status_json_is_truthful_unsupported_contract; cargo test --manifest-path rust/Cargo.toml -p commands renders_help_with_grouped_categories_and_keyboard_shortcuts; python3 -m unittest tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_filter_excludes_plugin_sources tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_aliases_execute_as_local_commands tests.test_porting_workspace.PortingWorkspaceTests.test_route_plugin_slash_commands_match_commands tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_stream_emits_command_match tests.test_porting_workspace.PortingWorkspaceTests.test_turn_loop_plugin_commands_are_not_prompt_only

Not-tested: full workspace test suite and clippy -D warnings; pre-existing api dead_code warnings remain.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 11:54:09 +09:00
bellman
ab27f61597 omx(team): auto-checkpoint worker-4 [7] 2026-05-15 11:49:15 +09:00
bellman
5a43d3b553 Keep G011 verification commands runnable
Replace placeholder pytest/nonexistent cargo filters with repository-native unittest and exact cargo filters so the final UX/verification matrix can be rerun without extra dependencies.

Constraint: worker-4 task 7 owns the final G011 verification map and must not mutate .omx/ultragoal.

Rejected: leaving pytest-based guidance | pytest is not installed in this worktree and unittest covers the existing plugin regression cases.

Confidence: high

Scope-risk: narrow

Directive: Keep desktop, marketplace, clipboard, and clickable-link surfaces explicit as deferrals until their contracts have implementation tests.

Tested: git diff --check; python3 .github/scripts/check_doc_source_of_truth.py; python3 .github/scripts/check_release_readiness.py; cargo check --manifest-path rust/Cargo.toml -p commands -p rusty-claude-cli; targeted commands/rusty-claude-cli/output_format cargo tests; python3 -m unittest plugin porting tests.

Not-tested: full cargo test --workspace.

Co-authored-by: OmX <omx@local>
2026-05-15 11:49:12 +09:00
bellman
2c601ef22d Gate issue and PR triage on evidence
G011 W3 needs non-destructive anti-slop issue and PR triage templates plus a fresh PR/issue snapshot so the leader can complete the aggregate gate without merging or closing from a worker lane.

Constraint: Worker lanes must not mutate .omx/ultragoal or merge/close remote PRs/issues; task 3 claim conflicted, so task 6 carried this support slice.

Rejected: Remote PR/issue mutation from the worker lane | Maintainer-owned approval is required for merge/close actions.

Confidence: high

Scope-risk: narrow

Directive: Refresh the GitHub snapshots at final gate time because the documented counts are point-in-time evidence, not terminal proof.

Tested: python3 .github/scripts/check_doc_source_of_truth.py; ruby YAML parse of .github/ISSUE_TEMPLATE/anti_slop_triage.yml; git diff --cached --check; cargo check --workspace; cargo test --workspace (fails only existing rusty-claude-cli tests resume_usage_mentions_latest_shortcut and session_lifecycle_prefers_running_process_over_idle_shell, reproduced individually).

Not-tested: Full cargo test workspace green due unrelated pre-existing rusty-claude-cli failures.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 11:49:08 +09:00
bellman
4cd2bb859b omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:49:01 +09:00
bellman
62bc7b6a17 Stabilize G011 integrated evidence
Keep the integrated G011 worker outputs buildable and whitespace-clean before final stream verification.

Constraint: worker auto-checkpoints left a test-scope import gap and committed trailing whitespace.

Rejected: waiting for another worker auto-merge | leader verification already identified the narrow repair.

Confidence: high

Scope-risk: narrow

Directive: preserve the ACP unsupported JSON contract and anti-slop template cleanliness when editing G011 artifacts.

Tested: git diff --check; python3 .github/scripts/check_doc_source_of_truth.py; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p rusty-claude-cli; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli parses_acp_command_surfaces; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli acp_status_json_is_truthful_unsupported_contract

Not-tested: full workspace tests pending G011 final gate.
2026-05-15 11:47:28 +09:00
bellman
9278748038 omx(team): auto-checkpoint worker-4 [7] 2026-05-15 11:41:11 +09:00
bellman
02889d701a omx(team): auto-checkpoint worker-3 [6] 2026-05-15 11:41:03 +09:00
bellman
7b63c0a2eb omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:40:54 +09:00
bellman
b11cdf34b3 omx(team): merge worker-2 2026-05-15 11:39:07 +09:00
bellman
de0f1bba2e omx(team): auto-checkpoint worker-2 [4] 2026-05-15 11:39:06 +09:00
bellman
4d78e91229 Start G011 ecosystem ops UX stream
Advance the Claw Code 2.0 ultragoal from completed G010 into G011 ecosystem, issue-ops, and UX lateral work.\n\nConstraint: G011 must remain an intermediate OMX story under the active aggregate Codex goal; G012 final gate remains pending.\nRejected: calling update_goal | aggregate delivery is not complete until G011 and G012 finish.\nConfidence: high\nScope-risk: narrow\nDirective: Implement G011 with team lanes for ACP/Zed/JSON-RPC serve plan/status, anti-slop issue/PR triage/templates, navigation/file-context docs, TUI UX improvements, and explicit desktop/marketplace deferrals.\nTested: omx ultragoal start-next --json selected G011-ecosystem-ops-ux after G010 checkpoint.\nNot-tested: G011 implementation not started in this commit.
2026-05-15 11:21:45 +09:00
bellman
cf5eb157e1 Update ultragoal ledger for G010 completion
Record G010-session-hygiene as complete so the remaining Claw Code 2.0 delivery can advance to G011/G012.\n\nConstraint: OMX ultragoal checkpoint requires fresh Codex get_goal evidence and durable .omx/ultragoal ledger/goals artifacts.\nRejected: marking the aggregate Codex goal complete | only G010 is complete; G011 and G012 remain pending.\nConfidence: high\nScope-risk: narrow\nDirective: Continue with G011-ecosystem-ops-ux next; do not call update_goal until G012/final audit is complete.\nTested: omx ultragoal checkpoint --goal-id G010-session-hygiene --status complete with .omx/ultragoal/get-goal-G010-session-hygiene.complete.json and .omx/ultragoal/quality-gate-G010-session-hygiene.json; G010 final verification evidence in .omx/ultragoal/g010-final-quality-gate-rerun.log.\nNot-tested: G011/G012 not started in this commit.
2026-05-15 11:20:58 +09:00
bellman
8019999ce5 Record G010 final verification rerun
Preserve the passing G010 quality-gate evidence after fixing resumed /session exhaustiveness.\n\nConstraint: Ultragoal checkpoints require concrete verification artifacts that mention G010-session-hygiene and .omx/ultragoal state.\nRejected: relying only on terminal scrollback | rerun log is durable and reviewable.\nConfidence: high\nScope-risk: narrow\nDirective: Use this rerun log, not the earlier failed g010-final-quality-gate.log, as the passing G010 quality evidence.\nTested: .omx/ultragoal/g010-final-quality-gate-rerun.log records fmt, targeted runtime/commands/CLI tests, workspace check, and git diff --check passing.\nNot-tested: full cargo test --workspace remains not run for G010.
2026-05-15 11:20:20 +09:00
bellman
21bbbb7f1f Route resumed session commands exhaustively
Keep G010 resumed session UX compilable after worker integrations by routing every /session action through the shared resumed-session command handler.\n\nConstraint: Rust exhaustive matching rejected partial /session arms after task integrations introduced action-without-target cases.\nRejected: adding ad hoc match arms per action | the shared handler already owns list, exists, delete, and unsupported action behavior.\nConfidence: high\nScope-risk: narrow\nDirective: Preserve run_resumed_session_command as the single resumed /session dispatch point.\nTested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --bin claw session_exists_resume_command_reports_json_contract -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --bin claw resumed_session_exists_and_delete_have_json_contracts -- --nocapture; cargo check --manifest-path rust/Cargo.toml --workspace; git diff --check\nNot-tested: full cargo test --workspace not run.
2026-05-15 11:19:14 +09:00
bellman
124d55f13e omx(team): merge worker-4 2026-05-15 11:17:27 +09:00
bellman
eb7a2088e2 omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 11:16:58 +09:00
bellman
11c6a6007f omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 11:16:58 +09:00
bellman
060603c196 omx(team): merge worker-1 2026-05-15 11:16:53 +09:00
bellman
4ccbd8f97c Keep resumed session handling exhaustive
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>
2026-05-15 11:16:11 +09:00
bellman
2221dd4f0f omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:16:04 +09:00
bellman
c5a18e1864 Preserve resumed session command exhaustiveness
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.
2026-05-15 11:15:53 +09:00
bellman
d7f1ad7139 omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 11:12:25 +09:00
bellman
238c0a49d1 omx(team): merge worker-1 2026-05-15 11:12:11 +09:00
bellman
d04a74cc97 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:12:10 +09:00
bellman
8fd5894022 omx(team): merge worker-4 2026-05-15 11:09:57 +09:00
bellman
0f8717834f omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 11:09:06 +09:00
bellman
fb9095c611 omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 11:09:06 +09:00
bellman
761e50d1c6 omx(team): merge worker-1 2026-05-15 11:08:59 +09:00
bellman
5155225b25 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:08:59 +09:00
bellman
deeb1efde8 omx(team): merge worker-1 2026-05-15 11:07:21 +09:00
bellman
c9b34a2947 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:07:21 +09:00
bellman
5e0cf62be5 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:05:44 +09:00
bellman
51fa5a7048 omx(team): auto-checkpoint worker-3 [unknown] 2026-05-15 11:05:40 +09:00
bellman
33ac5c30d3 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:05:36 +09:00
bellman
89d1052f3a omx(team): auto-checkpoint worker-3 [unknown] 2026-05-15 11:03:45 +09:00
bellman
afd88088d6 omx(team): auto-checkpoint worker-2 [5] 2026-05-15 11:03:40 +09:00
bellman
673d37d86a omx(team): merge worker-1 2026-05-15 11:03:36 +09:00
bellman
fc35dc878c omx(team): auto-checkpoint worker-1 [1] 2026-05-15 11:03:35 +09:00
bellman
e199a392fb Start G010 session hygiene stream
The ultragoal has advanced from Windows/docs readiness to Stream 9 session hygiene. Record the in-progress marker before launching the implementation team so ledger state matches the active work.

Constraint: .omx/ultragoal/goals.json and ledger.jsonl are the durable audit trail for stream transitions.

Confidence: high

Scope-risk: narrow

Tested: omx ultragoal start-next --json; get_goal confirmed aggregate Codex goal remains active

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 11:00:05 +09:00
bellman
f27bd46759 Update ultragoal ledger for G009 completion
G009 checkpoint updated the aggregate goals file and durable ledger. Commit those ignored runtime artifacts explicitly so the roadmap stream audit remains reproducible in-repo.

Constraint: Ultragoal source of truth requires .omx/ultragoal/goals.json and ledger.jsonl evidence.

Confidence: high

Scope-risk: narrow

Tested: omx ultragoal checkpoint --goal-id G009-windows-docs-release --status complete

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 10:58:18 +09:00
bellman
5294648373 Record G009 release readiness quality gate
The Windows docs/release stream now has terminal team tasks and final local verification evidence. Persist the Codex goal snapshot and quality gate so ultragoal checkpointing can reconcile against durable artifacts.

Constraint: Ultragoal completion requires get_goal reconciliation and quality-gate evidence before checkpointing.

Confidence: high

Scope-risk: narrow

Tested: python3 .github/scripts/check_release_readiness.py; python3 .github/scripts/check_doc_source_of_truth.py; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test cli_flags_and_config_defaults local_smoke_commands_do_not_require_live_credentials -- --nocapture; cargo check --manifest-path rust/Cargo.toml --workspace; git diff --check; G009 coverage check

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 10:57:32 +09:00
bellman
a3af0133e0 Preserve Windows checksum verification after docs merge
G009 final verification showed worker docs integration had overwritten checksum-specific release guidance. Restore the Windows asset checksum path and make provider-routing docs explicit about not needing CLAUDE_CODE_PROVIDER.

Constraint: Stream 8 acceptance requires release artifact checksums and safe provider-switching examples.

Rejected: Treating team completion as sufficient | final coverage gate found missing terms.

Confidence: high

Scope-risk: narrow

Tested: python3 .github/scripts/check_release_readiness.py; python3 .github/scripts/check_doc_source_of_truth.py; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test cli_flags_and_config_defaults local_smoke_commands_do_not_require_live_credentials -- --nocapture; cargo check --manifest-path rust/Cargo.toml --workspace; git diff --check; G009 coverage check

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 10:56:47 +09:00
bellman
1bceda2063 Clarify Windows release onboarding
PowerShell-first install and provider-switching guidance was missing from the release-readiness path, so add a focused quickstart and link it from the primary docs without touching runtime code.

Constraint: G009 task 4 scope is docs-only install/release quickstart and provider switching; do not mutate .omx/ultragoal.

Rejected: Runtime or installer changes | task asks for command-copyable docs and source-of-truth alignment, not product behavior changes.

Confidence: high

Scope-risk: narrow

Directive: Keep Windows examples placeholder-secret only and prefer explicit provider model prefixes when multiple credentials may exist.

Tested: cargo fmt --check; cargo check --workspace; cargo clippy --workspace --no-deps; markdown link/fence/grep validation.

Not-tested: cargo test --workspace has pre-existing failing rusty-claude-cli::tests::session_lifecycle_prefers_running_process_over_idle_shell unrelated to docs; targeted rerun reproduces.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 10:53:36 +09:00
bellman
99efb2131e Ensure release docs are auditable before Windows adoption
Add the missing repository policy files and a dependency-free release-readiness validator so Windows/install release docs have a local and CI-friendly policy/link/command safety gate.

Constraint: G009 worker-4 scope requires license, contribution, security, support, conduct, docs link, and command-example validation without mutating .omx/ultragoal.

Rejected: relying on manual review only | release-readiness docs need a repeatable local check.

Confidence: high

Scope-risk: narrow

Directive: Keep release policy links and command examples covered by .github/scripts/check_release_readiness.py when adding new onboarding docs.

Tested: python3 .github/scripts/check_release_readiness.py; python3 .github/scripts/check_doc_source_of_truth.py; cargo fmt --all --check; cargo check --workspace; cargo test -p rusty-claude-cli --bin claw render_session_markdown; claw --help; claw doctor --help

Not-tested: cargo test --workspace has two pre-existing/unrelated rusty-claude-cli failures: rejects_empty_allowed_tools_flag and session_lifecycle_prefers_running_process_over_idle_shell.

Co-authored-by: OmX <omx@local>
2026-05-15 10:52:43 +09:00
bellman
7d859ae8a2 Close Windows release artifact verification gap
G009 Stream 8 acceptance requires a Windows release artifact quickstart with checksum evidence, not just source-build docs. Add Windows release asset packaging and make the release-readiness check assert the workflow/docs contract.

Constraint: Stream 8 requires release artifact quickstart with checksums and no-credential smoke paths.

Rejected: Documenting a future Windows asset without workflow support | would leave acceptance unverifiable.

Confidence: high

Scope-risk: narrow

Tested: python3 .github/scripts/check_release_readiness.py; python3 .github/scripts/check_doc_source_of_truth.py; git diff --check

Not-tested: actual GitHub release workflow execution on windows-latest.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 10:52:29 +09:00
bellman
c886cbca99 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:47:49 +09:00
bellman
f8270b34e6 omx(team): merge worker-1 2026-05-15 10:46:11 +09:00
bellman
0940253376 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:46:11 +09:00
bellman
0bb145141e omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 10:45:24 +09:00
bellman
7b21ac12b9 omx(team): auto-checkpoint worker-2 [unknown] 2026-05-15 10:45:19 +09:00
bellman
2db0a5f70d omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 10:43:45 +09:00
bellman
8c9e41aab4 omx(team): auto-checkpoint worker-3 [unknown] 2026-05-15 10:43:41 +09:00
bellman
90c1d38d40 omx(team): merge worker-2 2026-05-15 10:43:36 +09:00
bellman
3767addd11 omx(team): auto-checkpoint worker-2 [unknown] 2026-05-15 10:43:35 +09:00
bellman
8c9a05e71b Restore provider compatibility diagnostics as API types
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>
2026-05-15 10:37:20 +09:00
bellman
d5620c06b1 Document provider compatibility diagnostics and passthrough
Explain the provider compatibility behavior that now has executable coverage so future changes preserve routing, custom gateway slugs, diagnostics, and extra-body passthrough semantics.

Constraint: Task 2 requires docs/status evidence while leader owns Ultragoal checkpoints and .omx/ultragoal remains untouched.
Rejected: Add a new CLI status command | broader than the assigned provider compatibility documentation/status surface.
Confidence: high
Scope-risk: narrow
Directive: Keep docs aligned with provider_diagnostics_for_model, MessageRequest::extra_body, and OpenAI-compatible slash-model tests.
Tested: cargo test --manifest-path rust/Cargo.toml -p api custom_openai_gateway_preserves_slash_model_ids_and_extra_body_params; cargo test --manifest-path rust/Cargo.toml -p api provider_diagnostics_explain_openai_compatible_capabilities; cargo check --manifest-path rust/Cargo.toml -p api
Not-tested: Live provider calls requiring external credentials.
Co-authored-by: OmX <omx@local>
2026-05-15 10:35:13 +09:00
bellman
b63a1bf2bf omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:33:55 +09:00
bellman
dccb3e72d9 Stabilize OpenAI-compatible mock transport verification
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>
2026-05-15 10:30:19 +09:00
bellman
ea95bf2576 omx(team): auto-checkpoint worker-3 [unknown] 2026-05-15 10:30:16 +09:00
bellman
dec8efa5c8 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:30:09 +09:00
bellman
ce02ace3a2 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:30:06 +09:00
bellman
bc32639ce3 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:30:03 +09:00
bellman
a212c662e5 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:30:00 +09:00
bellman
2cac66cd38 Stabilize provider compatibility integration verification
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>
2026-05-15 10:28:50 +09:00
bellman
1a110bd870 omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 10:23:46 +09:00
bellman
685f078204 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:23:37 +09:00
bellman
e4ef0f7f19 omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 10:22:03 +09:00
bellman
76581f7239 omx(team): auto-checkpoint worker-3 [unknown] 2026-05-15 10:21:58 +09:00
bellman
82ec223ed4 omx(team): auto-checkpoint worker-2 [unknown] 2026-05-15 10:21:55 +09:00
bellman
a6ca5c489b omx(team): auto-checkpoint worker-4 [unknown] 2026-05-15 10:21:28 +09:00
bellman
3ff8743e79 omx(team): auto-checkpoint worker-2 [unknown] 2026-05-15 10:21:23 +09:00
bellman
4cf9d43e71 omx(team): merge worker-1 2026-05-15 10:21:19 +09:00
bellman
29029bfc14 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:21:18 +09:00
bellman
22024102dd map MCP lifecycle maturity surfaces
Document the current degraded startup, required/optional, interpolation, redaction, and plugin lifecycle boundaries so follow-up implementation work has a verified source map.

Constraint: Task 7 requested a mapping artifact and worker scope forbids .omx/ultragoal mutation.

Rejected: Implement interpolation or redaction policy changes in this slice | task wording and prior Task 3/6 lanes only required mapping plus verification.

Confidence: high

Scope-risk: narrow

Directive: Treat required MCP server handling as metadata-only until a future policy slice explicitly defines fail-hard semantics.

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 test -p plugins installed_plugin_registry_report_collects_load_failures_from_install_root -- --nocapture; cargo test -p rusty-claude-cli --test output_format_contract plugins_json_surfaces_lifecycle_contract_when_plugin_is_installed -- --nocapture

Not-tested: cargo clippy remains blocked by pre-existing runtime::policy_engine::LaneContext clippy::struct_excessive_bools from earlier verification.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 10:14:05 +09:00
bellman
98204a73d4 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:09:08 +09:00
bellman
8565b68fb1 omx(team): merge worker-4 2026-05-15 10:08:06 +09:00
bellman
7ed1cabc14 Prove observable MCP required optional contracts
Added CLI JSON regression coverage for MCP required versus optional flags, redacted env/header values, degraded malformed config reporting, and failed unsupported usage reporting without touching runtime internals.

Constraint: Task 12 scope preferred rusty-claude-cli tests and avoid worker-1/3 MCP internals.

Rejected: Runtime lifecycle edits | existing observable JSON contracts already expose required, redacted keys, degraded config, and unsupported-action failure semantics.

Confidence: high

Scope-risk: narrow

Directive: Preserve secret-value redaction by exposing env/header keys only; keep degraded config distinct from usage errors.

Tested: cargo fmt --manifest-path Cargo.toml -p rusty-claude-cli --check; cargo test --manifest-path Cargo.toml -p rusty-claude-cli --test output_format_contract mcp_ -- --nocapture; cargo check --manifest-path Cargo.toml -p rusty-claude-cli.

Not-tested: Full output_format_contract currently has unrelated pre-existing failures in plugin/doctor contract tests.
2026-05-15 10:07:47 +09:00
bellman
5de73ecf12 Prevent plugin command aliases from becoming prompts
Added focused Python-port regressions for plugin, plugins, marketplace, and reload-plugins command routing, execution, stream, and turn-loop paths; narrowed runtime routing to honor explicit leading commands before fuzzy inventory matches.

Constraint: Task 10 scope limited changes to tests/test_porting_workspace.py plus narrow src parser/runtime fixes only if required.

Rejected: Test-only coverage without alias routing fix | route /plugin previously preferred fuzzy AddMarketplace over exact plugin command.

Confidence: high

Scope-risk: narrow

Directive: Keep --no-plugin-commands excluding plugin source hints; do not reinterpret that intentional filter as fallthrough.

Tested: python3 -m unittest tests.test_porting_workspace; python3 -m compileall src tests/test_porting_workspace.py; CLI route/turn-loop/filter smoke for /plugin, /plugins, /marketplace, /reload-plugins.

Not-tested: Full repository non-Python/Rust suites.
2026-05-15 10:07:47 +09:00
bellman
b655d49bd1 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:07:42 +09:00
bellman
e05268e216 omx(team): merge worker-2 2026-05-15 10:07:10 +09:00
bellman
d6b4349a7d Keep G007 mock parity references executable
The follow-up docs gate needed the parity manifest reference string that the mock parity diff checker requires, so this records the verified map-only repair after leader integration.

Constraint: Task 11 scope allowed only PARITY.md, rust/MOCK_PARITY_HARNESS.md, and docs/g007-plugin-mcp-verification-map.md; only PARITY.md needed a change.

Rejected: Editing harness code or .omx/ultragoal | checks proved the remaining gap was docs-map text only and Ultragoal state is leader-owned.

Confidence: high

Scope-risk: narrow

Directive: Keep auto_compact_triggered parity_refs synchronized with PARITY.md when mock parity scenarios change.

Tested: cd rust && python3 scripts/run_mock_parity_diff.py; python3 scripts/validate_cc2_board.py --board .omx/cc2/board.json; rg -n auto_compact_triggered token_cost_reporting auto_compaction threshold from env G007 Plugin/MCP G007-plugin-mcp 12 scripted scenarios 21 captured PARITY.md rust/MOCK_PARITY_HARNESS.md docs/g007-plugin-mcp-verification-map.md; git diff --check

Not-tested: Full workspace suite; Task 11 was a docs/parity-map follow-up with focused executable map validation.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 10:06:46 +09:00
bellman
ccd99a5188 omx(team): auto-checkpoint worker-2 [2] 2026-05-15 10:06:46 +09:00
bellman
557ab8a9dd surface required MCP server semantics
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>
2026-05-15 10:06:39 +09:00
bellman
1f00771fd2 Keep plugin lifecycle JSON complete after team merges
Preserve the direct and resumed plugin JSON arrays that downstream G007 regressions require while retaining the degraded config envelope from the malformed-MCP work.

Constraint: G007 verification requires plugins[] and load_failures[] on plugin JSON plus status/config_load_error consistency.
Rejected: Only relaxing output_format_contract tests | that would hide a shipped CLI JSON regression.
Confidence: high
Scope-risk: narrow
Directive: Keep direct, resumed, and interactive plugin command payloads routed through one payload helper before changing plugin JSON fields.
Tested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract inventory_commands_emit_structured_json_when_requested -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract plugins_json_surfaces_lifecycle_contract_when_plugin_is_installed -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract structured_json_when_requested -- --nocapture; cargo check --manifest-path rust/Cargo.toml -p runtime -p tools -p rusty-claude-cli -p commands -p plugins
Not-tested: Full workspace clippy remains blocked by pre-existing LaneContext struct_excessive_bools lint.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 10:06:23 +09:00
bellman
0bcab573f3 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:05:46 +09:00
bellman
4a76632f6c omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:05:42 +09:00
bellman
9910d5805e omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:01:44 +09:00
bellman
39568feff6 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:01:04 +09:00
bellman
686cc89a36 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 10:00:40 +09:00
bellman
d3ae7beefb omx(team): auto-checkpoint worker-2 [2] 2026-05-15 09:59:36 +09:00
bellman
faa7551ac2 omx(team): merge worker-2 2026-05-15 09:59:29 +09:00
bellman
7ce6b78d3a Document G007 mock parity verification boundaries
Record the W4 verification map and parity-doc alignment so plugin/MCP lifecycle mock coverage can be audited without scraping stale harness prose.

Constraint: Task 9 owns mock integration and regression verification/docs mapping, not parser fixes or leader-owned Ultragoal state.

Rejected: Editing .omx/ultragoal or duplicating Worker-4 plugin command fallthrough fixes | leader checkpointing and parser implementation are separate lanes.

Confidence: high

Scope-risk: narrow

Directive: Keep rust/mock_parity_scenarios.json, rust/MOCK_PARITY_HARNESS.md, PARITY.md, and mock_parity_harness.rs aligned when adding scenarios.

Tested: cd rust && python3 scripts/run_mock_parity_diff.py --no-run; cd rust && ./scripts/run_mock_parity_harness.sh; cd rust && python3 scripts/run_mock_parity_diff.py; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli build_runtime_plugin_state -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli build_runtime_runs_plugin_lifecycle_init_and_shutdown -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p plugins plugin_registry_runs_initialize_and_shutdown_for_enabled_plugins -- --nocapture; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p rusty-claude-cli -p mock-anthropic-service; git diff --check

Not-tested: Full workspace test suite; W4 scope is docs/mock verification map and focused plugin/MCP lifecycle regression evidence.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 09:59:10 +09:00
bellman
2831c45f71 omx(team): auto-checkpoint worker-2 [2] 2026-05-15 09:59:10 +09:00
bellman
ace260139e omx(team): auto-checkpoint worker-3 [4] 2026-05-15 09:59:06 +09:00
bellman
db6f30fa33 verify plugin lifecycle JSON contract
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>
2026-05-15 09:59:02 +09:00
bellman
983ceb939c omx(team): auto-checkpoint worker-1 [1] 2026-05-15 09:57:03 +09:00
bellman
cac73b4410 omx(team): auto-checkpoint worker-3 [4] 2026-05-15 09:57:00 +09:00
bellman
9ae6aa3f30 Keep plugin introspection available when MCP config is malformed
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.
2026-05-15 09:56:56 +09:00
bellman
985c6e97f9 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 09:56:50 +09:00
bellman
c522dc970f Preserve plugin lifecycle JSON in G007 CLI output
Constraint: G007 worker integrations made plugin command JSON degraded-aware but omitted the structured plugin/load-failure arrays expected by inventory contracts.\nRejected: Drop lifecycle arrays from tests | G007 requires plugin lifecycle state to stay machine-readable across plugin surfaces.\nConfidence: high\nScope-risk: narrow\nDirective: Keep  carrying plugin entries, lifecycle state, and load failures even when config loading degrades.\nTested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; 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 --test output_format_contract plugins_json_surfaces_lifecycle_contract_when_plugin_is_installed -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract inventory_commands_emit_structured_json_when_requested -- --nocapture; git diff --check\nNot-tested: full workspace suite\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 09:56:46 +09:00
bellman
db91a235e9 omx(team): merge worker-2 2026-05-15 09:55:47 +09:00
bellman
f0e8896d2e omx(team): auto-checkpoint worker-2 [2] 2026-05-15 09:55:43 +09:00
bellman
2454f012b6 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 09:52:29 +09:00
bellman
17b4ab45c6 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 09:50:23 +09:00
bellman
80b8984b62 omx(team): auto-checkpoint worker-4 [5] 2026-05-15 09:49:36 +09:00
bellman
b01192dde7 omx(team): auto-checkpoint worker-3 [4] 2026-05-15 09:49:33 +09:00
bellman
12ca5550fa omx(team): auto-checkpoint worker-1 [1] 2026-05-15 09:49:30 +09:00
bellman
1a6e475f74 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 09:49:22 +09:00
bellman
0cd1eabb5d Keep G007 plugin command integration compiling
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>
2026-05-15 09:49:16 +09:00
bellman
f2ba3648d6 omx(team): auto-checkpoint worker-3 [4] 2026-05-15 09:45:57 +09:00
bellman
76920c7d6c omx(team): auto-checkpoint worker-1 [1] 2026-05-15 09:45:54 +09:00
bellman
0a14f8511e omx(team): auto-checkpoint worker-4 [5] 2026-05-15 09:45:33 +09:00
bellman
391e343220 omx(team): merge worker-2 2026-05-15 09:45:30 +09:00
bellman
18805b565a omx(team): auto-checkpoint worker-2 [2] 2026-05-15 09:45:29 +09:00
bellman
65a144c3f7 Keep G006 packet regressions aligned with shipped schema
Constraint: G006 worker auto-checkpoints added useful regression coverage after the pushed leader schema.\nRejected: Reapply later worker policy/lane merges | they produced duplicate definitions and syntax failures during shutdown integration.\nConfidence: high\nScope-risk: narrow\nDirective: Preserve the shipped G006 packet schema shape unless a future migration updates callers and tests together.\nTested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p runtime -p tools -p rusty-claude-cli; cargo test --manifest-path rust/Cargo.toml -p runtime task_packet -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools run_task_packet -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime task_registry -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime session_heartbeat -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime policy_engine -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli status_json_surfaces -- --nocapture; git diff --check\nNot-tested: full workspace test suite\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 09:41:38 +09:00
bellman
6d809cb278 omx(team): auto-checkpoint worker-1 [1] 2026-05-15 09:37:18 +09:00
bellman
f7235ca932 Make G006 task policy state machine executable
Typed task packets, policy decisions, lane board status, and session liveness now have concrete runtime contracts and focused regressions for Stream 4.

Constraint: G006 requires task/lane operation without pane scraping while preserving legacy task packet callers.
Rejected: waiting on stale worker worktrees | all G006 worker worktrees remained at main with no commits, so leader integrated the verified slice directly.
Confidence: high
Scope-risk: moderate
Directive: Keep task packet serde defaults when adding fields so older packets continue to deserialize.
Tested: git diff --check; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p runtime -p tools -p rusty-claude-cli; cargo test --manifest-path rust/Cargo.toml -p runtime task_packet -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime policy_engine -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime task_registry -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime session_heartbeat -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools run_task_packet_creates_packet_backed_task -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools lane_completion -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli status_json_surfaces -- --nocapture
Not-tested: full workspace test suite; PR/issue reconciliation deferred to G011/G012

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 09:29:26 +09:00
bellman
41b769fc5a Merge commit '204af77596345c120e25ce9d433dad0676a59b37' 2026-05-14 21:43:23 +09:00
bellman
7426ede2eb map branch recovery verification evidence
Record why the G005 branch-recovery work satisfies the roadmap pinpoints without touching leader-owned Ultragoal state.

Constraint: Task 2 requested ROADMAP.md/plan pinpoint mapping and explicitly forbids .omx/ultragoal mutation.

Rejected: leader-only mailbox note | the task prefers a repo-local docs/g005 verification map when unclaimed and absent.

Confidence: high

Scope-risk: narrow

Directive: Keep this map evidence-only; do not treat it as a substitute for leader Ultragoal checkpoints.

Tested: documentation-only map cross-checked against ROADMAP.md, prd.json, and task-1 verification output.

Not-tested: no code tests rerun after documentation-only commit.
2026-05-14 18:40:16 +09:00
bellman
8f7eaffcef Close the G005 verification gaps before checkpoint
Constraint: G005 requires stale-base doctor consistency, green-contract policy integration, hung-test evidence, and a durable verification map before ultragoal checkpointing.\nRejected: Treat worker task status alone as complete | worker-2 lifecycle was stale-failed despite landed recovery evidence, so leader verification and explicit map are required.\nConfidence: medium\nScope-risk: moderate\nDirective: Keep PR/issue reconciliation deferred to G011/G012; do not mutate .omx/ultragoal outside checkpoint commands.\nTested: git diff --check; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p rusty-claude-cli; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli workspace_health_warns_when_stale_base_diverged -- --nocapture; cargo check --manifest-path rust/Cargo.toml -p tools\nNot-tested: full workspace test suite due known unrelated permission/lifecycle failures from worker evidence.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-14 18:38:22 +09:00
bellman
d2b5f5d498 require provenance for green contracts
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.
2026-05-14 18:33:51 +09:00
bellman
607f071ca8 harden branch recovery reporting
Ensure branch-recovery verification surfaces compile cleanly under focused lint by preserving trusted-root fallback without clippy noise.

Constraint: G005 worker task requires verified branch/test awareness and recovery reporting evidence without mutating .omx/ultragoal.

Rejected: ignoring focused clippy failure | would leave modified tools surface with avoidable lint noise.

Confidence: high

Scope-risk: narrow

Directive: Keep recovery surfaces machine-readable; do not collapse test hangs back into generic timeouts.

Tested: cargo test -p runtime; cargo test -p tools targeted branch/hung/preflight tests; cargo check -p runtime -p tools; cargo clippy -p runtime --all-targets -- -D warnings; cargo clippy -p tools --lib --no-deps -- -D warnings.

Not-tested: full cargo test -p tools remains red on pre-existing permission-enforcer expectation failures unrelated to this change.
2026-05-14 18:33:48 +09:00
bellman
d3f8ff9916 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 18:28:21 +09:00
bellman
204af77596 Keep recovery recipe lint green for ledger reporting
Scoped to G005 recovery recipe status reporting verification; preserves existing machine-readable ledger/status fields and allows the intentionally long recovery attempt flow to satisfy strict clippy without touching unrelated bash lint debt.\n\nConstraint: Task scope limited to recovery_recipes.rs and smallest adjacent exports.\nRejected: Refactor attempt_recovery during branch recovery | higher regression risk than preserving established flow.\nConfidence: high\nScope-risk: narrow\nDirective: Do not expand this task into unrelated bash.rs clippy cleanup.\nTested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p runtime; cargo test --manifest-path rust/Cargo.toml -p runtime recovery_ -- --nocapture; cargo clippy --manifest-path rust/Cargo.toml -p runtime --lib -- -D warnings -A clippy::single-match-else\nNot-tested: full clippy without allow still fails on pre-existing rust/crates/runtime/src/bash.rs single_match_else outside task scope.
2026-05-14 18:26:58 +09:00
bellman
5c40d4e778 omx(team): auto-checkpoint worker-3 [4] 2026-05-14 18:26:55 +09:00
bellman
5625ba597b omx(team): auto-checkpoint worker-1 [1] 2026-05-14 18:26:49 +09:00
bellman
4f60cf70f1 omx(team): merge worker-2 2026-05-14 18:24:51 +09:00
bellman
6a37442ee1 omx(team): auto-checkpoint worker-2 [3] 2026-05-14 18:24:51 +09:00
bellman
0bca524c8c omx(team): auto-checkpoint worker-1 [1] 2026-05-14 18:22:37 +09:00
bellman
2ad56860df omx(team): merge worker-1 2026-05-14 18:21:26 +09:00
bellman
1fbde9f47f omx(team): auto-checkpoint worker-1 [1] 2026-05-14 18:21:26 +09:00
bellman
879962b826 map g004 event report verification lanes
Give the leader a durable integration map for G004 event/report contracts, including ownership boundaries, focused verification commands, hazards, and worker commit evidence.\n\nConstraint: Task 5 is audit/coordination only and leader owns .omx/ultragoal checkpoints.\nRejected: Expanding into approval-token implementation | worker-2 owns that lane in the G004 split.\nConfidence: high\nScope-risk: narrow\nDirective: Re-run runtime and targeted tools contracts after integrating worker branches; do not infer Codex goal state from this doc.\nTested: map content backed by task 1/2/4 verification evidence and current git history.\nNot-tested: full workspace suite.
2026-05-14 18:14:24 +09:00
bellman
0b0d55d7ec omx(team): auto-checkpoint worker-1 [1] 2026-05-14 18:11:53 +09:00
bellman
7214573f35 Keep approval token contracts in their own runtime module
Constraint: G004 task 3 now owns approval-token contracts through rust/crates/runtime/src/approval_tokens.rs, while auto-integration left a duplicate unused copy in permissions.rs.\nRejected: suppressing dead-code warnings | the duplicate implementation was obsolete after the dedicated module landed.\nConfidence: high\nScope-risk: narrow\nDirective: Keep permission-mode authorization in permissions.rs and approval-token policy handoff in approval_tokens.rs.\nTested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p runtime; cargo test --manifest-path rust/Cargo.toml -p runtime approval_token -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime --test g004_conformance -- --nocapture\nNot-tested: full workspace test suite; G004 tasks 2/4/5 remain non-terminal.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-14 18:11:20 +09:00
bellman
dcf11f8190 harden report contract projection identity
Add a runtime report schema v1 contract so downstream consumers can negotiate structured fields, verify canonical report identity, and audit projection redactions without reverse-engineering prose.\n\nConstraint: Task 2 scope was limited to report schema/projection/redaction modules/docs/tests and prohibited .omx/ultragoal mutation.\nRejected: Wiring into broader CLI report emitters | kept this slice focused on the reusable contract and deterministic fixtures.\nConfidence: high\nScope-risk: narrow\nDirective: Future report emitters should build canonical payloads through CanonicalReportV1 before projecting audience-specific views.\nTested: cargo test -p runtime report_schema -- --nocapture; cargo test -p runtime lane_events -- --nocapture; cargo check -p runtime\nNot-tested: cargo clippy -p runtime --all-targets -- -D warnings remains blocked by pre-existing non-task warnings in compact.rs, file_ops.rs, policy_engine.rs, sandbox.rs.
2026-05-14 18:09:36 +09:00
bellman
f79ca989ba omx(team): merge worker-3 2026-05-14 18:07:29 +09:00
bellman
e1641aa010 Prove G004 contract bundles are machine-checkable
Constraint: Task 6 needed a regression harness without overwriting Task 1-4 implementation files.\nRejected: Editing lane_events/report-schema/approval-token owners directly | would create shared-file conflicts with active lanes.\nConfidence: high\nScope-risk: narrow\nDirective: Keep this harness as a consumer-facing conformance layer; extend fixtures after Task 2/3 land schema/token producers.\nTested: cd rust && cargo test -p runtime --test g004_conformance -- --nocapture; cd rust && cargo check -p runtime; cd rust && cargo fmt --check; git diff --check\nNot-tested: cargo clippy -p runtime --tests -- -D warnings fails on pre-existing runtime lint debt outside changed files.
2026-05-14 18:07:11 +09:00
bellman
5cebdd999d omx(team): auto-checkpoint worker-2 [3] 2026-05-14 18:07:05 +09:00
bellman
bf533d77a7 task: approval token chain
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.
2026-05-14 18:07:03 +09:00
bellman
e34209ff7f omx(team): auto-checkpoint worker-2 [3] 2026-05-14 18:07:00 +09:00
bellman
ff37d395bb Stabilize G004 contract integration after worker merges
Constraint: G004 worker integrations introduced unparseable approval-token tests and a conformance path bug that blocked leader verification.\nRejected: waiting for another auto-integration cycle | local leader verification had exact parse and fixture failures to repair safely.\nConfidence: high\nScope-risk: moderate\nDirective: Keep approval-token regression tests in cfg(test) modules or integration tests, never inside type definitions.\nTested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p runtime; cargo test --manifest-path rust/Cargo.toml -p runtime approval_token -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime --test g004_conformance -- --nocapture; python3 .github/scripts/check_doc_source_of_truth.py\nNot-tested: full workspace test suite; remaining G004 tasks 1-5 still non-terminal.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-14 18:06:14 +09:00
bellman
f8d744bb37 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 18:05:26 +09:00
bellman
c8c936ede1 omx(team): auto-checkpoint worker-3 [6] 2026-05-14 18:00:23 +09:00
bellman
57b3e3258b omx(team): auto-checkpoint worker-2 [3] 2026-05-14 18:00:19 +09:00
bellman
06e545325d omx(team): auto-checkpoint worker-1 [1] 2026-05-14 18:00:16 +09:00
bellman
ed3ccae844 omx(team): auto-checkpoint worker-4 [unknown] 2026-05-14 17:58:49 +09:00
bellman
f4e08d0ecf omx(team): auto-checkpoint worker-2 [3] 2026-05-14 17:58:46 +09:00
bellman
030f2ef20f omx(team): merge worker-2 2026-05-14 17:57:59 +09:00
bellman
16d6525de4 omx(team): auto-checkpoint worker-2 [3] 2026-05-14 17:57:59 +09:00
bellman
42c79218c9 Merge commit '4e0211d36c0180e787e73f96d52381f40a4c7ac4' 2026-05-14 17:54:45 +09:00
bellman
4e0211d36c Expose boot preflight evidence in diagnostic JSON
Task 5 needed machine-readable status/doctor evidence for reliable worker boot checks. This keeps the contract local to CLI diagnostics and validates relative trustedRoots handling for preflight allowlist decisions.

Constraint: G003 worker task forbids .omx/ultragoal mutation and scopes changes to session/preflight/doctor JSON surfaces.

Rejected: broad runtime worker boot refactor | other workers own worker_boot.rs and trust resolver implementation lanes.

Confidence: high

Scope-risk: narrow

Directive: Keep boot_preflight JSON fields stable for downstream automation; add fields rather than renaming existing keys.

Tested: cargo fmt --manifest-path rust/Cargo.toml --package rusty-claude-cli; cargo check --manifest-path rust/Cargo.toml -p rusty-claude-cli; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli boot_preflight_snapshot_reports_machine_readable_contract_fields -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli branch_freshness_parses_ahead_behind_status_header -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli status_json_surfaces_session_lifecycle_for_clawhip -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract -- --nocapture

Not-tested: cargo clippy --manifest-path rust/Cargo.toml -p rusty-claude-cli --all-targets -- -D warnings fails on pre-existing runtime clippy warnings in compact.rs, file_ops.rs, policy_engine.rs, sandbox.rs before reaching changed CLI checks.
2026-05-14 17:52:41 +09:00
bellman
aec291caab omx(team): auto-checkpoint worker-4 [unknown] 2026-05-14 17:51:53 +09:00
bellman
43b182882a Lock doctor JSON boot preflight contract
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>
2026-05-14 17:51:47 +09:00
bellman
307b23d27f omx(team): auto-checkpoint worker-4 [unknown] 2026-05-14 17:50:36 +09:00
bellman
8c11dd16f4 task: preserve startup no-evidence timestamp evidence
Lock the startup-no-evidence contract so prompt timestamps remain the original send time while lifecycle and pane timestamps prove timeout ordering.

Constraint: task 4 scope limited changes to runtime worker boot/session/startup modules and tests; .omx/ultragoal not mutated.

Rejected: CLI-surface changes | runtime evidence contract already exposes the typed worker.startup_no_evidence payload.

Confidence: high

Scope-risk: narrow

Directive: Keep startup timeout evidence timestamps stable across later lifecycle observations.

Tested: cargo test -p runtime worker_boot -- --nocapture; cargo check --workspace

Not-tested: cargo clippy -p runtime --tests -- -D warnings is blocked by pre-existing runtime warnings in compact.rs, file_ops.rs, policy_engine.rs, and sandbox.rs.
2026-05-14 17:50:33 +09:00
bellman
2012718749 Map G003 boot session verification
Document the current G003 worker boot, trust, session-control, and preflight verification surfaces so leader integration can sequence worker-owned patches without mutating Ultragoal state.\n\nConstraint: Task 2 is audit-only/coordination; no .omx/ultragoal mutation and no shared implementation/test edits.\nRejected: Fixing clippy warnings in runtime integration tests | outside audit-only scope and owned by integration cleanup.\nConfidence: high\nScope-risk: narrow\nDirective: Keep this map updated when G003 worker splits or verification commands change.\nTested: ../scripts/fmt.sh --check; cargo test -p runtime worker_boot -- --nocapture; cargo test -p tools worker_ -- --nocapture; cargo check -p runtime -p tools -p commands\nNot-tested: cargo clippy -p runtime -p tools -p commands --all-targets --no-deps -- -D warnings fails on pre-existing runtime integration_tests duration_suboptimal_units warnings.
2026-05-14 17:50:30 +09:00
bellman
79d3b809f9 omx(team): auto-checkpoint worker-4 [unknown] 2026-05-14 17:46:16 +09:00
bellman
9ec4d8398e omx(team): auto-checkpoint worker-3 [unknown] 2026-05-14 17:46:13 +09:00
bellman
5f45740408 omx(team): auto-checkpoint worker-2 [unknown] 2026-05-14 17:46:10 +09:00
bellman
675d9ddc78 Harden workspace path classification
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.
2026-05-14 17:46:07 +09:00
bellman
087e31d190 Keep G003 integrated runtime tests compiling
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>
2026-05-14 17:45:46 +09:00
bellman
a6ee51baab omx(team): auto-checkpoint worker-3 [unknown] 2026-05-14 17:40:32 +09:00
bellman
6df60a4683 omx(team): auto-checkpoint worker-2 [unknown] 2026-05-14 17:40:29 +09:00
bellman
3cf0db8f79 omx(team): merge worker-1 2026-05-14 17:38:59 +09:00
bellman
964458ad4a omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:38:59 +09:00
bellman
d87c3e6400 Make roadmap PR intake durable for CC2
Constraint: User explicitly requested all roadmap PRs be merged when correct and mapped into the Ultragoal backlog when not immediately mergeable.
Rejected: leaving the PR inventory as ignored OMX-only state | roadmap merge obligations need a tracked handoff for later G011/G012 gates.
Confidence: high
Scope-risk: narrow
Directive: Refresh this intake after each roadmap PR merge batch and regenerate the CC2 board if ROADMAP.md changes.
Tested: gh pr list --state open --search roadmap in:title --json number,title,author,mergeable,isDraft,statusCheckRollup,headRefName,baseRefName,updatedAt,url --limit 200
Not-tested: individual PR mergeability was not forced in this intake commit.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-14 17:36:15 +09:00
bellman
ac888623a8 Merge commit '3a8ce832341884322ede0855b150e3ceebe9180d' 2026-05-14 17:34:07 +09:00
bellman
3a8ce83234 Deny scoped file reads before tool dispatch
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>
2026-05-14 17:34:03 +09:00
bellman
37b2b75287 Keep G002 path-scope tests aligned with enforced denials
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>
2026-05-14 17:33:47 +09:00
bellman
f2dc615a8a Prevent workspace escape through tool path resolution
File and shell tool dispatch now resolves path-sensitive operations through workspace-scoped wrappers so direct paths, globs, symlinks, shell expansion, and Windows absolute path probes fail before execution when they leave the workspace.

Constraint: G002-alpha-security requires alpha-blocking workspace/path scope enforcement without mutating .omx/ultragoal

Rejected: string-prefix only checks | they miss canonical symlink and glob expansion escapes

Confidence: high

Scope-risk: moderate

Directive: keep new file/shell tool entrypoints wired through workspace-aware wrappers before dispatch

Tested: python3 -m unittest discover -s tests -v; python3 -m compileall -q src tests; cargo test -p runtime workspace --manifest-path rust/Cargo.toml --quiet; cargo test -p tools workspace --manifest-path rust/Cargo.toml --quiet; cargo test -p tools given_workspace_write_enforcer_when_bash --manifest-path rust/Cargo.toml --quiet; cargo test -p tools file_tools_reject --manifest-path rust/Cargo.toml --quiet; cargo fmt --all --manifest-path rust/Cargo.toml -- --check; cargo check --manifest-path rust/Cargo.toml --workspace

Not-tested: full unfiltered cargo test workspace due task-time constraints; targeted runtime/tools workspace security tests and full cargo check passed

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-14 17:30:57 +09:00
bellman
9bc55f9946 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:30:54 +09:00
bellman
180ebb3b02 Reject Windows absolute PowerShell paths from workspace scope
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>
2026-05-14 17:29:57 +09:00
bellman
534442b8da Document G002 security verification ownership for integration
Constraint: Task 5 is reporting/map ownership only; worker-1 owns implementation changes and shared security/path tests.\nRejected: Editing runtime enforcement failures from this lane | shared implementation/test ownership belongs to other workers unless re-scoped.\nConfidence: high\nScope-risk: narrow\nDirective: Keep this artifact synchronized with exact verification output before leader aggregation.\nTested: 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; scripts/fmt.sh --check; cargo check --workspace; targeted runtime permission/path tests; mock parity harness.\nNot-tested: Full clippy and cargo test --workspace are not green due pre-existing/shared runtime/CLI failures documented in the artifact.
2026-05-14 17:29:33 +09:00
bellman
9c2ebb4f39 task: prefer tests before fixes
Add focused regression coverage for path-scope enforcement before implementation changes land, preserving worker-1 ownership of the fix path.

Constraint: task 4 requested tests-first coverage for direct path, symlink, glob/shell expansion, worktree, and Windows-style path cases.\nRejected: implementation edits in enforcement code | worker-1 owns minimal implementation changes.\nConfidence: high\nScope-risk: narrow\nDirective: Keep these regressions red until path canonicalization/enforcement blocks outside-workspace reads before dispatch.\nTested: cargo fmt -p tools -- --check; cargo check -p tools; cargo clippy -p tools --test path_scope_enforcement (warnings only, pre-existing); cargo test -p tools --test path_scope_enforcement (expected red: 4 failing path-scope gaps, 2 passing baselines).\nNot-tested: Full workspace test suite because the new regression tests intentionally fail until implementation lands.
2026-05-14 17:29:31 +09:00
bellman
2c48400293 omx(team): auto-checkpoint worker-3 [4] 2026-05-14 17:27:21 +09:00
bellman
713ca7aee4 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:27:18 +09:00
bellman
02b591ac64 omx(team): auto-checkpoint worker-3 [4] 2026-05-14 17:22:09 +09:00
bellman
f789525839 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:22:06 +09:00
bellman
b1d8a66515 Gate CC2 completion on PR and issue resolution
The Ultragoal now has an explicit repository-operations gate so final completion cannot rely only on roadmap implementation while correct PRs or resolvable issues remain unhandled.

Constraint: The user explicitly added that all PRs should be merged and all issues resolved when they are correct and resolvable.

Rejected: Treating the existing roadmap board as sufficient | it did not require per-PR and per-issue final triage evidence.

Confidence: high

Scope-risk: narrow

Directive: Refresh GitHub PR and issue snapshots at the final gate; do not merge unsafe or incorrect PRs merely to reduce counts.

Tested: gh auth status; gh pr list --state open --limit 200 captured 50 records; gh issue list --state open --limit 1000 captured 1000 records.

Not-tested: Full PR/issue triage is deferred to the dedicated gate and later streams.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-14 17:21:21 +09:00
bellman
ad9e0234a9 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:19:25 +09:00
bellman
145413d624 omx(team): auto-checkpoint worker-4 [5] 2026-05-14 17:19:01 +09:00
bellman
17da2964d7 omx(team): auto-checkpoint worker-3 [4] 2026-05-14 17:18:58 +09:00
bellman
9ab569e626 omx(team): auto-checkpoint worker-2 [3] 2026-05-14 17:18:55 +09:00
bellman
4af5664ff8 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:18:52 +09:00
bellman
1864ce38ad omx(team): auto-checkpoint worker-3 [4] 2026-05-14 17:18:06 +09:00
bellman
74cc590407 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:18:03 +09:00
bellman
a4b20ea34d omx(team): merge worker-3 2026-05-14 17:17:12 +09:00
bellman
8d0cee46d5 omx(team): auto-checkpoint worker-3 [4] 2026-05-14 17:17:11 +09:00
bellman
45b43b5a96 Make the CC2 board schema executable for G001
The canonical Stream 0 board must be machine-checkable before Ultragoal can checkpoint G001, so the generated board and validation wrapper now share the same rich board schema and Markdown renderer.

Constraint: G001 requires .omx/cc2/board.json and .omx/cc2/board.md to prove all frozen ROADMAP.md headings and ordered actions are mapped.

Rejected: Relying on worker-reported validation alone | leader-side validation found schema drift between the status-only and lifecycle_status board entrypoints.

Confidence: high

Scope-risk: narrow

Directive: Keep scripts/generate_cc2_board.py, scripts/validate_cc2_board.py, scripts/cc2_board.py, and .omx/cc2/render_board_md.py aligned on board schema changes.

Tested: python3 scripts/generate_cc2_board.py; python3 scripts/validate_cc2_board.py; python3 scripts/cc2_board.py validate; python3 .omx/cc2/validate_issue_parity_intake.py; python3 .omx/cc2/render_board_md.py .omx/cc2/board.json .omx/cc2/board.md --check; python3 -m py_compile scripts/generate_cc2_board.py scripts/validate_cc2_board.py scripts/cc2_board.py .omx/cc2/validate_issue_parity_intake.py .omx/cc2/render_board_md.py; cargo check --manifest-path rust/Cargo.toml --workspace.

Not-tested: Full cargo test workspace has unrelated existing failures reported by workers in session lifecycle/permission-mode tests.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-14 17:14:07 +09:00
bellman
d15268e2cc Create a canonical CC2 board so every frozen ROADMAP heading is verifiably mapped
Derive the board from ROADMAP.md heading anchors and record the required local research and adaptive-plan sources as immutable manifest metadata. Add a validation command that fails if any ROADMAP heading lacks a board item or required lifecycle fields.

Constraint: Workers must not mutate .omx/ultragoal; board outputs live under .omx/cc2 and source research is read-only.
Rejected: Hand-maintained board rows | too easy to leave ROADMAP headings unmapped and hard to validate.
Confidence: high
Scope-risk: narrow
Directive: Regenerate with scripts/cc2_board.py after ROADMAP.md changes, then run the validate command before checkpointing G001.
Tested: python3 -m py_compile scripts/cc2_board.py; python3 scripts/cc2_board.py validate; cargo check --workspace; cargo fmt --all -- --check
Not-tested: cargo test --workspace has unrelated failing rusty-claude-cli lifecycle assertion tests::session_lifecycle_prefers_running_process_over_idle_shell.
2026-05-14 17:08:52 +09:00
bellman
424825f8cb task: G001 human board and docs rendering
Render the canonical CC2 board into a human-readable review artifact while preserving worker-1's generated schema as the source of truth.\n\nConstraint: leader owns Ultragoal state; .omx/ultragoal was not mutated.\nRejected: hand-editing board.md without a renderer | it would make coverage drift harder to validate.\nConfidence: high\nScope-risk: narrow\nDirective: regenerate board.md with .omx/cc2/render_board_md.py after board.json changes.\nTested: python3 .omx/cc2/render_board_md.py .omx/cc2/board.json .omx/cc2/board.md --check; python3 -m py_compile .omx/cc2/render_board_md.py; cargo check --workspace; cargo test --workspace (fails one unrelated lifecycle test).\nNot-tested: cargo test --workspace is not fully green because tests::session_lifecycle_prefers_running_process_over_idle_shell fails persistently in rusty-claude-cli without touching Rust sources.
2026-05-14 17:08:49 +09:00
bellman
07dad88e8c Classify issue and parity intake for CC2 board integration
Constraint: Task 3 scope is limited to G001 issue/parity intake and must not mutate .omx/ultragoal
Rejected: Editing canonical board.json directly | worker-1 owns Task 2 canonical board output and coordination requires a mergeable fragment
Confidence: high
Scope-risk: narrow
Directive: Integrate these rows into .omx/cc2/board.json and board.md without reclassifying the frozen evidence unless the source snapshot changes
Tested: python3 .omx/cc2/validate_issue_parity_intake.py; python3 -m py_compile .omx/cc2/validate_issue_parity_intake.py; python3 -m json.tool .omx/cc2/issue-parity-intake.json; cargo check --manifest-path rust/Cargo.toml --workspace
Not-tested: cargo test --manifest-path rust/Cargo.toml --workspace has 2 pre-existing/environmental failures in rusty-claude-cli tests unrelated to .omx/cc2 intake files
2026-05-14 17:07:43 +09:00
bellman
5c77896dec omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:07:40 +09:00
bellman
74bbf4b36f omx(team): auto-checkpoint worker-4 [unknown] 2026-05-14 17:00:14 +09:00
bellman
481585f865 omx(team): auto-checkpoint worker-1 [1] 2026-05-14 17:00:11 +09:00
bellman
c6e2a7dee4 omx(team): merge worker-1 2026-05-14 16:58:43 +09:00
bellman
83116555ff omx(team): auto-checkpoint worker-1 [1] 2026-05-14 16:58:43 +09:00
YeonGyu-Kim
8f55870dad docs(roadmap): add #448 — sandbox JSON has contradictory enabled/supported/active flags
Pinpoint: 'enabled:true, supported:false' is semantic nonsense.
'filesystem_active:true allowed_mounts:[]' contradicts 'workspace-only'.
'active:false filesystem_active:true' has no documented aggregation rule.
Renaming 'enabled' to 'requested' and exposing 'active_components:[]'
would surface real isolation state to automation.
2026-05-11 23:32:30 +09:00
YeonGyu-Kim
7244a82b36 docs(roadmap): add #447 — JSON error envelopes go to stderr; stdout empty on error
Pinpoint: claw --no-such-flag --output-format json writes the JSON
envelope to stderr (115 bytes) while stdout is 0 bytes. Same for
missing_credentials, session_load_failed, invalid_model_syntax —
all 4 error kinds tested put JSON on stderr. Breaks the standard
'output=$(cmd --output-format json)' pattern. Every major CLI
(kubectl/gh/aws/jq/terraform -json) puts JSON on stdout regardless
of success/failure. Sibling: deprecation warnings precede the JSON
envelope on stderr, breaking 'tail -1 | jq' parsing.
2026-05-11 23:01:46 +09:00
YeonGyu-Kim
5ab969e7ae docs(roadmap): add #446 — config loaded 2-3x per invocation; identical deprecation warnings spam
Pinpoint: status emits 3x deprecation warnings, doctor 2x, mcp 2x,
version 0x. Each duplicate is byte-identical (same file/line/field).
Config-load pipeline is fanned out across commands without a cache.
15 redundant disk reads in worst case. Real warnings drowned out by
copies. Count fluctuates between HEADs (3 at 6c0c305a, 4 at d7dbe951,
3 at 5a4cc506) — no architectural fix landed.
2026-05-11 22:33:34 +09:00
YeonGyu-Kim
5a4cc506d5 docs(roadmap): add #445 — skill name-vs-dirname mismatch silently accepted; sibling silent drops
Pinpoint: .claw/skills/wrong-name/SKILL.md with frontmatter name:
actually-different-name silently loads as the frontmatter name. Users
referencing by dir name get skill_not_found while skills list shows
the frontmatter name. Siblings: subdir without SKILL.md silently
skipped; loose .md at skills root dropped; no --scope filter for
workspace vs user merge.
2026-05-11 22:01:12 +09:00
YeonGyu-Kim
9e1eafd02d docs(roadmap): add #444 — no broad-cwd guard for --resume; ROOT/HOME silently writable
Pinpoint: claw --resume latest from / hits 'Read-only file system'
(OS error 30) — only saved by root being read-only. From /tmp or
$HOME, silently creates .claw/sessions/<fingerprint>/ droppings.
Exit code 0 on the read-only-FS error. Stale /tmp/.claw from 13:31
dogfood still present at 21:30 (10 hours, 6+ HEADs later) — #435's
deferred-creation fix hasn't landed. The broad-cwd guard only covers
shorthand prompt path, not resume/status/doctor.
2026-05-11 21:31:33 +09:00
YeonGyu-Kim
b2048856f3 docs(roadmap): add #443 — acp serve exits 0 with status:discoverability_only; #413 still unfixed
Pinpoint: claw acp serve --output-format json exits 0 with explicit
'not implemented' message + supported:false. Automation gating on $?
sees success from a no-op. ROADMAP #413's internal-tracking leak
(discoverability_tracking, tracking) confirmed UNFIXED 11 days later.
Sibling: claw acp status returns kind:unknown (14th catch-all occurrence).
2026-05-11 21:01:24 +09:00
YeonGyu-Kim
19aaf9d05e docs(roadmap): add #442 — agents require TOML format, .md files silently dropped
Pinpoint: claw-code only loads .toml files from .claw/agents/. Claude
Code uses .md with YAML frontmatter — schema divergence. Source code
at commands/src/lib.rs:3378 silently skips non-.toml extensions with
no warning. Help text omits the format requirement. Same silent-drop
pattern as #440 (MCP) and #441 (hooks). Also: .claude/agents/ never
discovered; required fields undocumented; no scaffolding command.
2026-05-11 20:31:50 +09:00
YeonGyu-Kim
8499599b70 docs(roadmap): add #441 — hooks schema diverges from Claude Code documented format
Pinpoint: claw-code expects {hooks:{PreToolUse:['cmd-string']}} while
Claude Code docs specify {hooks:{PreToolUse:[{matcher,hooks:[{type,command}]}]}}.
Users copy-pasting from Claude Code docs get the cryptic 'must be an
array of strings, got an array' error. PR #3000 already addresses
this but is conflicting and unmerged. Siblings: unknown hook event
rejects entire hooks config (#440 pattern); first-error-only halting;
kind:unknown catch-all (13th occurrence).
2026-05-11 20:01:33 +09:00
YeonGyu-Kim
86ff83c233 docs(roadmap): add #440 — one invalid mcpServers entry blocks ALL valid servers
Pinpoint: .claw.json with one valid mcpServers entry + one missing-command
entry → mcp list returns configured_servers:0, servers:[]. The valid
server is silently dropped because parser halts on first error.
Five invalid entries in the same file produce only ONE error message
(first one); user must iterate N times to discover all problems.
Violates ROADMAP product principle #5 (partial success first-class).
2026-05-11 19:31:23 +09:00
YeonGyu-Kim
bd126905db docs(roadmap): add #439 — ancestor CLAUDE.md walk causes silent context bleed
Pinpoint: from /tmp/proj/sub/deep, claw walks ALL ancestors loading
every CLAUDE.md up to $HOME boundary. Stale /tmp/CLAUDE.md silently
bleeds into every workspace under /tmp/*. No --no-parent-memory flag,
no .claw-root boundary marker, no per-file attribution in status JSON.
Git-root is NOT a discovery boundary either.
2026-05-11 19:01:50 +09:00
YeonGyu-Kim
f4a9674086 docs(roadmap): add #438 — memory file discovery only finds CLAUDE.md, ignores AGENTS.md + CLAW.md
Pinpoint: claw-code reads CLAUDE.md (inherited from upstream Claude Code)
but silently ignores AGENTS.md (industry convention used by OpenCode/
Codex/Aider/Cursor/Continue.dev) and CLAW.md (project's own brand name).
Users with mixed-tool workflows maintaining a shared AGENTS.md see
memory_file_count stay low with no warning.
2026-05-11 18:31:17 +09:00
YeonGyu-Kim
d3a982dda9 docs(roadmap): add #437 — version JSON missing is_dirty/branch/commit_date/rustc; git_sha truncated
Pinpoint: claw version --output-format json omits is_dirty, branch,
commit_date, commit_timestamp, rustc_version. git_sha is 7-char short
form (collision risk + no git rev-parse round-trip). executable_path
leaks compile-host path /tmp/claw-dog-0530/... Sibling: prose 'message'
field still duplicates structured data (#391 supposedly fixed).
2026-05-11 18:00:57 +09:00
YeonGyu-Kim
8cf628a53c docs(roadmap): add #436 — init template sets permissions.defaultMode:dontAsk + empty .claw/
Pinpoint: claw init creates .claw.json with permissions.defaultMode:
dontAsk (disabled permission prompts by default) — compounds with #428.
Sibling: .claw/ artifact created as an empty directory (no
settings.json template inside). When .claw/ pre-exists, init skips
the entire artifact without materializing expected sub-content.
2026-05-11 17:31:17 +09:00
YeonGyu-Kim
b8f989b605 docs(roadmap): add #435 — --resume failure: exit 0 text/1 json + creates partition dir
Pinpoint: claw --resume latest on fresh workspace exits 0 in text mode
but 1 in JSON mode (same input, different outcome). Side effect:
.claw/sessions/<fingerprint>/ created on disk despite failure. Siblings:
claw --compact alone drops into REPL; claw --compact 'hello' rejects
shorthand prompt; kind:unknown catch-all yet again.
2026-05-11 17:01:30 +09:00
YeonGyu-Kim
e29010ed48 docs(roadmap): add #434 — POSIX -- separator not recognized; shorthand prompts can't start with dash
Pinpoint: claw -- 'anything' returns 'unknown option: --' with the
misleading 'Did you mean -V?' hint. Every other major CLI (cargo,
git, gh, kubectl, grep) honors POSIX -- as end-of-flags. Shorthand
prompt mode cannot accept any TEXT starting with - or --, forcing
users to remember the explicit 'prompt' verb.
2026-05-11 16:31:21 +09:00
YeonGyu-Kim
0e5f695844 docs(roadmap): add #433 — repeated --output-format silent override + case-sensitive enum
Pinpoint: --output-format json --output-format text silently picks
text, no warning, scripts that compose flags get wrong format.
Siblings: JSON (uppercase) rejected as kind:unknown; CLAW_OUTPUT_FORMAT
env silently ignored; RUST_LOG/CLAW_LOG undocumented.
2026-05-11 16:01:05 +09:00
YeonGyu-Kim
ce39d5c598 docs(roadmap): add #432 — --allowedTools naming inconsistency + missing-value parser bug
Pinpoint: tool-name registry mixes snake_case/PascalCase/UPPERCASE
in single error message; undocumented CamelCase->snake_case alias map
(Read->read_file etc.); missing flag value consumes next positional
(subcommand swallowed). kind:unknown catch-all yet again.
2026-05-11 15:31:25 +09:00
YeonGyu-Kim
fad53e2df9 docs(roadmap): add #431 — skills uninstall requires creds; install error leaks OS string
Pinpoint: claw skills uninstall <bogus> requires API creds despite
being a pure local filesystem op. Siblings: skills install <bogus>
returns raw 'No such file or directory (os error 2)' with kind:unknown;
skills install (no args) treats valid subcommand as unknown action;
agents create doesn't exist (no scaffolding command for agents).
2026-05-11 15:03:45 +09:00
YeonGyu-Kim
328fd114ff docs(roadmap): add #430 — dump-manifests requires upstream TS source; export PATH dropped
Pinpoint: dump-manifests --help advertises 'emit manifests for current
cwd' but actually requires CLAUDE_CODE_UPSTREAM env or --manifests-dir
pointing at upstream TypeScript Claude Code source. Unusable for users
without the original TS repo. Siblings: derivative-work disclosure leak,
kind drift between manifests-dir override path vs default path, export
<PATH> positional silently dropped before validation.
2026-05-11 15:01:37 +09:00
YeonGyu-Kim
075c214439 docs(roadmap): add #429 — no global --cwd flag; misleading 'Did you mean --acp' hint
Pinpoint: claw --cwd PATH rejected as unknown option globally. --cwd
exists ONLY for system-prompt subcommand. Every other major CLI
(cargo -C, git -C, npm --prefix) has global cwd override. Sibling:
'Did you mean --acp?' hint algorithm matches first-character not
semantic category — --acp is ACP/Zed integration, unrelated to cwd.
2026-05-11 14:31:31 +09:00
YeonGyu-Kim
ec882f4c88 docs(roadmap): add #428 — default permission_mode is danger-full-access
Pinpoint: claw runs with full filesystem+network+tool access by default,
no opt-in flag, doctor stays silent. Fix shape: change default to
workspace-write, require explicit opt-in for danger-full-access, add
permissions check to doctor that warns when mode source is default.
Siblings: kind:unknown for invalid_permission_mode (typed-error
catch-all bug), --skip-permissions flag rejected (Claude Code parity).
2026-05-11 14:00:58 +09:00
YeonGyu-Kim
7204844982 docs(roadmap): add #427 — subcommand --help requires auth/config; resume hits auth gate
Pinpoint: claw resume --help, session --help, compact --help all hit
missing_credentials without producing usage. resume <bogus-id> also
requires API creds instead of local-first session_not_found lookup.
Sibling: exit code 0 on these error envelopes (parity bug from #422).
2026-05-11 13:31:38 +09:00
YeonGyu-Kim
1fecdf096b docs(roadmap): add #426 — ANTHROPIC_MODEL env bypasses invalid_model validator
Pinpoint: --model rejects 'bogus-model-xyz' as invalid_model_syntax
but ANTHROPIC_MODEL=bogus-model-xyz returns status:ok with the bogus
value. Siblings: opus alias resolves to 4-6 not 4-7 (current frontier),
CLAW_MODEL and ANTHROPIC_DEFAULT_MODEL silently ignored.
2026-05-11 13:01:08 +09:00
YeonGyu-Kim
3730b459a2 docs(roadmap): add #425 — config precedence undocumented; deprecation warning 4×
Pinpoint: .claw/settings.json silently wins over .claw.json. config
--output-format json reports both loaded:true with no precedence_rank
or per-key attribution. Sibling: deprecation warning fired 3× in
#424's probe, now 4× — config load count regressing upward.
2026-05-11 12:31:16 +09:00
YeonGyu-Kim
d7dbe951ce docs(roadmap): add #424 — bare canonical model names rejected; stale 4-6 suggestion
Pinpoint from Jobdori dogfood. claw --model claude-opus-4-7 returns
invalid_model_syntax error suggesting 'claude-opus-4-6' (one model
behind). Sibling: settings.json deprecation warning repeats 3x per
status invocation (config loaded 3x).
2026-05-11 12:01:33 +09:00
YeonGyu-Kim
6c0c305a4b docs(roadmap): add #423 — claw prompt ignores stdin; kind:unknown for missing arg
Pinpoint from Jobdori dogfood. `echo X | claw prompt` returns
'prompt subcommand requires a prompt string' instead of reading stdin.
Sibling: error kind is 'unknown' not typed 'missing_argument'.
2026-05-11 11:31:12 +09:00
YeonGyu-Kim
3c563fa1dc docs(roadmap): add #422 — unknown subcommand silently sent as chat prompt
Pinpoint from Jobdori dogfood. claw <bogus> with valid creds reaches
Anthropic API as a chat message. Sibling exit-code parity bug:
api_http_error envelope exits 0 while cli_parse exits 1.
2026-05-11 11:01:09 +09:00
YeonGyu-Kim
6aa4b85c95 docs(roadmap): add #421 — JSON cwd leaks /private symlink canonicalization on macOS
Pinpoint from Jobdori dogfood on b98b9a71 in response to Clawhip nudge.
status.workspace.cwd, mcp.working_directory all canonicalize cwd
(intentional for #151 session bleed) but leak the result into JSON
output, breaking string-match automation across macOS symlinks.
2026-05-11 10:32:18 +09:00
118 changed files with 30517 additions and 397 deletions

View File

@@ -0,0 +1,54 @@
name: Anti-slop triage
about: Classify low-signal, duplicate, generated, or unsafe reports before engineering work starts.
title: "triage: "
labels: ["needs-triage"]
body:
- type: markdown
attributes:
value: |
Use this form for issue intake that needs evidence-backed classification before anyone closes, fixes, or escalates it.
Do not paste secrets, live tokens, private logs, or non-public customer data.
- type: dropdown
id: classification
attributes:
label: Initial classification
description: Pick the strongest current classification. Update it if evidence changes.
options:
- actionable-bug
- actionable-docs
- actionable-feature
- duplicate
- spam-or-promotion
- generated-slop-or-hallucinated
- unsafe-or-security-sensitive
- not-reproducible-yet
- externally-blocked
validations:
required: true
- type: textarea
id: evidence
attributes:
label: Evidence
description: Link the PR, issue, command output, docs page, reproduction, duplicate, or policy that supports the classification.
placeholder: "Evidence: ..."
validations:
required: true
- type: textarea
id: safe_next_action
attributes:
label: Safe next action
description: State the next non-destructive action. If closure or merge is proposed, name the required owner/gate.
placeholder: "Next action: label only / request repro / link duplicate / fix docs / defer with rationale / owner review required"
validations:
required: true
- type: checkboxes
id: guardrails
attributes:
label: Guardrails
options:
- label: I did not close, merge, or mutate remote state as part of this triage-only report.
required: true
- label: I checked for duplicates or related PRs/issues before recommending action.
required: true
- label: If this touches credentials, security, or private data, I avoided public reproduction details and routed to the appropriate private/security path.
required: true

17
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,17 @@
## Summary
- TBD
## Anti-slop triage
- Classification: <!-- actionable-fix | docs-only | duplicate | generated-slop | unsafe | out-of-scope | needs-maintainer-decision -->
- Evidence: <!-- issue link, repro command, failing test, docs source, or duplicate PR -->
- Non-destructive review result: <!-- merge candidate | request changes | close/defer with rationale | needs owner gate -->
## Verification
- [ ] Targeted tests/docs checks ran, or the gap is explicitly recorded.
- [ ] `git diff --check` passes.
- [ ] No live secrets, tokens, private logs, or unrelated generated churn are included.
## Resolution gate
- [ ] If this PR resolves an issue, the issue number and fix evidence are linked.
- [ ] If this PR should not merge, the rejection/defer rationale is evidence-backed and does not rely on vibes.
- [ ] I did not merge/close remote PRs or issues from an automation lane without owner approval.

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Validate release-readiness docs that are easy to regress.
The check is intentionally dependency-free so it can run on developer machines,
Windows CI, and minimal release jobs. It validates:
* required repository policy files exist;
* local Markdown links and image targets resolve;
* local heading anchors referenced from Markdown resolve; and
* command examples do not present the deprecated `cargo install claw-code`
package as an executable install path.
"""
from __future__ import annotations
from pathlib import Path
from urllib.parse import unquote, urlparse
import re
import sys
ROOT = Path(__file__).resolve().parents[2]
REQUIRED_POLICY_FILES = [
"LICENSE",
"CONTRIBUTING.md",
"SECURITY.md",
"SUPPORT.md",
"CODE_OF_CONDUCT.md",
]
MARKDOWN_ROOTS = [
ROOT / "README.md",
ROOT / "USAGE.md",
ROOT / "PARITY.md",
ROOT / "PHILOSOPHY.md",
ROOT / "ROADMAP.md",
ROOT / "CONTRIBUTING.md",
ROOT / "SECURITY.md",
ROOT / "SUPPORT.md",
ROOT / "CODE_OF_CONDUCT.md",
ROOT / "docs",
ROOT / "rust" / "README.md",
ROOT / "rust" / "USAGE.md",
ROOT / "rust" / "MOCK_PARITY_HARNESS.md",
]
LINK_PATTERN = re.compile(r"(?<!!)\[[^\]\n]+\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
HTML_LINK_PATTERN = re.compile(r"""<(?:a|img)\b[^>]*(?:href|src)=["']([^"']+)["']""", re.I)
FENCE_PATTERN = re.compile(r"```(?P<lang>[^\n`]*)\n(?P<body>.*?)```", re.S)
def iter_markdown_files() -> list[Path]:
files: set[Path] = set()
for entry in MARKDOWN_ROOTS:
if entry.is_file():
files.add(entry)
elif entry.is_dir():
files.update(entry.rglob("*.md"))
return sorted(files)
def github_anchor(heading: str) -> str:
anchor = heading.strip().lower()
anchor = re.sub(r"<[^>]+>", "", anchor)
anchor = re.sub(r"`([^`]*)`", r"\1", anchor)
anchor = re.sub(r"[^a-z0-9 _-]", "", anchor)
anchor = anchor.replace(" ", "-")
anchor = re.sub(r"-+", "-", anchor)
return anchor.strip("-")
def anchors_for(path: Path) -> set[str]:
anchors: set[str] = set()
for line in path.read_text(encoding="utf-8").splitlines():
match = re.match(r"^(#{1,6})\s+(.+?)\s*#*\s*$", line)
if match:
anchors.add(github_anchor(match.group(2)))
return anchors
def is_external(target: str) -> bool:
parsed = urlparse(target)
return parsed.scheme in {"http", "https", "mailto"}
def validate_policies(errors: list[str]) -> None:
for relative in REQUIRED_POLICY_FILES:
path = ROOT / relative
if not path.is_file():
errors.append(f"missing required policy file: {relative}")
def validate_markdown_links(errors: list[str]) -> None:
anchor_cache: dict[Path, set[str]] = {}
for path in iter_markdown_files():
text = path.read_text(encoding="utf-8")
candidates = [m.group(1) for m in LINK_PATTERN.finditer(text)]
candidates.extend(m.group(1) for m in HTML_LINK_PATTERN.finditer(text))
for target in candidates:
if (
not target
or is_external(target)
or target.startswith(("mailto:", "tel:", "data:"))
):
continue
link_path, _, raw_anchor = target.partition("#")
if not link_path:
destination = path
else:
destination = (path.parent / unquote(link_path)).resolve()
try:
destination.relative_to(ROOT)
except ValueError:
errors.append(
f"{path.relative_to(ROOT)}: link escapes repo root: {target}"
)
continue
if not destination.exists():
errors.append(
f"{path.relative_to(ROOT)}: missing local link target: {target}"
)
continue
if raw_anchor and destination.suffix.lower() == ".md":
anchor = unquote(raw_anchor).lower()
anchor_cache.setdefault(destination, anchors_for(destination))
if anchor not in anchor_cache[destination]:
errors.append(
f"{path.relative_to(ROOT)}: missing anchor `{raw_anchor}` in "
f"{destination.relative_to(ROOT)}"
)
def validate_command_examples(errors: list[str]) -> None:
for path in iter_markdown_files():
text = path.read_text(encoding="utf-8")
for match in FENCE_PATTERN.finditer(text):
lang = match.group("lang").strip().lower()
if lang not in {"bash", "sh", "shell", "zsh", "powershell", "ps1"}:
continue
body = match.group("body")
for offset, line in enumerate(body.splitlines(), start=1):
stripped = line.strip()
if not stripped or stripped.startswith(("#", ">")):
continue
if re.search(r"\bcargo\s+install\s+claw-code\b", stripped):
line_no = text.count("\n", 0, match.start()) + offset + 1
errors.append(
f"{path.relative_to(ROOT)}:{line_no}: deprecated "
"`cargo install claw-code` appears in an executable "
"command block; use build-from-source docs instead"
)
def main() -> int:
errors: list[str] = []
validate_policies(errors)
validate_markdown_links(errors)
validate_command_examples(errors)
if errors:
print("release-readiness check failed:", file=sys.stderr)
for error in errors:
print(f" - {error}", file=sys.stderr)
return 1
print("release-readiness check passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -32,6 +32,10 @@ jobs:
os: macos-14
bin: claw
artifact_name: claw-macos-arm64
- name: windows-x64
os: windows-latest
bin: claw.exe
artifact_name: claw-windows-x64.exe
defaults:
run:
working-directory: rust
@@ -47,22 +51,27 @@ jobs:
- name: Build release binary
run: cargo build --release -p rusty-claude-cli
- name: Package artifact
- name: Package artifact and checksum
shell: bash
run: |
mkdir -p dist
cp "target/release/${{ matrix.bin }}" "dist/${{ matrix.artifact_name }}"
chmod +x "dist/${{ matrix.artifact_name }}"
(cd dist && sha256sum "${{ matrix.artifact_name }}" > "${{ matrix.artifact_name }}.sha256")
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: rust/dist/${{ matrix.artifact_name }}
path: |
rust/dist/${{ matrix.artifact_name }}
rust/dist/${{ matrix.artifact_name }}.sha256
- name: Upload release asset
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: rust/dist/${{ matrix.artifact_name }}
files: |
rust/dist/${{ matrix.artifact_name }}
rust/dist/${{ matrix.artifact_name }}.sha256
fail_on_unmatched_files: true

View File

@@ -9,8 +9,14 @@ on:
paths:
- .github/workflows/rust-ci.yml
- .github/scripts/check_doc_source_of_truth.py
- .github/scripts/check_release_readiness.py
- .github/FUNDING.yml
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
- README.md
- SECURITY.md
- SUPPORT.md
- USAGE.md
- PARITY.md
- PHILOSOPHY.md
@@ -23,8 +29,14 @@ on:
paths:
- .github/workflows/rust-ci.yml
- .github/scripts/check_doc_source_of_truth.py
- .github/scripts/check_release_readiness.py
- .github/FUNDING.yml
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
- README.md
- SECURITY.md
- SUPPORT.md
- USAGE.md
- PARITY.md
- PHILOSOPHY.md
@@ -58,6 +70,8 @@ jobs:
python-version: "3.x"
- name: Check docs and metadata for stale branding
run: python .github/scripts/check_doc_source_of_truth.py
- name: Check release policy docs and local links
run: python .github/scripts/check_release_readiness.py
fmt:
name: cargo fmt
@@ -98,3 +112,42 @@ jobs:
workspaces: rust -> target
- name: Run workspace clippy
run: cargo clippy --workspace
windows-smoke:
name: windows PowerShell smoke
runs-on: windows-latest
defaults:
run:
working-directory: rust
shell: pwsh
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust -> target
- name: Build CLI for Windows smoke
run: cargo build -p rusty-claude-cli
- name: Smoke local commands without live credentials
env:
ANTHROPIC_API_KEY: ""
ANTHROPIC_AUTH_TOKEN: ""
OPENAI_API_KEY: ""
XAI_API_KEY: ""
DASHSCOPE_API_KEY: ""
run: |
$ErrorActionPreference = "Stop"
$env:CLAW_CONFIG_HOME = Join-Path $env:RUNNER_TEMP "claw config home"
New-Item -ItemType Directory -Force -Path $env:CLAW_CONFIG_HOME | Out-Null
$workspace = Join-Path $env:RUNNER_TEMP "claw path smoke"
New-Item -ItemType Directory -Force -Path $workspace | Out-Null
$claw = Join-Path $env:GITHUB_WORKSPACE "rust\target\debug\claw.exe"
Push-Location $workspace
try {
& $claw help
& $claw status
& $claw config env
& $claw doctor
} finally {
Pop-Location
}

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@ archive/
.claw/sessions/
.clawhip/
status-help.txt
# Legacy Python port session scratch artifacts
.port_sessions/

14886
.omx/cc2/board.json Normal file

File diff suppressed because one or more lines are too long

842
.omx/cc2/board.md Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,429 @@
{
"schema_version": "cc2.issue_parity_intake.v1",
"generated_at": "2026-05-14T08:02:00Z",
"task_id": "3",
"owner": "worker-2",
"goal": "G001-stream0-board",
"notes": [
"Leader owns Ultragoal; this artifact does not mutate .omx/ultragoal.",
"Rows are scoped intake/classification evidence for Worker 1/Task 2 board integration."
],
"source_manifest": {
"claw_open_latest": {
"path": ".omx/research/claw-open-latest.json",
"sha256_prefix_from_plan": "89e3e027fa735f38",
"covered_issue_numbers": [3028, 3029, 3030, 3031, 3032, 3033, 3034, 3035, 3036, 3037, 3038]
},
"claw_issues": {
"path": ".omx/research/claw-issues.json",
"sha256_prefix_from_plan": "e64fdba7df3b78ed",
"covered_issue_numbers": [2997, 3003, 3004, 3005, 3006, 3007, 3020, 3023]
},
"opencode": {
"repo_path": ".omx/research/repos/opencode",
"metadata_path": ".omx/research/opencode-repo.json",
"issues_path": ".omx/research/opencode-issues.json",
"head_from_plan": "27ac53aaacc677b1401c4e75ca7a7dadf8b2c349"
},
"codex": {
"repo_path": ".omx/research/repos/codex",
"metadata_path": ".omx/research/codex-repo.json",
"issues_path": ".omx/research/codex-issues.json",
"head_from_plan": "6a225e4005209f2325ab3c681c7c6beba2907d4d"
}
},
"issue_clusters": [
{
"id": "CC2-ISSUE-3007",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3007",
"source_type": "github_issue",
"source_number": 3007,
"title": "Permission modes do not enforce path scope on file tools or shell expansion in bash",
"theme": "security/path-scope",
"release_bucket": "alpha_blocker",
"lifecycle_status": "active",
"roadmap_anchor": "ROADMAP.md#11-policy-engine-for-autonomous-coding; ROADMAP.md#9-green-ness-contract",
"dependencies": ["permission path canonicalization", "file tool target validation", "bash command/path validation reachability", "policy regression fixtures"],
"verification_required": ["workspace-write cannot read/write/delete outside workspace", "shell expansion and symlink traversal are rejected or policy-blocked", "file tools and bash use the same target-scope decision record"],
"deferral_rationale": null,
"classification_rationale": "Security/sandbox escape class; plan names #3007 as alpha blocker."
},
{
"id": "CC2-ISSUE-3020",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3020",
"source_type": "github_issue",
"source_number": 3020,
"title": "OpenAI-compatible model IDs with slashes are stripped before request",
"theme": "provider/model-routing",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#provider-routing-model-name-prefix-must-win-over-env-var-presence-fixed-2026-04-08-0530c50",
"dependencies": ["provider profile contract", "wire model-id preservation option", "routing-prefix source reporting"],
"verification_required": ["OpenAI-compatible endpoint receives exact model id when preservation is enabled", "status JSON reports raw model input, route, and wire model id"],
"deferral_rationale": null,
"classification_rationale": "Core provider correctness but below alpha state/security contracts unless it blocks the selected alpha model path."
},
{
"id": "CC2-ISSUE-3006",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3006",
"source_type": "github_issue",
"source_number": 3006,
"title": "Not Working in windows",
"theme": "windows/install",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#immediate-backlog-from-current-real-pain",
"dependencies": ["Windows support policy", "PowerShell install path", "dependency/version matrix", "diagnostic setup output"],
"verification_required": ["fresh Windows/PowerShell setup smoke documented", "unsupported native paths fail with actionable WSL2/native guidance"],
"deferral_rationale": null,
"classification_rationale": "Real adoption blocker; plan places Windows/install in beta adoption overlay."
},
{
"id": "CC2-ISSUE-3005",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3005",
"source_type": "github_issue",
"source_number": 3005,
"title": "DeepSeek V4-flash/pro fails with 400 Bad Request (missing reasoning_content) while deepseek-reasoner works",
"theme": "provider/response-shape",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#5-failure-taxonomy; ROADMAP.md#provider-routing-model-name-prefix-must-win-over-env-var-presence-fixed-2026-04-08-0530c50",
"dependencies": ["OpenAI-compatible diagnostics playbook", "provider error taxonomy", "reasoning/thinking field compatibility tests"],
"verification_required": ["provider 400 response classified with actionable remediation", "DeepSeek-compatible response-shape fixture does not hide assistant output"],
"deferral_rationale": null,
"classification_rationale": "Provider compatibility issue that shares the #3032 diagnostics lane."
},
{
"id": "CC2-ISSUE-3004",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3004",
"source_type": "github_issue",
"source_number": 3004,
"title": "When can we adapt to zed?",
"theme": "ide/acp",
"release_bucket": "ga_ecosystem",
"lifecycle_status": "deferred_with_rationale",
"roadmap_anchor": "ROADMAP.md#phase-5-plugin-and-mcp-lifecycle-maturity",
"dependencies": ["stable session/control API", "plugin/MCP lifecycle", "engine API or ACP bridge decision"],
"verification_required": ["Zed/ACP smoke once core state/control contracts exist"],
"deferral_rationale": "IDE integration is valuable but should wait until boot/session/event/control truth surfaces are stable.",
"classification_rationale": "Matches plan's GA ecosystem lane for Zed/ACP."
},
{
"id": "CC2-ISSUE-3003",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3003",
"source_type": "github_issue",
"source_number": 3003,
"title": ".claude/sessions should not be submitted to repo",
"theme": "session-hygiene/gitignore",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#9-green-ness-contract; ROADMAP.md#8-recovery-recipes-for-common-failures",
"dependencies": ["artifact ignore policy", "session storage boundary docs", "repo hygiene check"],
"verification_required": ["session directories are ignored", "status/doctor warns about tracked session artifacts"],
"deferral_rationale": null,
"classification_rationale": "Small but user-visible session hygiene and data-leak prevention item."
},
{
"id": "CC2-ISSUE-2997",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/2997",
"source_type": "github_issue",
"source_number": 2997,
"title": "License?",
"theme": "docs/license",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#immediate-backlog-from-current-real-pain",
"dependencies": ["maintainer license decision", "LICENSE file", "README/USAGE attribution wording"],
"verification_required": ["repository license file exists", "package metadata and docs reference the same license"],
"deferral_rationale": null,
"classification_rationale": "Adoption/readiness documentation gap; requires maintainer decision before implementation."
},
{
"id": "CC2-ISSUE-3023",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3023",
"source_type": "github_issue",
"source_number": 3023,
"title": "Protect claw-code from AI slop PRs",
"theme": "repo-hygiene/anti-slop",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#immediate-backlog-from-current-real-pain",
"dependencies": ["contributor policy", "PR quality gate selection", "false-positive review escape hatch"],
"verification_required": ["selected PR quality gate runs on sample good/bad PR fixtures", "maintainers can override false positives"],
"deferral_rationale": null,
"classification_rationale": "Protects project throughput but should not precede alpha core safety contracts."
},
{
"id": "CC2-ISSUE-3028",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3028",
"source_type": "github_issue",
"source_number": 3028,
"title": "docs: add navigation and file-context usage guide",
"theme": "docs/navigation-context",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#7-human-ux-still-leaks-into-claw-workflows",
"dependencies": ["current TUI/shell key behavior inventory", "file context syntax docs", "secret-handling guidance"],
"verification_required": ["docs include terminal history, scrollback, @file context, attach/external file caveats", "examples work against current CLI"],
"deferral_rationale": null,
"classification_rationale": "Documentation support item from latest open issue refresh."
},
{
"id": "CC2-ISSUE-3029",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3029",
"source_type": "github_issue",
"source_number": 3029,
"title": "build: add cross-platform installer path and release artifact quickstart",
"theme": "install/distribution",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#immediate-backlog-from-current-real-pain",
"dependencies": ["release artifact policy", "install.sh/install.ps1 contract", "PATH/update/uninstall instructions"],
"verification_required": ["install quickstart smoke on supported OS/arch", "failed install prints actionable diagnostics"],
"deferral_rationale": null,
"classification_rationale": "Distribution friction belongs in adoption overlay."
},
{
"id": "CC2-ISSUE-3030",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3030",
"source_type": "github_issue",
"source_number": 3030,
"title": "feat: make provider/model setup less env-var-driven",
"theme": "provider/setup-profiles",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#3-structured-session-control-api; ROADMAP.md#145-boot-preflight-doctor-contract",
"dependencies": ["provider profiles", "setup wizard or dry-run", "secret redaction", "base-url/model smoke test"],
"verification_required": ["setup validates provider route without echoing keys", "session-only versus persisted profile behavior is explicit"],
"deferral_rationale": null,
"classification_rationale": "Directly reduces current provider setup support churn."
},
{
"id": "CC2-ISSUE-3031",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3031",
"source_type": "github_issue",
"source_number": 3031,
"title": "feat: auto-compact or clearly recover from context-window provider errors",
"theme": "session-recovery/context-window",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#8-recovery-recipes-for-common-failures; ROADMAP.md#158-compact_messages_if_needed-drops-turns-silently-no-structured-compaction-event-emitted",
"dependencies": ["provider error classifier", "safe compact retry policy", "compaction event/audit trail", "retry loop cap"],
"verification_required": ["context-window error either compacts+retries once safely or emits exact recovery command", "compaction event is machine-visible"],
"deferral_rationale": null,
"classification_rationale": "Recovery reliability item; promoted only if selected alpha provider path hits it."
},
{
"id": "CC2-ISSUE-3032",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3032",
"source_type": "github_issue",
"source_number": 3032,
"title": "docs: add OpenAI-compatible/local provider diagnostics playbook",
"theme": "provider/diagnostics-docs",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#5-failure-taxonomy",
"dependencies": ["raw chat-completions smoke tests", "tool-call response-shape examples", "provider failure taxonomy"],
"verification_required": ["playbook distinguishes Claw bugs from wrapper/tool-call-shape bugs", "curl examples cover non-streaming and streaming tool calls"],
"deferral_rationale": null,
"classification_rationale": "Shared diagnostic lane for #3005/#3020/local model reports."
},
{
"id": "CC2-ISSUE-3033",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3033",
"source_type": "github_issue",
"source_number": 3033,
"title": "feat: add minimal claw serve JSON-RPC engine API",
"theme": "engine-api/control-plane",
"release_bucket": "ga_ecosystem",
"lifecycle_status": "deferred_with_rationale",
"roadmap_anchor": "ROADMAP.md#3-structured-session-control-api; ROADMAP.md#phase-4-claws-first-task-execution",
"dependencies": ["stable session state API", "event schema v1", "permission policy contract", "cancel/prompt stream semantics"],
"verification_required": ["protocol conformance fixtures for session/create prompt/stream cancel error", "capability negotiation backwards compatibility"],
"deferral_rationale": "Engine API should expose, not invent, stable core control-plane semantics after alpha contracts land.",
"classification_rationale": "Useful integration surface but too broad for alpha unless narrowed to existing session control API."
},
{
"id": "CC2-ISSUE-3034",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3034",
"source_type": "github_issue",
"source_number": 3034,
"title": "docs: define evidence-gated Hermes handoff loop for Claw Code execution",
"theme": "sdlc/evidence-handoff",
"release_bucket": "post_2_0_research",
"lifecycle_status": "deferred_with_rationale",
"roadmap_anchor": "ROADMAP.md#4-canonical-lane-event-schema; ROADMAP.md#10-typed-task-packet-format",
"dependencies": ["typed task packet", "evidence bundle schema", "report gate status vocabulary"],
"verification_required": ["handoff packet fixture validates scope/success/test evidence fields", "post-flight gate consumes evidence instead of free-text summary"],
"deferral_rationale": "Can inform event/report/task contracts, but Hermes-specific loop should stay research/docs until core schemas are stable.",
"classification_rationale": "Only the generic evidence-gated contract is Claw 2.0; Hermes branding is not core."
},
{
"id": "CC2-ISSUE-3035",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3035",
"source_type": "github_issue",
"source_number": 3035,
"title": "fix: improve compacted session resume discoverability",
"theme": "session-resume/discoverability",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#8-recovery-recipes-for-common-failures; ROADMAP.md#160-session_store-has-no-list_sessions-delete_session-or-session_exists",
"dependencies": ["session enumeration", "latest-session workspace search boundary", "compacted session marker"],
"verification_required": ["/resume latest finds newest eligible compacted session", "/session or status lists resumable compacted sessions with path/id"],
"deferral_rationale": null,
"classification_rationale": "Session recovery/adoption item."
},
{
"id": "CC2-ISSUE-3036",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3036",
"source_type": "github_issue",
"source_number": 3036,
"title": "docs: add official Ollama/llama.cpp/vLLM local model examples",
"theme": "provider/local-docs",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#145-boot-preflight-doctor-contract; ROADMAP.md#5-failure-taxonomy",
"dependencies": ["known-good local provider examples", "raw /v1 smoke test", "tool-call limitation warning"],
"verification_required": ["docs include Ollama/llama.cpp/vLLM examples and HELLO smoke", "tool-call caveats are explicit"],
"deferral_rationale": null,
"classification_rationale": "Local provider adoption support."
},
{
"id": "CC2-ISSUE-3037",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3037",
"source_type": "github_issue",
"source_number": 3037,
"title": "docs: clarify Claw Code positioning as multi-provider Claude-Code-shaped runtime",
"theme": "docs/product-positioning",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"roadmap_anchor": "ROADMAP.md#goal; ROADMAP.md#definition-of-clawable",
"dependencies": ["README positioning copy", "provider support truth table", "identity leak bug policy"],
"verification_required": ["README/docs answer Claude-only question directly", "provider support wording matches implemented routes"],
"deferral_rationale": null,
"classification_rationale": "Clarifies product identity for adoption without broad implementation."
},
{
"id": "CC2-ISSUE-3038",
"source_anchor": "https://github.com/ultraworkers/claw-code/issues/3038",
"source_type": "github_issue",
"source_number": 3038,
"title": "roadmap: track skills/plugins/marketplace ecosystem gap after core UX stabilizes",
"theme": "plugin-marketplace/ecosystem",
"release_bucket": "ga_ecosystem",
"lifecycle_status": "deferred_with_rationale",
"roadmap_anchor": "ROADMAP.md#13-first-class-pluginmcp-lifecycle-contract; ROADMAP.md#14-mcp-end-to-end-lifecycle-parity",
"dependencies": ["plugin/MCP lifecycle contract", "extension point inventory", "discovery/install/update flow design"],
"verification_required": ["extension point inventory exists", "marketplace work explicitly depends on core UX stabilization"],
"deferral_rationale": "Marketplace breadth should wait until core setup/auth/provider/session UX and plugin lifecycle are reliable.",
"classification_rationale": "Matches plan's ga_ecosystem/post-2.0 caution for marketplace parity."
}
],
"parity_rows": [
{
"id": "CC2-PARITY-OPENCODE-PLUGIN-ECOSYSTEM",
"source_anchor": "anomalyco/opencode@27ac53aa packages/app/web/desktop/plugin/sdk/extensions/zed/slack/containers plus issue #3038",
"source_type": "repo_clone_and_local_issue",
"title": "Plugin/skills/marketplace ecosystem inventory",
"release_bucket": "ga_ecosystem",
"lifecycle_status": "deferred_with_rationale",
"dependencies": ["Claw plugin/MCP lifecycle contract", "current extension-point inventory"],
"verification_required": ["inventory maps current Claw plugin/skill/MCP extension points before marketplace implementation"],
"deferral_rationale": "Adapt ecosystem discovery only after core setup/provider/session reliability is stable."
},
{
"id": "CC2-PARITY-OPENCODE-PERMISSION-PRESETS",
"source_anchor": "https://github.com/anomalyco/opencode/issues/27464 and ROADMAP.md#11-policy-engine-for-autonomous-coding",
"source_type": "external_issue_and_roadmap",
"title": "Quick permission preset switching mapped onto Claw policy profiles",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"dependencies": ["policy profile model", "approval-token audit trail"],
"verification_required": ["preset switch is visible in status/report output and cannot bypass path-scope enforcement"],
"deferral_rationale": null
},
{
"id": "CC2-PARITY-OPENCODE-CUSTOM-PROVIDER-PARAMS",
"source_anchor": "https://github.com/anomalyco/opencode/issues/27462 and #3030/#3032",
"source_type": "external_issue_and_local_issue",
"title": "Custom API parameter passthrough for provider profiles",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"dependencies": ["provider profile schema", "secret redaction", "request audit surface"],
"verification_required": ["custom params are schema-validated, redacted, and visible as provenance without leaking secrets"],
"deferral_rationale": null
},
{
"id": "CC2-PARITY-OPENCODE-TODOWRITE-AUTOCOMPLETE",
"source_anchor": "https://github.com/anomalyco/opencode/issues/27453 and ROADMAP.md#10-typed-task-packet-format",
"source_type": "external_issue_and_roadmap",
"title": "Task/Todo completion assistance via typed task lifecycle",
"release_bucket": "ga_ecosystem",
"lifecycle_status": "deferred_with_rationale",
"dependencies": ["typed task packet", "task lifecycle events", "evidence-gated completion"],
"verification_required": ["auto-complete suggestions cannot mark work complete without evidence bundle or explicit user approval"],
"deferral_rationale": "Useful UX should follow, not precede, typed task lifecycle and evidence contract."
},
{
"id": "CC2-PARITY-OPENCODE-WINDOWS-DISTRIBUTION",
"source_anchor": "https://github.com/anomalyco/opencode/issues/27476 https://github.com/anomalyco/opencode/issues/27459 https://github.com/anomalyco/opencode/issues/27470 and #3006/#3029",
"source_type": "external_issues_and_local_issues",
"title": "Windows/GLIBC/distribution reliability parity lessons",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"dependencies": ["install artifact matrix", "Windows encoding guidance", "minimum Linux/GLIBC support statement"],
"verification_required": ["release quickstart documents supported OS matrix and known terminal/encoding caveats"],
"deferral_rationale": null
},
{
"id": "CC2-PARITY-CODEX-GRANULAR-PERMISSIONS",
"source_anchor": "https://github.com/openai/codex/issues/22595 and Codex docs permissions/app/plugin concepts",
"source_type": "external_issue_and_docs",
"title": "Granular app/plugin permissions adapted to Claw policy engine",
"release_bucket": "alpha_blocker",
"lifecycle_status": "active",
"dependencies": ["permission enforcer path-scope fix", "plugin/MCP capability model", "approval-token replay protection"],
"verification_required": ["granular permission grants do not widen workspace path scope implicitly"],
"deferral_rationale": null
},
{
"id": "CC2-PARITY-CODEX-SESSION-RECOVERY",
"source_anchor": "https://github.com/openai/codex/issues/22619 https://github.com/openai/codex/issues/22597 https://github.com/openai/codex/issues/22593 and #3035",
"source_type": "external_issues_and_local_issue",
"title": "Safe local session/thread recovery without storage amplification",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"dependencies": ["session enumeration", "resume latest boundary", "JSONL/storage compaction policy"],
"verification_required": ["recoverable sessions are discoverable and session forks avoid unbounded duplicate history"],
"deferral_rationale": null
},
{
"id": "CC2-PARITY-CODEX-PROXY-NETWORK",
"source_anchor": "https://github.com/openai/codex/issues/22623 and #3032",
"source_type": "external_issue_and_local_issue",
"title": "Provider/network diagnostics include proxy behavior",
"release_bucket": "beta_adoption",
"lifecycle_status": "open",
"dependencies": ["HTTP client proxy detection", "provider diagnostics playbook"],
"verification_required": ["diagnostics report whether proxy env/config is honored for provider calls"],
"deferral_rationale": null
},
{
"id": "CC2-PARITY-CODEX-CLI-AGENT-FLAG",
"source_anchor": "https://github.com/openai/codex/issues/22615 and ROADMAP.md#10-typed-task-packet-format",
"source_type": "external_issue_and_roadmap",
"title": "CLI flag for agent/subagent mode mapped to Claw typed task packets",
"release_bucket": "ga_ecosystem",
"lifecycle_status": "deferred_with_rationale",
"dependencies": ["typed task packet", "session control API", "policy-scoped worker launch"],
"verification_required": ["CLI agent mode cannot bypass task policy or evidence requirements"],
"deferral_rationale": "Implement only after core task/session control contracts are stable."
}
],
"coverage": {
"required_latest_open_range_3028_3038": [3028, 3029, 3030, 3031, 3032, 3033, 3034, 3035, 3036, 3037, 3038],
"required_existing_issue_numbers": [3007, 3006, 3020, 3005, 3003, 2997, 3023, 3004],
"issue_rows_expected": 19,
"parity_rows_expected_minimum": 6
}
}

View File

@@ -0,0 +1,47 @@
# CC2 Issue / Parity Intake Mapping
Generated by `worker-2` for team task 3 (`G001 issue/parity intake mapping`). This is a board-integration fragment for Stream 0; it intentionally does **not** mutate `.omx/ultragoal`.
## Covered local issue clusters
| Issue | Theme | Bucket | Lifecycle | Board anchor |
|---:|---|---|---|---|
| #3007 | security/path-scope | `alpha_blocker` | `active` | Policy engine + green-ness contract |
| #3020 | provider/model-routing | `beta_adoption` | `open` | Provider routing/model source status |
| #3006 | windows/install | `beta_adoption` | `open` | Immediate backlog / install readiness |
| #3005 | provider/response-shape | `beta_adoption` | `open` | Failure taxonomy / provider diagnostics |
| #3004 | ide/acp | `ga_ecosystem` | `deferred_with_rationale` | Plugin/MCP lifecycle maturity |
| #3003 | session-hygiene/gitignore | `beta_adoption` | `open` | Green-ness / recovery hygiene |
| #2997 | docs/license | `beta_adoption` | `open` | Adoption docs/license readiness |
| #3023 | repo-hygiene/anti-slop | `beta_adoption` | `open` | Immediate backlog / PR quality gate |
| #3028 | docs/navigation-context | `beta_adoption` | `open` | Human UX leaks into claw workflows |
| #3029 | install/distribution | `beta_adoption` | `open` | Cross-platform release quickstart |
| #3030 | provider/setup-profiles | `beta_adoption` | `open` | Boot preflight / structured session control |
| #3031 | session-recovery/context-window | `beta_adoption` | `open` | Recovery recipes / compaction event |
| #3032 | provider/diagnostics-docs | `beta_adoption` | `open` | Failure taxonomy |
| #3033 | engine-api/control-plane | `ga_ecosystem` | `deferred_with_rationale` | Structured session control API |
| #3034 | sdlc/evidence-handoff | `post_2_0_research` | `deferred_with_rationale` | Event/report/task contract input |
| #3035 | session-resume/discoverability | `beta_adoption` | `open` | Recovery recipes / session enumeration |
| #3036 | provider/local-docs | `beta_adoption` | `open` | Provider setup and diagnostics docs |
| #3037 | docs/product-positioning | `beta_adoption` | `open` | Goal / definition of clawable |
| #3038 | plugin-marketplace/ecosystem | `ga_ecosystem` | `deferred_with_rationale` | Plugin/MCP lifecycle maturity |
## Parity intake rows
| Row | Source | Bucket | Lifecycle | Adaptation rule |
|---|---|---|---|---|
| `CC2-PARITY-OPENCODE-PLUGIN-ECOSYSTEM` | opencode repo + #3038 | `ga_ecosystem` | `deferred_with_rationale` | Inventory Claw extension points before marketplace work. |
| `CC2-PARITY-OPENCODE-PERMISSION-PRESETS` | opencode #27464 | `beta_adoption` | `open` | Permission preset UX must not bypass Claw path-scope policy. |
| `CC2-PARITY-OPENCODE-CUSTOM-PROVIDER-PARAMS` | opencode #27462 + #3030/#3032 | `beta_adoption` | `open` | Custom provider params need schema validation, redaction, and provenance. |
| `CC2-PARITY-OPENCODE-TODOWRITE-AUTOCOMPLETE` | opencode #27453 | `ga_ecosystem` | `deferred_with_rationale` | Auto-complete task UX follows typed task lifecycle/evidence gates. |
| `CC2-PARITY-OPENCODE-WINDOWS-DISTRIBUTION` | opencode #27476/#27459/#27470 + #3006/#3029 | `beta_adoption` | `open` | Use external pain as release-matrix and diagnostics evidence. |
| `CC2-PARITY-CODEX-GRANULAR-PERMISSIONS` | Codex #22595 + docs | `alpha_blocker` | `active` | Adapt granular permissions only through Claw policy engine and approval tokens. |
| `CC2-PARITY-CODEX-SESSION-RECOVERY` | Codex #22619/#22597/#22593 + #3035 | `beta_adoption` | `open` | Session discovery/recovery must avoid storage amplification. |
| `CC2-PARITY-CODEX-PROXY-NETWORK` | Codex #22623 + #3032 | `beta_adoption` | `open` | Provider diagnostics should expose proxy behavior. |
| `CC2-PARITY-CODEX-CLI-AGENT-FLAG` | Codex #22615 | `ga_ecosystem` | `deferred_with_rationale` | CLI agent mode waits for typed task/session control contracts. |
Validation command:
```bash
python3 .omx/cc2/validate_issue_parity_intake.py
```

250
.omx/cc2/render_board_md.py Executable file
View File

@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""Render the Claw Code 2.0 canonical board JSON as a human-readable Markdown board."""
from __future__ import annotations
import argparse
import json
import sys
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
STATUS_DESCRIPTIONS = {
"context": "Context-only heading or evidence anchor; not an implementation work item.",
"active": "Current Claw Code 2.0 implementation surface that should remain visible on the board.",
"open": "Actionable unresolved work that needs implementation or acceptance evidence.",
"done_verify": "Marked as done upstream but retained for verification against current CC2 behavior.",
"stale_done": "Historically completed or merged work that may be stale and needs freshness checks before relying on it.",
"superseded": "Replaced by a newer item; keep as traceability context only.",
"deferred_with_rationale": "Intentionally deferred; rationale must be present in the board item.",
"rejected_not_claw": "Excluded because it is not Claw Code product work.",
}
BUCKET_DESCRIPTIONS = {
"alpha_blocker": "Must be resolved before alpha-quality autonomous coding lanes are dependable.",
"beta_adoption": "Important for broader dogfood/adoption once alpha blockers are controlled.",
"ga_ecosystem": "Required for mature plugin/MCP/provider ecosystem behavior.",
"2.x_intake": "Post-2.0 intake or follow-up candidate retained for sequencing.",
"post_2_0_research": "Research-oriented item not required for the CC2 board cut.",
"context": "Non-actionable roadmap context.",
"rejected_not_claw": "Explicit non-Claw rejection bucket.",
}
LANE_TITLES = {
"stream_0_governance": "Stream 0 — Governance, intake, and cross-cutting roadmap triage",
"stream_1_worker_boot_session_control": "Stream 1 — Worker boot and session control",
"stream_2_event_reporting_contracts": "Stream 2 — Event/reporting contracts",
"stream_3_branch_test_recovery": "Stream 3 — Branch/test recovery",
"stream_4_claws_first_execution": "Stream 4 — Claws-first task execution",
"stream_5_plugin_mcp_lifecycle": "Stream 5 — Plugin/MCP lifecycle",
"adoption_overlay": "Adoption overlay — user-visible parity and release polish",
"parity_overlay": "Parity overlay — opencode/codex comparison context",
}
REQUIRED_ITEM_FIELDS = [
"id",
"title",
"source_anchor",
"source_type",
"release_bucket",
"lifecycle_status",
"dependencies",
"verification_required",
"deferral_rationale",
]
def load_board(path: Path) -> dict[str, Any]:
with path.open() as f:
board = json.load(f)
if not isinstance(board, dict):
raise ValueError("board JSON root must be an object")
items = board.get("items")
if not isinstance(items, list):
raise ValueError("board JSON must contain an items array")
return board
def validate_board(board: dict[str, Any]) -> list[str]:
errors: list[str] = []
coverage = board.get("coverage", {})
if coverage.get("unmapped_roadmap_heading_lines"):
errors.append(f"unmapped roadmap heading lines: {coverage['unmapped_roadmap_heading_lines']}")
if coverage.get("roadmap_headings_mapped") != coverage.get("roadmap_headings_total"):
errors.append("roadmap heading coverage is incomplete")
if coverage.get("roadmap_actions_mapped") != coverage.get("roadmap_actions_total"):
errors.append("roadmap ordered-action coverage is incomplete")
allowed_status = set(board.get("generation_policy", {}).get("status_values", []))
allowed_buckets = set(board.get("generation_policy", {}).get("release_buckets", []))
seen_ids: set[str] = set()
for index, item in enumerate(board["items"], 1):
for field in REQUIRED_ITEM_FIELDS:
if field not in item:
errors.append(f"item {index} missing required field {field}")
item_id = item.get("id")
if item_id in seen_ids:
errors.append(f"duplicate item id {item_id}")
seen_ids.add(item_id)
status = item.get("lifecycle_status")
bucket = item.get("release_bucket")
if allowed_status and status not in allowed_status:
errors.append(f"{item_id} has unknown lifecycle_status {status!r}")
if allowed_buckets and bucket not in allowed_buckets:
errors.append(f"{item_id} has unknown release_bucket {bucket!r}")
if status == "deferred_with_rationale" and not str(item.get("deferral_rationale", "")).strip():
errors.append(f"{item_id} is deferred without deferral_rationale")
return errors
def table(headers: list[str], rows: list[list[Any]]) -> list[str]:
out = ["| " + " | ".join(headers) + " |", "| " + " | ".join("---" for _ in headers) + " |"]
for row in rows:
out.append("| " + " | ".join(str(cell) for cell in row) + " |")
return out
def fmt_list(value: Any) -> str:
if not value:
return "none"
if isinstance(value, list):
return ", ".join(f"`{v}`" for v in value) if value else "none"
return f"`{value}`"
def render(board: dict[str, Any]) -> str:
items: list[dict[str, Any]] = board["items"]
summary = board.get("summary", {})
coverage = board.get("coverage", {})
sources = board.get("sources", {})
policy = board.get("generation_policy", {})
by_lane = Counter(item.get("owner_lane", "unassigned") for item in items)
by_status = Counter(item.get("lifecycle_status", "unknown") for item in items)
by_bucket = Counter(item.get("release_bucket", "unknown") for item in items)
by_source = Counter(item.get("source_type", "unknown") for item in items)
lines: list[str] = []
lines.append("# Claw Code 2.0 Canonical Board")
lines.append("")
lines.append(f"Generated from board schema: `{board.get('generated_at', 'unknown')}`")
lines.append(f"Schema version: `{board.get('schema_version', 'unknown')}`")
lines.append("Ultragoal mutation policy: `.omx/ultragoal` is leader-owned and was not modified by this rendering task.")
lines.append("")
lines.append("## Evidence Freeze")
lines.append("")
roadmap = sources.get("roadmap", {})
research = sources.get("research", {})
plan = sources.get("approved_plan", {})
lines.extend(table(["Source", "Frozen evidence"], [
["Roadmap", f"`{roadmap.get('path', 'ROADMAP.md')}` sha256 prefix `{roadmap.get('sha256_prefix', 'unknown')}`; {roadmap.get('heading_count', '?')} headings; {roadmap.get('ordered_action_count', '?')} ordered actions"],
["Approved plan", f"`{plan.get('path', '.omx/plans/claw-code-2-0-adaptive-plan.md')}` sha256 prefix `{plan.get('sha256_prefix', 'unknown')}`"],
["Research bundle", f"root `{research.get('root', '.omx/research')}`; latest open issues {research.get('claw_open_latest_count', '?')}; issue corpus {research.get('claw_issues_count', '?')}; codex/opencode clone metadata included"],
]))
lines.append("")
lines.append("## Roadmap Coverage Summary")
lines.append("")
heading_total = coverage.get("roadmap_headings_total", 0)
heading_mapped = coverage.get("roadmap_headings_mapped", 0)
action_total = coverage.get("roadmap_actions_total", 0)
action_mapped = coverage.get("roadmap_actions_mapped", 0)
lines.extend(table(["Coverage gate", "Mapped", "Total", "Status"], [
["ROADMAP headings", heading_mapped, heading_total, "PASS" if heading_mapped == heading_total and not coverage.get("unmapped_roadmap_heading_lines") else "FAIL"],
["ROADMAP ordered actions", action_mapped, action_total, "PASS" if action_mapped == action_total else "FAIL"],
["Duplicate heading lines", len(coverage.get("duplicate_roadmap_heading_lines", [])), 0, "PASS" if not coverage.get("duplicate_roadmap_heading_lines") else "WARN"],
]))
lines.append("")
lines.append(f"Total canonical board items: **{len(items)}**")
lines.append("")
lines.append("## Lifecycle Enum Reference")
lines.append("")
status_rows = []
for status in policy.get("status_values", sorted(by_status)):
status_rows.append([f"`{status}`", by_status.get(status, 0), STATUS_DESCRIPTIONS.get(status, "Board-defined lifecycle status.")])
lines.extend(table(["Lifecycle", "Count", "Meaning"], status_rows))
lines.append("")
lines.append("## Release Bucket Reference")
lines.append("")
bucket_rows = []
for bucket in policy.get("release_buckets", sorted(by_bucket)):
bucket_rows.append([f"`{bucket}`", by_bucket.get(bucket, 0), BUCKET_DESCRIPTIONS.get(bucket, "Board-defined release bucket.")])
lines.extend(table(["Bucket", "Count", "Meaning"], bucket_rows))
lines.append("")
lines.append("## Stream Summaries")
lines.append("")
lane_rows = []
for lane, count in sorted(by_lane.items()):
lane_items = [item for item in items if item.get("owner_lane") == lane]
lane_status = Counter(item.get("lifecycle_status") for item in lane_items)
open_like = lane_status.get("active", 0) + lane_status.get("open", 0) + lane_status.get("done_verify", 0)
lane_rows.append([
LANE_TITLES.get(lane, lane),
count,
open_like,
", ".join(f"`{k}` {v}" for k, v in sorted(lane_status.items())),
])
lines.extend(table(["Stream / lane", "Items", "Active+open+verify", "Lifecycle mix"], lane_rows))
lines.append("")
lines.append("## Source-Type Mix")
lines.append("")
lines.extend(table(["Source type", "Items"], [[f"`{k}`", v] for k, v in sorted(by_source.items())]))
lines.append("")
lines.append("## Board Items by Stream")
lines.append("")
for lane in sorted(by_lane):
lane_items = [item for item in items if item.get("owner_lane") == lane]
lines.append(f"### {LANE_TITLES.get(lane, lane)}")
lines.append("")
lines.extend(table(
["ID", "Title", "Source", "Bucket", "Lifecycle", "Verification", "Dependencies", "Deferral"],
[[
f"`{item.get('id')}`",
str(item.get("title", "")).replace("|", "\\|"),
f"`{item.get('source_anchor')}` / `{item.get('source_type')}`",
f"`{item.get('release_bucket')}`",
f"`{item.get('lifecycle_status')}`",
f"`{item.get('verification_required')}`",
fmt_list(item.get("dependencies")),
str(item.get("deferral_rationale") or "").replace("|", "\\|"),
] for item in lane_items]
))
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("board_json", type=Path)
parser.add_argument("board_md", type=Path)
parser.add_argument("--check", action="store_true", help="fail if board_md is not up to date")
args = parser.parse_args()
board = load_board(args.board_json)
errors = validate_board(board)
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
rendered = render(board)
if args.check:
existing = args.board_md.read_text() if args.board_md.exists() else ""
if existing != rendered:
print(f"ERROR: {args.board_md} is not up to date", file=sys.stderr)
return 1
print(f"PASS: {args.board_md} is up to date and roadmap coverage is complete")
return 0
args.board_md.parent.mkdir(parents=True, exist_ok=True)
args.board_md.write_text(rendered)
print(f"wrote {args.board_md}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Validate the worker-2 CC2 issue/parity intake fragment."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
INTAKE = ROOT / ".omx" / "cc2" / "issue-parity-intake.json"
REQUIRED_ISSUES = set(range(3028, 3039)) | {3007, 3006, 3020, 3005, 3003, 2997, 3023, 3004}
ALLOWED_STATUS = {
"context",
"active",
"open",
"done_verify",
"stale_done",
"superseded",
"deferred_with_rationale",
"rejected_not_claw",
}
ALLOWED_BUCKETS = {"alpha_blocker", "beta_adoption", "ga_ecosystem", "post_2_0_research"}
def require(condition: bool, message: str) -> None:
if not condition:
raise SystemExit(f"FAIL: {message}")
def main() -> None:
data = json.loads(INTAKE.read_text())
issue_rows = data.get("issue_clusters", [])
parity_rows = data.get("parity_rows", [])
seen = {row.get("source_number") for row in issue_rows}
missing = sorted(REQUIRED_ISSUES - seen)
extra = sorted(seen - REQUIRED_ISSUES)
require(not missing, f"missing required issue rows: {missing}")
require(not extra, f"unexpected issue rows in scoped intake: {extra}")
require(len(issue_rows) == len(REQUIRED_ISSUES), "duplicate or missing issue row count")
ids = [row.get("id") for row in issue_rows + parity_rows]
require(len(ids) == len(set(ids)), "duplicate ids present")
for row in issue_rows + parity_rows:
row_id = row.get("id")
for field in ["source_anchor", "source_type", "release_bucket", "lifecycle_status", "dependencies", "verification_required"]:
require(row.get(field) not in (None, "", []), f"{row_id} missing {field}")
require(row["release_bucket"] in ALLOWED_BUCKETS, f"{row_id} invalid release_bucket {row['release_bucket']}")
require(row["lifecycle_status"] in ALLOWED_STATUS, f"{row_id} invalid lifecycle_status {row['lifecycle_status']}")
if row["lifecycle_status"] == "deferred_with_rationale":
require(row.get("deferral_rationale"), f"{row_id} deferred without rationale")
require(len(parity_rows) >= data["coverage"]["parity_rows_expected_minimum"], "not enough parity rows")
print(f"PASS issue/parity intake: {len(issue_rows)} issue rows, {len(parity_rows)} parity rows")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,583 @@
G010 final leader verification rerun started 2026-05-15T02:19:36Z
== artifact checklist ==
PASS docs/g010-clone-disambiguation-metadata.md exists
PASS docs/g010-session-hygiene-verification-map.md exists
.claw/sessions/example.jsonl
rust/.claw/sessions/example.jsonl
.claude/sessions/example.json
== fmt ==
== runtime session_control retry ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 15 tests
test session_control::tests::latest_session_prefers_semantic_updated_at_over_file_mtime ... ok
test session_control::tests::session_store_from_cwd_canonicalizes_equivalent_paths ... ok
test session_control::tests::session_store_fork_stays_in_same_namespace ... ok
test session_control::tests::session_exists_and_delete_are_scoped_to_workspace_store ... ok
test session_control::tests::resolves_latest_alias_and_loads_session_from_workspace_root ... ok
test session_control::tests::forks_session_into_managed_storage_with_lineage ... ok
test session_control::tests::workspace_fingerprint_is_deterministic_and_differs_per_path ... ok
test session_control::tests::session_store_create_and_load_round_trip ... ok
test session_control::tests::session_store_from_cwd_isolates_sessions_by_workspace ... ok
test session_control::tests::creates_and_lists_managed_sessions ... ok
test session_control::tests::session_store_from_data_dir_namespaces_by_workspace ... ok
test session_control::tests::session_store_latest_and_resolve_reference ... ok
test session_control::tests::session_store_loads_safe_legacy_session_from_same_workspace ... ok
test session_control::tests::session_store_rejects_legacy_session_from_other_workspace ... ok
test session_control::tests::session_store_loads_unbound_legacy_session_from_same_workspace ... ok
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 542 filtered out; finished in 0.02s
Running tests/g004_conformance.rs (rust/target/debug/deps/g004_conformance-90f36d1f871b6313)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/integration_tests.rs (rust/target/debug/deps/integration_tests-526d4f853fc590de)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
== runtime jsonl safeguards ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.12s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 1 test
test session::tests::jsonl_persistence_redacts_and_truncates_oversized_payload_fields ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 556 filtered out; finished in 0.02s
Running tests/g004_conformance.rs (rust/target/debug/deps/g004_conformance-90f36d1f871b6313)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/integration_tests.rs (rust/target/debug/deps/integration_tests-526d4f853fc590de)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
== runtime compact ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.12s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 17 tests
test compact::tests::compaction_does_not_split_tool_use_tool_result_pair ... ok
test compact::tests::formats_compact_summary_like_upstream ... ok
test compact::tests::ignores_existing_compacted_summary_when_deciding_to_recompact ... ok
test compact::tests::infers_pending_work_from_recent_messages ... ok
test compact::tests::extracts_key_files_from_message_content ... ok
test compact::tests::leaves_small_sessions_unchanged ... ok
test compact::tests::truncates_long_blocks_in_summary ... ok
test conversation::tests::auto_compaction_threshold_defaults_and_parses_values ... ok
test compact::tests::compacts_older_messages_into_a_system_summary ... ok
test conversation::tests::compaction_health_probe_skips_empty_compacted_session ... ok
test conversation::tests::compaction_health_probe_blocks_turn_when_tool_executor_is_broken ... ok
test conversation::tests::auto_compacts_when_cumulative_input_threshold_is_crossed ... ok
test conversation::tests::skips_auto_compaction_below_threshold ... ok
test prompt::tests::displays_context_paths_compactly ... ok
test conversation::tests::compacts_session_after_turns ... ok
test compact::tests::keeps_previous_compacted_context_when_compacting_again ... ok
test session::tests::persists_compaction_metadata ... ok
test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 540 filtered out; finished in 0.01s
Running tests/g004_conformance.rs (rust/target/debug/deps/g004_conformance-90f36d1f871b6313)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/integration_tests.rs (rust/target/debug/deps/integration_tests-526d4f853fc590de)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
== commands parses_supported_slash_commands ==
Compiling commands v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/commands)
Finished `test` profile [unoptimized + debuginfo] target(s) in 2.34s
Running unittests src/lib.rs (rust/target/debug/deps/commands-0104b50ff2e54ccc)
running 1 test
test tests::parses_supported_slash_commands ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 41 filtered out; finished in 0.00s
== commands compacts_sessions_via_slash_command ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.12s
Running unittests src/lib.rs (rust/target/debug/deps/commands-0104b50ff2e54ccc)
running 1 test
test tests::compacts_sessions_via_slash_command ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 41 filtered out; finished in 0.00s
== cli session json contracts ==
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
Finished `test` profile [unoptimized + debuginfo] target(s) in 3.47s
Running unittests src/main.rs (rust/target/debug/deps/claw-f425f0b21e915b27)
running 1 test
test tests::session_exists_resume_command_reports_json_contract ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 193 filtered out; finished in 0.00s
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
Finished `test` profile [unoptimized + debuginfo] target(s) in 2.76s
Running unittests src/main.rs (rust/target/debug/deps/claw-f425f0b21e915b27)
running 1 test
test tests::resumed_session_exists_and_delete_have_json_contracts ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 193 filtered out; finished in 0.01s
== cli resume slash commands ==
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
Finished `test` profile [unoptimized + debuginfo] target(s) in 4.23s
Running tests/resume_slash_commands.rs (rust/target/debug/deps/resume_slash_commands-6c1fb347be3842ef)
running 12 tests
test resumed_stub_command_emits_not_implemented_json ... ok
test resumed_help_command_emits_structured_json ... ok
test resumed_no_command_emits_restored_json ... ok
test resumed_sandbox_command_emits_structured_json_when_requested ... ok
test resumed_export_command_emits_structured_json ... ok
test resumed_config_command_loads_settings_files_end_to_end ... ok
test resumed_binary_accepts_slash_commands_with_arguments ... ok
test resumed_version_command_emits_structured_json ... ok
test resumed_status_surfaces_persisted_model ... ok
test resume_latest_restores_the_most_recent_managed_session ... ok
test status_command_applies_cli_flags_end_to_end ... ok
test resumed_status_command_emits_structured_json_when_requested ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.25s
== cli compact output ==
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
Finished `test` profile [unoptimized + debuginfo] target(s) in 2.72s
Running tests/compact_output.rs (rust/target/debug/deps/compact_output-988ab05f11fedc49)
running 4 tests
test compact_flag_with_json_output_emits_structured_json ... ok
test compact_flag_streaming_text_only_emits_final_message_text ... ok
test compact_flag_prints_only_final_assistant_text_without_tool_call_details ... ok
test text_prompt_mode_prints_final_assistant_text_after_spinner ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.14s
== workspace check ==
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.49s
== diff check ==
G010 final leader verification rerun completed 2026-05-15T02:20:06Z

View File

@@ -0,0 +1,644 @@
G010 final leader verification started 2026-05-15T02:17:45Z
== artifact checklist ==
PASS docs/g010-clone-disambiguation-metadata.md exists
PASS docs/g010-session-hygiene-verification-map.md exists
.gitignore:.claw/sessions/
rust/.gitignore:.claw/sessions/
.claw/sessions/example.jsonl
rust/.claw/sessions/example.jsonl
.claude/sessions/example.json
== fmt ==
== runtime session_control ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.14s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 15 tests
test session_control::tests::latest_session_prefers_semantic_updated_at_over_file_mtime ... ok
test session_control::tests::session_store_from_cwd_canonicalizes_equivalent_paths ... ok
thread 'session_control::tests::session_store_fork_stays_in_same_namespace' (403821665) panicked at crates/runtime/src/session_control.rs:775:14:
session should persist: Io(Os { code: 2, kind: NotFound, message: "No such file or directory" })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test session_control::tests::session_exists_and_delete_are_scoped_to_workspace_store ... ok
test session_control::tests::forks_session_into_managed_storage_with_lineage ... ok
test session_control::tests::creates_and_lists_managed_sessions ... ok
test session_control::tests::session_store_from_cwd_isolates_sessions_by_workspace ... ok
test session_control::tests::session_store_latest_and_resolve_reference ... ok
test session_control::tests::session_store_loads_safe_legacy_session_from_same_workspace ... ok
test session_control::tests::workspace_fingerprint_is_deterministic_and_differs_per_path ... ok
test session_control::tests::session_store_create_and_load_round_trip ... ok
test session_control::tests::session_store_fork_stays_in_same_namespace ... FAILED
test session_control::tests::session_store_loads_unbound_legacy_session_from_same_workspace ... ok
test session_control::tests::session_store_from_data_dir_namespaces_by_workspace ... ok
test session_control::tests::session_store_rejects_legacy_session_from_other_workspace ... ok
test session_control::tests::resolves_latest_alias_and_loads_session_from_workspace_root ... ok
failures:
failures:
session_control::tests::session_store_fork_stays_in_same_namespace
test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 542 filtered out; finished in 0.03s
error: test failed, to rerun pass `-p runtime --lib`
== runtime jsonl safeguards ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 1 test
test session::tests::jsonl_persistence_redacts_and_truncates_oversized_payload_fields ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 556 filtered out; finished in 0.02s
Running tests/g004_conformance.rs (rust/target/debug/deps/g004_conformance-90f36d1f871b6313)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/integration_tests.rs (rust/target/debug/deps/integration_tests-526d4f853fc590de)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
== runtime compact ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 17 tests
test compact::tests::formats_compact_summary_like_upstream ... ok
test compact::tests::truncates_long_blocks_in_summary ... ok
test compact::tests::ignores_existing_compacted_summary_when_deciding_to_recompact ... ok
test compact::tests::infers_pending_work_from_recent_messages ... ok
test compact::tests::compaction_does_not_split_tool_use_tool_result_pair ... ok
test compact::tests::leaves_small_sessions_unchanged ... ok
test conversation::tests::auto_compaction_threshold_defaults_and_parses_values ... ok
test compact::tests::extracts_key_files_from_message_content ... ok
test compact::tests::compacts_older_messages_into_a_system_summary ... ok
test prompt::tests::displays_context_paths_compactly ... ok
test conversation::tests::skips_auto_compaction_below_threshold ... ok
test conversation::tests::compaction_health_probe_skips_empty_compacted_session ... ok
test conversation::tests::compacts_session_after_turns ... ok
test conversation::tests::compaction_health_probe_blocks_turn_when_tool_executor_is_broken ... ok
test conversation::tests::auto_compacts_when_cumulative_input_threshold_is_crossed ... ok
test compact::tests::keeps_previous_compacted_context_when_compacting_again ... ok
test session::tests::persists_compaction_metadata ... ok
test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 540 filtered out; finished in 0.01s
Running tests/g004_conformance.rs (rust/target/debug/deps/g004_conformance-90f36d1f871b6313)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/integration_tests.rs (rust/target/debug/deps/integration_tests-526d4f853fc590de)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
== commands session/compact slash ==
error: unexpected argument 'compacts_sessions_via_slash_command' found
Usage: cargo test [OPTIONS] [TESTNAME] [-- [ARGS]...]
For more information, try '--help'.
== cli session json contracts ==
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
error[E0004]: non-exhaustive patterns: `&SlashCommand::Session { action: Some(_), target: None }` not covered
--> crates/rusty-claude-cli/src/main.rs:3823:11
|
3823 | match command {
| ^^^^^^^ pattern `&SlashCommand::Session { action: Some(_), target: None }` not covered
|
note: `SlashCommand` defined here
--> crates/commands/src/lib.rs:1040:1
|
1040 | pub enum SlashCommand {
| ^^^^^^^^^^^^^^^^^^^^^
...
1089 | Session {
| ------- not covered
= note: the matched value is of type `&SlashCommand`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4200 ~ | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
4201 ~ &SlashCommand::Session { action: Some(_), target: None } => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `rusty-claude-cli` (bin "claw" test) due to 1 previous error
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
error[E0004]: non-exhaustive patterns: `&SlashCommand::Session { action: Some(_), target: None }` not covered
--> crates/rusty-claude-cli/src/main.rs:3823:11
|
3823 | match command {
| ^^^^^^^ pattern `&SlashCommand::Session { action: Some(_), target: None }` not covered
|
note: `SlashCommand` defined here
--> crates/commands/src/lib.rs:1040:1
|
1040 | pub enum SlashCommand {
| ^^^^^^^^^^^^^^^^^^^^^
...
1089 | Session {
| ------- not covered
= note: the matched value is of type `&SlashCommand`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4200 ~ | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
4201 ~ &SlashCommand::Session { action: Some(_), target: None } => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `rusty-claude-cli` (bin "claw" test) due to 1 previous error
== cli resume slash commands ==
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
error[E0004]: non-exhaustive patterns: `&SlashCommand::Session { action: Some(_), target: None }` not covered
--> crates/rusty-claude-cli/src/main.rs:3823:11
|
3823 | match command {
| ^^^^^^^ pattern `&SlashCommand::Session { action: Some(_), target: None }` not covered
|
note: `SlashCommand` defined here
--> crates/commands/src/lib.rs:1040:1
|
1040 | pub enum SlashCommand {
| ^^^^^^^^^^^^^^^^^^^^^
...
1089 | Session {
| ------- not covered
= note: the matched value is of type `&SlashCommand`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4200 ~ | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
4201 ~ &SlashCommand::Session { action: Some(_), target: None } => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `rusty-claude-cli` (bin "claw") due to 1 previous error
== cli compact output ==
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
error[E0004]: non-exhaustive patterns: `&SlashCommand::Session { action: Some(_), target: None }` not covered
--> crates/rusty-claude-cli/src/main.rs:3823:11
|
3823 | match command {
| ^^^^^^^ pattern `&SlashCommand::Session { action: Some(_), target: None }` not covered
|
note: `SlashCommand` defined here
--> crates/commands/src/lib.rs:1040:1
|
1040 | pub enum SlashCommand {
| ^^^^^^^^^^^^^^^^^^^^^
...
1089 | Session {
| ------- not covered
= note: the matched value is of type `&SlashCommand`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4200 ~ | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
4201 ~ &SlashCommand::Session { action: Some(_), target: None } => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `rusty-claude-cli` (bin "claw") due to 1 previous error
== workspace check ==
Checking runtime v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/runtime)
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
Checking api v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/api)
Checking commands v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/commands)
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Checking tools v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/tools)
Checking mock-anthropic-service v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/mock-anthropic-service)
Checking compat-harness v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/compat-harness)
error[E0004]: non-exhaustive patterns: `&SlashCommand::Session { action: Some(_), target: None }` not covered
--> crates/rusty-claude-cli/src/main.rs:3823:11
|
3823 | match command {
| ^^^^^^^ pattern `&SlashCommand::Session { action: Some(_), target: None }` not covered
|
note: `SlashCommand` defined here
--> crates/commands/src/lib.rs:1040:1
|
1040 | pub enum SlashCommand {
| ^^^^^^^^^^^^^^^^^^^^^
...
1089 | Session {
| ------- not covered
= note: the matched value is of type `&SlashCommand`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4200 ~ | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
4201 ~ &SlashCommand::Session { action: Some(_), target: None } => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `rusty-claude-cli` (bin "claw") due to 1 previous error
== diff check ==
G010 final leader verification completed 2026-05-15T02:18:11Z

View File

@@ -0,0 +1,321 @@
== fmt ==
== runtime session_control ==
Compiling runtime v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/runtime)
Finished `test` profile [unoptimized + debuginfo] target(s) in 10.29s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 15 tests
test session_control::tests::latest_session_prefers_semantic_updated_at_over_file_mtime ... ok
test session_control::tests::session_store_from_cwd_canonicalizes_equivalent_paths ... ok
test session_control::tests::session_store_create_and_load_round_trip ... ok
test session_control::tests::session_exists_and_delete_are_scoped_to_workspace_store ... ok
test session_control::tests::forks_session_into_managed_storage_with_lineage ... ok
test session_control::tests::workspace_fingerprint_is_deterministic_and_differs_per_path ... ok
test session_control::tests::session_store_from_cwd_isolates_sessions_by_workspace ... ok
test session_control::tests::creates_and_lists_managed_sessions ... ok
test session_control::tests::session_store_fork_stays_in_same_namespace ... ok
test session_control::tests::session_store_from_data_dir_namespaces_by_workspace ... ok
test session_control::tests::session_store_latest_and_resolve_reference ... ok
test session_control::tests::session_store_loads_safe_legacy_session_from_same_workspace ... ok
test session_control::tests::session_store_loads_unbound_legacy_session_from_same_workspace ... ok
test session_control::tests::session_store_rejects_legacy_session_from_other_workspace ... ok
test session_control::tests::resolves_latest_alias_and_loads_session_from_workspace_root ... ok
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 542 filtered out; finished in 0.02s
Running tests/g004_conformance.rs (rust/target/debug/deps/g004_conformance-90f36d1f871b6313)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/integration_tests.rs (rust/target/debug/deps/integration_tests-526d4f853fc590de)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
== runtime session jsonl/bloat ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.18s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 8 tests
test session::tests::rejects_jsonl_record_with_unknown_type ... ok
test session::tests::rejects_jsonl_message_record_without_message_payload ... ok
test session::tests::rejects_jsonl_record_without_type ... ok
test session::tests::persists_assistant_thinking_block_round_trip_through_jsonl ... ok
test session::tests::persists_and_restores_session_jsonl ... ok
test conversation::tests::persists_conversation_turn_messages_to_jsonl_session ... ok
test session::tests::appends_messages_to_persisted_jsonl_session ... ok
test session::tests::jsonl_persistence_redacts_and_truncates_oversized_payload_fields ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 549 filtered out; finished in 0.04s
Running tests/g004_conformance.rs (rust/target/debug/deps/g004_conformance-90f36d1f871b6313)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/integration_tests.rs (rust/target/debug/deps/integration_tests-526d4f853fc590de)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
== runtime compact ==
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.12s
Running unittests src/lib.rs (rust/target/debug/deps/runtime-0e7d3d46ae40aa07)
running 17 tests
test compact::tests::formats_compact_summary_like_upstream ... ok
test compact::tests::ignores_existing_compacted_summary_when_deciding_to_recompact ... ok
test compact::tests::compaction_does_not_split_tool_use_tool_result_pair ... ok
test compact::tests::leaves_small_sessions_unchanged ... ok
test compact::tests::infers_pending_work_from_recent_messages ... ok
test compact::tests::truncates_long_blocks_in_summary ... ok
test conversation::tests::auto_compaction_threshold_defaults_and_parses_values ... ok
test compact::tests::extracts_key_files_from_message_content ... ok
test compact::tests::compacts_older_messages_into_a_system_summary ... ok
test conversation::tests::compaction_health_probe_blocks_turn_when_tool_executor_is_broken ... ok
test conversation::tests::skips_auto_compaction_below_threshold ... ok
test conversation::tests::auto_compacts_when_cumulative_input_threshold_is_crossed ... ok
test conversation::tests::compaction_health_probe_skips_empty_compacted_session ... ok
test conversation::tests::compacts_session_after_turns ... ok
test prompt::tests::displays_context_paths_compactly ... ok
test compact::tests::keeps_previous_compacted_context_when_compacting_again ... ok
test session::tests::persists_compaction_metadata ... ok
test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 540 filtered out; finished in 0.01s
Running tests/g004_conformance.rs (rust/target/debug/deps/g004_conformance-90f36d1f871b6313)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Running tests/integration_tests.rs (rust/target/debug/deps/integration_tests-526d4f853fc590de)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
== cli resume_slash_commands ==
Compiling runtime v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/runtime)
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
Compiling api v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/api)
Compiling commands v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/commands)
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
Compiling tools v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/tools)
Compiling mock-anthropic-service v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/mock-anthropic-service)
warning: `api` (lib) generated 13 warnings
Compiling compat-harness v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/compat-harness)
error[E0004]: non-exhaustive patterns: `&SlashCommand::Session { action: None, .. }` not covered
--> crates/rusty-claude-cli/src/main.rs:3794:11
|
3794 | match command {
| ^^^^^^^ pattern `&SlashCommand::Session { action: None, .. }` not covered
|
note: `SlashCommand` defined here
--> crates/commands/src/lib.rs:1040:1
|
1040 | pub enum SlashCommand {
| ^^^^^^^^^^^^^^^^^^^^^
...
1089 | Session {
| ------- not covered
= note: the matched value is of type `&SlashCommand`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4197 ~ | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
4198 ~ &SlashCommand::Session { action: None, .. } => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `rusty-claude-cli` (bin "claw") due to 1 previous error
== cli compact_output ==
warning: enum `ProviderWireProtocol` is never used
--> crates/api/src/providers/mod.rs:54:10
|
54 | pub enum ProviderWireProtocol {
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
warning: enum `ProviderFeatureSupport` is never used
--> crates/api/src/providers/mod.rs:61:10
|
61 | pub enum ProviderFeatureSupport {
| ^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderCapabilityReport` is never constructed
--> crates/api/src/providers/mod.rs:68:12
|
68 | pub struct ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^
warning: enum `ProviderDiagnosticSeverity` is never used
--> crates/api/src/providers/mod.rs:88:10
|
88 | pub enum ProviderDiagnosticSeverity {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: struct `ProviderDiagnostic` is never constructed
--> crates/api/src/providers/mod.rs:94:12
|
94 | pub struct ProviderDiagnostic {
| ^^^^^^^^^^^^^^^^^^
warning: function `provider_capabilities_for_model` is never used
--> crates/api/src/providers/mod.rs:384:8
|
384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_diagnostics_for_request` is never used
--> crates/api/src/providers/mod.rs:452:8
|
452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `metadata_for_provider_kind` is never used
--> crates/api/src/providers/mod.rs:517:4
|
517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `provider_label` is never used
--> crates/api/src/providers/mod.rs:541:10
|
541 | const fn provider_label(provider: ProviderKind) -> &'static str {
| ^^^^^^^^^^^^^^
warning: function `has_openai_tuning_parameters` is never used
--> crates/api/src/providers/mod.rs:550:4
|
550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `declares_tool` is never used
--> crates/api/src/providers/mod.rs:558:4
|
558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
| ^^^^^^^^^^^^^
warning: function `web_passthrough_diagnostic` is never used
--> crates/api/src/providers/mod.rs:567:4
|
567 | fn web_passthrough_diagnostic(
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
warning: function `strip_routing_prefix` is never used
--> crates/api/src/providers/openai_compat.rs:901:4
|
901 | fn strip_routing_prefix(model: &str) -> &str {
| ^^^^^^^^^^^^^^^^^^^^
warning: `api` (lib) generated 13 warnings
Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)
error[E0004]: non-exhaustive patterns: `&SlashCommand::Session { action: None, .. }` not covered
--> crates/rusty-claude-cli/src/main.rs:3794:11
|
3794 | match command {
| ^^^^^^^ pattern `&SlashCommand::Session { action: None, .. }` not covered
|
note: `SlashCommand` defined here
--> crates/commands/src/lib.rs:1040:1
|
1040 | pub enum SlashCommand {
| ^^^^^^^^^^^^^^^^^^^^^
...
1089 | Session {
| ------- not covered
= note: the matched value is of type `&SlashCommand`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4197 ~ | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
4198 ~ &SlashCommand::Session { action: None, .. } => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `rusty-claude-cli` (bin "claw") due to 1 previous error
== diff check ==

View File

@@ -0,0 +1 @@
{"goal":{"threadId":"019e2560-a38d-7282-bb33-58c944cdcbc9","objective":"Complete the approved Claw Code 2.0 ultragoal delivery: implement all classified ROADMAP.md backlog work through execution-sized stream goals G001-G012, using .omx/ultragoal/ledger.jsonl as the durable audit trail and .omx/plans/claw-code-2-0-adaptive-plan.md as the source plan.","status":"active","tokensUsed":4536320,"timeUsedSeconds":13975,"createdAt":1778745278,"updatedAt":1778810208},"remainingTokens":null,"completionBudgetReport":null}

View File

@@ -0,0 +1 @@
{"goal":{"threadId":"019e2560-a38d-7282-bb33-58c944cdcbc9","objective":"Complete the approved Claw Code 2.0 ultragoal delivery: implement all classified ROADMAP.md backlog work through execution-sized stream goals G001-G012, using .omx/ultragoal/ledger.jsonl as the durable audit trail and .omx/plans/claw-code-2-0-adaptive-plan.md as the source plan.","status":"active","tokensUsed":4747486,"timeUsedSeconds":14669,"createdAt":1778745278,"updatedAt":1778810902},"remainingTokens":null,"completionBudgetReport":null}

View File

@@ -0,0 +1 @@
{"goal":{"threadId":"019e2560-a38d-7282-bb33-58c944cdcbc9","objective":"Complete the approved Claw Code 2.0 ultragoal delivery: implement all classified ROADMAP.md backlog work through execution-sized stream goals G001-G012, using .omx/ultragoal/ledger.jsonl as the durable audit trail and .omx/plans/claw-code-2-0-adaptive-plan.md as the source plan.","status":"active","tokensUsed":4771357,"timeUsedSeconds":14733,"createdAt":1778745278,"updatedAt":1778810966},"remainingTokens":null,"completionBudgetReport":null}

View File

@@ -0,0 +1 @@
{"goal":{"threadId":"019e2560-a38d-7282-bb33-58c944cdcbc9","objective":"Complete the approved Claw Code 2.0 ultragoal delivery: implement all classified ROADMAP.md backlog work through execution-sized stream goals G001-G012, using .omx/ultragoal/ledger.jsonl as the durable audit trail and .omx/plans/claw-code-2-0-adaptive-plan.md as the source plan.","status":"active","tokensUsed":4726793,"timeUsedSeconds":14653,"createdAt":1778745278,"updatedAt":1778810885},"remainingTokens":null,"completionBudgetReport":null}

View File

@@ -0,0 +1 @@
{"goal":{"threadId":"019e2560-a38d-7282-bb33-58c944cdcbc9","objective":"Complete the approved Claw Code 2.0 ultragoal delivery: implement all classified ROADMAP.md backlog work through execution-sized stream goals G001-G012, using .omx/ultragoal/ledger.jsonl as the durable audit trail and .omx/plans/claw-code-2-0-adaptive-plan.md as the source plan.","status":"active","tokensUsed":5024990,"timeUsedSeconds":15387,"createdAt":1778745278,"updatedAt":1778811620},"remainingTokens":null,"completionBudgetReport":null}

154
.omx/ultragoal/goals.json Normal file
View File

@@ -0,0 +1,154 @@
{
"version": 1,
"createdAt": "2026-05-14T07:53:46.061Z",
"updatedAt": "2026-05-15T04:38:54.887Z",
"briefPath": ".omx/ultragoal/brief.md",
"goalsPath": ".omx/ultragoal/goals.json",
"ledgerPath": ".omx/ultragoal/ledger.jsonl",
"codexGoalMode": "aggregate",
"goals": [
{
"id": "G001-stream0-board",
"title": "Stream 0: Generate canonical CC2 board",
"objective": "Generate the canonical Claw Code 2.0 board from frozen ROADMAP.md, latest issue snapshot, parity evidence, and approved plan. Classify every actionable roadmap item and context heading with source_anchor, source_type, release_bucket, lifecycle status, dependencies, verification_required, and deferral rationale. Emit machine JSON plus human markdown.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-14T08:14:23.206Z",
"startedAt": "2026-05-14T07:54:26.032Z",
"completedAt": "2026-05-14T08:14:23.206Z",
"evidence": "G001-stream0-board complete via team ultragoal-g001-stream-e61d2271: team status phase=team-verify, tasks 5/5 completed; worker-2 produced issue/parity intake, worker-3 produced board Markdown/rendering, worker-4 recorded validation evidence, worker-1 completed initial board artifacts. Leader reconciliation commit 45b43b5 aligned scripts/generate_cc2_board.py, scripts/validate_cc2_board.py, scripts/cc2_board.py, .omx/cc2/render_board_md.py. Evidence artifacts: .omx/cc2/board.json, .omx/cc2/board.md, .omx/cc2/issue-parity-intake.json, .omx/cc2/issue-parity-intake.md; .omx/ultragoal/goals.json and .omx/ultragoal/ledger.jsonl remain leader-owned. Verification passed: python3 scripts/generate_cc2_board.py; python3 scripts/validate_cc2_board.py; python3 scripts/cc2_board.py validate; python3 .omx/cc2/validate_issue_parity_intake.py; python3 .omx/cc2/render_board_md.py .omx/cc2/board.json .omx/cc2/board.md --check; python3 -m py_compile scripts/generate_cc2_board.py scripts/validate_cc2_board.py scripts/cc2_board.py .omx/cc2/validate_issue_parity_intake.py .omx/cc2/render_board_md.py; cargo check --manifest-path rust/Cargo.toml --workspace."
},
{
"id": "G002-alpha-security",
"title": "Stream 6: Day-one security and permissions gate",
"objective": "Implement/verify alpha-blocking security scope: file tools and shell enforce workspace/path scope across direct paths, symlinks, globbing, shell expansion, worktrees, and Windows path cases. Add regression fixtures for #3007 class behavior and permission-mode event/status visibility.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-14T08:34:04.243Z",
"startedAt": "2026-05-14T08:14:46.422Z",
"completedAt": "2026-05-14T08:34:04.243Z",
"evidence": "G002-alpha-security team ultragoal-g002-alpha-e61d2271 reached phase=complete with 5/5 tasks completed and no worker .omx/ultragoal mutation. Integrated commits through 37b2b75 on main: workspace/path enforcement in rust/crates/runtime/src/file_ops.rs, rust/crates/runtime/src/lib.rs, rust/crates/tools/src/lib.rs, regressions in rust/crates/tools/tests/path_scope_enforcement.rs and rust/crates/rusty-claude-cli/tests/output_format_contract.rs, verification map docs/g002-security-verification-map.md. Fresh leader validation passed: git diff --check; 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. .omx/ultragoal artifacts retained as leader-owned durable audit trail; fresh get_goal JSON captured at .omx/ultragoal/get-goal-G002-alpha-security.json. Known unrelated non-gating gaps from worker verification: full cargo test --workspace has pre-existing session_lifecycle_prefers_running_process_over_idle_shell failure; clippy all-targets has pre-existing runtime lint warnings."
},
{
"id": "G003-boot-session",
"title": "Stream 1: Reliable worker boot/session control",
"objective": "Implement/verify worker lifecycle, first prompt acceptance SLA, startup-no-evidence classifier, trust resolver/default trusted roots, structured session control API, and boot preflight/doctor JSON contracts.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-14T08:54:40.729Z",
"startedAt": "2026-05-14T08:34:19.605Z",
"completedAt": "2026-05-14T08:54:40.729Z",
"evidence": "G003-boot-session team g003-boot-session-ult-e61d2271 reached phase=complete with 5/5 tasks completed and no worker .omx/ultragoal mutation. Implemented/verified Stream 1 reliable worker boot/session control: worker lifecycle/prompt SLA and path guardrails, default trusted roots merge via runtime config and WorkerCreate, startup-no-evidence evidence/classifier timestamp coverage, structured boot preflight/status/doctor JSON, and docs/g003-boot-session-verification-map.md. Integrated/pushed through origin/main aec291c. Final leader validation passed: 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 trust_resolver -- --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 worker_create_merges_config_trusted_roots -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools path_scope -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli boot_preflight -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli branch_freshness -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli status_json_surfaces_session_lifecycle_for_clawhip -- --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; 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. Fresh get_goal JSON captured at .omx/ultragoal/get-goal-G003-boot-session.complete.json and .omx/ultragoal goals/ledger remain leader-owned audit artifacts. Known non-gating gaps from worker clippy attempts are pre-existing unrelated runtime clippy warnings and full workspace tests remain deferred to final gates."
},
{
"id": "G004-events-reports",
"title": "Stream 2: Event/report contract families",
"objective": "Implement/verify canonical lane events, ordering/provenance/identity/dedupe/ownership, report schema/projection/redaction/capability negotiation, approval-token chain, and pinpoint closure batches with golden fixtures.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-14T09:15:44.223Z",
"startedAt": "2026-05-14T08:54:55.093Z",
"completedAt": "2026-05-14T09:15:44.223Z",
"evidence": "G004-events-reports complete: team g004-events-reports-u-e61d2271 phase complete with 7/7 tasks completed; pushed main through 879962b; leader verification passed cargo fmt --manifest-path rust/Cargo.toml --all -- --check, cargo check --manifest-path rust/Cargo.toml -p runtime, cargo test --manifest-path rust/Cargo.toml -p runtime -- --nocapture (535 unit + g004_conformance 2 + integration 12 + doctests), python3 .github/scripts/check_doc_source_of_truth.py; evidence recorded against .omx/ultragoal/goals.json and .omx/ultragoal/ledger.jsonl"
},
{
"id": "G005-branch-recovery",
"title": "Stream 3: Branch/test awareness and recovery",
"objective": "Implement/verify stale branch detection before broad tests, recovery recipes and ledger, green-ness contract, test provenance, hung-test classification, and recovery/status reporting.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-14T12:41:48.997Z",
"startedAt": "2026-05-14T09:16:01.781Z",
"completedAt": "2026-05-14T12:41:48.997Z",
"evidence": "G005-branch-recovery complete and pushed at 7426ede; team g005-branch-recovery-e61d2271 has 5/5 tasks completed; leader verification passed for branch freshness before broad tests, recovery ledger/status reporting, green-ness contract/test provenance, stale-base doctor/status consistency, hung-test classification, and docs/g005-branch-recovery-verification-map.md. Evidence recorded against .omx/ultragoal/goals.json and .omx/ultragoal/ledger.jsonl."
},
{
"id": "G006-task-policy-board",
"title": "Stream 4: Task packets, policy engine, lane board",
"objective": "Implement/verify typed task packet schema, executable policy engine, active lane board/dashboard, running-state liveness heartbeat, and task/lane status JSON.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-15T00:42:05.094Z",
"startedAt": "2026-05-14T12:41:57.815Z",
"completedAt": "2026-05-15T00:42:05.094Z",
"evidence": "G006-task-policy-board complete in pushed origin/main commit 65a144c; team g006-task-policy-boar-e61d2271 terminal with 5 completed/0 failed after leader reconciliation; verification map docs/g006-task-policy-board-verification-map.md plus quality gate JSON record cargo fmt/check/tests/diff/push; .omx/ultragoal/goals.json and .omx/ultragoal/ledger.jsonl preserved; workers did not mutate .omx/ultragoal."
},
{
"id": "G007-plugin-mcp",
"title": "Stream 5: Plugin/MCP lifecycle maturity",
"objective": "Implement/verify plugin/MCP lifecycle states, healthy/degraded/failed startup, required vs optional behavior, malformed config consistency across status/doctor/mcp/plugins, and mock MCP/plugin tests.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-15T01:16:43.414Z",
"startedAt": "2026-05-15T00:42:16.309Z",
"completedAt": "2026-05-15T01:16:43.414Z",
"evidence": "G007-plugin-mcp complete: team g007-plugin-mcp-ultra-e61d2271 phase complete with 13/13 tasks completed, verification passed, pushed head 2202410, and durable ultragoal artifacts updated in .omx/ultragoal/goals.json and .omx/ultragoal/ledger.jsonl."
},
{
"id": "G008-provider-compat",
"title": "Stream 7: Provider/model compatibility",
"objective": "Implement/verify OpenAI-compatible slash-containing model IDs, provider prefix routing over env sniffing, DeepSeek/reasoning diagnostics, web search/fetch behavior, proxy/custom parameter passthrough, token/cost accounting, and provider diagnostics.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-15T01:38:22.717Z",
"startedAt": "2026-05-15T01:17:53.783Z",
"completedAt": "2026-05-15T01:38:22.717Z",
"evidence": "G008-provider-compat complete: team g008-provider-compat-e61d2271 phase complete with 5/5 tasks terminal; provider/model compatibility implemented and verified; pushed origin/main 2cac66c..8c9a05e; evidence recorded in .omx/ultragoal/goals.json and .omx/ultragoal/ledger.jsonl plus quality gate .omx/ultragoal/quality-gate-G008-provider-compat.json."
},
{
"id": "G009-windows-docs-release",
"title": "Stream 8: Windows/install/docs/license readiness",
"objective": "Implement/verify PowerShell-first docs, safe provider switching examples, Windows smoke CI, release artifact quickstart, license/contribution/security/support policies, and command/link validation.",
"status": "complete",
"attempt": 0,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-15T01:57:41.565Z",
"completedAt": "2026-05-15T01:57:41.565Z",
"evidence": "G009-windows-docs-release complete at commit 5294648 with team g009-windows-docs-rel-e61d2271 phase complete, 5/5 tasks completed; evidence in .omx/ultragoal/quality-gate-G009-windows-docs-release.json, .omx/ultragoal/get-goal-G009-windows-docs-release.complete.json, .omx/ultragoal/goals.json, and .omx/ultragoal/ledger.jsonl."
},
{
"id": "G010-session-hygiene",
"title": "Stream 9: Session hygiene/local state/recovery UX",
"objective": "Implement/verify session file hygiene, .gitignore state paths, per-worktree session isolation, list/delete/exists/compact/resume, compact/provider-context recovery, JSONL payload bloat safeguards, interrupt recovery, and clone disambiguation metadata.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-15T02:20:46.558Z",
"startedAt": "2026-05-15T01:59:22.219Z",
"completedAt": "2026-05-15T02:20:46.558Z",
"evidence": "G010-session-hygiene complete: team g010-session-hygiene-e61d2271 phase complete with 7/7 tasks completed; final verification passed in .omx/ultragoal/g010-final-quality-gate-rerun.log; durable state recorded in .omx/ultragoal/goals.json and .omx/ultragoal/ledger.jsonl."
},
{
"id": "G011-ecosystem-ops-ux",
"title": "Streams 1012: Ecosystem, issue ops, and UX laterals",
"objective": "Implement/verify gated ACP/Zed/JSON-RPC serve plan/status, anti-slop issue/PR triage, issue templates, navigation/file-context docs, TUI/rendering/copy/paste/clickable path improvements, and defer desktop/marketplace features until contracts are stable.",
"status": "complete",
"attempt": 1,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-15T02:55:26.988Z",
"startedAt": "2026-05-15T02:21:31.360Z",
"completedAt": "2026-05-15T02:55:26.988Z",
"evidence": "G011-ecosystem-ops-ux complete: team g011-ecosystem-ops-ux-e61d2271 phase=complete with 7/7 tasks completed; final pushed HEAD 1ac8ce8; verification evidence in .omx/ultragoal/g011-final-quality-gate.log and .omx/ultragoal/quality-gate-G011-ecosystem-ops-ux.json; ultragoal artifacts tracked in .omx/ultragoal/goals.json and .omx/ultragoal/ledger.jsonl."
},
{
"id": "G012-final-gate",
"title": "Final release gate: Verify Claw Code 2.0 delivery",
"objective": "Run final cross-stream quality gate: roadmap board has no unmapped actionable items, fmt/clippy/tests and focused contract suites pass, ai-slop-cleaner on changed files passes/no-ops, code-review approves, and final alpha/beta/GA readiness report is written. Final completion is blocked until docs/pr-issue-resolution-gate.md has fresh evidence showing every open PR and issue was triaged, with correct PRs merged and resolvable correct issues fixed or closed.",
"status": "complete",
"attempt": 0,
"createdAt": "2026-05-14T07:54:21.409575Z",
"updatedAt": "2026-05-15T04:38:54.887Z",
"evidence": "G012-final-gate complete: team g012-final-gate-ultra-e61d2271 8/8 tasks complete; final gate log /tmp/g012-final-quality-gate-pass4.log; commit 04c2abb pushed; docs/pr-triage-g012-final-gate.json docs/pr-issue-resolution-gate.md docs/g012-final-release-readiness-report.md; .omx/ultragoal/goals.json and ledger.jsonl updated; aiSlopCleaner and codeReview evidence included in quality gate JSON.",
"completedAt": "2026-05-15T04:38:54.887Z"
}
],
"codexObjective": "Complete the approved Claw Code 2.0 ultragoal delivery: implement all classified ROADMAP.md backlog work through execution-sized stream goals G001-G012, using .omx/ultragoal/ledger.jsonl as the durable audit trail and .omx/plans/claw-code-2-0-adaptive-plan.md as the source plan."
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,42 @@
{
"goal_id": "G009-windows-docs-release",
"timestamp_utc": "2026-05-15T01:57:16Z",
"commit": "a3af0133e0cf8d529465950ada88623e3cf3b3f2",
"team": "g009-windows-docs-rel-e61d2271",
"team_phase": "complete",
"tasks": "5/5 completed",
"verification": {
"release_readiness": "passed",
"doc_source_of_truth": "passed",
"cargo_fmt": "passed",
"targeted_windows_no_credentials_smoke_test": "passed",
"cargo_check_workspace": "passed with existing api dead_code warnings",
"git_diff_check": "passed",
"coverage_check": "passed"
},
"known_gaps": [
{
"scope": "actual GitHub windows-latest execution",
"status": "not run locally"
},
{
"scope": "full cargo test --workspace",
"status": "known pre-existing unrelated CLI failures reported by workers; targeted changed-surface tests pass"
}
],
"artifacts": [
".github/workflows/rust-ci.yml",
".github/workflows/release.yml",
"docs/windows-install-release.md",
"docs/g009-windows-docs-release-verification-map.md",
"LICENSE",
"CONTRIBUTING.md",
"SECURITY.md",
"SUPPORT.md",
"CODE_OF_CONDUCT.md",
".github/scripts/check_release_readiness.py",
"/tmp/g009-final-verify.log"
],
"git_status": "## main...origin/main [ahead 13]",
"log_tail": " | ^^^^^^^^^^^^^^^^^^^^\n |\n = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\nwarning: enum `ProviderFeatureSupport` is never used\n --> crates/api/src/providers/mod.rs:61:10\n |\n61 | pub enum ProviderFeatureSupport {\n | ^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: struct `ProviderCapabilityReport` is never constructed\n --> crates/api/src/providers/mod.rs:68:12\n |\n68 | pub struct ProviderCapabilityReport {\n | ^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: enum `ProviderDiagnosticSeverity` is never used\n --> crates/api/src/providers/mod.rs:88:10\n |\n88 | pub enum ProviderDiagnosticSeverity {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: struct `ProviderDiagnostic` is never constructed\n --> crates/api/src/providers/mod.rs:94:12\n |\n94 | pub struct ProviderDiagnostic {\n | ^^^^^^^^^^^^^^^^^^\n\nwarning: function `provider_capabilities_for_model` is never used\n --> crates/api/src/providers/mod.rs:384:8\n |\n384 | pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `provider_diagnostics_for_request` is never used\n --> crates/api/src/providers/mod.rs:452:8\n |\n452 | pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `metadata_for_provider_kind` is never used\n --> crates/api/src/providers/mod.rs:517:4\n |\n517 | fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `provider_label` is never used\n --> crates/api/src/providers/mod.rs:541:10\n |\n541 | const fn provider_label(provider: ProviderKind) -> &'static str {\n | ^^^^^^^^^^^^^^\n\nwarning: function `has_openai_tuning_parameters` is never used\n --> crates/api/src/providers/mod.rs:550:4\n |\n550 | fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `declares_tool` is never used\n --> crates/api/src/providers/mod.rs:558:4\n |\n558 | fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {\n | ^^^^^^^^^^^^^\n\nwarning: function `web_passthrough_diagnostic` is never used\n --> crates/api/src/providers/mod.rs:567:4\n |\n567 | fn web_passthrough_diagnostic(\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nwarning: function `strip_routing_prefix` is never used\n --> crates/api/src/providers/openai_compat.rs:901:4\n |\n901 | fn strip_routing_prefix(model: &str) -> &str {\n | ^^^^^^^^^^^^^^^^^^^^\n\nwarning: `api` (lib) generated 13 warnings\n Compiling rusty-claude-cli v0.1.0 (/Users/bellman/Documents/Workspace/claw-code/rust/crates/rusty-claude-cli)\n Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.35s\nG009 coverage check passed"
}

View File

@@ -0,0 +1,32 @@
{
"goal_id": "G010-session-hygiene",
"status": "passed",
"team": "g010-session-hygiene-e61d2271",
"team_phase": "complete",
"tasks": {"completed": 7, "failed": 0, "blocked": 0, "pending": 0, "in_progress": 0},
"evidence": [
".omx/ultragoal/g010-final-quality-gate-rerun.log",
"docs/g010-clone-disambiguation-metadata.md",
"docs/g010-session-hygiene-verification-map.md",
".omx/ultragoal/goals.json",
".omx/ultragoal/ledger.jsonl"
],
"verification_passed": [
"cargo fmt --manifest-path rust/Cargo.toml --all -- --check",
"cargo test --manifest-path rust/Cargo.toml -p runtime session_control -- --nocapture",
"cargo test --manifest-path rust/Cargo.toml -p runtime jsonl_persistence_redacts_and_truncates_oversized_payload_fields -- --nocapture",
"cargo test --manifest-path rust/Cargo.toml -p runtime compact -- --nocapture",
"cargo test --manifest-path rust/Cargo.toml -p commands parses_supported_slash_commands -- --nocapture",
"cargo test --manifest-path rust/Cargo.toml -p commands compacts_sessions_via_slash_command -- --nocapture",
"cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --bin claw session_exists_resume_command_reports_json_contract -- --nocapture",
"cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --bin claw resumed_session_exists_and_delete_have_json_contracts -- --nocapture",
"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",
"cargo check --manifest-path rust/Cargo.toml --workspace",
"git diff --check"
],
"known_gaps": [
"full cargo test --workspace not run for G010",
"clippy -D warnings remains blocked by pre-existing unrelated lint debt noted in task 5/task 7 results"
]
}

View File

@@ -0,0 +1,8 @@
{
"session_id": "b035f648d5b549aa836ea01f6727ec62",
"messages": [
"review MCP tool"
],
"input_tokens": 3,
"output_tokens": 13
}

View File

@@ -0,0 +1,9 @@
{
"session_id": "b234acb1eb8c486e80544ddc7e13e6d8",
"messages": [
"review MCP tool",
"review MCP tool"
],
"input_tokens": 6,
"output_tokens": 32
}

View File

@@ -0,0 +1,9 @@
{
"session_id": "b67e062748f04e10ac5770df9285e4bd",
"messages": [
"review MCP tool",
"review MCP tool"
],
"input_tokens": 6,
"output_tokens": 32
}

View File

@@ -0,0 +1,9 @@
{
"session_id": "bb88fd20433840a8b19237e3f306c6e3",
"messages": [
"review MCP tool",
"review MCP tool"
],
"input_tokens": 6,
"output_tokens": 32
}

32
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,32 @@
# Code of Conduct
## Our pledge
We aim to make Claw Code a practical, respectful, and evidence-oriented
community. Contributors and maintainers are expected to communicate with
patience, assume good intent, and focus critique on the work rather than the
person.
## Expected behavior
- Be respectful and direct.
- Welcome newcomers and explain project-specific context when it matters.
- Give actionable feedback with evidence, commands, logs, or links.
- Respect privacy and do not pressure others to disclose credentials, private
prompts, employer information, or personal details.
## Unacceptable behavior
- Harassment, threats, insults, or discriminatory language.
- Publishing another person's private information without permission.
- Sharing secrets, exploit payloads, or private vulnerability details in public
channels.
- Repeated off-topic disruption after maintainers ask for a thread to stop or
move.
## Enforcement
Maintainers may remove comments, close threads, restrict participation, or ban
accounts that violate this code of conduct. Report concerns through the support
or security paths described in [SUPPORT.md](./SUPPORT.md) and
[SECURITY.md](./SECURITY.md).

66
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,66 @@
# Contributing to Claw Code
Thanks for helping improve Claw Code. This repository is a Rust-first CLI
workspace with supporting docs and compatibility fixtures.
## Ground rules
- Keep changes small, reviewable, and tied to a concrete issue or behavior.
- Do not commit secrets, API keys, session transcripts with credentials, or
generated build output.
- Prefer existing crate boundaries and utilities before adding dependencies.
- Update documentation when a user-facing command, config key, or provider
behavior changes.
- Keep examples copy/paste safe. Use placeholder keys such as `sk-ant-...` and
avoid commands that require live credentials unless the text explicitly says
so.
## Local setup
```bash
git clone https://github.com/ultraworkers/claw-code
cd claw-code/rust
cargo build --workspace
cargo test --workspace
```
On Windows PowerShell, build from the same `rust` workspace and run the binary
with the `.exe` suffix:
```powershell
cd claw-code\rust
cargo build --workspace
.\target\debug\claw.exe --help
```
## Checks before opening a pull request
Run the smallest relevant tests for your change, then the broader checks when
you touch shared runtime, CLI, or docs surfaces:
```bash
cd rust
cargo fmt --all --check
cargo test --workspace
cargo clippy --workspace
```
For documentation and release-readiness changes, also run:
```bash
python .github/scripts/check_doc_source_of_truth.py
python .github/scripts/check_release_readiness.py
```
## Pull request guidance
- Describe the user-visible reason for the change.
- List the commands you ran and any known gaps.
- Call out compatibility risks for CLI output, JSON schemas, plugin contracts,
provider behavior, or Windows/PowerShell examples.
- Keep unrelated cleanup out of feature or fix pull requests.
## License
By contributing, you agree that your contributions are licensed under the
project's [MIT License](./LICENSE).

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 UltraWorkers and Claw Code contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -8,7 +8,7 @@ Last updated: 2026-04-03
- Requested 9-lane checkpoint: **All 9 lanes merged on `main`.**
- Current `main` HEAD: `ee31e00` (stub implementations replaced with real AskUserQuestion + RemoteTrigger).
- Repository stats at this checkpoint: **292 commits on `main` / 293 across all branches**, **9 crates**, **48,599 tracked Rust LOC**, **2,568 test LOC**, **3 authors**, date range **2026-03-31 → 2026-04-03**.
- Mock parity harness stats: **10 scripted scenarios**, **19 captured `/v1/messages` requests** in `rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs`.
- Mock parity harness stats: **12 scripted scenarios**, **21 captured `/v1/messages` requests** in `rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs`.
## Mock parity harness — milestone 1
@@ -23,6 +23,8 @@ Last updated: 2026-04-03
- [x] Scripted permission prompt coverage: `bash_permission_prompt_approved`, `bash_permission_prompt_denied`
- [x] Scripted plugin-path coverage: `plugin_tool_roundtrip`
- [x] Behavioral diff/checklist runner: `rust/scripts/run_mock_parity_diff.py`
- [x] Scripted session-compaction metadata coverage: `auto_compact_triggered`
- [x] Scripted token/cost JSON coverage: `token_cost_reporting`
## Harness v2 behavioral checklist
@@ -172,8 +174,9 @@ Canonical scenario map: `rust/mock_parity_scenarios.json`
- [ ] End-to-end MCP runtime lifecycle beyond the registry bridge now on `main`
- [x] Output truncation (large stdout/file content)
- [ ] Session compaction behavior matching
- [ ] Token counting / cost tracking accuracy
- [x] Session compaction behavior matching
- auto_compaction threshold from env
- [x] Token counting / cost tracking accuracy
- [x] Bash validation lane merged onto `main`
- [ ] CI green on every commit

View File

@@ -11,6 +11,10 @@
·
<a href="./ROADMAP.md">Roadmap</a>
·
<a href="./CONTRIBUTING.md">Contributing</a>
·
<a href="./SECURITY.md">Security</a>
·
<a href="https://discord.gg/5TUQKqFWd">UltraWorkers Discord</a>
</p>
@@ -32,9 +36,9 @@ Claw Code is the public Rust implementation of the `claw` CLI agent harness.
The canonical implementation lives in [`rust/`](./rust), and the current source of truth for this repository is **ultraworkers/claw-code**.
> [!IMPORTANT]
> Start with [`USAGE.md`](./USAGE.md) for build, auth, CLI, session, and parity-harness workflows. Make `claw doctor` your first health check after building, use [`rust/README.md`](./rust/README.md) for crate-level details, read [`PARITY.md`](./PARITY.md) for the current Rust-port checkpoint, and see [`docs/container.md`](./docs/container.md) for the container-first workflow.
> Start with [`USAGE.md`](./USAGE.md) for build, auth, CLI, session, and parity-harness workflows. For file submission/navigation questions, see [Navigation and file context](./docs/navigation-file-context.md). For local OpenAI-compatible models and offline skill installs, see [Local OpenAI-compatible providers and skills setup](./docs/local-openai-compatible-providers.md). Windows users can jump to the PowerShell-first [Windows install and release quickstart](./docs/windows-install-release.md). Make `claw doctor` your first health check after building, use [`rust/README.md`](./rust/README.md) for crate-level details, read [`PARITY.md`](./PARITY.md) for the current Rust-port checkpoint, and see [`docs/container.md`](./docs/container.md) for the container-first workflow.
>
> **ACP / Zed status:** `claw-code` does not ship an ACP/Zed daemon entrypoint yet. Run `claw acp` (or `claw --acp`) for the current status instead of guessing from source layout; `claw acp serve` is currently a discoverability alias only, and real ACP support remains tracked separately in `ROADMAP.md`.
> **ACP / Zed status:** `claw-code` does not ship an ACP/Zed daemon or JSON-RPC entrypoint yet. Run `claw acp` (or `claw --acp`) for the current status instead of guessing from source layout; `claw acp serve` is currently a discoverability alias only, returns status with exit code 0, and real ACP support remains tracked separately in `ROADMAP.md`. For the public JSON contract, see [`docs/g011-acp-json-rpc-status-contract.md`](./docs/g011-acp-json-rpc-status-contract.md).
## Current repository shape
@@ -96,6 +100,8 @@ export ANTHROPIC_API_KEY="sk-ant-..."
.\target\debug\claw.exe prompt "say hello"
```
For release ZIPs, PATH setup, provider switching, and notification smoke checks, see [`docs/windows-install-release.md`](./docs/windows-install-release.md).
**Git Bash / WSL** are optional alternatives, not requirements. If you prefer bash-style paths (`/c/Users/you/...` instead of `C:\Users\you\...`), Git Bash (ships with Git for Windows) works well. In Git Bash, the `MINGW64` prompt is expected and normal — not a broken install.
## Post-build: locate the binary and verify
@@ -130,6 +136,18 @@ Test the binary directly using its path:
.\rust\target\debug\claw.exe doctor
```
PowerShell smoke commands that do not require live credentials:
```powershell
$env:CLAW_CONFIG_HOME = Join-Path $env:TEMP "claw config home"
New-Item -ItemType Directory -Force -Path $env:CLAW_CONFIG_HOME | Out-Null
Remove-Item Env:\ANTHROPIC_API_KEY, Env:\ANTHROPIC_AUTH_TOKEN, Env:\OPENAI_API_KEY -ErrorAction SilentlyContinue
.\rust\target\debug\claw.exe help
.\rust\target\debug\claw.exe status
.\rust\target\debug\claw.exe config env
.\rust\target\debug\claw.exe doctor
```
If these commands succeed, the build is working. `claw doctor` is your first health check — it validates your API key, model access, and tool configuration.
### Optional: Add to PATH
@@ -188,11 +206,17 @@ cargo test --workspace
## Documentation map
- [`USAGE.md`](./USAGE.md) — quick commands, auth, sessions, config, parity harness
- [`docs/navigation-file-context.md`](./docs/navigation-file-context.md) — terminal navigation, scrollback, `@path` file context, attachments, and secret-safety guidance
- [`docs/local-openai-compatible-providers.md`](./docs/local-openai-compatible-providers.md) — Ollama/llama.cpp/vLLM setup, Claw multi-provider positioning, and local skills install checks
- [`docs/windows-install-release.md`](./docs/windows-install-release.md) — PowerShell-first install, release artifact, provider switching, and Windows/WSL notification smoke paths
- [`rust/README.md`](./rust/README.md) — crate map, CLI surface, features, workspace layout
- [`PARITY.md`](./PARITY.md) — parity status for the Rust port
- [`rust/MOCK_PARITY_HARNESS.md`](./rust/MOCK_PARITY_HARNESS.md) — deterministic mock-service harness details
- [`ROADMAP.md`](./ROADMAP.md) — active roadmap and open cleanup work
- [`docs/g004-events-reports-contract.md`](./docs/g004-events-reports-contract.md) — Stream 2 lane event/report contract guidance for consumers
- [`PHILOSOPHY.md`](./PHILOSOPHY.md) — why the project exists and how it is operated
- [`CONTRIBUTING.md`](./CONTRIBUTING.md), [`SECURITY.md`](./SECURITY.md), [`SUPPORT.md`](./SUPPORT.md), and [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md) — contribution, vulnerability-reporting, support, and community policies
- [`LICENSE`](./LICENSE) — MIT license for this repository
## Ecosystem

View File

@@ -6338,3 +6338,210 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
420. **`plugins help --output-format json` returns the mutation response shape (`message`, `reload_runtime`, `target`) instead of the help envelope (`action:"help"`, `kind`, `unexpected`, `usage`) that `mcp help`, `agents help`, and `skills help` all use — schema drift within the same command family** — dogfooded 2026-05-01 by Jobdori on `e939777f`. Running `claw plugins help --output-format json` returns `{"action":"help","kind":"plugin","message":"Unknown /plugins action 'help'. Use list, install, enable, disable, uninstall, or update.","reload_runtime":false,"target":null}`. By contrast, `claw mcp help --output-format json`, `claw agents help --output-format json`, and `claw skills help --output-format json` all return a help envelope: `{"action":"help","kind":"<surface>","unexpected":null,"usage":{"direct_cli":"...","slash_command":"...","sources":[...]}}`. The `plugins` subgroup has not adopted the help envelope schema used by all sibling subgroups. Instead it uses the mutation response shape (`message`, `reload_runtime`, `target`) with an error string in `message` that calls `help` an "unknown action." Automation that checks `usage.direct_cli` to discover plugin commands gets a `TypeError` (key not found) on the plugins help path while succeeding on all sibling subgroups. **Required fix shape:** (a) make `plugins help` return the same help envelope as `mcp help`/`agents help`/`skills help`: `{action:"help", kind:"plugin", unexpected:null, usage:{direct_cli:"claw plugins [list|enable|disable|install|uninstall|update|help]", slash_command:"/plugins [...]", sources:[...]}`; (b) drop `reload_runtime` and `target` from help responses for all plugin subcommands; (c) add regression coverage proving `plugins help --output-format json` contains a `usage.direct_cli` field matching the same envelope shape as `mcp help`/`agents help`/`skills help`; (d) audit all subgroup `help` handlers for the same mutation-envelope contamination. **Why this matters:** help discovery is the bootstrap surface for automation. If `plugins help --output-format json` returns a mutation envelope with an error message instead of a usage envelope, automated schema discovery fails silently for the entire plugins subgroup while working for every other subgroup. Source: Jobdori live dogfood, `e939777f`, 2026-05-01.
421. **`status`, `mcp list`, `doctor` JSON output leak macOS `/private` symlink-canonicalized cwd instead of user-invocation cwd — automation that string-matches on cwd breaks across symlinked filesystems** — dogfooded 2026-05-11 by Jobdori on `b98b9a71` in response to Clawhip pinpoint nudge at `1503207549447573574`. Reproduction on macOS: invoke from `/tmp/claw-dog-cwd` (where `/tmp` symlinks to `/private/tmp`), then `claw status --output-format json` returns `workspace.cwd: "/private/tmp/claw-dog-cwd"`, `claw mcp list --output-format json` returns `working_directory: "/private/tmp/claw-dog-cwd"`. The user's invocation cwd (`$PWD`, `pwd`) is `/tmp/claw-dog-cwd`. Source: `session_control.rs:34` calls `fs::canonicalize(cwd)` for #151 cross-worktree session-bleed prevention, then leaks the canonicalized path through every JSON envelope that reports cwd. **Required fix shape:** (a) keep canonicalized cwd for session keying internally, but report user-input cwd (the value passed by `env::current_dir()` or `--cwd` flag) in JSON output as `cwd`; (b) optionally expose canonical path as a separate field `cwd_canonical` for diagnostic purposes; (c) audit every `--output-format json` surface that emits `cwd` / `working_directory` / `workspace.cwd` for the same leak (status, mcp list, doctor, session list, init, etc.); (d) add regression coverage proving JSON cwd matches `$PWD` on macOS where `/tmp -> /private/tmp` symlink exists. **Why this matters:** automation pipelines that route work to lanes by cwd, or that compare cwd against a registry, break across macOS hosts because the canonicalized form differs from the form the user/orchestrator passed. The leak is silent — no documentation indicates the path will be rewritten. Source: Jobdori live dogfood, `b98b9a71`, 2026-05-11.
422. **Unknown top-level subcommands fall through to chat prompt path instead of returning `unknown_subcommand` error — typos silently send the subcommand string as a chat message to the configured LLM** — dogfooded 2026-05-11 by Jobdori on `b98b9a71` in response to Clawhip pinpoint nudge at `1503215095088676956`. Reproduction: `unset ANTHROPIC_AUTH_TOKEN; export ANTHROPIC_API_KEY=fake-key-for-routing-test; claw completely-bogus-subcommand --output-format json` returns `{"error":"api returned 401 Unauthorized (authentication_error) [trace req_011...]: invalid x-api-key","kind":"api_http_error"}` — proving the unknown token reached the Anthropic API endpoint as a chat prompt. With valid credentials, the bogus subcommand string would be silently consumed as a chat message, billing the user for a typo and producing whatever continuation the LLM generates. **Pre-error path:** `claw <unknown> --output-format json` with no creds returns `kind:"missing_credentials"` (the auth gate fires first), masking the routing bug. Only with creds present does the fallthrough manifest as the actual prompt being sent. **Sibling exit-code bug:** when the chat-path 401 returns, the JSON envelope is `kind:"api_http_error"` but exit code is **0**, while `cli_parse` errors (e.g. `--no-such-flag`) and `missing_credentials` errors correctly exit **1**. Exit-code parity between error envelopes is broken — automation that gates on `$?` will treat the 401-as-chat as success. **Required fix shape:** (a) reserve unknown top-level tokens that match no registered subcommand and emit `kind:"unknown_subcommand"` with `unknown:<token>` field and exit code 1, BEFORE the chat fallback path; (b) when a token is intended as a chat prompt, require an explicit verb (`prompt`, `chat`, `ask`) or `--prompt` flag; (c) ensure exit codes are non-zero for all `kind:*_error` envelopes; (d) regression test: `claw <bogus> --output-format json` with valid auth returns `kind:"unknown_subcommand"` exit 1, never reaches the API. **Why this matters:** automation that calls `claw <subcommand>` with a programmatically constructed verb (typo, version drift, refactored command) silently bills tokens and produces hallucinated output instead of a typed error. Cross-cluster with #108 (CLI fallthrough discovered earlier) — #422 is the post-#108 audit confirming the routing bug still bites with valid credentials. Source: Jobdori live dogfood, `b98b9a71`, 2026-05-11.
423. **`claw prompt` does not read prompt text from stdin when no positional prompt arg is provided — `echo "what is 2+2" | claw prompt --output-format json` returns `kind:"unknown" error:"prompt subcommand requires a prompt string"` instead of consuming stdin** — dogfooded 2026-05-11 by Jobdori on `3c563fa1` in response to Clawhip pinpoint nudge at `1503222644739276951`. Reproduction: `echo "what is 2+2" | claw prompt --output-format json``{"error":"prompt subcommand requires a prompt string","hint":null,"kind":"unknown","type":"error"}` exit 1. Same for `claw prompt --output-format json` with stdin redirected from a file. The most common Unix automation pattern (`cmd | claw prompt`) is broken because the prompt subcommand only reads the positional argument, never falls through to stdin. **Sibling envelope-kind bug:** the error `kind` is `"unknown"` instead of a typed `"missing_argument"` or `"validation_error"`. The `unknown` discriminator is the catch-all bucket — automation that switches on `kind` to differentiate input-validation errors from runtime errors gets no signal here. **Required fix shape:** (a) when `prompt` subcommand has no positional prompt arg AND stdin is not a TTY (i.e., piped or redirected), read stdin to EOF and use that as the prompt; (b) emit `kind:"missing_argument"` (not `"unknown"`) when both positional arg and stdin are absent; (c) add `--prompt-stdin` or `--stdin` opt-in flag for explicit control; (d) regression tests: `echo X | claw prompt --output-format json` reaches the runtime with prompt=X, AND `claw prompt < /dev/null` returns `kind:"missing_argument"` exit 1. **Why this matters:** Unix pipelines are the foundation of CLI automation. Every other major CLI (curl, jq, gh, kubectl) accepts stdin as the primary input when no positional arg is given. Breaking this convention forces automation to either inline the prompt as a shell-quoted string (escaping nightmare for multiline/code) or write to a temp file first. The `kind:"unknown"` error category compounds the problem by making the failure indistinguishable from a runtime crash. Source: Jobdori live dogfood, `3c563fa1`, 2026-05-11.
424. **`--model` rejects bare canonical Anthropic model names (`claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`) as `invalid_model_syntax` — only short aliases (`opus`, `sonnet`, `haiku`) and full prefixed form (`anthropic/claude-opus-4-7`) work; sibling: error message stale-suggests `claude-opus-4-6` not `4-7`** — dogfooded 2026-05-11 by Jobdori on `6c0c305a` in response to Clawhip pinpoint nudge at `1503230194889134103`. Reproduction: `claw --model claude-opus-4-7 status --output-format json``{"error":"invalid model syntax: 'claude-opus-4-7'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku)","kind":"invalid_model_syntax"}`. Same for `claude-opus-4-6`, `claude-sonnet-4-6`. Forcing `--model anthropic/claude-opus-4-7` works (`model:"anthropic/claude-opus-4-7"`, `model_source:"flag"`). Three problems compounded: (a) Anthropic-canonical model names without provider prefix are rejected even though the `claude-` prefix unambiguously identifies the provider; (b) the error suggests `anthropic/claude-opus-4-6` as the example — `4-7` shipped 2026-04-16 and is the current production Anthropic frontier model, the suggestion is one model behind; (c) the alias list `opus, sonnet, haiku` doesn't disambiguate version (which `opus` does the alias resolve to — `opus-4-6` or `opus-4-7`?). **Required fix shape:** (a) accept bare `claude-*` and `gpt-*` model names as canonical-named-without-prefix and route via name-prefix detection (already implemented for prefix-routed mode); (b) update the example in `invalid_model_syntax` error to current frontier (`anthropic/claude-opus-4-7`); (c) document or expose `opus` → exact-version mapping in the error message and in `claw doctor`/`status` output (`model_alias_resolved_to: "claude-opus-4-7"`); (d) regression test: `claw --model claude-opus-4-7 status --output-format json` returns `model_source:"flag"`, not `kind:"invalid_model_syntax"`. **Sibling bug observed in same probe:** `enabledPlugins` deprecation warning repeats 3 times in stderr for the same `~/.claw/settings.json` load — config file is being loaded/parsed 3 times during a single `status` invocation. **Why this matters:** every Anthropic doc, every CCAPI route, every internal tooling references models by their bare canonical name (`claude-opus-4-7`). Forcing the `anthropic/` prefix breaks copy-paste from Anthropic's own examples and adds a redundant token to every invocation. The stale `4-6` suggestion in the error message actively misdirects users away from the current model. Source: Jobdori live dogfood, `6c0c305a`, 2026-05-11.
425. **Config file precedence (`.claw/settings.json` always wins over `.claw.json`) is undocumented in user-facing surfaces — `config --output-format json` reports both files as `loaded:true` with no `precedence_rank` or `wins_for_keys` attribution; sibling: deprecation warning fires 4× per status invocation (was 3× in #424, regression upward)** — dogfooded 2026-05-11 by Jobdori on `d7dbe951` in response to Clawhip pinpoint nudge at `1503237744451649537`. Reproduction: create `.claw.json` with `{"model":"anthropic/claude-sonnet-4-6"}` and `.claw/settings.json` with `{"model":"anthropic/claude-opus-4-7"}` in the same workspace. `claw status --output-format json` returns `model:"anthropic/claude-opus-4-7", model_source:"config"`. Reverse the files (.claw.json=opus, settings.json=sonnet) → `model:"anthropic/claude-sonnet-4-6"`. Confirmed: `.claw/settings.json` **always** wins over `.claw.json` for conflicting keys, regardless of file mtime or alphabetical order. `claw config --output-format json` reports both as `loaded:true` with no `precedence_rank`, `effective_for_keys`, or `shadowed_keys` attribution. The only signal of precedence is the final merged value in `status` — automation cannot programmatically discover which file contributed which key without re-implementing the merge logic. **Sibling bug (regression from #424):** the `enabledPlugins` deprecation warning now fires **4 times** in stderr per single `status` invocation (was 3× in #424's probe at HEAD `6c0c305a`; current HEAD `d7dbe951` shows 4×). Config load count went up by 1. **Sibling bug observed in config-section probe:** `claw config model --output-format json` with a `.claw.json` that contains a benign unknown key (e.g., `"alpha":"x"`) returns `{"error":"/path/.claw.json: unknown key \"alpha\" (line 1)","kind":"unknown"}` — the entire config command fails with a generic `unknown` kind instead of (a) tolerating unrecognized keys with a warning, or (b) emitting a typed `kind:"unknown_key"` error scoped to the offending file/key. **Required fix shape:** (a) document precedence order in `USAGE.md` (`.claw/settings.local.json > .claw/settings.json > .claw.json` for project scope; `user`/`system` scope at each layer); (b) add `precedence_rank:int` and optional `wins_for_keys:[string]` / `shadowed_keys:[string]` to each entry in `config --output-format json` `files[]`; (c) dedupe the deprecation warning to fire **once per discovered file** instead of N× per load pass; (d) make `config <section> --output-format json` tolerate unknown keys with warnings, OR emit `kind:"unknown_key"` with `path:` and `key:` fields scoped to the offending file. **Why this matters:** users mixing legacy `.claw.json` with new `.claw/settings.json` have no way to verify which file is actually controlling their runtime. The undocumented precedence + missing per-key attribution forces trial-and-error to debug config drift. Cross-references #407 (config files no load_error) and #415 (config section returns merged_keys count not values). Source: Jobdori live dogfood, `d7dbe951`, 2026-05-11.
426. **`ANTHROPIC_MODEL` env var bypasses the `invalid_model_syntax` validator that `--model` enforces — bogus model strings are accepted with `status:"ok"`, deferred-failing only when the first API call is made** — dogfooded 2026-05-11 by Jobdori on `3730b459` in response to Clawhip pinpoint nudge at `1503245298800136296`. Reproduction (asymmetric validation): `claw --model bogus-model-xyz status --output-format json` returns `kind:"invalid_model_syntax"` exit 1; `ANTHROPIC_MODEL=bogus-model-xyz claw status --output-format json` returns `model:"bogus-model-xyz", model_raw:"bogus-model-xyz", model_source:"env", status:"ok"` — the doctor surface lies that the configured model is valid when it is not. The bogus model only manifests as a failure when the first prompt fires and the API rejects it with 404/400. Three sibling discoveries in the same probe: (a) **alias indirection invisible**: `ANTHROPIC_MODEL=opus claw status --output-format json` returns `model:"claude-opus-4-6", model_raw:"opus", model_source:"env"` — the `opus` alias resolves to `claude-opus-4-6` (the *previous* frontier, not the current `claude-opus-4-7` released 2026-04-16). Users typing `opus` get yesterday's model with no warning. (b) **`CLAW_MODEL` env var silently ignored**: `CLAW_MODEL=opus claw status` shows `model:"claude-opus-4-6" model_source:"default"` — the `CLAW_MODEL` env var (the project-namespaced equivalent that users expect) does not exist; only `ANTHROPIC_MODEL` is honored. No warning when a `CLAW_*` env var that looks like it should work is set. (c) **`ANTHROPIC_DEFAULT_MODEL` also silently ignored**: the longer-named env var that some Anthropic SDKs use is not recognized. **Required fix shape:** (a) symmetric validation: `ANTHROPIC_MODEL` env value must pass the same `invalid_model_syntax` check that `--model` does, and `claw status` must return `kind:"invalid_model"` / `status:"warn"` (not `status:"ok"`) when the resolved model is unrecognized; (b) expose alias resolution in `status`: add `model_alias_resolved_to:string|null` field so automation can see `opus → claude-opus-4-6`; (c) bump the `opus` alias to `claude-opus-4-7` (current frontier) or document the alias-to-version mapping policy explicitly; (d) accept `CLAW_MODEL` and `ANTHROPIC_DEFAULT_MODEL` env vars with parity to `ANTHROPIC_MODEL`, OR emit a warning when those env vars are set but unrecognized. **Why this matters:** the most common automation pattern is `export ANTHROPIC_MODEL=...` in a shell rc file. Bogus values pass silently, alias indirection hides the actual model in use, and `CLAW_MODEL` looking like a working name but doing nothing is a footgun. Cross-references #424 (bare canonical names rejected at validator level) — together #424 + #426 make model selection inconsistent across CLI flag, env var, and alias paths. Source: Jobdori live dogfood, `3730b459`, 2026-05-11.
427. **Subcommand `--help` paths (`resume`, `session`, `compact`) hit the auth gate and trigger config validation before returning static help — `claw resume --help` with no credentials returns `missing_credentials` error instead of help text** — dogfooded 2026-05-11 by Jobdori on `1fecdf09` in response to Clawhip pinpoint nudge at `1503252843669491892`. Reproduction (no env vars, isolated `CLAW_CONFIG_HOME`): `claw resume --help` returns `{"error":"missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY..."}` instead of usage text. Same for `claw session --help`, `claw compact --help`. By contrast, `claw prompt --help` and `claw --help` (top-level) return proper usage text without auth. Even worse: with a broken `.claw.json` discovered up the parent directory tree (e.g., `mcpServers.missing-command: missing string field command`), the subcommand `--help` paths fail with `[error-kind: unknown]` from config validation — config load is happening before `--help` is parsed. **Sibling exit-code bug:** `claw resume --help --output-format json` returns `kind:"missing_credentials"` but exits **0** (the exit-code parity bug from #422 reproduces on this path too — only `cli_parse` exits 1 consistently). **Sibling: `claw resume <bogus-id>` should be local-only** but also hits `missing_credentials``resume` of a session that doesn't exist on disk should return `kind:"session_not_found"` from a local lookup, not require API credentials. Same class as ROADMAP #357 (session list requires creds) and #369 (session help/fork require credentials) — now confirmed for `resume`. **Required fix shape:** (a) `--help` MUST short-circuit before any auth check, config load, or session resolution — emit static usage text from a compiled-in string table, no I/O; (b) `resume <id>` must check the local session store first; if the id is absent on disk, emit `kind:"session_not_found"` with `sessions_dir` field; only require auth when resuming a known-on-disk session that requires re-establishing API context; (c) ensure exit code 1 for all error envelopes including `missing_credentials` returned from a `--help` path that should never have reached the auth gate; (d) regression test: with empty `CLAW_CONFIG_HOME` and no env vars, every `claw <subcommand> --help` returns usage text on stdout, exit 0, no `kind:*_error` envelope. **Why this matters:** `--help` is the universal CLI discovery primitive. Failing `--help` because of missing API credentials or broken config files makes claw undiscoverable to users debugging an already-broken setup. Cross-references #357 (session list), #369 (session help/fork), #422 (exit code parity), #108 (subcommand fallthrough). Source: Jobdori live dogfood, `1fecdf09`, 2026-05-11.
428. **Default `permission_mode` is `danger-full-access` — claw runs with FULL filesystem + network + tool access out of the box, with no opt-in flag and no warning from `doctor`** — dogfooded 2026-05-11 by Jobdori on `72048449` in response to Clawhip pinpoint nudge at `1503260393622212628`. Reproduction (no env vars, isolated `CLAW_CONFIG_HOME`, no config files, no CLI flags): `claw status --output-format json` returns `permission_mode:"danger-full-access"` as the default. The three supported modes per the validator error message are `read-only`, `workspace-write`, `danger-full-access` — and `danger-full-access` is chosen with zero user opt-in. `claw doctor --output-format json` produces a `sandbox` check with `status:"warn", summary:"sandbox was requested but is not currently active"` (because macOS lacks Linux `unshare`), but **emits no warning, info, or summary about the permission_mode itself being danger-full-access**. There is no `permissions` check in `doctor` output at all. **Required fix shape:** (a) change default `permission_mode` to `workspace-write` (safe-by-default: filesystem write limited to cwd, network limited to LLM endpoints, no arbitrary command exec); (b) require explicit `--permission-mode danger-full-access` or `--dangerously-skip-permissions` to opt into full access; (c) add a `permissions` check to `doctor --output-format json` that emits `status:"warn"` when `permission_mode == "danger-full-access"` without explicit source (flag/env/config), with details like `mode:"danger-full-access", source:"default", message:"running with full access without explicit opt-in"`; (d) document the three modes and the default in USAGE.md with one-paragraph descriptions of what each mode allows. **Sibling typed-error bug:** `claw --permission-mode bogus-mode status --output-format json` returns `kind:"unknown"` instead of `kind:"invalid_permission_mode"` — same catch-all problem as #424, #426. **Sibling flag-name asymmetry:** `--dangerously-skip-permissions` works but `--skip-permissions` (Claude Code's flag) returns `kind:"cli_parse"` `unknown option`. Users migrating from Claude Code lose the short flag name. **Why this matters:** every other security-conscious CLI (Docker, kubectl, terraform) requires explicit opt-in for dangerous modes. Defaulting to `danger-full-access` is a footgun for first-time users who pipe `curl install.sh | sh` and immediately get a tool with full filesystem write and arbitrary command exec. The doctor surface is the only diagnostic users consult before trusting the tool, and it stays silent about the most permissive setting. Cross-references #50, #87, #91, #94, #97, #101, #106, #115, #123 (permission-audit sweep) — those all cover permission *rule* and *list* surfaces; #428 covers the *mode default* itself. Source: Jobdori live dogfood, `72048449`, 2026-05-11.
429. **No global `--cwd`/`-C`/`--directory` flag — `claw` cannot be invoked against an arbitrary working directory without first `cd`-ing into it; `--cwd` only exists as a subcommand option for `system-prompt`, and the `cli_parse` "Did you mean --acp?" suggestion is misleading (the `--acp` flag is unrelated to directory selection)** — dogfooded 2026-05-11 by Jobdori on `ec882f4c` in response to Clawhip pinpoint nudge at `1503267943285264394`. Reproduction: `claw --cwd /tmp/claw-dog-cwd status --output-format json``{"error":"unknown option: --cwd","hint":"Did you mean --acp?\nRun `claw --help` for usage.","kind":"cli_parse"}`. Same error for `--cwd <relative>`, `--cwd <nonexistent>`, `--cwd <file-not-dir>`, `--cwd ""`. Inspecting `claw --help`: `--cwd PATH` appears ONLY in the usage line `claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]` — it is not a global flag and is not accepted by `status`, `doctor`, `mcp list`, `init`, or any other subcommand. Users programmatically running claw against multiple workspaces must `cd` into each one before invoking, breaking the `subprocess.run(['claw', 'status', '--cwd', ws], cwd=other_dir)` pattern that every other major CLI (cargo `-C`, git `-C`, npm `--prefix`, gh `--repo` semantically, kubectl `--kubeconfig`+`--context`) supports. **Sibling misleading-suggestion bug:** the `cli_parse` error's `hint` field suggests `Did you mean --acp?` for `--cwd`. `--acp` is the alias for ACP/Zed editor integration (entirely unrelated to working directory). The Levenshtein-distance auto-complete is matching on first-character similarity without considering semantic relatedness. Users following the hint get a totally orthogonal feature. **Required fix shape:** (a) add a global `--cwd PATH` / `-C PATH` flag accepted before any subcommand, parsed in the global flag pre-pass; (b) validate the path exists and is a directory; emit `kind:"invalid_cwd"` with `path:` and `reason:` (`"not_found"`/`"not_a_directory"`/`"empty"`) when validation fails; (c) document the precedence: `--cwd` flag > `$PWD` > `env::current_dir()`; (d) fix the "Did you mean" hint algorithm to filter suggestions by semantic category (don't suggest `--acp` for `--cwd`; suggest `claw system-prompt --cwd PATH` if the user clearly wants `cwd` override but used the wrong scope); (e) regression test: `claw --cwd /tmp status --output-format json` from any `$PWD` returns `workspace.cwd:"/private/tmp"` (or `cwd:"/tmp"` after #421 fix). **Why this matters:** every claw automation orchestrator runs claw against multiple workspaces from a single parent process. Forcing `cd` before each invocation breaks parallelism (can't use shared cwd across concurrent invocations), breaks subprocess wrappers that want to pass cwd explicitly, and breaks `xargs`/`parallel`-style pipelines. Cross-references #421 (cwd canonicalization leak — fix should canonicalize but report user-input via `--cwd`). Source: Jobdori live dogfood, `ec882f4c`, 2026-05-11.
430. **`dump-manifests` is documented as "emit every skill/agent/tool manifest the resolver would load for the current cwd" but actually requires the upstream Claude Code TypeScript source files (`src/commands.ts`, `src/tools.ts`, `src/entrypoints/cli.tsx`) — the command is unusable for any user who installed claw without cloning the original Claude Code repo** — dogfooded 2026-05-11 by Jobdori on `075c2144` in response to Clawhip pinpoint nudge at `1503275502046023690`. Reproduction: `claw dump-manifests --output-format json` returns `{"error":"Manifest source files are missing.","hint":"repo root: /private/tmp/claw-dog-0530\n missing: src/commands.ts, src/tools.ts, src/entrypoints/cli.tsx\n Hint: set CLAUDE_CODE_UPSTREAM=/path/to/upstream or pass \`claw dump-manifests --manifests-dir /path/to/upstream\`.","kind":"missing_manifests"}`. The fresh-main worktree at `/private/tmp/claw-dog-0530` does not contain these TypeScript files because the Rust port doesn't include the upstream TS source. The `--help` text says the command works against "the current cwd" but in practice it requires `CLAUDE_CODE_UPSTREAM=` pointing at an unshipped TS source tree. **Three sibling problems compounded:** (a) **derivative-work disclosure leak**: the error message exposes that `claw-code` is a port of Claude Code (`CLAUDE_CODE_UPSTREAM` env var name) — even if true, surfacing this in a casual diagnostic message couples user-facing behavior to upstream provenance details. (b) **kind drift**: `claw dump-manifests --manifests-dir /tmp/nonexistent --output-format json` returns `kind:"unknown"`, while `claw dump-manifests` (no override) returns `kind:"missing_manifests"`. Same root cause (no usable upstream), two different `kind` discriminators — automation cannot switch on a single error type. (c) **export-positional-arg silently dropped**: probed in the same run — `claw export <bogus-positional>` ignores the path and returns `kind:"no_managed_sessions"` regardless of what positional arg was passed. The `--help` advertises `[PATH]` as the output-file destination but the path is discarded before validation, indistinguishable from invocation with no args. **Required fix shape:** (a) make `dump-manifests` emit the manifests claw-code itself ships with (Rust-resolver-discovered skills/agents/tools), independent of any upstream TS source — that matches the `--help` description; (b) if upstream-comparison is genuinely needed for parity work, move it to a separate command like `parity dump-upstream-manifests` and remove the upstream dependency from `dump-manifests`; (c) standardize on one error `kind` for the manifest-missing failure mode (`missing_manifests` is more descriptive than `unknown`); (d) `claw export <PATH>` must validate the path positional arg before the session-discovery check, so users see `kind:"invalid_output_path"` (or similar) when the path is malformed instead of always seeing `kind:"no_managed_sessions"`. **Why this matters:** `dump-manifests` is the inventory surface a downstream automation lane would call to learn what claw can do in the current workspace. If it's broken without upstream TS source, downstream lanes can't introspect — they have to fall back to `agents list`/`skills list`/`mcp list` separately and re-aggregate. Cross-references #422 (kind:unknown for unknown_subcommand), #423 (kind:unknown for missing_argument), #428 (kind:unknown for invalid_permission_mode) — `kind:"unknown"` keeps appearing as the catch-all for surfaces that should have typed kinds. Source: Jobdori live dogfood, `075c2144`, 2026-05-11.
431. **`skills uninstall <name>` requires Anthropic credentials despite being a local filesystem operation — `claw skills uninstall nonexistent-skill-xyz --output-format json` returns `kind:"missing_credentials"` instead of resolving locally that the skill doesn't exist** — dogfooded 2026-05-11 by Jobdori on `328fd114` in response to Clawhip pinpoint nudge at `1503275502046023690` (sibling probe to #430). Reproduction (no creds, isolated `CLAW_CONFIG_HOME`): `claw skills uninstall nonexistent-skill-xyz --output-format json` returns `{"error":"missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY...","kind":"missing_credentials"}`. Uninstalling a skill is a pure local filesystem operation: read the skills directory, find the named skill, remove its files. There is no semantic reason to require API credentials. Same class of bug as #357 (`session list` requires creds), #369 (`session help/fork` require creds), and #427 (`resume <bogus-id>` requires creds). **Three sibling findings in same probe:** (a) `claw skills install <bogus-name>` returns `{"error":"No such file or directory (os error 2)","kind":"unknown"}` — leaks raw OS error string with no hint about expected install source format (path vs name vs URL?), and the catch-all `kind:"unknown"` again instead of typed `kind:"skill_install_source_not_found"`. (b) `claw skills install` (no args) returns `action:"help"` with `unexpected:"install"` — but `install` IS a documented subcommand. The handler treats it as "unknown action" instead of "missing required argument". Should emit `kind:"missing_argument"` with `argument:"install_source"`. (c) `claw agents create my-agent` returns `action:"help"` with `unexpected:"create my-agent"` — there is no agent-creation surface at all. Users must hand-craft `.claw/agents/<name>.md` files with no scaffolding command, while `claw init` only creates the top-level `.claw/` skeleton. **Required fix shape:** (a) `skills uninstall <name>` must be local-first: enumerate the local skills dir, return `kind:"skill_not_found"` (with `skills_dir:` and `available_names:[]` fields) for missing, or remove the files and return `kind:"skills"` with `action:"uninstall", removed:<name>` for present skills; (b) `skills install <source>` must distinguish source forms (`path:`, `name:`, `url:`) and emit `kind:"invalid_install_source"` with the parsed-and-failed reason; (c) `skills install` (no args) emits `kind:"missing_argument"` with `argument:"install_source"`; (d) add `claw agents create <name>` (or `claw init agent <name>`) that scaffolds `.claw/agents/<name>.md` with a stub frontmatter; or document explicitly that agents are user-authored only. **Why this matters:** lifecycle commands (`uninstall`, `install`, `create`) are the primary surface for managing claw's extension surface area. If `uninstall` requires API creds, an offline user who fat-fingered an install can't undo it. If `install` returns a raw OS error, automation can't programmatically recover. If `agents create` doesn't exist, agent authoring is undocumented file-touching only. Cross-references #357, #369, #427 (auth-gate-on-local-ops cluster), and #422/#423/#428/#430 (`kind:"unknown"` catch-all cluster). Source: Jobdori live dogfood, `328fd114`, 2026-05-11.
432. **`--allowedTools` validator inconsistency: tool name list is half snake_case (`bash`, `read_file`, `write_file`, `edit_file`, `glob_search`, `grep_search`) and half PascalCase (`WebFetch`, `WebSearch`, `TodoWrite`, `Skill`, `Agent`, `Sleep`) with three UPPERCASE entries (`REPL`, `LSP`, `MCP`); accepts undocumented CamelCase aliases (`Read`, `Write`, `Edit`) and silently translates them to snake_case; argument parsing consumes the next positional when value is missing** — dogfooded 2026-05-11 by Jobdori on `fad53e2d` in response to Clawhip pinpoint nudge at `1503283046856655029`. Reproduction: `claw --allowedTools status --output-format json``{"error":"unsupported tool in --allowedTools: status (expected one of: bash, read_file, write_file, edit_file, glob_search, grep_search, WebFetch, WebSearch, TodoWrite, Skill, Agent, ToolSearch, NotebookEdit, Sleep, SendUserMessage, Config, EnterPlanMode, ExitPlanMode, StructuredOutput, REPL, PowerShell, AskUserQuestion, TaskCreate, RunTaskPacket, TaskGet, TaskList, TaskStop, TaskUpdate, TaskOutput, WorkerCreate, WorkerGet, WorkerObserve, WorkerResolveTrust, WorkerAwaitReady, WorkerSendPrompt, WorkerRestart, WorkerTerminate, WorkerObserveCompletion, TeamCreate, TeamDelete, CronCreate, CronDelete, CronList, LSP, ListMcpResources, ReadMcpResource, McpAuth, RemoteTrigger, MCP, TestingPermission)","kind":"unknown"}`. The `status` subcommand was consumed as the `--allowedTools` value because the flag parser doesn't distinguish missing-value from end-of-flag-args. The error reveals **the supported tool list mixes naming conventions inconsistently within a single error message**: snake_case (`bash`, `read_file`, `write_file`, `edit_file`, `glob_search`, `grep_search`), PascalCase (`WebFetch`, `WebSearch`, `TodoWrite`, `Skill`, `Agent`, `Sleep`, `Config`, `PowerShell`, `AskUserQuestion`, `TaskCreate`, `WorkerCreate`, `TeamCreate`, `CronCreate`), UPPERCASE (`REPL`, `LSP`, `MCP`), and CamelCase compounds (`McpAuth`, `RemoteTrigger`). **Hidden alias mapping**: `claw --allowedTools Read,Write,Edit status --output-format json` is accepted and returns `allowed_tools.entries:["edit_file","read_file","write_file"]` — proving the validator has an undocumented CamelCase→snake_case alias map (`Read``read_file`, `Write``write_file`, `Edit``edit_file`) that is not surfaced in the error message. Users who copy-paste tool names from Claude Code documentation work, users who copy from the validator error don't. **Sibling missing-value bug:** `claw --allowedTools status` with `status` as a positional subcommand is interpreted as `--allowedTools=status`, swallowing the subcommand. The flag parser must require a value for `--allowedTools` and emit `kind:"missing_argument"` when followed by a recognized subcommand or `--`-prefixed flag instead of silently treating the next arg as a tool name. **Sibling typed-kind bug:** both errors use `kind:"unknown"` instead of typed `kind:"invalid_tool_name"` / `kind:"missing_argument"` — the catch-all keeps appearing (#422/#423/#424/#428/#430/#431/#432). **Required fix shape:** (a) standardize the canonical tool-name registry on one casing convention (snake_case is most CLI-ergonomic) and update both the registry and all CamelCase aliases; (b) document and expose the alias map (`tool_aliases:{Read:"read_file",...}`) in `claw doctor`/`status` and in the validator error; (c) flag parser must require a value for `--allowedTools` and refuse to consume a recognized subcommand or `-`/`--`-prefixed token as the value, emit `kind:"missing_argument"` with `argument:"--allowedTools"`; (d) emit `kind:"invalid_tool_name"` with `tool_name:` and `available:[]` fields instead of `kind:"unknown"`; (e) regression test that `claw --allowedTools <subcommand>` rejects with `missing_argument`, and that the canonical name list in errors uses the same casing as the alias map. **Why this matters:** `--allowedTools` is the primary surface for restricting claw's tool surface area (security-relevant). Inconsistent naming between the validator error and the alias map means users following the error message guidance pick names that work in some places and fail in others. The missing-value bug silently swallows a subcommand, leading to confusing "unsupported tool: status" errors when the user actually wanted to run `claw status`. Cross-references #94/#97/#101/#106/#115/#123 (permission-rule audit), #428 (default permission_mode), #422/#423/#424/#428/#430/#431 (`kind:"unknown"` catch-all). Source: Jobdori live dogfood, `fad53e2d`, 2026-05-11.
433. **Repeated `--output-format` flag silently takes the last value without warning — `claw --output-format json --output-format text status` produces text output, no signal that the prior `json` was overridden; sibling: `--output-format` value is case-sensitive (`JSON` rejected as `kind:"unknown"`); sibling: no `CLAW_OUTPUT_FORMAT` env var for default format override** — dogfooded 2026-05-11 by Jobdori on `ce39d5c5` in response to Clawhip pinpoint nudge at `1503290592556220488`. Reproduction: `claw --output-format json --output-format text status` returns the text-format `Status\n Model claude-opus-4-6...` table — the first `--output-format json` was silently overridden. No warning, no `format_overridden:true` field, no stderr message. Scripts that compose flag arrays from multiple sources (`flags=("${BASE_FLAGS[@]}" --output-format json)` while `BASE_FLAGS` already contains `--output-format text`) silently get the wrong format. **Three sibling findings in same probe:** (a) **case-sensitivity drift**: `claw --output-format JSON status` returns `{"error":"unsupported value for --output-format: JSON (expected text or json)","kind":"unknown"}` — error message tells user to use lowercase `json` but doesn't accept the uppercase form that users often type from muscle memory. Most CLI flag-value validators (cargo, kubectl, gh) are case-insensitive for enum values or accept both forms with normalization. (b) **`kind:"unknown"` for invalid format value**: same catch-all bucket bug as #422/#423/#424/#428/#430/#431/#432 — should be `kind:"invalid_output_format"` with `value:` and `expected:["text","json"]` fields. (c) **no env-var default for output format**: `CLAW_OUTPUT_FORMAT=json claw status` silently ignored — no env override for the global default, forcing scripts to repeat `--output-format json` on every invocation. Other major CLIs honor `KUBECTL_OUTPUT=`, `AWS_DEFAULT_OUTPUT=`, `GH_NO_PROMPT=` etc. (d) **silently-ignored env vars `CLAW_LOG`/`RUST_LOG`**: no env-based log level control surfaced in `claw doctor` — debug logging requires undocumented `RUST_LOG=` (Rust convention) but `claw --help` doesn't mention either. **Required fix shape:** (a) repeated `--output-format` (or any flag that takes a value, not a count flag) emits a warning to stderr (`warning: --output-format specified multiple times; using last value 'text'`) and adds a `format_source:"flag", format_overridden:[]` field to the JSON envelope; (b) accept case-insensitive enum values for `--output-format` (`JSON`, `Json`, `json` all work), document the canonical lowercase form in `--help`; (c) emit `kind:"invalid_output_format"` (not `kind:"unknown"`) when value is invalid; (d) accept `CLAW_OUTPUT_FORMAT` env var as the default for `--output-format`, with flag-overrides-env precedence documented; (e) document `RUST_LOG` / `CLAW_LOG` in `--help` or doctor output as the log-level env vars; (f) regression test: repeated flag emits stderr warning + JSON metadata field; case-insensitive enum accepts all three casings; env-var default is honored when flag is absent. **Why this matters:** scripts that compose flag arrays from multiple sources (CI envs + per-invocation flags) silently get the wrong output format. Case-sensitive enum values trip up users typing from muscle memory. Missing env-var defaults force per-invocation flag repetition. Cross-references #422/#423/#424/#428/#430/#431/#432 (`kind:"unknown"` catch-all cluster). Source: Jobdori live dogfood, `ce39d5c5`, 2026-05-11.
434. **POSIX `--` end-of-flags separator is not recognized — `claw -- "-prompt-with-dash"` returns `{"error":"unknown option: --","hint":"Did you mean -V?","kind":"cli_parse"}` instead of treating subsequent args as positional; shorthand prompt mode cannot accept dash-prefixed prompts at all** — dogfooded 2026-05-11 by Jobdori on `0e5f6958` in response to Clawhip pinpoint nudge at `1503298142286905484`. Reproduction: `claw -- "-prompt-with-dash" --output-format json` returns `{"error":"unknown option: --","hint":"Did you mean -V?\nRun \`claw --help\` for usage.","kind":"cli_parse"}`. The POSIX/GNU CLI convention — universally honored by cargo, git, npm, gh, kubectl, grep, ls, find, etc. — is that `--` terminates flag parsing and treats everything after it as positional arguments. claw rejects `--` itself as an unknown flag. **Sibling misleading-suggestion bug (recurring from #429):** the `cli_parse` hint suggests `Did you mean -V?` for `--`. `-V` is the version flag; `--` is the end-of-flags separator. They have no semantic relationship; the auto-complete is matching on prefix-character similarity only. **Sibling shorthand-prompt limitation:** `claw "-just a prompt" --output-format json` returns `{"error":"unknown option: -just a prompt","kind":"cli_parse"}` and `claw "--bogus-flag-like" --output-format json` returns the same. The shorthand non-interactive prompt mode (documented as `claw [--model MODEL] [--output-format text|json] TEXT`) cannot accept any TEXT that starts with `-` or `--`, even when the entire string is shell-quoted as a single token. Users must use the explicit `prompt` verb (`claw prompt "-prompt-with-dash"` works) to escape this, but the explicit verb is documented as alternative not required. **Required fix shape:** (a) accept POSIX `--` as the end-of-flags marker globally — every arg after `--` is positional; (b) shorthand prompt mode must distinguish "this looks like a flag" from "this is a quoted positional that happens to start with `-`" by looking at whether the token matches any registered flag name (`-h`, `-V`, `--help`, `--version`, etc.) — strings that don't match any flag should be treated as prompt text; (c) fix the "Did you mean" hint algorithm to filter by semantic category (don't suggest `-V` for `--`, suggest "use \`--\` to terminate flag parsing" if the user types just `--`); (d) regression test: `claw -- "-foo"` reaches the runtime with prompt=`-foo`; `claw "-not-a-flag"` is treated as shorthand prompt when no registered flag matches; canonical `--` is recognized. **Why this matters:** POSIX `--` is the universal mechanism for passing arbitrary text (filenames starting with `-`, prompts containing flag-like syntax, log lines, etc.) to a CLI. Failing on `--` makes claw fundamentally unergonomic in shell pipelines (`echo "-q for quiet" | xargs claw` fails). The shorthand-prompt limitation forces users to remember the `prompt` verb specifically when their prompt happens to start with `-`. Cross-references #422 (unknown subcommand fallthrough), #423 (stdin not consumed by prompt), #429 ("Did you mean --acp" misleading suggestion). Source: Jobdori live dogfood, `0e5f6958`, 2026-05-11.
435. **`claw --resume latest` on a fresh workspace exit code is 0 in text mode but 1 in JSON mode (text mode lies about success); sibling: failed `--resume` creates the `.claw/sessions/<fingerprint>/` directory tree as a filesystem side effect of the failure** — dogfooded 2026-05-11 by Jobdori on `e29010ed` in response to Clawhip pinpoint nudge at `1503305692566655096`. Reproduction (fresh empty dir, no `.claw/`, no sessions): `claw --resume latest` (text mode) prints `failed to restore session: no managed sessions found in .claw/sessions/0ead448127a2de44/` and exits **0**. Same invocation with `--output-format json` correctly exits **1** with `kind:"session_load_failed"`. Exit-code parity broken on the same input depending on format flag. **Sibling filesystem-side-effect bug:** after the failed `--resume latest` on a fresh empty workspace, the directory `.claw/sessions/0ead448127a2de44/` (the workspace-fingerprint partition) is created on disk despite the operation failing. The user did not opt into creating workspace metadata — they asked to resume an existing session, the resume failed, and now there's a partition directory hanging around. The fingerprint directory ought to be created lazily on first successful session save, not as a side effect of every resume attempt. **Three sibling findings in the same probe:** (a) **`claw --compact` alone (no other args) drops into the interactive REPL with the ANSI welcome banner** — `--compact` is documented as a modifier that strips tool call details in text mode for piping (`--compact ... useful for piping`), not as a verb that activates the REPL. Running `claw --compact` with no positional should be a no-op or an error explaining the flag needs a subcommand or prompt; entering the REPL is the wrong default. (b) **`claw --compact "hello"` (shorthand prompt) returns `{"error":"unknown subcommand: hello.","hint":"Did you mean help","kind":"unknown"}``--compact` disables shorthand prompt mode entirely**, treating the positional as a subcommand instead of as prompt text. Users must use the explicit `prompt` verb (`claw --compact prompt "hello"`) which contradicts the `claw [flags] TEXT` usage line in `--help`. (c) `kind:"unknown"` again for the unknown-subcommand error in --compact path — same catch-all bucket bug appearing for the 11th time across pinpoints. **Required fix shape:** (a) exit code 1 for all `failed_to_restore` / `session_load_failed` text-mode failures; text mode should print to stderr and exit non-zero, not print to stdout and exit 0; (b) defer `.claw/sessions/<fingerprint>/` creation to first successful save; failed `--resume` must not leave filesystem droppings; (c) `claw --compact` alone (no positional, no subcommand, stdin is TTY) should emit `kind:"missing_argument"` with `argument:"prompt or subcommand"` rather than activating the REPL; (d) `--compact` must be transparent to shorthand prompt mode parsing — `claw --compact "hello"` is equivalent to `claw --compact prompt "hello"`, both should reach the prompt path; (e) emit typed `kind:"unknown_subcommand"` not `kind:"unknown"` for fallthrough cases. **Why this matters:** scripts that gate on `$?` after `claw --resume latest` see success on text mode and failure on JSON mode — the same operation, two outcomes. The filesystem side effect pollutes a user's worktree with workspace partitions they didn't ask for, and CI pipelines that snapshot `.claw/` size silently grow on every failed `--resume`. Cross-references #422 (exit-code parity across error envelopes), #423 (`kind:"unknown"` for `missing_argument`), #434 (shorthand prompt limitations). Source: Jobdori live dogfood, `e29010ed`, 2026-05-11.
436. **`claw init` shipped `.claw.json` template explicitly sets `permissions.defaultMode:"dontAsk"` — every user who runs `claw init` gets a config file that disables permission prompts by default; sibling: `init` creates an empty `.claw/` directory with no settings.json template inside, and when `.claw/` already exists it skips the whole artifact (no settings template materialized)** — dogfooded 2026-05-11 by Jobdori on `b8f989b6` in response to Clawhip pinpoint nudge at `1503313241751949335`. Reproduction: `mkdir /tmp/probe && cd /tmp/probe && claw init --output-format json` returns `artifacts:[{name:".claw/",status:"created"},{name:".claw.json",status:"created"},...]`. Inspecting the created `.claw.json`: `{"permissions":{"defaultMode":"dontAsk"}}`. This is the polar opposite of safe-by-default: every user who follows the documented onboarding flow (`claw init` after `curl install.sh`) ships their workspace with permission prompts disabled. Compounds with **#428** (default runtime permission_mode is `danger-full-access`) — between the runtime default and the init template, a fresh claw setup has zero user-facing safety friction. **Sibling: `.claw/` artifact is an empty directory.** After `claw init`, `find .claw -type f` returns nothing. No `settings.json`, no template, no scaffolding — just `mkdir .claw`. The `--help` description implies init produces a usable workspace, but `.claw/settings.json` (the project-scope counterpart of `~/.claw/settings.json`) is never templated. **Sibling: `.claw/` skip-on-exists drops the entire artifact.** If `.claw/` already exists (e.g., from a partial setup, a `--resume` failure side effect per #435, or manual creation), `claw init` returns `.claw/: skipped` and does not materialize any expected sub-content. The other artifacts (`.claw.json`, `.gitignore`, `CLAUDE.md`) are still created, but a future `claw skills install` or `claw plugins enable` may expect `.claw/` to contain template files that are now missing. **Required fix shape:** (a) the shipped `.claw.json` template must default to `permissions.defaultMode:"acceptEdits"` or `"plan"` (safe-by-default modes per #428 spec) — `"dontAsk"` requires explicit opt-in; (b) `claw init` must materialize `.claw/settings.json` with documented schema defaults inside `.claw/` so the directory is useful on its own; (c) when `.claw/` already exists, `init` must report `partial` status (not `skipped`) and still try to create missing sub-files like `.claw/settings.json` without overwriting existing files; (d) emit per-sub-file artifact entries for `.claw/settings.json` and `.claw/sessions/` (skipped status if absent, deferred-to-first-save acceptable) so automation knows what's present; (e) regression test: `claw init` produces a `.claw.json` whose `permissions.defaultMode` is NOT `dontAsk`; `.claw/` contains at least one templated file. **Why this matters:** init is the primary onboarding surface. Every first-time user piping `curl install.sh | sh && claw init` gets a workspace pre-configured to skip permission prompts — and that workspace gets committed to the user's repo via the `init`-added entry. The `.claw/` empty-directory bug means feature discovery (skills, plugins) lacks the scaffolding it implies. Cross-references #428 (runtime default permission_mode), #50/#87/#91/#94/#97/#101/#106/#115/#123 (permission-rule audit), #435 (filesystem side effects on failed resume). Source: Jobdori live dogfood, `b8f989b6`, 2026-05-11.
437. **`version --output-format json` omits build provenance fields — no `is_dirty`, `branch`, `commit_date`, `commit_timestamp`, `rustc_version`; `git_sha` is truncated to 7 chars instead of full 40-char hash; sibling: `executable_path` leaks the build host's path (`/tmp/claw-dog-0530/...`) into runtime output** — dogfooded 2026-05-11 by Jobdori on `8cf628a5` in response to Clawhip pinpoint nudge at `1503320791582900344`. Reproduction: `claw version --output-format json` returns `{"build_date":"2026-05-11","executable_path":"/tmp/claw-dog-0530/rust/target/release/claw","git_sha":"b98b9a7","kind":"version","message":"Claw Code\n Version 0.1.0\n Git SHA b98b9a7\n Target aarch64-apple-darwin\n Build date 2026-05-11","target":"aarch64-apple-darwin","version":"0.1.0"}`. Critical provenance fields missing: (a) **`is_dirty`** — was the working tree clean at build time? Automation that pins on build provenance cannot tell if the binary was built from a clean commit or includes uncommitted changes; (b) **`branch`** — was this built from `main`, `dev/rust`, a release tag, or a feature branch? The `git_sha` alone doesn't reveal the integration point; (c) **`commit_date` / `commit_timestamp`** — only `build_date` (when the binary was compiled) is exposed; the commit itself might be days/weeks older if the build happened later. Reproducibility audits need both; (d) **`rustc_version`** — what Rust compiler version produced this binary? Critical for security advisories (e.g., known regressions in specific rustc versions); (e) **`git_sha` truncated to 7 chars** ("b98b9a7" instead of full "b98b9a71..."): 7-char shas have known collision rates in large repos and prevent unambiguous git rev-parse round-trip. **Sibling: `executable_path` leaks build-host path.** The `executable_path` field returns `/tmp/claw-dog-0530/rust/target/release/claw` — the directory where the binary was compiled, embedded into the binary metadata. For a binary copied/installed/symlinked to a different location, this field still reports the build path, not the actual invocation path. Either the field should reflect the runtime path via `std::env::current_exe()` at runtime (not compile-time), or it should be dropped to avoid leaking compile-host filesystem layout. **Sibling: prose `message` field duplicates structured data.** The `message` field still contains the entire text-mode prose version block (`"Claw Code\n Version 0.1.0\n Git SHA b98b9a7\n..."`) — every field present as structured JSON (`version`, `git_sha`, `target`, `build_date`) is also embedded in the prose. Same issue as #391 (`version json includes prose message field`) which was closed as "fixed" — the prose remains. **Required fix shape:** (a) add `is_dirty:bool`, `branch:string|null`, `commit_date:string` (ISO-8601), `commit_timestamp:int` (Unix epoch), `rustc_version:string` to the JSON envelope; (b) preserve full 40-char `git_sha` and add `git_sha_short:string` as a derived field if 7-char form is needed for UX; (c) `executable_path` should be `std::env::current_exe()` at runtime, not the compile-time path; (d) drop the prose `message` field from JSON or rename it `human_readable:string` and make it explicitly secondary to the structured fields; (e) re-verify #391 closure — the prose `message` is still present, the fix didn't fully land. **Why this matters:** version surface is the canonical provenance probe for security audits, build reproducibility, and bug-report metadata. Missing `is_dirty` means automated triage cannot distinguish "issue against a clean main commit" from "issue against a developer's uncommitted hack". Truncated `git_sha` blocks unambiguous git lookup. Leaked `executable_path` exposes build-host layout. Cross-references #391 (version prose duplication — apparently not fully fixed), #334 (version json omits build_date — fixed, but partial scope), #100 (commit identity audit). Source: Jobdori live dogfood, `8cf628a5`, 2026-05-11.
438. **Memory file discovery only recognizes `CLAUDE.md` — `AGENTS.md` (industry convention used by OpenCode/Codex/Aider/Cursor) and `CLAW.md` (project's own brand name) are silently ignored despite being present in the workspace** — dogfooded 2026-05-11 by Jobdori on `d3a982dd` in response to Clawhip pinpoint nudge at `1503328341422244012`. Reproduction (fresh empty dir, isolated `CLAW_CONFIG_HOME`): create three files in cwd — `CLAUDE.md` (marker `MARKER-FROM-CLAUDE-MD`), `AGENTS.md` (marker `MARKER-FROM-AGENTS-MD`), `CLAW.md` (marker `MARKER-FROM-CLAW-MD`). Run `claw status --output-format json``workspace.memory_file_count: 1`. Run `claw system-prompt --output-format json` and search the `message` field for each marker: only `MARKER-FROM-CLAUDE-MD` is found; `MARKER-FROM-AGENTS-MD` and `MARKER-FROM-CLAW-MD` are absent. `claw-code` exclusively recognizes the Claude-branded filename inherited from upstream Claude Code; the project's own `CLAW.md` brand name and the cross-tool industry convention `AGENTS.md` are both silently dropped. **Three sibling implications:** (a) **brand-consistency gap**: a project rebranded from Claude Code to Claw Code that introduces `CLAUDE.md` as its only memory file is internally inconsistent. Users naturally expect `claw <subcommand>` to read `CLAW.md`. (b) **industry-convention gap**: `AGENTS.md` is the convergent convention for OpenCode (oh-my-opencode/sisyphus), OpenAI Codex CLI, Aider, Cursor, Continue.dev, and most ACP harnesses. Users with mixed-tool workflows maintain a shared `AGENTS.md` and expect every AI coding tool to honor it. (c) **silent failure mode**: there is no warning when `AGENTS.md` or `CLAW.md` exist but are not loaded. Users who copy-paste `AGENTS.md` from another tool's docs see `memory_file_count` stay at 0 or 1 and have to guess why their instructions aren't applied. **Required fix shape:** (a) discover and load **`CLAUDE.md`, `CLAW.md`, `AGENTS.md`** in that priority order (existing config-precedence pattern); (b) all three contribute to `memory_file_count` with `memory_files:[{path, source:"claude_md"|"claw_md"|"agents_md", chars}]` array exposed in `status --output-format json`; (c) when multiple files exist, merge or document the precedence: project-specific `CLAUDE.md`/`CLAW.md` overrides industry-shared `AGENTS.md`; (d) `claw doctor --output-format json` adds a `memory` check that warns when `AGENTS.md` exists but is not the loaded variant (alerting users that they may be relying on the wrong file); (e) regression test: workspace with all three files results in `memory_file_count >= 1` and the system prompt contains markers from at least the highest-precedence file. **Why this matters:** `AGENTS.md` is the lingua-franca instruction file for cross-tool AI coding workflows. A team using OpenCode for one project and Claw Code for another keeps their conventions in a shared `AGENTS.md`. Forcing them to also maintain a `CLAUDE.md` for claw-code (with identical content) is friction that breaks the value proposition of a fork. Cross-references #438 itself (the multi-file convention), and AGENTS.md ecosystem references in oh-my-opencode/sisyphus docs. Source: Jobdori live dogfood, `d3a982dd`, 2026-05-11.
439. **Memory file discovery walks ALL ancestor directories up to `$HOME` boundary, silently loading any `CLAUDE.md` it finds — `/tmp/CLAUDE.md` left from a previous test silently bleeds into every project under `/tmp/*/`; no `--no-parent-memory` flag, no `.no-claude-md-boundary` marker file to limit discovery scope** — dogfooded 2026-05-11 by Jobdori on `f4a96740` in response to Clawhip pinpoint nudge at `1503335892461293675`. Reproduction: create three nested `CLAUDE.md` files with unique markers — `/tmp/claw-nested-probe/CLAUDE.md` (`PARENT_CLAUDE`), `subproj/CLAUDE.md` (`CHILD_CLAUDE`), `subproj/deep/CLAUDE.md` (`DEEP_CLAUDE`). Run `claw system-prompt --output-format json` from `subproj/deep/nest/` (note: `nest` has no `CLAUDE.md`). The `message` field contains **all three markers** (PARENT + CHILD + DEEP) and `status --output-format json` reports `memory_file_count: 3`. Boundary tests: (a) `$HOME/CLAUDE.md` is NOT picked up from `/tmp/no-claude-dir` (discovery stops at `$HOME` boundary, good); (b) From `/tmp/deep` (no nested CLAUDE.md), `/tmp/CLAUDE.md` IS picked up (count: 1); (c) git-root is NOT a discovery boundary — running from a git subdir still walks above the git root. **Ambient-context-bleed footgun:** any stale `/tmp/CLAUDE.md` (or `/home/<user>/projects/CLAUDE.md`, or any ancestor-path CLAUDE.md left over from a previous experiment, copy-paste, or AI-generated example) silently bleeds into every workspace nested below it. The user has no signal in `status --output-format json` indicating which ancestor file is contributing — only the aggregate `memory_file_count`. **Three required fixes:** (a) **expose discovery list**: `status --output-format json` and `system-prompt --output-format json` must include `memory_files:[{path, source:"workspace"|"ancestor"|"parent_dir"|"home", chars, contributes:bool}]` so users can see what's leaking in; (b) **add `--no-parent-memory` flag** to limit discovery to cwd only (no ancestor walk), or add a boundary marker (`.claude-no-walk`, `.claw-root`, or honor `.git` as the boundary by default — most users expect repo-root scope); (c) **`doctor` warns** when ancestor `CLAUDE.md` files are loaded from outside the current git repo (suggests they may be unintentional). **Sibling discovery scope question:** discovery walks up to `$HOME` — but for a user with a project at `/Users/foo/work/proj`, that's `/Users/foo/work/CLAUDE.md` + `/Users/foo/CLAUDE.md` (if it exists) both load. The home boundary is exclusive, but the entire `/Users/foo` tree under home is in scope. **Why this matters:** test workspaces, scratch dirs, AI-generated example projects, and shared `/tmp` workdirs are full of stale `CLAUDE.md` files. The current discovery rule means every claw invocation can silently inherit context from arbitrary ancestor paths. Cross-references #438 (memory discovery only finds CLAUDE.md, not AGENTS.md or CLAW.md), #421 (cwd canonicalization leak — the canonicalized form determines which ancestor walk path is used). Source: Jobdori live dogfood, `f4a96740`, 2026-05-11.
440. **One invalid `mcpServers` entry blocks ALL OTHER valid MCP servers from loading — `mcp list --output-format json` returns `configured_servers: 0, servers: []` when even one server has a missing/invalid `command` field, despite other servers in the same config being well-formed; sibling: config parser halts on first invalid entry, never reports the remaining invalid entries** — dogfooded 2026-05-11 by Jobdori on `bd126905` in response to Clawhip pinpoint nudge at `1503343442904879156`. Reproduction: write `.claw.json` containing six `mcpServers` entries — one valid (`valid-server: {command:"/bin/echo", args:["hello"]}`) and five with progressive defects (missing-command, empty-command, null-command, wrong-type-command, extra-unknown-field). Run `claw mcp list --output-format json``{"action":"list","config_load_error":"/private/tmp/claw-mcp-probe/.claw.json: mcpServers.missing-command-server: missing string field command","configured_servers":0,"kind":"mcp","servers":[],"status":"degraded"}`. The error mentions only `missing-command-server` (the first invalid entry in JSON-object iteration order); the other four invalid entries are never surfaced. The valid `valid-server` entry is silently dropped because the parser bails on the first error. `status --output-format json` correctly propagates the same `config_load_error` and sets `status:"degraded"`, but no field tells automation which servers are valid vs broken — `servers:[]` is the only signal. **Three problems compounded:** (a) **all-or-nothing loading**: ROADMAP product principle #5 says "partial success is first-class," but mcp config loading is binary. One bad server kills the entire MCP plane; (b) **first-error-only reporting**: a `.claw.json` with five invalid entries surfaces only one error message — the user fixes that one and runs again, gets the next error, and so on. Five iterations needed to discover all errors; (c) **no per-server status**: even with the partial-success fix, the JSON envelope needs `servers:[{name, valid:bool, error?, command?, args?}]` so automation can see which entries are usable. **Required fix shape:** (a) the MCP config parser must collect ALL invalid entries into an `invalid_servers:[{name, error_field, reason}]` array and load all valid ones into `servers:[]`; do not abort on first error; (b) `configured_servers` reflects the count of *valid* loaded servers (not zero) when there are valid entries alongside invalid ones; (c) expose `total_configured:int` (count of entries in source `.claw.json`) AND `valid_count:int` (loaded), AND `invalid_count:int` (rejected) — three distinct counts; (d) `doctor --output-format json` adds an `mcp_validation` check that lists each invalid entry with its error message; (e) regression test: `.claw.json` with one valid + one invalid entry results in `configured_servers: 1, invalid_servers: [{name:"...", reason:"..."}]`. **Why this matters:** users iterate on MCP server lists during onboarding — one typo kills the entire plane, including servers they got working previously. The first-error-only reporting forces N iterations through N invalid entries instead of a single fix-everything-at-once pass. Cross-references #407 (config files no load_error per-file), #415 (config section merged_keys count only), #416 (plugins list prose), #428 (default permission mode), and Product Principle #5. Source: Jobdori live dogfood, `bd126905`, 2026-05-11.
441. **`hooks` config schema diverges from Claude Code documented format — claw-code expects `{"hooks":{"PreToolUse":["command-string"]}}` (array of command strings) while Claude Code documentation specifies `{"hooks":{"PreToolUse":[{"matcher":"Read","hooks":[{"type":"command","command":"..."}]}]}}` (structured matcher objects); users copy-pasting from Claude Code docs see `field "hooks.PreToolUse" must be an array of strings`** — dogfooded 2026-05-11 by Jobdori on `86ff83c2` in response to Clawhip pinpoint nudge at `1503350990680887418`. Reproduction: write `.claw.json` with the Claude-Code-documented hook format `{"hooks":{"PreToolUse":[{"matcher":"Read","hooks":[{"type":"command","command":"/bin/echo pretool"}]}]}}`. Run `claw status --output-format json``config_load_error: "/private/tmp/claw-hook-probe/.claw.json: field \"hooks.PreToolUse\" must be an array of strings, got an array (line 3)"`, `status: "degraded"`. The error wording ("must be an array of strings, got an array") is confusingly tautological — the user did provide an array; the parser objects that the array contains objects instead of strings. Replacing with the claw-code-actual format `{"hooks":{"PreToolUse":["/bin/echo pretool"]}}` succeeds: `config_load_error: null, status: "ok"`. The two formats are fundamentally incompatible: claw-code drops the `matcher` field (no tool-specific filtering at the config layer), drops the `type:"command"` discriminator (no future expansion to other hook types), and treats each entry as a bare command string instead of a structured hook spec. **Sibling: PR #3000 (justcode049) was attempting to tolerate object-style hook entries** — that PR's title `fix: tolerate object-style hook entries in config parser` confirms this is a known user complaint, but the PR is still conflicting and unmerged. **Three sibling findings in same probe:** (a) **unknown event names reject entire hooks config**: `.claw.json` with `hooks.InvalidEvent` (not a real event name like `PreToolUse`/`PostToolUse`/`Stop`/`Notification`) triggers `config_load_error: "unknown key \"hooks.InvalidEvent\""` and rejects ALL hooks in the same file, even valid ones — same "one bad apple kills all" pattern as #440 (MCP servers). (b) **`kind:"unknown"` for the validation error** — should be `kind:"invalid_hooks_config"` or `kind:"unknown_hook_event"` (catch-all cluster #422/#423/#424/#428/#430/#431/#432/#433/#435 — 13th occurrence). (c) **first-error-only halting**: a `.claw.json` with `hooks.Stop:"not-an-array"` (type mismatch) AND `hooks.InvalidEvent` (unknown name) AND `hooks.Notification:[{}]` (empty entry) surfaces only the FIRST error in iteration order — user must fix one at a time across 3 iterations. **Required fix shape:** (a) **adopt Claude Code's structured hook format as the canonical**: support `{matcher, hooks:[{type, command}]}` natively, with `matcher` for tool-filtering, `type` for hook-type discriminator (future-proof for `inline`/`webhook`/etc beyond just `command`); (b) **keep backward compat for bare command strings**: legacy `["command-string"]` arrays still load, but emit a deprecation warning suggesting migration to the structured form; (c) **partial-success loading**: invalid hook entries surface in `invalid_hooks:[{event, index, reason}]` while valid ones load — same fix as #440 for MCP; (d) **typed `kind:"invalid_hooks_config"` envelope** instead of `kind:"unknown"`; (e) **rebase and merge PR #3000** which addresses this directly; (f) regression test: Claude-Code-documented hook config loads without error on claw-code. **Why this matters:** users migrating from Claude Code to Claw Code hit this on their first `.claw.json` write. The error message ("array of strings, got an array") is unhelpful; the documentation doesn't surface the schema divergence; and Claude Code's structured format is strictly more expressive (matchers, types) than claw-code's bare-string format. Cross-references #407 (config files no load_error), #410 (list-envelope schema drift), #428 (default permission mode), #440 (one invalid MCP entry blocks all), PR #3000 (justcode049's pending fix). Source: Jobdori live dogfood, `86ff83c2`, 2026-05-11.
442. **`agents` discovery requires TOML format (`.toml` files) while Claude Code documents agents as Markdown with YAML frontmatter (`.md`) — claw-code silently ignores `.md` files in `.claw/agents/` without any warning; the help text lists `.claw/agents, ~/.claw/agents, $CLAW_CONFIG_HOME/agents` as sources but does not mention the `.toml` file format requirement** — dogfooded 2026-05-11 by Jobdori on `8499599b` in response to Clawhip pinpoint nudge at `1503358540230692876`. Reproduction: write `.claw/agents/valid-agent.md` with Claude-Code-format YAML frontmatter `---\nname: valid-agent\ndescription: A simple test agent\ntools: [bash, read_file]\n---\nYou are a helpful agent.` Run `claw agents list --output-format json``{"agents":[], "count":0, "summary":{"active":0,"shadowed":0,"total":0}}`. The valid `.md` agent is silently dropped. Replace with `.claw/agents/toml-agent.toml` containing TOML format `name = "toml-agent"\ndescription = "..."` → loads correctly with `count:1`. Source code confirms (`rust/crates/commands/src/lib.rs:3378`): `if entry.path().extension().is_none_or(|ext| ext != "toml") { continue; }` — only `.toml` extension is recognized, all others (including `.md`) skipped without warning. The help text `claw agents --help` documents the source paths but **omits the file-format requirement**. **Five sibling problems compounded:** (a) **schema divergence from Claude Code**: Claude Code's `agents` are documented as `.md` files with YAML frontmatter (matching the `CLAUDE.md`/`.claude/agents/` convention upstream). claw-code chose TOML for no documented reason. Users migrating from Claude Code or copy-pasting community agent definitions hit silent failure. (b) **silent file drop**: invalid agent files (wrong extension, broken frontmatter, missing required fields, file-name vs frontmatter-name mismatch) are all silently ignored with `count:0`. No `invalid_agents:[]` array, no warning, no `kind:"agent_load_failed"` envelope. Same all-or-nothing pattern as #440 (MCP servers) and #441 (hooks). (c) **no documentation of the schema**: `claw agents --help --output-format json` (per #427, this hits the auth gate; without auth it doesn't return the schema either). The required TOML fields (`name`, `description`, `model`, `model_reasoning_effort` per source code) aren't documented in any user-facing surface. (d) **missing `.claude/agents/` discovery**: many existing projects have `.claude/agents/` from Claude Code installs. claw-code only looks at `.claw/agents/` — users have to copy/move their existing agents. (e) **no agent-scaffolding command**: cross-reference #431 — there's no `claw agents create <name>` to generate a valid `.toml` skeleton; users must hand-craft. **Required fix shape:** (a) accept BOTH `.md` (with YAML frontmatter) AND `.toml` formats in `.claw/agents/`; prefer YAML frontmatter for Claude Code parity, keep TOML for back-compat; (b) include `.claude/agents/` in the discovery sources alongside `.claw/agents/` with documented precedence; (c) expose `invalid_agents:[{path, reason}]` array in `agents list --output-format json` so users can see what was skipped and why; (d) document the agent schema (required + optional fields) in `claw agents --help` and in USAGE.md; (e) add `claw agents create <name>` scaffolding command per #431; (f) regression test: `.claw/agents/foo.md` with YAML frontmatter loads correctly. **Why this matters:** agents are the primary extension surface for custom workflows. A silent-drop on the wrong file format breaks the discoverability promise of CLI agents. Claude Code's `.md`-with-YAML convention is the lingua franca across AI coding tools; deviating to TOML breaks copy-paste compatibility. Cross-references #430 (dump-manifests needs upstream), #431 (skills/agents lifecycle), #440 (MCP all-or-nothing), #441 (hooks all-or-nothing), #438 (memory file discovery only CLAUDE.md). Source: Jobdori live dogfood, `8499599b`, 2026-05-11.
443. **`claw acp serve` exits 0 with `status:"discoverability_only", supported:false` instead of failing — automation pipelines see "success" from a command that explicitly says "not implemented"; ROADMAP #413's internal-tracking leak (`discoverability_tracking:"ROADMAP #64a"`, `tracking:"ROADMAP #76"`) still present despite being filed 2026-04-30** — dogfooded 2026-05-11 by Jobdori on `19aaf9d0` in response to Clawhip pinpoint nudge at `1503366101533200435`. Reproduction: `claw acp serve --output-format json` returns exit code **0** with envelope `{aliases:["acp","--acp","-acp"], discoverability_tracking:"ROADMAP #64a", kind:"acp", launch_command:null, message:"ACP/Zed editor integration is not implemented in claw-code yet. \`claw acp serve\` is only a discoverability alias today; it does not launch a daemon or Zed-specific protocol endpoint. Use the normal terminal surfaces for now and track ROADMAP #76 for real ACP support.", recommended_workflows:["claw prompt TEXT","claw","claw doctor"], serve_alias_only:true, status:"discoverability_only", supported:false, tracking:"ROADMAP #76"}`. The exit code is 0 (success) but the command explicitly states it is not implemented. Pipeline like `claw acp serve && zed --connect localhost:12345` will proceed to the zed connect step despite `acp serve` being a no-op. The only signal of no-op is `supported:false` in the JSON body — easy to miss for automation gating on `$?`. **ROADMAP #413 reproduction confirmed unfixed:** #413 (filed 2026-04-30) called out `discoverability_tracking:"ROADMAP #64a"` and `tracking:"ROADMAP #76"` as internal ticket references leaked into public JSON. **11 days later, both fields are still present in the envelope.** The fix was prescribed but never landed. Also `recommended_workflows:["claw prompt TEXT","claw","claw doctor"]` is internal scaffolding (curated suggestion list) exposed as a top-level public field — not normally part of an "ACP status" public contract. **Sibling unknown-subcommand bug:** `claw acp status --output-format json` (a reasonable next-thing-to-try) returns `{"error":"unsupported ACP invocation. Use \`claw acp\`, \`claw acp serve\`, \`claw --acp\`, or \`claw -acp\`.","kind":"unknown"}` exit 0 — the `kind:"unknown"` catch-all yet again (#422/#423/#424/#428/#430/#431/#432/#433/#435/#440/#441/#442 — **14th occurrence**), should be `kind:"unsupported_acp_invocation"`. **Required fix shape:** (a) `claw acp serve` exits **non-zero** (exit code 2 = "not implemented" is conventional) so automation `$?`-gating detects the no-op; (b) deliver #413's fix: remove `discoverability_tracking` and `tracking` top-level fields, OR move them under an optional `_meta` sub-object gated on a debug flag; (c) replace `message` prose with a typed `reason:"not_implemented"` enum + optional `detail` string for downstream pipelines that need a stable signal; (d) drop `recommended_workflows` from the ACP envelope OR move it under `_meta`; (e) the `status:"discoverability_only"` value is non-standard — replace with `status:"not_implemented"` (matching the `supported:false` boolean); (f) typed `kind:"unsupported_acp_invocation"` for the bad-arg path. **Why this matters:** ACP/Zed integration is the integration point for IDE-based AI workflows. A "success" exit code on a "not implemented" stub breaks the contract for any wrapper script that tries to detect ACP availability via `claw acp serve && ...`. The internal-tracking-ID leak (#413) being unfixed for 11 days suggests the JSON envelope audit isn't being executed against the ROADMAP backlog. Cross-references #413 (internal tracking leak — unfixed), #422 (exit-code parity), `kind:"unknown"` catch-all cluster. Source: Jobdori live dogfood, `19aaf9d0`, 2026-05-11.
444. **No broad-cwd safety guard for `--resume` — `claw --resume latest` from `/` attempts to `mkdir /.claw/sessions/<fingerprint>/` and is only stopped by the read-only filesystem at root; from any writable system directory (`/tmp`, `/var/tmp`, `$HOME` itself) it silently creates `.claw/sessions/<fingerprint>/` droppings; exit code is 0 (success) on the read-only filesystem error path** — dogfooded 2026-05-11 by Jobdori on `b2048856` in response to Clawhip pinpoint nudge at `1503373639884607629`. Reproduction: `cd / && claw --resume latest --output-format json` returns `{"error":"failed to restore session: Read-only file system (os error 30)","hint":null,"kind":"session_load_failed","type":"error"}` exit **0**. The OS permission denial is the only thing preventing claw from creating `/.claw/sessions/<fingerprint>/` in the root filesystem. Compare with `cd /tmp && claw --resume latest --output-format json`: silently creates `/tmp/.claw/sessions/<fingerprint>/` partition (confirmed by `ls /tmp/.claw` showing a directory from a prior dogfood session at `13:31` — the May 11 11:00 pinpoint #435 dropping is still there 10+ hours later, despite documented cleanup). Same dogfood session: `cd $HOME && claw --resume latest` would silently create `~/.claw/sessions/<fingerprint>/` (the user's home claw config dir). The shorthand prompt path has a broad-cwd guard (`claw is running from a very broad directory (/). The agent can read and search everything under this path. Use --allow-broad-cwd to proceed anyway`) — but the guard does NOT fire on `--resume`, `--status`, or `claw status` invocations. Inconsistent safety surface: the dangerous path (LLM prompt with full tool access) has a guard, but session-management paths that create filesystem artifacts in broad locations have none. **Three sibling findings in same probe:** (a) **exit-code 0 on filesystem error** (`session_load_failed` envelope returns exit code 0): the read-only-filesystem error from `/.claw` creation path is an unrecoverable failure but the process exits 0 — same exit-parity bug as #422/#435; (b) **stale filesystem droppings**: `/tmp/.claw/` from a 13:31 dogfood session at HEAD `6c0c305a` is still present at 21:30 (10 hours later, 6+ HEADs later). The "deferred cleanup" or "lazy creation" fix prescribed in #435 hasn't landed; (c) **broad-cwd guard misfires on resume**: the existing guard from `run` path (visible in `claw --help` as "Use --allow-broad-cwd to proceed anyway") never fires on `--resume`. Either both paths should guard, or the guard should be promoted to a global pre-check. **Required fix shape:** (a) extend the broad-cwd guard to `--resume`, `claw status`, `claw doctor`, and every command that may create filesystem artifacts; `cd / && claw --resume latest` must fail fast with `kind:"broad_cwd_blocked"` before any filesystem operation; (b) `cd $HOME && claw` should warn that the workspace is your home directory and ask for `--allow-broad-cwd` (the LLM with full filesystem access in `$HOME` is the same blast radius as in `/`); (c) exit code 1 for `session_load_failed` regardless of underlying cause; (d) deliver #435's "defer fingerprint directory creation to first successful save" fix — failed `--resume` must not leave filesystem droppings; (e) cleanup `/tmp/.claw/` style scratch-dir artifacts via a `claw doctor --cleanup` or similar opt-in mechanism; (f) regression test: failed `--resume` does not create any directories under cwd. **Why this matters:** users running claw as part of CI/cron from system directories silently accumulate `.claw/sessions/<fingerprint>/` artifacts in /tmp, /var, /opt, $HOME, etc. Running as root from / would (with a writable root) silently pollute the root filesystem. The broad-cwd guard exists but only covers one entry point. Cross-references #427 (broad-cwd guard fires on resume too — actually it doesn't, that note in #427 was inaccurate), #428 (default permission_mode danger-full-access — compounds with this: full access + no broad-cwd guard = serious blast radius), #435 (filesystem side effects on failed resume), #422 (exit-code parity). Source: Jobdori live dogfood, `b2048856`, 2026-05-11.
445. **Skill name-vs-directory mismatch is silently accepted — `.claw/skills/wrong-name/SKILL.md` with frontmatter `name: actually-different-name` loads as "actually-different-name" without any warning; users who reference the skill by directory name (`claw skills run wrong-name`) get `skill_not_found` while `skills list` shows it under the frontmatter name; sibling: loose `.md` files at the skills-dir root and subdirs without `SKILL.md` are silently dropped** — dogfooded 2026-05-11 by Jobdori on `9e1eafd0` in response to Clawhip pinpoint nudge at `1503381189539528897`. Reproduction: create `.claw/skills/wrong-name/SKILL.md` with frontmatter `---\nname: actually-different-name\ndescription: Skill where dir name and frontmatter name disagree\n---`. Run `claw skills list --output-format json` → the skill is listed with `name: "actually-different-name"` (the frontmatter value), no warning about the dir-vs-name mismatch. Users who type `claw skills run wrong-name` (the dirname they know from `ls`) get a `skill_not_found` error; `claw skills run actually-different-name` works. The two names are decoupled with no surfaced relationship. **Three sibling silent-drop bugs in same probe:** (a) **subdir without SKILL.md silently skipped**: `.claw/skills/no-skill-md/` containing only `README.md` (no `SKILL.md`) is silently skipped from `skills list`. No `invalid_skills:[{path, reason:"missing_SKILL.md"}]` array, no warning, just absent from output. (b) **Loose `.md` at skills dir root silently dropped**: `.claw/skills/loose-skill.md` (not inside a per-skill subdirectory) is silently ignored. Discovery only walks `.claw/skills/*/SKILL.md` — no support for flat `.claw/skills/<name>.md`. (c) **Workspace + user skills merged without per-source filter**: `skills list` returns 74 entries including all `~/.claw/skills/*` user-home skills alongside the project skills. There's no `--scope workspace` flag to limit output to just project-local skills; automation has to filter by `source.id == "project_claw"` post-hoc. **Required fix shape:** (a) when SKILL.md frontmatter `name` differs from the parent directory name, emit a `skills_metadata_drift:[{dir_name, frontmatter_name, path}]` array OR enforce `name = dir_name` as a hard rule; if neither, at minimum a stderr warning on each invocation; (b) skill subdirectories without `SKILL.md` should surface as `invalid_skills:[{path, reason}]` in `skills list --output-format json` (same pattern as #440 MCP servers, #441 hooks, #442 agents); (c) support loose `.md` files at skills-dir root OR document explicitly that only subdirectories with `SKILL.md` are discovered; (d) add `--scope workspace|user|all` flag to `skills list` for filtering; (e) regression test: dir/frontmatter mismatch triggers a deterministic warning or error; subdirs without SKILL.md show in invalid array. **Why this matters:** skill discovery is a security-relevant surface — a user's `claw skills run X` could end up running a different skill than they thought if dir-name and frontmatter-name diverge. The silent drops mean users can't tell why their skill files aren't recognized, leading to "I copied the example and it doesn't work" forum questions. Cross-references #440 (MCP all-or-nothing), #441 (hooks all-or-nothing), #442 (agents need TOML, .md dropped), #431 (skills install raw OS error). Source: Jobdori live dogfood, `9e1eafd0`, 2026-05-11.
446. **Config is loaded 2-3 times per command invocation; each load re-emits identical deprecation warnings without deduplication — `status` triggers 3× `enabledPlugins` warning, `doctor`/`mcp` trigger 2× each, only `version` (config-free) emits 0** — dogfooded 2026-05-11 by Jobdori on `5a4cc506` in response to Clawhip pinpoint nudge at `1503388740595224717`. Reproduction: with a `~/.claw/settings.json` containing the deprecated `enabledPlugins` key, run each command from a fresh empty cwd and count `warning: ... is deprecated` lines on stderr — `claw status 2>&1 >/dev/null | grep -c deprecated` returns **3**, `claw doctor` returns **2**, `claw mcp` returns **2**, `claw version` returns **0**. Each duplicate is byte-identical (same file path, same line number, same field name). The pattern proves the config-load pipeline is invoked 2-3 times within a single command process; warnings are emitted at each load without checking a `warned_files: HashSet<PathBuf>` deduplication set. **Three sibling implications:** (a) **load-count varies by command** — status:3, doctor:2, mcp:2, version:0 — suggesting each command implements its own config-load call rather than going through a shared cached loader; (b) **noise pollution**: users running `claw status` once see the same 64-character warning 3 times in their terminal scrollback, making real warnings (other config errors, real deprecations) lost in the duplicate noise; (c) **performance signal**: 3× config load means 3× JSON parsing of `~/.claw/settings.json`, `~/.claw.json`, `$CLAW_CONFIG_HOME/settings.json`, and the project-local `.claw.json` / `.claw/settings.json` / `.claw/settings.local.json`. For a workspace with 5 config files, that's 15 redundant disk reads per status invocation. Earlier roadmap entries observed 3× (#424) and 4× (#425) warning counts at different HEADs; the count keeps fluctuating, suggesting the underlying issue is config-load fan-out that nobody has refactored. **Required fix shape:** (a) introduce a `ConfigLoader` cache scoped to the command-process lifetime: first load reads files and emits warnings; subsequent calls hit the cache and emit zero warnings; (b) move config validation/warnings to a single canonical entry point (`ConfigLoader::load_with_diagnostics()` returns `(RuntimeConfig, Vec<Warning>)` exactly once); (c) every command that needs config goes through the cached loader instead of re-reading from disk; (d) `doctor --output-format json` exposes `config_load_count:int` field so we can regression-test that loads are deduplicated; (e) regression test: any single command invocation emits each deprecation warning at most once. **Why this matters:** repeated identical warnings train users to ignore stderr noise. Real warnings (a new deprecation, a config error from a different file, an MCP server failure) get drowned out by 3-4 copies of the same notice. The 15-disk-read worst case is wasted I/O that adds startup latency. The fact that count fluctuates between HEADs (3 at `6c0c305a`, 4 at `d7dbe951`, back to 3 at `5a4cc506`) suggests dev velocity is moving config loads around without an architectural fix. Cross-references #424 (deprecation warning 3×), #425 (deprecation warning 4×), #421 (cwd canonicalization — possibly tied to per-load symlink resolution), #428 (default permission_mode loaded from same config files). Source: Jobdori live dogfood, `5a4cc506`, 2026-05-11.
447. **All JSON error envelopes go to STDERR not STDOUT; stdout is empty (0 bytes) on every `--output-format json` failure — breaks the standard automation pattern `output=$(claw cmd --output-format json)` which captures nothing on error and forces ugly `2>&1` redirects to even see the JSON** — dogfooded 2026-05-11 by Jobdori on `5ab969e7` in response to Clawhip pinpoint nudge at `1503396289071808523`. Reproduction (stderr-vs-stdout discipline audit): `claw --no-such-flag --output-format json >stdout.txt 2>stderr.txt` → stdout = **0 bytes**, stderr = 115 bytes containing `{"error":"unknown option: --no-such-flag","hint":"Run \`claw --help\` for usage.","kind":"cli_parse","type":"error"}`. Same pattern across four error envelopes probed: (a) `cli_parse` → stdout 0 / stderr 115; (b) `missing_credentials` → stdout 0 / stderr 853 (includes deprecation warnings ahead of envelope); (c) `session_load_failed` → stdout 0 / stderr 322; (d) `invalid_model_syntax` → stdout 0 / stderr 199. Success paths route correctly: `claw status --output-format json` → stdout 1496 / stderr 0. **The asymmetry is wrong on two axes:** (a) **JSON-format outputs should always go to stdout regardless of success/failure**: every major CLI in this class (kubectl, gh, aws, jq, terraform `-json`, `npm --json`) emits JSON on stdout for both ok and error paths; consumers parse `stdout | jq .kind` and switch on the kind to detect errors. claw's split forces consumers to capture both streams or use `2>&1` which then includes deprecation prose alongside the JSON envelope and breaks parsing. (b) **Deprecation/info warnings leak into the JSON error envelope on stderr**: when stderr is the only path to get the JSON, the deprecation warning prefix (`warning: ... enabledPlugins ... is deprecated`) precedes the JSON, making `tail -1 stderr.txt | jq .` fragile. **Three sibling problems:** (i) **breaks the canonical Bash idiom** `if ! output=$(cmd --output-format json); then echo "$output" | jq .error; fi` — `$output` is empty on error so the `jq` call sees nothing. (ii) **forces N-line stderr parsing**: to get the JSON envelope from stderr, automation must read until EOF, then skip leading `warning:` lines, then parse only the last `{...}` JSON. This is a brittle heuristic that breaks if more warnings are added. (iii) **inconsistent with text mode**: text-mode error output ALSO goes to stderr (e.g., `claw --no-such-flag` → stderr `[error-kind: cli_parse]\nerror: ...`) — that's correct for text mode (stderr is the diagnostic channel). The bug is JSON mode inheriting the same routing. **Required fix shape:** (a) JSON error envelopes go to STDOUT when `--output-format json` is active; (b) keep text-mode error output on stderr (no change for text path); (c) deprecation/info warnings should ALSO go to stderr in JSON mode (they're diagnostic prose, not part of the JSON contract) — separate channels: JSON envelope on stdout, prose warnings on stderr; (d) add `--quiet` / `--no-warn` flag to fully suppress stderr warnings for clean automation; (e) regression test: every `--output-format json` failure path emits the JSON envelope on stdout, exit non-zero, no JSON ever on stderr. **Why this matters:** the entire point of `--output-format json` is enabling automation. Splitting JSON success vs error across stdout vs stderr defeats the purpose — automation must capture both, dedupe sources, and parse mixed streams. Cross-references #422 (exit-code parity across error envelopes), #424 (deprecation warnings noise), #428 (envelope vs prose tension), #446 (multi-load deprecation duplication). Source: Jobdori live dogfood, `5ab969e7`, 2026-05-11.
448. **`sandbox --output-format json` has contradictory state flags — `enabled:true, supported:false, active:false, filesystem_active:true, allowed_mounts:[]`: claim that sandbox is "enabled" while OS doesn't support namespace isolation and `allowed_mounts:[]` is empty contradicts `filesystem_active:true filesystem_mode:"workspace-only"`** — dogfooded 2026-05-11 by Jobdori on `7244a82b` in response to Clawhip pinpoint nudge at `1503403842920779917` (using fresh-current-main runner at `/tmp/claw-dog-1430` per gajae's 14:00 protocol switch). Reproduction: `claw sandbox --output-format json` on macOS (where `unshare` is unavailable) returns `{"active":false,"active_namespace":false,"active_network":false,"allowed_mounts":[],"enabled":true,"fallback_reason":"namespace isolation unavailable (requires Linux with \`unshare\`)","filesystem_active":true,"filesystem_mode":"workspace-only","in_container":false,"kind":"sandbox","markers":[],"requested_namespace":true,"requested_network":false,"supported":false}`. **Three contradictions in the same envelope:** (a) `enabled:true` AND `supported:false`: what does "enabled" mean if the OS doesn't support sandboxing? Read literally, sandbox is *enabled but unsupported* — semantic nonsense. The likely intent is "user requested sandbox in config" but the field name `enabled` says "is ON". A better name would be `requested:true` or `config_intent:true`, with `enabled` reserved for the actually-active state. (b) `filesystem_active:true, filesystem_mode:"workspace-only"` AND `allowed_mounts:[]`: if the filesystem fence is active in workspace-only mode, the workspace directory itself MUST be an allowed mount. An empty `allowed_mounts:[]` array combined with `filesystem_active:true` means either (i) the fence is being misreported (it's not really active), (ii) the workspace is implicit and `allowed_mounts` only lists *additional* mounts, or (iii) the fence has no allowed paths and nothing is readable — all three are inconsistent with the user-facing summary. (c) `active:false` AND `filesystem_active:true`: the top-level `active` field is a single boolean summary, but it disagrees with `filesystem_active:true` (one component is active). Either `active` is "all components active" (then it should be `false` when any component is off) or "any component active" (then it should be `true` when filesystem is). The current value is `false` despite filesystem being active. **Sibling: no `claw sandbox --help`**: `claw sandbox status` and `claw sandbox --help` go to LLM-prompt fallback or hang (gajae confirmed at 13:00 that `sandbox status` returns typed `cli_parse` but `sandbox --help` is bounded — schema is non-uniform across help paths). **Required fix shape:** (a) rename `enabled` to `requested` or `config_intent` to disambiguate from "currently active"; (b) make `allowed_mounts` explicitly include the workspace when filesystem_mode is "workspace-only" (`allowed_mounts:[{path:"<cwd>",writable:true,reason:"workspace_root"}]`); (c) document the `active` aggregate semantics: pick either "all" or "any" composition rule and document the choice; (d) add `active_components:["filesystem"]` array as a richer alternative to the single boolean — surfaces exactly which sandbox subsystems are live; (e) regression test: when `filesystem_mode == "workspace-only"`, `allowed_mounts` MUST contain the cwd and `active` must agree with the documented composition rule. **Why this matters:** sandbox is the trust surface — automation that checks `sandbox.active == true` before running a risky LLM prompt sees `false` (no namespace, no network) and assumes no isolation, but `filesystem_active:true` means there IS partial isolation. The mixed signal forces consumers to OR all `*_active` fields together. Cross-references #428 (default permission_mode=danger-full-access — paired with sandbox-not-active means zero isolation), #444 (no broad-cwd guard — sandbox is the only safety net and its status is unclear). Source: Jobdori live dogfood, `7244a82b`, 2026-05-11.
449. **`claw session list --output-format json` routes through `CliAction::ResumeSession` and hits the auth gate, returning `kind:"missing_credentials"` — but `session list` is a pure local filesystem read that requires no API credentials; by contrast, `claw session` (without `list`) correctly short-circuits with `kind:"unknown"` + "is a slash command" message without touching the auth gate** — dogfooded 2026-05-12 by Jobdori on `8f55870d` in response to Clawhip pinpoint nudge at `1503638404842131456`. Reproduction (no creds, isolated env): `env -i HOME=$HOME PATH=$PATH claw session list --output-format json``{"error":"missing Anthropic credentials...","kind":"missing_credentials"}` exit 1. `env -i HOME=$HOME PATH=$PATH claw session --output-format json``{"error":"`claw session` is a slash command...","kind":"unknown"}` exit 1 (no auth check). Root cause: the parser routes `session list` via `parse_resume_session_args` treating `list` as a session-path token, producing `CliAction::ResumeSession { session_path: "list", commands: [] }`. `resume_session()` then calls `LiveCli::new()` which instantiates the Anthropic client and fires the credentials guard. The `SlashCommand::Session { action: Some("list") }` special-case path in `run_resume_command()` (line 3654 comment: "`/session list` can be served from the sessions directory without a live session") is only reachable after auth passes — the no-creds guard fires before the slash-command dispatch loop. **Asymmetry:** the internal code already knows `session list` is credential-free (the comment at line 3654 says so), but the CLI entrypoint forces creds before the command ever reaches that branch. **Sibling: `session list` with no sessions returns `kind:"session_load_failed"` (from `--resume latest` fallback) rather than `{"kind":"session_list","sessions":[],"session_details":[]}` — the empty-sessions case is misrouted to the resume-failure path instead of a list-success with zero entries.** **Required fix shape:** (a) add a dedicated `CliAction::SessionList { output_format }` variant dispatched when `claw session list` is parsed — do not route through `ResumeSession`; (b) implement `run_session_list(output_format)` as a credentials-free function that calls `list_managed_sessions()` directly (same logic as the slash-command special-case at line 3659); (c) ensure empty sessions returns `{"kind":"session_list","sessions":[],"session_details":[],"active":null}` with exit 0, not a `session_load_failed` error; (d) add the same fix for sibling local-only commands that currently hit the auth gate: `session delete <id>`, `session export <id>`; (e) regression test: `claw session list --output-format json` with no credentials returns `kind:"session_list"` exit 0. **Why this matters:** session list is the canonical inventory surface for automation pipelines — `claw session list --output-format json | jq '.session_details[] | .id'` is the idiomatic way to enumerate sessions for replay, export, or resume. Requiring API credentials to read a local directory listing breaks offline use, CI environments with no API key configured, and any scripting that runs before credential setup. Cross-references #357 (session list requires creds — this is the same bug surfaced by that entry; #449 provides the root-cause path trace), #369 (session help/fork require creds), #427 (resume --help hits auth gate), #431 (skills uninstall requires creds). Source: Jobdori live dogfood, `8f55870d`, 2026-05-12.
450. **`prompt` emits `kind:"missing_credentials"` JSON on STDERR (not stdout), leaving stdout at 0 bytes — automation pattern `output=$(claw prompt hello --output-format json)` captures nothing on auth-absent failure; `doctor` correctly surfaces `auth.status:"warn"` with `api_key_present:false` but exposes no `prompt_ready:false` field that automation can check before invoking `prompt`** — dogfooded 2026-05-16 by Jobdori on `a35ee9a0` in response to Clawhip pinpoint nudge at `1505208225321062521`. Exact reproduction (isolated env, no creds, fresh git repo, HEAD `a35ee9a0`): `timeout 5 env -i HOME=$ISOLATED_HOME PATH=$PATH CLAW_CONFIG_HOME=$PROBE/.claw-cfg claw prompt hello --output-format json > stdout.txt 2> stderr.txt` → stdout = **0 bytes**, stderr = 195 bytes containing `{"error":"missing Anthropic credentials…","exit_code":1,"hint":null,"kind":"missing_credentials","type":"error"}`, exit code 1. Confirms Gaebal's `1505208553793781792` pinpoint that `prompt` timeout + zero bytes was the prior state — HEAD `a35ee9a0` now correctly exits 1 with `kind:"missing_credentials"` **but the envelope is still routed to stderr** (issue #447 class, same class as prior entries #422, #435). **Contrast with `doctor`:** `claw doctor --output-format json 2>/dev/null` succeeds to stdout with `checks[auth].status:"warn"`, `api_key_present:false`, `auth_token_present:false` — but the auth check has no `prompt_ready:false` field. Automation that gates on `doctor` before invoking `prompt` must re-derive readiness from `api_key_present && auth_token_present` — there is no single canonical boolean. **Three compound problems:** (a) **stdout-empty on `--output-format json` failure**: same class as #447; `prompt`'s error envelope goes to stderr, not stdout. The canonical automation idiom `if ! result=$(claw prompt "q" --output-format json); then echo "$result" | jq .kind; fi` sees `$result=""` on failure — the jq call gets nothing. All `--output-format json` error paths must route JSON to stdout per #447 contract; (b) **`doctor` missing `prompt_ready` field**: `doctor --output-format json` already knows auth is absent (`api_key_present:false`) but surfaces no derived `prompt_ready:bool` or `prompt_blocked_reason:string` field. Automation must infer readiness from `api_key_present || auth_token_present || legacy_*_present` — a 5-field OR across legacy fields that is fragile as auth mechanisms evolve. A single `prompt_ready:false` (with `prompt_blocked_reason:"auth_missing"`) inside the `auth` check would give downstream a stable contract; (c) **`claw prompt` with no auth does no preflight and fires straight at the API**: the preflight check that `doctor` runs (auth discovery) is not reused by `prompt` to emit a fast typed error before attempting the network call. Both Gaebal's pinpoint (prompt hanging silently on older HEAD) and the current behavior (prompt hitting auth gate after a brief API attempt) stem from the same root: prompt does not short-circuit at the point where `doctor` already knows auth is absent. If `doctor` can emit `kind:"doctor"` with `auth.status:"warn"` in ~20ms without a network call, `prompt` should emit `kind:"missing_credentials"` in the same window and output it to stdout. **Required fix shape:** (a) `prompt --output-format json` must write the `kind:"missing_credentials"` JSON envelope to **stdout**, not stderr — same fix as #447 for all error envelopes; (b) add `prompt_ready:bool` and `prompt_blocked_reason:string|null` to the `auth` check in `doctor --output-format json`; derive it as `api_key_present || auth_token_present || legacy_saved_oauth_present`; (c) `prompt` must run the credential preflight check (same codepath as doctor's auth check) before attempting any API call and emit `{"kind":"missing_credentials","prompt_blocked_reason":"auth_missing"}` on **stdout** with exit 1 if the check fails; (d) `--output-format json` stdout routing fix must cover: `prompt`, `session list` (cross-ref #449), `skills uninstall` (cross-ref #431), `resume` (cross-ref #435), `acp serve` (cross-ref #443) — the full `kind:"missing_credentials"` class; (e) regression test: `claw prompt hello --output-format json` with no creds writes JSON to stdout (0 bytes stderr), exits 1, `kind:"missing_credentials"`, in under 200ms (no network attempt). **Why this matters:** `prompt` is the primary consumer entry point. Auth-absent failure routing to stderr breaks every automation wrapper that captures `$(claw prompt ... --output-format json)`. The `doctor` preflight metadata gap means auth-readiness checks require parsing 5 legacy fields instead of reading one boolean. Cross-references #447 (all JSON error envelopes on stderr), #449 (session list hits auth gate), #431 (skills uninstall hits auth gate), #357 (auth gate on local ops cluster), #422 (exit-code parity). Source: Jobdori live dogfood, `a35ee9a0`, 2026-05-16.
451. **Dogfood automation can silently run probes in the wrong repository/worktree when adjacent checkouts share similarly named binaries and stale build artifacts, so reports may mix evidence from Clawdbot/OMX with claw-code** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 12:00/12:30 UTC nudge cycle. While following a tmux-hook JSON lifecycle probe, the shell reported `/home/bellman/clawd` as the top-level worktree and executed `node dist/cli/omx.js ...` from Clawdbot/OMX artifacts instead of a claw-code checkout; a later correction found the actual claw-code repos under `/home/bellman/Workspace/claw-code-*`, including one `main` checkout hundreds of commits behind `origin/main`. The transcript therefore briefly contained plausible-looking CLI evidence from the wrong product tree before git provenance checks caught it. **Required fix shape:** (a) before dogfood probes, emit a mandatory machine-readable provenance preflight with repo root, remote URL, branch, HEAD, upstream HEAD, ahead/behind counts, binary path, and embedded build SHA when available; (b) make report templates include this provenance block before any command evidence; (c) warn or block when the requested product name does not match the remote/package/binary identity, or when the checkout is behind the target upstream by a configured threshold; (d) add regression coverage around multi-worktree/multi-product environments proving dogfood harnesses cannot silently attribute evidence from a neighboring repo or stale artifact. **Why this matters:** stale-branch confusion is not just a git annoyance; it corrupts the evidence chain. Claws can land or report fixes against the wrong codebase if the harness does not prove repo and binary identity before probing. Source: gaebal-gajae dogfood response to Clawhip messages `1506265193863446711` and `1506272743895728249` on 2026-05-19.
452. **Validated claw-code checkouts can have no runnable local debug binary, and the failure is a raw shell `No such file or directory` instead of a typed build/provenance preflight result** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 13:00 UTC nudge after applying the provenance guard from #451. The corrected claw-code checkout was `/home/bellman/Workspace/claw-code-pr2967` with remote `https://github.com/ultraworkers/claw-code.git`, branch `docs/roadmap-workdir-provenance`, HEAD `6183d95`, upstream `origin/main` at `f8e1bb7`, and ahead/behind `1/0`. The first real probe then failed before reaching claw-code logic: `timeout --kill-after=1s 8s ./rust/target/debug/claw plugins list --output-format json` exited `127` with stderr `timeout: failed to run command './rust/target/debug/claw': No such file or directory` and empty stdout. This is distinct from stale-binary mismatch: here the selected checkout is identifiable, but there is no built binary and no canonical instruction/result telling automation whether to build, locate an installed `claw`, or stop. **Required fix shape:** (a) provide a canonical `claw dogfood preflight --output-format json` or equivalent script that checks expected binary paths, installed binary fallback, embedded build SHA, workspace HEAD, and build freshness before any product probe; (b) when the expected local binary is absent, return a typed result such as `kind:"dogfood_preflight"`, `binary_status:"missing"`, `expected_path`, `recommended_build_command`, and `can_use_installed_binary:false|true`; (c) integrate the preflight into dogfood report templates so a missing build artifact is reported as startup friction, not a raw shell 127; (d) add regression/fixture coverage for missing binary, stale binary, matching debug binary, and installed-binary fallback cases. **Why this matters:** after #451 proves the repo is right, claws still need to prove the executable exists and corresponds to that repo. A raw shell missing-file error wastes a nudge cycle and tempts operators to run whatever stale binary happens to be nearby. Source: gaebal-gajae dogfood response to Clawhip message `1506280293831544997` on 2026-05-19.
453. **Plugin list JSON can report bundled plugin `source` paths from a stale user registry in a different checkout, with no stale-source warning or current-bundled-root distinction** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 13:30 UTC nudge after a successful local build in `/home/bellman/Workspace/claw-code-pr2967` (branch `docs/roadmap-workdir-provenance`, HEAD `25d663d`, binary `./rust/target/debug/claw` reporting `git_sha:"25d663d"`). Running `./rust/target/debug/claw plugins list --output-format json` returned structured `plugins[]`, but both bundled plugin entries reported `source` under `/home/bellman/Workspace/claw-code-parity-worktrees/clawcode-ux-enhance/...` instead of the current checkout. Cleaning and rebuilding the `plugins` crate did not change the output; the stale paths came from `~/.claw/plugins/installed.json`, where bundled plugin records persisted old `source.path` values. The JSON payload gave no `source_stale`, `source_exists`, `current_bundled_root`, `registry_path`, or `source_origin:"registry"` cue, so automation would treat another worktree's bundled plugin path as current truth. **Required fix shape:** (a) for bundled plugins, derive/display source from the current binary/workspace bundled root rather than a persistent user registry path when possible; (b) if registry source is retained, expose `registry_path`, `source_origin`, `source_exists`, `source_matches_current_bundle_root`, and `current_bundled_root` fields; (c) warn in text mode and JSON diagnostics when bundled plugin registry records point outside the current binary/workspace provenance; (d) add regression coverage where `installed.json` contains stale bundled paths from another checkout and `plugins list --output-format json` either self-heals or marks the source stale. **Why this matters:** plugin lifecycle actions rely on source provenance. If a fresh build from checkout A reports bundled plugin sources from checkout B, claws can inspect, enable, update, or debug the wrong plugin tree and misattribute lifecycle failures to current code. Source: gaebal-gajae dogfood response to Clawhip message `1506287843021160500` on 2026-05-19.
454. **`plugins help --output-format json` returns a success-shaped plugin inventory plus `Unknown /plugins action 'help'` prose instead of structured plugin command help or supported lifecycle actions** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 14:00 UTC nudge on a freshly built binary from `/home/bellman/Workspace/claw-code-pr2967` (`./rust/target/debug/claw version --output-format json` reported `git_sha:"25d663d"`; the worktree had roadmap-only commits ahead of that source build). Running `./rust/target/debug/claw plugins help --output-format json` exited `0` and returned JSON with `kind:"plugin"`, `action:"help"`, `status:"ok"`, a full `plugins[]` inventory, and message `Unknown /plugins action 'help'. Use list, install, enable, disable, uninstall, or update.` It did not return `supported_actions[]`, usage, action metadata, destructive-action markers, target requirements, or a typed `unsupported_action` / `help_unavailable` status. The same probe also confirmed `plugins show example-bundled --output-format json` still returns `status:"ok"` plus an unknown-action message and no selected `plugin` object, matching the existing unsupported-show class, but the new pinpoint is the absence of a plugin lifecycle discovery/help contract. **Required fix shape:** (a) implement `plugins help --output-format json` as a real help/discovery payload with `supported_actions[]`, per-action `requires_target`, `destructive`, `resume_safe`/automation notes, usage, and examples; (b) if `help` is intentionally unsupported, return a non-ok typed JSON envelope with `code:"unsupported_plugin_action"` and structured `supported_actions[]`, not `status:"ok"`; (c) avoid attaching full plugin inventory to unsupported/help responses unless requested, or mark it as incidental; (d) add regression coverage proving plugin lifecycle help is machine-readable and does not require scraping `message` for available actions. **Why this matters:** plugin lifecycle commands include install/enable/disable/update/uninstall; claws need a safe discovery surface before attempting mutations. A success-shaped unknown-help response with only prose action names keeps lifecycle automation brittle and encourages trial-and-error against mutating commands. Source: gaebal-gajae dogfood response to Clawhip message `1506295397285494905` on 2026-05-19.
455. **Plugin help entrypoints hang before producing any help bytes, and with normal user config they emit only a deprecation warning before timeout** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 14:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. Bounded probes of `plugins --help --output-format json`, `plugins help --output-format json`, and `plugins list --help --output-format json` each timed out after 8s with `stdout=0`; under the normal user config each had `stderr=121` containing only the repeated config deprecation warning, and under an isolated clean `HOME`/`CLAW_CONFIG_HOME` even `plugins --help` and JSON help forms timed out with both stdout and stderr empty. This is distinct from #454's success-shaped unknown-help JSON observed on the action-dispatch path: the flag-style/local help path can hang before returning any help or typed JSON at all. **Required fix shape:** (a) route `plugins --help`, `plugins help`, and subcommand help forms through static help rendering before config/plugin registry loading; (b) make JSON help return bounded stdout JSON with `kind:"plugin"`, `action:"help"`, `supported_actions[]`, and usage metadata; (c) if dynamic plugin state is intentionally consulted, enforce a short internal timeout and return a typed `plugin_help_unavailable` JSON error instead of zero-byte hangs; (d) add regression coverage with clean home and deprecated-config home proving plugin help emits bytes promptly and does not initialize slow lifecycle/registry paths. **Why this matters:** help must be the safe escape hatch when plugin lifecycle is broken. If every plugin help spelling can hang before bytes, claws cannot discover valid plugin actions, recover from registry issues, or explain how to fix lifecycle state without external docs. Source: gaebal-gajae dogfood response to Clawhip message `1506302942754639892` on 2026-05-19.
456. **Static `--help --output-format json` hangs across multiple local lifecycle namespaces under a clean home, so help discovery is not a reliable no-side-effect escape hatch** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 15:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. After pushing the accumulated roadmap branch, clean-environment probes using isolated `HOME` and `CLAW_CONFIG_HOME` showed that `mcp --help --output-format json`, `agents --help --output-format json`, `skills --help --output-format json`, `memory --help --output-format json`, and `session --help --output-format json` each timed out after 8s with `stdout=0` and `stderr=0`. This extends the plugin-specific #455 into a shared parser/help-layer gap: even command namespaces whose help should be static and local can enter a zero-byte hang before any JSON or text help is emitted. **Required fix shape:** (a) centralize `--help`/`help` handling before config, auth, registry, session, MCP, memory, or plugin initialization for all local lifecycle namespaces; (b) make `--output-format json` help return a bounded stdout payload with `kind:"help"`, namespace, usage, supported actions/sections, output formats, and side-effect/auth requirements; (c) add a global deterministic help timeout guard that returns typed JSON such as `kind:"help_unavailable"` instead of allowing zero-byte hangs; (d) add clean-home regression coverage for `mcp`, `agents`, `skills`, `memory`, `session`, and `plugins` help forms proving they emit bytes promptly and do not touch slow lifecycle providers. **Why this matters:** help is the only safe discovery path when lifecycle state is broken. If help itself can hang with no bytes, claws cannot learn how to inspect or recover MCP, agents, skills, memory, sessions, or plugins without external docs or guesswork. Source: gaebal-gajae dogfood response to Clawhip message `1506310493080518767` on 2026-05-19.
457. **Root help is bounded in text mode, but `help --output-format json` and command `--help --output-format json` convert help into a zero-byte hang** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 15:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. In an isolated clean `HOME`/`CLAW_CONFIG_HOME`, `./rust/target/debug/claw --help` and `./rust/target/debug/claw help` both exited 0 and printed 7403 bytes of root text help. But `help --output-format json`, `version --help --output-format json`, `doctor --help --output-format json`, and `status --help --output-format json` each timed out after 8s with `stdout=0` and `stderr=0`. This narrows #456: the help renderer itself is capable of returning promptly, but the parser/order path that combines help with JSON output appears to route into a different slow/non-returning path. **Required fix shape:** (a) parse `--help`/`help` before selecting command execution paths, auth, provider, session, or lifecycle initialization, while still preserving requested output format; (b) make root and command help JSON static/bounded with `kind:"help"`, `scope`, `command`, `usage`, `options`, `examples`, and `supported_output_formats`; (c) add regression coverage proving `claw --help`, `claw help`, `claw help --output-format json`, and representative command help forms all return within a small deterministic budget under clean home; (d) ensure JSON help does not silently fall back to text or zero-byte timeout. **Why this matters:** this is a parser-order failure in the safest command surface. Operators can get text help, but the moment automation asks for JSON discovery, help becomes a hang, forcing claws back to prose scraping or external docs. Source: gaebal-gajae dogfood response to Clawhip message `1506318038167847045` on 2026-05-19.
458. **Global output-format flag ordering is parser-hostile, and even `version --output-format json` can hang under clean env despite normal-env success** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 16:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with normal-env `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. Clean-environment flag-order probes showed `--output-format json --help`, `--output-format json help`, `--output-format json version`, `--version --output-format json`, and `--output-format json --version` each failed immediately with text-mode stderr only, e.g. `[error-kind: cli_parse] error: unknown option: --output-format json --help`, and `stdout=0`. The parser appears to treat the whole trailing string as one unknown option rather than recognizing a global output-format flag before the command. Separately, in the same clean `HOME`/`CLAW_CONFIG_HOME`, canonical `version --output-format json` timed out after 8s with `stdout=0`/`stderr=0`, even though the same command succeeds in the normal environment. **Required fix shape:** (a) make global flags such as `--output-format json` accepted before or after the subcommand, or return structured JSON `cli_parse` errors on stdout when JSON format is requested anywhere in argv; (b) parse flag values as separate tokens in error reporting instead of echoing combined strings like `--output-format json --help` as one option; (c) ensure `version` is fully local/static and cannot hang under clean env or missing config/auth; (d) add clean-env regression coverage for `version --output-format json`, `--output-format json version`, `--version --output-format json`, and JSON parse errors with stdout envelopes. **Why this matters:** claws often put global flags first for CLI uniformity and run in sanitized envs. If global JSON selection is order-sensitive and local version can hang only under clean env, startup probes become unreliable exactly in CI/sandbox contexts. Source: gaebal-gajae dogfood response to Clawhip message `1506325598245748828` on 2026-05-19.
459. **Dogfood timeout claims lack a required retry/evidence contract, so transient hangs can be recorded as durable product gaps without immediate reproducibility metadata** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 16:30 UTC nudge while narrowing #458 on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. The previous 16:00 pass saw clean-env `version --output-format json` time out with zero bytes, but a focused 16:30 retry matrix using isolated `HOME`/`CLAW_CONFIG_HOME` plus minimal env variants (`TERM`, `USER`/`LOGNAME`, `SHELL`, `LANG`/`LC_ALL`, and all combined) returned valid version JSON every time. That means the earlier timeout may have been transient harness load, process scheduling, or invocation interference, while the report format had no mandatory retry count, timing, command log artifact, or “reproduced N/M” field to distinguish flaky evidence from stable behavior. **Required fix shape:** (a) dogfood timeout reports must include retry count, per-attempt exit code/stdout/stderr byte counts, elapsed duration, env summary, binary provenance, and whether the failure reproduced after process isolation; (b) add a standard `timeout_evidence` block to report templates and ROADMAP entries before filing zero-byte hang claims; (c) classify un-reproduced hangs as `flaky_unconfirmed` with follow-up probes instead of stable product bugs; (d) provide a small harness command that runs bounded retries and emits machine-readable evidence JSON. **Why this matters:** zero-byte timeouts are high-severity but easy to misattribute. Without a retry/evidence contract, claws can pollute the backlog with transient scheduler artifacts or miss real nondeterministic hangs because the evidence shape is too thin. Source: gaebal-gajae dogfood response to Clawhip message `1506333141571211314` on 2026-05-19.
460. **Root `help --output-format json` is reproducibly bounded but only wraps 7.4KB of prose in `{kind,message}`, with no structured command or slash-command schema** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 17:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. Applying the retry/evidence discipline from #459, three clean-home attempts of `./rust/target/debug/claw help --output-format json` all exited 0 with `stdout=7563`, `stderr=0`, valid JSON keys exactly `kind,message`, `kind:"help"`, and a `message` string of length 7401. There were no `commands[]`, `options[]`, `slash_commands[]`, resume-safety flags, output-format support metadata, side-effect/auth requirements, or examples as structured fields. This supersedes the flaky zero-byte hang framing from #457 for root help: the stable reproducible gap is schema opacity, not timeout. **Required fix shape:** (a) keep `message` for human rendering but add a versioned structured help schema with `schema_version`, `commands[]`, `global_options[]`, `slash_commands[]`, `examples[]`, and `related_docs[]`; (b) include per-command fields such as `name`, `aliases`, `usage`, `description`, `supports_json`, `requires_auth`, `side_effects`, and `resume_safe` where applicable; (c) expose slash-command metadata without requiring prose scraping; (d) add regression coverage proving root help JSON has stable structured fields and that old `message` remains optional/backward-compatible. **Why this matters:** help JSON is the bootstrap discovery surface for claws. Valid JSON that contains only prose still forces automation to scrape text before choosing safe commands or resume paths. Source: gaebal-gajae dogfood response to Clawhip message `1506340691431657472` on 2026-05-19.
461. **Command-specific `--help --output-format json` reproducibly zero-byte hangs even though root JSON help returns bounded prose JSON** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 17:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. After #460 confirmed root `help --output-format json` is bounded but schema-opaque, two clean-home attempts each for `status --help --output-format json`, `doctor --help --output-format json`, `version --help --output-format json`, and `sandbox --help --output-format json` all timed out after 8s with `stdout=0` and `stderr=0`. This establishes a split contract: root JSON help reaches the help serializer, while command-specific JSON help falls into a non-returning command execution/parser path. **Required fix shape:** (a) add a command-help dispatch layer that catches `<command> --help` before entering the command's runtime handler; (b) share the same bounded JSON help schema from #460 for command-specific help, with fields `kind:"help"`, `command`, `usage`, `options`, `examples`, `supports_json`, `requires_auth`, and `side_effects`; (c) ensure local/static commands like `version`, `status`, `doctor`, and `sandbox` never initialize slow providers just to render help; (d) add clean-home regression coverage proving command-specific JSON help emits bytes promptly for representative static and lifecycle commands. **Why this matters:** claws often discover command contracts one command at a time. If root help is available but every command's JSON help hangs, automation still cannot inspect option-level semantics safely and must scrape root prose or guess. Source: gaebal-gajae dogfood response to Clawhip message `1506348241128788111` on 2026-05-19.
462. **Text-mode `<command> --help` also zero-byte hangs, proving the bug is command-help dispatch rather than JSON serialization** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 18:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. After #461 showed command-specific JSON help hangs, a text-mode retry matrix in isolated clean `HOME`/`CLAW_CONFIG_HOME` showed two attempts each for `status --help`, `doctor --help`, `version --help`, `sandbox --help`, `mcp --help`, `agents --help`, and `skills --help` all timed out after 6s with `stdout=0` and `stderr=0`. Root `--help` and `help` remain bounded, so the split is not help text rendering generally and not JSON formatting specifically; it is the command-specific help dispatch path failing to intercept `<command> --help` before some non-returning runtime path. **Required fix shape:** (a) implement a first-stage argv parser that recognizes `<command> --help` and `<command> help` for every registered command before command runtime initialization; (b) render static text help in text mode and structured JSON help in JSON mode from the same command metadata registry; (c) add regression coverage for both text and JSON help for representative static commands (`version`, `status`, `doctor`, `sandbox`) and lifecycle commands (`mcp`, `agents`, `skills`, `plugins`); (d) ensure every command-help path has a bounded no-provider/no-auth/no-config execution budget. **Why this matters:** users do not only run root `--help`; they naturally ask `claw status --help` or `claw mcp --help`. If that path hangs silently, the product loses the most basic local recovery surface before any real action starts. Source: gaebal-gajae dogfood response to Clawhip message `1506355792582938665` on 2026-05-19.
463. **Root help advertises direct slash examples like `claw /skills`, but direct slash behavior is inconsistent: `/skills` runs a huge local report, `/help` aliases root help, while `/status` is rejected as interactive-only despite being marked resume-safe** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 18:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. Root help examples include `claw /skills`, and clean-home probes showed direct `claw /skills` exits 0 and prints a 27KB skill inventory, while direct `claw /help` exits 0 and prints root help. But direct `claw /status` exits 1 with `slash command /status is interactive-only... use claw --resume SESSION.jsonl /status ... when the command is marked [resume] in /help`, even though `/status` is explicitly marked `[resume]` in root help and has a top-level sibling `claw status`. The same surface therefore mixes three semantics for direct slash invocation: accepted alias, accepted local slash report, and rejected interactive-only/resume-only command. **Required fix shape:** (a) define a single direct-slash CLI contract: either reject all slash commands outside REPL/resume with structured guidance, or allow resume-safe/local slash commands consistently; (b) if allowing direct slash commands, route `/status` to the same local/resume-safe status serializer as `claw status` or require `--resume` with a typed `resume_required` code; (c) make help examples distinguish top-level commands from slash commands and avoid advertising `claw /skills` unless direct slash invocation is intentional for the whole supported set; (d) add regression coverage for `/help`, `/skills`, `/status`, `/mcp`, `/agents`, and their top-level equivalents proving consistent direct/resume/text/json behavior. **Why this matters:** claws copy help examples literally. If `claw /skills` works but `claw /status` says interactive-only despite `[resume]`, automation cannot infer which slash commands are safe outside the REPL and will oscillate between direct, top-level, and resume forms. Source: gaebal-gajae dogfood response to Clawhip message `1506363340048564425` on 2026-05-19.
464. **Global `--output-format json` placement is broken for top-level subcommands: post-subcommand placement silently hangs, while pre-subcommand placement is rejected as `cli_parse` despite help documenting `--output-format` as a global flag** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 19:00/19:30 UTC nudges on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. Root help lists `--output-format FORMAT` in the global Flags section and examples like `claw [--model MODEL] [--output-format text|json] prompt TEXT`, while top-level commands (`claw status`, `claw doctor`, `claw mcp`, `claw skills`, `claw version`) advertise local diagnostic/report surfaces. Clean-home probes showed two distinct bad paths: `claw version --output-format json`, `claw status --output-format json`, and `claw skills --output-format json` timed out after 5s with `stdout=0`/`stderr=0`; the more canonical GNU-style global placement `claw --output-format json version`, `claw --output-format json status`, and `claw --output-format json skills` exited 1 with `[error-kind: cli_parse] error: unknown option: --output-format json <command>`. The only working form observed for `version` is the special local parser path used by `claw version --output-format json` in a non-clean environment/provenance preflight, which contradicts the clean-home bounded test and suggests the parser/runtime path is environment-sensitive as well as placement-sensitive. **Required fix shape:** (a) make `--output-format` a true global flag accepted before any subcommand and before slash commands; (b) make top-level local commands also accept trailing `--output-format` if the project wants common CLI ergonomics; (c) normalize both placements into one parsed `OutputFormat` before command dispatch; (d) ensure parse errors in JSON-requested mode emit structured JSON rather than prose-on-stderr; (e) add clean-home regression coverage for `claw --output-format json version|status|doctor|mcp|skills` and `claw version|status|doctor|mcp|skills --output-format json`, with bounded no-provider execution. **Why this matters:** claws and shell users routinely place global flags either before or after subcommands. A machine-readable output flag that sometimes hangs and sometimes parse-errors means automation cannot reliably request JSON for the exact local diagnostics it needs during recovery. Source: gaebal-gajae dogfood response to Clawhip messages `1506370889653027012` and `1506378443812765756` on 2026-05-19.
465. **`claw skills` is technically bounded but operationally noisy: the default text output dumps full descriptions for every discovered skill (~27KB / 65 skills in clean-home dogfood), unlike `status`, `doctor`, `mcp`, `sandbox`, and `agents` which stay compact enough for recovery logs** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 20:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. Clean-home text probes showed `version`, `status`, `doctor`, `mcp`, `sandbox`, and `agents` all exit promptly with compact diagnostic reports (`version` 136 bytes, `status` 1274 bytes, `doctor` 2519 bytes, `mcp` 117 bytes, `sandbox` 352 bytes, `agents` 17 bytes). `skills` also exits 0, but emits 27,573 bytes by default because it prints every skill name plus full description. This makes `claw skills` a poor first-response diagnostic in clawhip logs, tmux tails, CI failure artifacts, and copied support snippets: the useful inventory count and roots are buried under pages of prose. This is distinct from discovery-scope/security issues (#85/#95): even when the skill set is legitimate, the default report shape is too verbose for a recovery command. **Required fix shape:** (a) make default text `claw skills` compact: counts by source/root plus names only, truncated with an explicit `--verbose` hint; (b) add `claw skills --verbose` or `claw skills list --verbose` for full descriptions; (c) add `--format compact|verbose` or reuse `--compact` if the CLI standardizes it; (d) keep JSON mode complete but add `summary` and `entries[].description` fields so consumers can choose; (e) add regression coverage enforcing that default text output for 50+ skills stays under a small byte/line budget while verbose preserves current detail. **Why this matters:** local diagnostics should be safe to paste and scan during failures. A 27KB default skill dump hides the actual signal and makes every dogfood/support loop noisier than necessary. Source: gaebal-gajae dogfood response to Clawhip message `1506385992368652489` on 2026-05-19.
466. **Syntactically valid MCP config with a nonexistent command is reported as healthy/configured by `doctor`, `mcp`, and `status` instead of surfacing executable reachability as degraded** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 20:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. In a temp workspace containing only `.claw.json` with `mcpServers.broken.command:"/definitely/not/a/real/mcp-server"`, clean-home text probes showed `doctor` exits 0 with `Failures 0`, `Config Status ok`, and `Boot preflight ... mcp=true · servers 1`; `mcp` exits 0 and lists `broken stdio project /definitely/not/a/real/mcp-server --serve`; `status` exits 0 and reports `Boot preflight ... mcp=true plugins=true last_failed=none`. None of the three surfaces attempts even a cheap executable existence/PATH reachability check, so a server that cannot possibly launch is indistinguishable from a configured usable MCP server until the first runtime tool call fails. This is distinct from malformed-config degraded-mode items (#143/#144/#440): here the JSON shape is valid and the parser succeeds, but lifecycle readiness is false. **Required fix shape:** (a) add an MCP executable preflight pass that classifies each stdio server as `reachable:true|false|unknown` using absolute-path existence/executable-bit checks and PATH lookup for bare commands, without launching untrusted code; (b) expose per-server fields in `claw mcp` / JSON (`launch_status`, `command_exists`, `command_executable`, `path_resolution`, `error_kind:"mcp_command_not_found"`); (c) make `doctor` add an `mcp_reachability` check with `status:"warn"` or `"fail"` when any configured server command is missing; (d) make `status` distinguish `mcp_configured:true` from `mcp_reachable:false` instead of the current single `mcp=true`; (e) regression test with one valid `/bin/echo` server and one nonexistent absolute path proving partial status is reported. **Fresh mixed-PATH proof (21:00 UTC):** a temp config containing one PATH-resolvable server (`pathEcho.command:"echo"`) and one missing bare command (`missingBare.command:"definitely-not-a-real-mcp-bare-command-xyz"`) still made `mcp` list both as configured, `doctor` report `Failures 0` and `mcp=true · servers 2`, and `status` report `mcp=true` with no degraded marker. So the fix must handle both absolute paths and PATH lookup for bare commands, and it must preserve partial success (`echo` reachable, missingBare not found) instead of collapsing both into `configured`. **Why this matters:** MCP failures are often setup/path mistakes. If doctor says failures 0 and mcp=true while the command path does not exist, claws and users waste turns discovering the break only after a blocked tool call. Source: gaebal-gajae dogfood response to Clawhip messages `1506393539385491598` and `1506401088801083495` on 2026-05-19.
467. **Bare `claw plugins` / `claw plugin` returns the plugin inventory, but the natural explicit action `claw plugins list` / `claw plugin list` zero-byte hangs; singular/plural aliases are wired for the default action but not for the `list` subaction** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 21:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. In isolated clean `HOME`/`CLAW_CONFIG_HOME`, `plugins` and `plugin` each exited 0 and printed the two bundled plugins (`example-bundled`, `sample-hooks`) in text mode. But `plugins list` and `plugin list` each timed out after 8s with `stdout=0` and `stderr=0`. `plugins help` and `plugin help` also timed out, while direct `/plugins` and `/plugin` were rejected as interactive-only, showing that there are at least three parser paths for the same lifecycle namespace. This is a narrower current-main follow-up to the historical plugin route/help items (#78/#145/#348/#420/#454/#455): the route now exists for the default inventory, but adding the explicit canonical `list` action drops into a different non-returning path. **Required fix shape:** (a) normalize `plugin` and `plugins` aliases plus omitted action into one parser action before dispatch (`None` and `Some("list")` must be equivalent); (b) route `help` to static plugin help before plugin registry/lifecycle initialization; (c) add a deterministic timeout guard or typed `plugin_action_unavailable` envelope around plugin action dispatch so unsupported/misparsed actions cannot hang silently; (d) add clean-home regression coverage for `plugins`, `plugins list`, `plugin`, `plugin list`, `plugins help`, and `plugin help` in text and JSON modes, proving the first four produce the same inventory and help emits bounded usage. **Why this matters:** `list` is the most obvious plugin lifecycle action and is already the documented/discovery word from other lifecycle namespaces (`mcp list`, `agents list`, `skills list`). If bare inventory works but explicit list hangs, claws cannot safely choose between terse default and explicit action forms, and plugin support remains trial-and-error. Source: gaebal-gajae dogfood response to Clawhip message `1506408638883954768` on 2026-05-19.
468. **Explicit subactions hang across local lifecycle namespaces: bare `agents`, `mcp`, `skills`, `sandbox`, and `doctor` return bounded diagnostics, but natural explicit forms (`agents list`, `mcp list`, `skills list`, `sandbox status`, `doctor check`) zero-byte hang** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 22:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. Clean-home text probes showed the bare commands are usable: `agents` exits 0 (`No agents found.`), `mcp` exits 0 with `Configured servers 0`, `skills` exits 0 with inventory, `sandbox` exits 0 with sandbox state, and `doctor` exits 0 with the full health report. But adding the most natural explicit action caused zero-byte timeouts after 8s: `agents list`, `mcp list`, `skills list`, `sandbox status`, and `doctor check` all produced `stdout=0` and `stderr=0`. This broadens #467's plugin-specific `plugins list` hang into a shared parser/action-normalization bug: omitted default action works, but spelling the default action explicitly routes to a non-returning path. **Required fix shape:** (a) define a per-namespace action table where omitted action normalizes to the documented default (`list` for inventory namespaces, `status` for status namespaces, `check`/`run` only if supported); (b) reject unsupported explicit actions with typed bounded errors (`unknown_action`, `supported_actions[]`) instead of falling through to prompt/runtime dispatch; (c) make `agents list`, `mcp list`, and `skills list` equivalent to their bare forms; decide/document whether `sandbox status` and `doctor check` are aliases or unsupported; (d) add clean-home regression coverage for bare/default/unsupported explicit actions in text and JSON across agents, mcp, skills, plugins, sandbox, and doctor. **Why this matters:** users and claws naturally prefer explicit commands in automation (`mcp list`, `agents list`) because they are self-documenting. If explicit defaults hang while bare commands work, every script has to learn undocumented terse forms and cannot safely derive commands from help/usage text. Source: gaebal-gajae dogfood response to Clawhip message `1506416189616947210` on 2026-05-19.
469. **Unexpected positional arguments after local verbs zero-byte hang instead of returning bounded parse errors; even `version extra` hangs, proving the parser does not fail closed once a local subcommand has extra tokens** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 22:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. In isolated clean `HOME`/`CLAW_CONFIG_HOME`, bounded 8s probes of `version extra`, `status extra`, `doctor extra`, `sandbox extra`, `agents extra`, `mcp extra`, `skills extra`, and `plugins extra` all timed out with `stdout=0` and `stderr=0`. This is broader than #468's explicit-default-action hang: the extra token does not need to be a plausible action like `list` or `status`; any unexpected positional can route a local, no-auth diagnostic command into a non-returning path. It also refreshes the older extra-arg/fallthrough findings (#127/#147) with current-binary evidence where the failure mode is now a silent hang rather than a visible missing-credentials prompt. **Required fix shape:** (a) every local subcommand parser must declare accepted positional arity and reject extra args before prompt/runtime dispatch; (b) emit a typed bounded error such as `kind:"unexpected_argument"`, `command`, `argument`, `supported_usage`, and `supported_actions[]` where relevant; (c) in JSON-requested mode, route that envelope to the documented JSON stream with nonzero exit; (d) add regression coverage for `version extra`, `status extra`, `doctor extra`, `sandbox extra`, `agents extra`, `mcp extra`, `skills extra`, and `plugins extra` proving they return promptly and do not enter provider/prompt/plugin/session runtime. **Why this matters:** typos and stale wrapper scripts commonly append leftover tokens. A local health command must fail closed with usage guidance, not hang with no bytes; otherwise orchestrators cannot distinguish typo, parser deadlock, provider stall, or lifecycle startup block. Source: gaebal-gajae dogfood response to Clawhip message `1506423740597141665` on 2026-05-19.
470. **Root flag aliases fail closed on extra tokens, but the word alias `help` zero-byte hangs with any trailing token (`help extra`, `help --help`, `help --version`) instead of returning root help or a bounded arity error** — dogfooded 2026-05-19 from the `#clawcode-building-in-public` 23:00/23:30 UTC nudges on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. Clean-home probes showed the flag aliases are healthy/bounded: `--version` and `-V` exit 0 with version text; `--version extra`, `-V extra`, and `--help extra` exit 1 with `[error-kind: cli_parse]`. But the word help alias is fragile: `help extra`, `help --help`, and `help --version` each timed out after 6s with `stdout=0` and `stderr=0`. This narrows #469: not all root alias extra-token paths hang; the flag parser has a fail-closed path, while the `help` word alias enters the same non-returning command/prompt dispatch when trailing tokens are present. **Required fix shape:** (a) treat `help` as a first-class root command with strict arity: bare `help` renders root help; `help <topic>` either renders a static topic if supported or returns typed `unknown_help_topic`; extra flags like `help --help` and `help --version` must be handled locally; (b) never route `help ...` into provider/prompt/runtime dispatch; (c) make JSON mode preserve the same bounded behavior with `kind:"help"` or `kind:"unknown_help_topic"`; (d) add clean-home regression coverage for `help`, `help extra`, `help --help`, `help --version`, `--help extra`, `--version extra`, and `-V extra`. **Why this matters:** `help` is the recovery primitive users type when every other command is broken. If `--help extra` reports a parse error but `help extra` hangs silently, claws cannot rely on the documented `claw help` alias as a safe discovery surface. Source: gaebal-gajae dogfood response to Clawhip messages `1506431285977940009` and `1506438835775737939` on 2026-05-19.
471. **Root version aliases (`--version`, `-V`) have no JSON form and reject `--output-format json`, while the `version --output-format json` subcommand hangs under clean home — version provenance is not reliably machine-readable from the safest startup path** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 00:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with provenance preflight `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"` in the normal environment. In isolated clean `HOME`/`CLAW_CONFIG_HOME`, `version --output-format json` timed out after 6s with `stdout=0` and `stderr=0`, while `--version --output-format json`, `--output-format json --version`, `-V --output-format json`, and `--output-format json -V` all exited 1 with `[error-kind: cli_parse] unknown option: ...`. Plain `--version`/`-V` work in text mode, but there is no bounded JSON equivalent through the root flag aliases, and the subcommand JSON path is environment/placement-sensitive. This is a focused provenance/startup slice of #464: the one command claws use to prove binary identity has split semantics across root flags and subcommand forms. **Required fix shape:** (a) make `--version --output-format json` and `--output-format json --version` valid aliases for `version --output-format json`; (b) make `-V` support the same formatting contract; (c) ensure `version --output-format json` is config-free/help-free/provider-free and bounded under clean home; (d) emit a typed JSON parse error on stdout/stderr according to the project-wide JSON error-stream contract if an unsupported combination remains; (e) add clean-home regression coverage for all five forms above. **Why this matters:** binary provenance is the first fact every dogfood report and orchestrator needs. If root version flags only produce text and the JSON subcommand can hang, claws cannot safely establish which executable they are testing without relying on a special normal-environment preflight. Source: gaebal-gajae dogfood response to Clawhip message `1506446390212169839` on 2026-05-20.
472. **Malformed project `.claw.json` is silently treated as `loaded 0/N` while `doctor` still says `Config Status ok` and `runtime config loaded successfully`; `status` reports no config error at all** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 00:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. In a temp workspace with an invalid `.claw.json` containing truncated JSON (`{"mcpServers":{"bad":{}`), root recovery surfaces correctly stayed config-free: `--help`, `help`, `--version`, `-V`, and `version` all exited 0. But config-aware diagnostics silently downplayed the broken file: `status` exited 0 and reported `Config files loaded 0/5` with no parse error or degraded marker; `doctor` exited 0 with `Failures 0`, `Config Status ok`, `Summary runtime config loaded successfully`, and details `Config files loaded 0/1` plus the discovered malformed file path. This is not the older #143 fatal-status path; it is the opposite failure mode: broken config is detected enough to count as present/discovered, but the parse error is dropped and the health summary remains OK. **Required fix shape:** (a) preserve config parse/load errors per discovered file even when continuing with defaults; (b) make `doctor` config check `status:"fail"` or `"warn"` when any discovered config file fails to parse, never `ok`; (c) make `status` expose `config_load_error` / `config_files[].error` and a degraded marker while still reporting independent workspace fields; (d) distinguish `present_count`, `loaded_count`, and `failed_count`; (e) add regression coverage with malformed/truncated `.claw.json` proving root help/version remain config-free while status/doctor surface the parse error. **Why this matters:** a user with a broken config needs diagnostics to point at the exact file and parse problem. Saying `runtime config loaded successfully` while also saying `loaded 0/1` is contradictory and makes the tool look healthy while silently ignoring the user's intended MCP/plugins/permissions/model settings. Source: gaebal-gajae dogfood response to Clawhip message `1506453939443204247` on 2026-05-20.
473. **Slash-only command guidance is inconsistent: bare `claw compact` fails fast with useful guidance, but `claw compact --help` and `claw compact extra` zero-byte hang instead of returning the same guidance/help or a bounded arity error** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 01:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`. After confirming the remote `docs/roadmap-workdir-provenance` branch still had HEAD `e9db12d` (Jobdori's claimed `f997a1a8` was not present on origin at probe time), compact-surface probes showed `claw compact` exits 1 with a clear local message: `` `claw compact` is a slash command. Use `claw --resume SESSION.jsonl /compact` or start `claw` and run `/compact`.`` Direct `/compact` also exits 1 with interactive/resume guidance, and `/compact --confirm` returns a bounded unexpected-argument help block. But `compact --help` and `compact extra` each timed out after 6s with `stdout=0` and `stderr=0`. This is a parser/help arity bug distinct from the compaction-internals sentinel issue Jobdori described: the CLI already knows how to explain that `compact` is slash-only, but adding `--help` or any extra token bypasses that safe guidance path. **Required fix shape:** (a) route `compact --help` to static compact help/guidance before prompt/runtime dispatch; (b) route `compact <unexpected>` to a typed bounded `unexpected_argument` or `slash_only_command` error with resume usage; (c) make slash-only top-level shims share one helper so bare/--help/extra forms cannot drift; (d) add clean-home regressions for `compact`, `compact --help`, `compact extra`, `/compact`, `/compact --confirm`, and JSON-mode equivalents. **Why this matters:** users who follow the bare-command guidance and ask for help on the same word hit a silent hang. Recovery commands must converge toward usage, not fall off a different parser cliff as soon as a user adds `--help`. Source: gaebal-gajae dogfood response to Clawhip message `1506461484950093967` on 2026-05-20.
474. **Local diagnostic verbs and slash-only shims hang on `--help`/extra-token forms even when their bare or slash forms already have bounded guidance (`status --help`, `status extra`, `doctor --help`, `doctor extra`, `clear --help`, `clear --confirm`, `clear extra`)** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 01:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`; branch and origin were both `docs/roadmap-workdir-provenance@1208b9a` before editing. Bounded clean-home probes showed `claw clear` exits 1 with useful slash-only guidance, `/clear` and `/clear --confirm` return bounded interactive/resume guidance, and `/clear extra` returns a bounded usage block. But `clear --help`, `clear --confirm`, and `clear extra` each timed out after 6s with `stdout=0`/`stderr=0`. The same arity/help drift affects bare local diagnostics: `status --help`, `status extra`, `doctor --help`, and `doctor extra` also timed out after 6s with no bytes, despite bare `status`/`doctor` being local, bounded diagnostic commands in other probes. This generalizes #473 from `compact` to the parser layer: after a recognized local verb, trailing tokens are not routed through a command-specific help/arity table and can enter the non-returning prompt/runtime path. **Required fix shape:** (a) define per-command arity/help metadata for every local verb before prompt fallback; (b) `status --help` and `doctor --help` render static help; `status extra` and `doctor extra` return typed `unexpected_argument`; (c) `clear --help` renders slash/resume guidance, while `clear --confirm` either maps to `/clear --confirm` guidance or returns typed `slash_only_command` without dispatch; (d) centralize slash-only top-level shim handling for `compact`, `clear`, and future resume-capable slash commands; (e) add clean-home timeout-guarded regression coverage for all forms above. **Why this matters:** `status` and `doctor` are the commands operators reach for during startup/config breakage. If adding `--help` or a stale wrapper token makes them silently hang, the recovery surface is unreliable and automation cannot distinguish operator typo from runtime deadlock. Source: gaebal-gajae dogfood response to Clawhip message `1506469039562555573` on 2026-05-20.
475. **Local diagnostic commands (`status`, `doctor`, `sandbox`) have no bounded JSON output-form contract: bare text works, suffix `--output-format json` hangs, and prefix `--output-format json <command>` is rejected as a parse error** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 02:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`; branch and origin were both `docs/roadmap-workdir-provenance@9495dbe` before editing. Clean-home probes showed `status`, `doctor`, and `sandbox` exit 0 with useful human-readable text. But `status --output-format json`, `doctor --output-format json`, and `sandbox --output-format json` each timed out after 6s with `stdout=0` and `stderr=0`. The prefix spelling `--output-format json status|doctor|sandbox` exits 1 with `[error-kind: cli_parse] unknown option`, also with no JSON envelope. This is distinct from #474's help/arity hang: here the option is a documented global formatting concept already used by `version` and prompt mode, but local diagnostics neither honor it nor fail closed consistently. **Required fix shape:** (a) define a project-wide rule for `--output-format` placement and apply it to local diagnostics; (b) support `status --output-format json`, `doctor --output-format json`, and `sandbox --output-format json` with stable `kind:"status"|"doctor"|"sandbox"` envelopes; (c) either support prefix global placement or reject it with a typed JSON-capable parse envelope; (d) ensure these paths are config/provider/prompt-free and bounded under clean home; (e) add timeout-guarded clean-home regressions for bare text, suffix JSON, and prefix JSON forms. **Why this matters:** operators and orchestrators need machine-readable health snapshots during startup failures. If the only working forms are text and JSON requests hang, automation has to scrape prose or misclassify a formatting request as runtime deadlock. Source: gaebal-gajae dogfood response to Clawhip message `1506476585543532596` on 2026-05-20.
476. **Read-only inventory commands (`agents`, `mcp`, `skills`, `plugins`) have no bounded JSON/list contract: bare text works, suffix `--output-format json` hangs, prefix global JSON is rejected, and `skills` dumps a huge prose list with no machine-readable counts/paths** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 02:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`; branch and origin were both `docs/roadmap-workdir-provenance@bb2cf3f` before editing. Clean-home probes showed bare inventory commands are local and bounded: `agents` prints `No agents found.`, `mcp` prints a short `Configured servers 0` report, `plugins` prints two disabled bundled plugins, and `skills` prints `65 available skills` plus ~27KB of prose descriptions. But `agents --output-format json`, `mcp --output-format json`, `skills --output-format json`, and `plugins --output-format json` each timed out after 6s with `stdout=0`/`stderr=0`. Prefix forms `--output-format json agents|mcp|skills|plugins` exit 1 with `[error-kind: cli_parse] unknown option` and no JSON envelope. This is adjacent to #475 but distinct: these are inventory surfaces that orchestrators need for routing, MCP/plugin lifecycle checks, and skill selection, not just health diagnostics. **Required fix shape:** (a) provide stable JSON envelopes for each command (`kind:"agents"|"mcp"|"skills"|"plugins"`) with counts, source paths, enabled/disabled state, and parse/load errors where relevant; (b) make `--output-format json` suffix bounded and optionally support/predictably reject prefix placement via the global formatting contract; (c) add compact text/list modes for `skills` so default output is not an unbounded prose wall; (d) ensure inventory JSON does not initialize providers or interactive prompt runtime; (e) add clean-home timeout-guarded regressions for bare text, suffix JSON, and prefix JSON forms. **Why this matters:** claws need structured inventories to decide which skills/plugins/MCP servers are available. Scraping 27KB of human prose or hitting a silent hang on JSON makes routing brittle and masks lifecycle breakage. Source: gaebal-gajae dogfood response to Clawhip message `1506484138927067286` on 2026-05-20.
477. **`system-prompt` only works as a bare command; every documented modifier (`--date`, `--cwd`, `--help`, `--output-format json`) and even invalid modifier cases zero-byte hang, making prompt provenance/reproducibility unusable outside the current cwd/date defaults** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 03:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`; branch and origin were both `docs/roadmap-workdir-provenance@8afdb94` before editing. Bare `system-prompt` exits 0 and prints the expected prompt with dynamic context, including working directory, date, git status, recent commits, and `CLAUDE.md` content. But `system-prompt --date 2026-05-20`, `system-prompt --cwd /tmp --date 2026-05-20`, `system-prompt --help`, `system-prompt extra`, `system-prompt --date nope`, `system-prompt --cwd /definitely/not/here`, and `system-prompt --output-format json` each timed out after 6s with `stdout=0` and `stderr=0`. Prefix global JSON (`--output-format json system-prompt`) exits 1 with `[error-kind: cli_parse] unknown option`. This is worse than a missing JSON parity issue: the help text advertises `claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]`, but the advertised valid modifiers enter the non-returning path. **Required fix shape:** (a) parse `system-prompt` options before prompt/runtime fallback and make `--cwd`/`--date` valid, bounded, and deterministic; (b) validate `--date` format and nonexistent `--cwd` with typed errors; (c) implement `system-prompt --help`; (d) support or typed-reject `--output-format json` with a stable prompt-provenance envelope (`kind:"system_prompt"`, `cwd`, `date`, `project_context_sources`, `git_snapshot`, `text`); (e) add clean-home regression tests for bare, valid modifier, invalid modifier, help, extra arg, and JSON forms. **Why this matters:** `system-prompt` is the main way to debug prompt misdelivery and stale-context complaints. If users cannot pin cwd/date or get structured provenance, they cannot reproduce why a session received the wrong instructions. Source: gaebal-gajae dogfood response to Clawhip message `1506491684064723004` on 2026-05-20.
478. **`export` has a bounded bare no-session error, but every advertised flag/help form (`--help`, `--session`, `--output`, `--output-format json`) zero-byte hangs, so session artifact export cannot be scripted or debugged from the documented CLI surface** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 03:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with `./rust/target/debug/claw version --output-format json` reporting `git_sha:"25d663d"`; branch and origin were both `docs/roadmap-workdir-provenance@90a0d38` before editing. Clean-home probes showed bare `export` exits 1 with a good typed error: `[error-kind: no_managed_sessions]`, the partitioned `.claw/sessions/<fingerprint>/` path, and guidance to start `claw` or rerun with `--resume latest`. But `export --help`, `export extra`, `export --session latest`, `export --session definitely-missing`, `export --output /tmp/claw-export-test.md`, and `export --output-format json` each timed out after 6s with `stdout=0`/`stderr=0`. Prefix `--output-format json export` exits 1 with `[error-kind: cli_parse]` and no JSON envelope. The help text advertises `claw export [PATH] [--session SESSION] [--output PATH]`, but those flags are not safely parsed. **Required fix shape:** (a) parse `export` options before prompt/runtime fallback; (b) implement `export --help`; (c) make `--session latest`, missing session IDs, positional PATH, and `--output PATH` return bounded results/errors; (d) support or typed-reject `--output-format json` with an envelope including `kind:"export"`, session fingerprint/path, output path, and skipped/error reason; (e) add clean-home regressions for bare no-session, help, extra arg, missing/latest session, output path, positional path, and JSON forms. **Why this matters:** `export` is the evidence path for stale session, prompt misdelivery, and event/log opacity bugs. If bare no-session works but every real option hangs, operators cannot reliably produce or inspect artifacts when debugging clawability issues. Source: gaebal-gajae dogfood response to Clawhip message `1506499237972545576` on 2026-05-20.
479. **`skills` accepts listing/help surfaces, but unknown skill invocation names zero-byte hang instead of returning a typed `skill_not_found`/usage error, so typoed skill calls look like runtime stalls** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 04:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@4d52703` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home probes confirmed healthy bounded surfaces for `skills` (exit 0, `No skills found.`), `skills help`/`skills --help` (usage), and `skills --output-format json` (empty typed inventory). But the actual invocation/error path hangs silently: `skills garbage --output-format json`, `skills missing --output-format json`, `skills help missing --output-format json`, and `skills /nope --output-format json` each timed out after 6s with `stdout=0` and `stderr=0`. In contrast, sibling surfaces are bounded: `agents garbage --output-format json` exits 0 with a typed help envelope including `unexpected:"garbage"`, `mcp garbage --output-format json` does the same, and `skills install /nope --output-format json` exits 1 with a JSON error envelope. **Required fix shape:** (a) resolve the first post-`skills` token against installed skill names before falling into runtime prompt/tool execution; (b) for unknown names return a bounded JSON/text error with `kind:"skill_not_found"`, `name`, `available_count`, and a hint to run `claw skills list`; (c) reject extra args after `skills help` with typed usage instead of trying to invoke `help` as a skill; (d) preserve `skills list <extra>` behavior only if intentionally documented, otherwise reject the extra arg; (e) add clean-home regressions for list/help/json, unknown-name JSON/text, `help extra`, `/pathlike` unknown names, and install missing-path. **Why this matters:** skills are a plugin-like lifecycle surface. A typo or missing local skill should be diagnosable immediately; a zero-byte hang is indistinguishable from MCP startup deadlock or prompt misdelivery and makes automation wrappers unable to classify the failure. Source: gaebal-gajae dogfood response to Clawhip message `1506514333394403409` on 2026-05-20.
480. **`claw session` correctly says it is resume-only when bare, but `session --help` and `session list` zero-byte hang instead of returning bounded guidance, so users trying to discover session management from the advertised slash command get no usable recovery path** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 05:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@93f20df` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home probe: bare `claw session` exits 1 with a useful typed stderr error: `[error-kind: unknown] error: \`claw session\` is a slash command. Use \`claw --resume SESSION.jsonl /session\` or start \`claw\` and run \`/session\`.` But the natural discovery and list forms do not share that bounded path: `claw session --help` timed out after 6s with `stdout=0`/`stderr=0`, and `claw session list` timed out after 6s with `stdout=0`/`stderr=0`; a longer wrapper run had to be killed after the `session list` probe stopped producing output. **Required fix shape:** (a) treat `session` as an explicit top-level alias for resume-safe session inspection rather than falling into prompt/runtime dispatch; (b) implement `session --help` with the same usage as `/session [list|exists|switch|fork|delete]` plus the `--resume` requirement where applicable; (c) make `session list` return a bounded empty-list result or a typed `resume_required`/`no_managed_sessions` error rather than hanging; (d) support or typed-reject `--output-format json` for session discovery/list/exists forms; (e) add clean-home regressions for bare `session`, `session --help`, `session list`, `session exists missing`, and the documented `--resume latest /session list` no-session path. **Why this matters:** session management is the recovery surface for stale-session confusion and prompt misdelivery. A user who sees `/session` in help naturally tries `claw session --help` or `claw session list`; hanging there blocks the exact diagnostics needed to find or resume the broken session. Source: gaebal-gajae dogfood response to Clawhip message `1506521887231180986` on 2026-05-20.
481. **Slash-only top-level aliases (`files`, `hooks`, `memory`) emit a useful bare resume-only error, but `--help` hangs with zero output, so users cannot discover the documented resume-safe command shape from the command they just tried** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 05:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@51e6040` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home probes show the bare aliases are bounded: `claw files`, `claw hooks`, and `claw memory` each exit 1 with `[error-kind: unknown]` and a helpful message: `Use \`claw --resume SESSION.jsonl /<verb>\` or start \`claw\` and run \`/<verb>\`.` Their JSON forms are also bounded (`files --output-format json`, `hooks --output-format json`, `memory --output-format json`) with the same message in a JSON error envelope. But adding the most natural discovery flag hangs: `claw files --help`, `claw hooks --help`, and `claw memory --help` each timed out after 6s with `stdout=0`/`stderr=0`. In the same clean-home sweep, real top-level local verbs (`config`, `config --help`, `config env --output-format json`) returned immediately, proving this is specific to slash-only alias help fallback. **Required fix shape:** (a) centralize slash-only alias handling so `--help`/`help` never enter prompt/runtime dispatch; (b) return a bounded help page for every slash-only alias, including direct CLI usage (`claw --resume SESSION.jsonl /files`) and whether the command is resume-safe; (c) make JSON help/error envelopes include `kind:"slash_command_alias"`, `slash:"/files"`, `resume_required:true`, and supported resume forms; (d) add clean-home regressions for bare, `--help`, `help`, and JSON forms for `files`, `hooks`, `memory`, plus the already-found `session` sibling (#480). **Why this matters:** these are exactly the diagnostic surfaces users reach for during prompt misdelivery and stale-session debugging. A helpful bare error followed by a zero-byte hang on `--help` is a recovery dead-end. Source: gaebal-gajae dogfood response to Clawhip message `1506529436466675713` on 2026-05-20.
482. **Session-inspection slash aliases (`cost`, `stats`, `history`, `tokens`) have bounded bare/JSON resume-only errors, but `--help` hangs with zero output, extending the slash-alias help dead-end to the telemetry/recovery commands users need during stale-session debugging** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 06:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@111e7e8` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home sweep showed local direct verbs behave differently: `diff` returns bounded no-git text/JSON and rejects `diff --help` with a bounded `unexpected extra arguments`; interactive-only write verbs (`commit`, `pr`, `issue`) return bounded bare/JSON errors and `--help` prints the global help. The broken cluster is the resume-safe telemetry alias family: `claw cost`, `claw stats`, `claw history`, and `claw tokens` each exit 1 with a useful resume-only message (`Use \`claw --resume SESSION.jsonl /<verb>\` or start \`claw\` and run \`/<verb>\`.`), and their `--output-format json` forms return bounded JSON error envelopes, but `claw cost --help`, `claw stats --help`, `claw history --help`, and `claw tokens --help` time out after 6s with `stdout=0`/`stderr=0`; the sweep had to be killed while stuck at `tokens --help`. **Required fix shape:** (a) fold the resume-safe telemetry slash aliases into the centralized slash-only alias help handler proposed in #481; (b) for each alias, return bounded text/JSON help listing the `--resume SESSION.jsonl /<verb>` form, accepted optional args (`history [count]`), and JSON support status; (c) do not fall through to prompt/runtime dispatch when `--help` follows any known slash alias; (d) add clean-home regressions for bare, `--help`, `help`, and JSON forms for `cost`, `stats`, `history`, `tokens`, `cache`, and `providers`. **Why this matters:** these commands are the observability surface for event/log opacity and stale-session confusion. If the first attempt to ask for help on `/tokens` or `/history` silently hangs, operators cannot tell whether the session store, runtime, or help parser is broken. Source: gaebal-gajae dogfood response to Clawhip message `1506536984976425070` on 2026-05-20.
483. **Interactive-only slash aliases (`approve`, `deny`, `model`) hang on `--help`, while `permissions --help` already returns bounded inline usage, proving the alias-help behavior is inconsistent within the same command family** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 06:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@625b8b0` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home sweep: `claw approve`, `claw deny`, and `claw model` each exit 1 with a bounded interactive-only message (`Start \`claw\` and run \`/<verb>\` inside the REPL.`), and their `--output-format json` forms return bounded JSON error envelopes. But `claw approve --help`, `claw deny --help`, and `claw model --help` time out after 6s with `stdout=0`/`stderr=0`. Same sweep shows a nearby positive control: `claw permissions --help` exits 1 with the interactive-only message plus an inline usage block (`Usage /permissions [read-only|workspace-write|danger-full-access]`). Resume-safe siblings `cache`, `providers`, and `clear` also still hang on `--help`, matching #481/#482. **Required fix shape:** (a) extend the centralized slash-alias help handler to interactive-only aliases, not just resume-safe aliases; (b) model it after the existing bounded `permissions --help` behavior, returning the interactive-only reason plus per-command usage; (c) include aliases (`/approve` = `/yes`/`/y`, `/deny` = `/no`/`/n`) in help text/JSON; (d) add clean-home regressions for `approve --help`, `deny --help`, `model --help`, `debug-tool-call --help`, and assert `permissions --help` remains bounded. **Why this matters:** permission prompts and model switching are high-friction startup/operator surfaces. If users naturally type `claw approve --help` or `claw model --help`, the CLI must explain that these are REPL-only instead of silently hanging and looking like a dead runtime. Source: gaebal-gajae dogfood response to Clawhip message `1506544532026687562` on 2026-05-20.
484. **Interactive helper slash aliases (`debug-tool-call`, `bughunter`, `teleport`) hang on `--help`, and argument-bearing invocations like `bughunter src --output-format json` also hang instead of returning an interactive-only error, so high-value debug/navigation entrypoints are misclassified as dead runtime work** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 07:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@dbd04ad` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home sweep: `claw bughunter` and `claw teleport` exit 1 with bounded interactive-only messages (`Start \`claw\` and run \`/<verb>\` inside the REPL.`), but `claw debug-tool-call --help`, `claw bughunter --help`, and `claw teleport --help` each timed out after 6s with `stdout=0`/`stderr=0`. Worse, a realistic argument-bearing form, `claw bughunter src --output-format json`, also timed out after 6s with zero output instead of saying `/bughunter` is REPL-only; the sweep had to be killed after `teleport --help` hung. **Required fix shape:** (a) classify debug/navigation slash aliases before prompt/runtime fallback, including when they carry positional arguments; (b) return bounded interactive-only help for `debug-tool-call`, `bughunter [scope]`, and `teleport <symbol-or-path>`; (c) for JSON mode, return an error envelope with `kind:"slash_command_repl_only"`, `slash`, `args`, and `usage`; (d) add clean-home regressions for bare, `--help`, JSON, and argument-bearing forms (`bughunter src`, `teleport main`, `ultraplan task`, `release-notes`). **Why this matters:** these are the tools operators reach for to diagnose brittle tests, find files, or replay tool calls. Hanging before the REPL boundary makes a simple usage mistake indistinguishable from prompt misdelivery or a stuck MCP/plugin lifecycle. Source: gaebal-gajae dogfood response to Clawhip message `1506552081589342383` on 2026-05-20.
485. **Planning/output slash aliases (`ultraplan`, `release-notes`) still enter zero-byte hang paths on `--help`, and `ultraplan <task> --output-format json` hangs instead of returning a REPL-only error, while nearby `release-notes --output-format json` is bounded — proving slash-alias argument classification is incomplete, not uniformly broken** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 07:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@51a450e` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home probes: `claw ultraplan --help` timed out after 6s with `stdout=0`/`stderr=0`; `claw ultraplan task --output-format json` also timed out with zero output; `claw release-notes --help` timed out with zero output. But `claw release-notes --output-format json` exits 1 with a bounded JSON error (`Start \`claw\` and run \`/release-notes\` inside the REPL.`). Nearby non-slash/plugin-like controls are bounded: `agents missing --output-format json` returns typed help with `unexpected:"missing"`; `mcp show missing --output-format json` returns `found:false`; `mcp list extra --output-format json` returns `unsupported_action`. **Required fix shape:** (a) add `ultraplan` and `release-notes` to the centralized slash-alias help/REPL-only classifier; (b) classify slash aliases before attempting to treat positional args as prompt text or runtime work; (c) return bounded text/JSON help for `ultraplan [task]` and `release-notes`, including whether any direct non-REPL invocation is intentionally unsupported; (d) add clean-home regressions for `ultraplan --help`, `ultraplan task --output-format json`, `release-notes --help`, `release-notes --output-format json`, plus positive controls for `agents`/`mcp` bounded unknown handling. **Why this matters:** `/ultraplan` is a prompt-construction/planning surface and `/release-notes` is an artifact/reporting surface. If direct invocations silently hang, users cannot distinguish unsupported REPL-only usage from prompt misdelivery, model startup, or artifact-generation deadlock. Source: gaebal-gajae dogfood response to Clawhip message `1506559635996414132` on 2026-05-20.
486. **`prompt` has bounded help and missing-prompt errors, but real one-shot prompt invocations zero-byte hang under clean-home/no-credentials conditions instead of returning an auth/config error, so wrappers cannot distinguish model startup from CLI dispatch deadlock** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 08:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@8a2e133` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home probes: `claw prompt --help` exits 0 with the global help, and `claw prompt --output-format json` exits 1 with a bounded JSON error (`prompt subcommand requires a prompt string`). But actual prompt dispatch hangs silently: `claw prompt hello --output-format json` timed out after 6s with `stdout=0`/`stderr=0`; the prefix form `claw --output-format json prompt hello` also timed out with zero output; `claw prompt --compact hello` timed out with zero output, and the sweep had to be killed before the remaining modifier probes. In a clean-home environment with isolated `HOME`, `CLAW_CONFIG_HOME`, `XDG_CONFIG_HOME`, and `XDG_DATA_HOME`, a one-shot prompt without usable credentials should fail quickly with a typed auth/config error, not enter an opaque silent wait. **Required fix shape:** (a) add a preflight auth/provider/config check before one-shot prompt runtime startup; (b) if credentials/config are missing, return bounded text/JSON error (`kind:"missing_credentials"` or provider-specific typed error) before opening any long-running runtime path; (c) preserve bounded behavior for `prompt --help` and missing prompt string; (d) add clean-home regressions for `prompt hello --output-format json`, `--output-format json prompt hello`, `prompt --compact hello`, and `--compact prompt hello` verifying nonzero bounded error with stderr/stdout body; (e) include elapsed-time assertion so prompt startup failures cannot regress into hangs. **Why this matters:** `prompt` is the primary non-interactive automation surface. A zero-byte hang on a basic clean-home one-shot prompt turns missing setup into event/log opacity and makes CI/wrappers classify the CLI as dead rather than misconfigured. Source: gaebal-gajae dogfood response to Clawhip message `1506567181570277556` on 2026-05-20.
487. **`prompt` validates missing strings and bad model syntax before runtime, but unknown/permissive post-prompt flags are swallowed into the runtime path and hang, so typoed one-shot modifiers become silent prompt misdelivery instead of CLI parse errors** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 08:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@32961cf` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home probes: bare shorthand `claw hello` and `claw --output-format json hello` are bounded unknown-subcommand errors with a hint to use `claw prompt -- ...`; global unknown flags (`claw --definitely-unknown hello`) are bounded `cli_parse`; `claw prompt --output-format json` is a bounded missing-prompt error; `claw prompt hello --model bad` is a bounded invalid-model-syntax error. But `claw prompt hello --definitely-unknown`, `claw prompt hello --foo`, `claw prompt hello --allowedTools read`, and `claw prompt hello --permission-mode read-only` each entered runtime and timed out after 6s instead of validating or rejecting the modifier; the `--permission-mode` case even emitted only spinner control bytes (`⠋ 🦀 Thinking...`) before hanging. **Required fix shape:** (a) give `prompt` a strict subcommand-specific parser that separates prompt text from supported modifiers; (b) reject unknown post-prompt flags with `kind:"cli_parse"` and echo the offending flag; (c) support documented global modifiers consistently before or after `prompt` only if intentionally allowed, otherwise reject with a hint to put them before `prompt`; (d) in JSON mode, never emit spinner/control bytes and always return a bounded JSON error for preflight failures; (e) add clean-home regressions for `prompt hello --foo`, `prompt hello --allowedTools read`, `prompt hello --permission-mode read-only`, `prompt hello --model bad`, and global-vs-post-subcommand flag placement. **Why this matters:** one-shot `prompt` is the automation entrypoint. If a typoed or misplaced flag silently becomes part of the model runtime path, wrappers cannot tell whether the prompt was delivered, rejected, or is waiting on model startup. Source: gaebal-gajae dogfood response to Clawhip message `1506574734610137200` on 2026-05-20.
488. **`status --output-format json` reports only git dirty counts (`changed_files`, `untracked_files`) but omits the actual changed/untracked path list, so automation cannot identify the files that make a workspace dirty without shelling out to git** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 09:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@92e97e0` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Clean-home git fixture: initialized a repo with one committed tracked file, then added untracked `untracked.txt` plus untracked `.gitignore` (with ignored `ignored.log`). `claw status --output-format json` returned `workspace.git_state:"dirty · 2 files · 2 untracked"`, `changed_files:2`, `staged_files:0`, `unstaged_files:0`, and `untracked_files:2`, but the JSON contained neither `untracked.txt` nor `.gitignore` anywhere; `ignored.log` was correctly absent, but there is no positive path inventory for the two files counted as dirty. Text status similarly summarizes counts without paths. **Required fix shape:** (a) add `workspace.changed_file_paths` or structured `workspace.git_files:{staged:[], unstaged:[], untracked:[]}` to status JSON; (b) cap the list with `truncated:true` and `total` fields for large repos; (c) preserve ignore behavior (ignored files stay absent unless an explicit include-ignored option lands); (d) add fixture regressions proving untracked `.gitignore` and regular untracked files appear by path while ignored files do not. **Why this matters:** status is the main machine-readable workspace preflight. Counts alone are insufficient for stale-branch/dirty-worktree gating, cleanup decisions, or explaining why a launch is blocked; wrappers must currently run their own `git status --porcelain`, duplicating logic and losing parity with claw's own dirty classification. Source: gaebal-gajae dogfood response to Clawhip message `1506582285292535818` on 2026-05-20.
489. **`status --output-format json` branch freshness is computed from local remote-tracking refs only and does not fetch or report ref age, so a branch can be reported `fresh:true behind:0` while the remote already has new commits** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 09:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@d541130` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Fixture: created a repo with `origin/master`, pushed initial commit, made one local commit (ahead 1), then from a second clone pushed one remote commit without fetching in the first worktree. Before `git fetch`, `claw status --output-format json` reported `workspace.branch_freshness:{"ahead":1,"behind":0,"fresh":true,"upstream":"origin/master"}` and `git_state:"clean"`. After a manual `git fetch`, the same command reported `ahead:1, behind:1, fresh:false`. This means the preflight freshness field can be stale-but-green whenever the local remote-tracking ref is old. **Required fix shape:** (a) either fetch (bounded/optional) before computing freshness, or expose `remote_ref_observed_at` / `fetch_age_seconds` and `freshness_source:"local_ref"`; (b) if no recent fetch occurred, mark freshness as `unknown` or `stale_reference` rather than `fresh:true`; (c) add a `--refresh`/`--no-refresh` policy if network access is intentionally avoided; (d) add fixture regression with a bare remote + second clone proving status does not report `fresh:true` from stale local refs. **Why this matters:** stale-branch confusion is a core clawability gap. Orchestrators gating launches/merges on `branch_freshness.fresh` will make the wrong decision if `status` presents old local refs as authoritative remote freshness. Source: gaebal-gajae dogfood response to Clawhip message `1506589831097221120` on 2026-05-20.
490. **`status`/`doctor` still run boot-preflight git metadata probes with blocking `git` subprocesses and no deadline, so slow `rev-parse`/branch/root discovery can zero-byte hang local diagnostics before any JSON is emitted** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 10:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@edcf5bf` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Fixture: clean isolated repo plus a fake `git` shim that sleeps 20s only for metadata probes (`rev-parse --is-inside-work-tree`, `rev-parse --git-dir`, `branch --show-current`, `rev-parse --show-toplevel`) and delegates all other git commands to `/usr/bin/git`. `claw status --output-format json` timed out after 6s with `stdout=0`/`stderr=0`; `claw doctor --output-format json` did the same. A control shim that delayed only `fetch`/`ls-remote` did not affect status/doctor, confirming the hang is local metadata probing, not network refresh. Code path: `build_boot_preflight_snapshot` calls `run_git_bool` and `run_git_capture_in` with `.output()` and no timeout; `parse_git_status_metadata_for` calls `resolve_git_branch_for` (`branch --show-current`, fallback `rev-parse --abbrev-ref HEAD`) and `find_git_root_in` (`rev-parse --show-toplevel`) similarly. **Required fix shape:** (a) route all local diagnostic git subprocesses through a shared `git_with_timeout(cwd,args,deadline)` helper; (b) use `--no-optional-locks` for read-only git probes; (c) on timeout, return bounded JSON with `git_probe_timeout`/`unknown` fields instead of aborting the whole status/doctor response; (d) add regressions with a fake `git` shim proving status/doctor still return within deadline and mark git metadata degraded. **Why this matters:** status and doctor are supposed to be the escape hatches when startup is broken. If local git metadata can hang them before emitting JSON, stale-branch and boot-preflight diagnostics fail exactly when a repo or filesystem is slow/locked. Source: gaebal-gajae dogfood response to Clawhip message `1506597387534209085` on 2026-05-20.
491. **`status`, `doctor`, and direct `diff` all block on dirty-state/diff git probes with no timeout, so a slow `git status` or `git diff` makes every local diagnostic surface zero-byte hang** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 10:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@d5aa815` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Fixture: clean isolated git repo plus fake `git` shim that sleeps 20s only for dirty/diff probes (`status --short`, `diff --cached`, and `diff`) and delegates all other git commands to `/usr/bin/git`. Results: `claw status --output-format json` timed out after 6s with `stdout=0`/`stderr=0`; `claw doctor --output-format json` timed out with zero output; direct `claw diff --output-format json` also timed out with zero output. This is distinct from the metadata-probe hang in #490: even when branch/root metadata is fast, dirty-state and diff collection can deadlock the supposedly local escape-hatch commands before they emit any degraded JSON. **Required fix shape:** (a) route `read_git_status`, `read_git_diff`, and direct `diff` command helpers through shared timeout-aware git execution; (b) emit partial/degraded status JSON with `git_status_timeout:true` and omit/cap diff payload instead of blocking; (c) make direct `claw diff --output-format json` return `kind:"diff", result:"git_timeout"` with command/stage metadata; (d) add fake-git shim regressions for slow `git status`, `git diff --cached`, and `git diff` proving status/doctor/diff stay bounded. **Why this matters:** dirty-state and diff are central to stale-branch, cleanup, and prompt-context decisions. If they can hang the health commands, operators cannot tell whether the repo is dirty, the runtime is stuck, or git itself is wedged. Source: gaebal-gajae dogfood response to Clawhip message `1506604934555242546` on 2026-05-20.
492. **`system-prompt --output-format json` and `status --output-format json` can both zero-byte hang on `GitContext`'s unbounded branch/log/staged-file probes, so prompt provenance and local diagnostics share the same startup deadlock surface** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 11:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@56555a3` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Fixture: clean isolated git repo plus fake `git` shim that sleeps 20s only for `rev-parse --abbrev-ref HEAD`, `log --oneline`, and `diff --cached --name-only` (the `GitContext::detect` branch/recent-commits/staged-files probes) while delegating all other git commands to `/usr/bin/git`. Results: `claw system-prompt --output-format json` timed out after 6s with `stdout=0`/`stderr=0`; `claw status --output-format json` also timed out with zero output. Earlier controls showed shims delaying unrelated `fetch`/`ls-remote` do not affect these commands. **Required fix shape:** (a) route `GitContext::detect` probes through the same timeout-aware git helper as #490/#491; (b) make `system-prompt` emit a bounded degraded prompt-provenance envelope when git context times out (`git_context:{status:"timeout", branch:null, recent_commits_truncated:true}`) instead of hanging; (c) make `status` omit/degrade `GitContext` fields independently from boot-preflight metadata; (d) add fake-git shim regressions for each GitContext probe (`rev-parse --abbrev-ref`, `log --oneline`, `diff --cached --name-only`) across both `system-prompt` and `status`. **Why this matters:** `system-prompt` is the primary prompt-misdelivery/provenance debugger. If the prompt renderer itself blocks on git context before emitting JSON, users cannot inspect what prompt would have been delivered when startup is slow or git is locked. Source: gaebal-gajae dogfood response to Clawhip message `1506612484520542258` on 2026-05-20.
493. **`status` and `doctor` block on `tmux --version` availability checks with no timeout, so a wedged tmux binary/socket makes the local health surfaces zero-byte hang even though prompt rendering still works** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 11:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@2bf6924` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Fixture: clean isolated git repo plus fake `tmux` shim at the front of `PATH` that sleeps 20s for every invocation; `git` was normal. Results: `claw status --output-format json` timed out after 6s with `stdout=0`/`stderr=0`; `claw doctor --output-format json` also timed out with zero output; control `claw system-prompt --output-format json` exited 0 with prompt JSON under the same fake-tmux environment. Code path: `build_boot_preflight_snapshot` populates `required_binaries` via `command_available("tmux")`, which runs `tmux --version` through blocking `.output()` and no deadline. **Required fix shape:** (a) route `command_available` checks through a small timeout helper; (b) mark slow binary probes as `{name:"tmux", available:null, timeout:true}` instead of blocking the entire status/doctor response; (c) avoid invoking `tmux` at all when `$TMUX` is absent if the field is only informational, or cache the availability probe; (d) add fake-binary shim regressions for slow `tmux` and slow `git --version` proving status/doctor stay bounded. **Why this matters:** status/doctor are the recovery surfaces for tmux/session lifecycle breakage. If a broken `tmux` itself prevents the health command from returning JSON, orchestrators lose the exact diagnostic path needed to explain session/pane failures. Source: gaebal-gajae dogfood response to Clawhip message `1506620033886060665` on 2026-05-20.
494. **`status`/`doctor` also block on `git --version` binary-availability checks with no timeout, while `diff` and `system-prompt` still work, so an unhealthy git binary can kill the health surfaces before they report degraded tool availability** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 12:00 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@4d185f0` and binary `./rust/target/debug/claw` built from source SHA `25d663d`. Fixture: clean isolated git repo plus fake `git` shim at the front of `PATH` that sleeps 20s only for `git --version` and delegates all other git commands to `/usr/bin/git`. Results: `claw status --output-format json` timed out after 6s with `stdout=0`/`stderr=0`; `claw doctor --output-format json` timed out with zero output. Positive controls under the same environment: `claw diff --output-format json` returned `{"kind":"diff","result":"clean"...}` and `claw system-prompt --output-format json` returned prompt JSON, proving ordinary git operations were fine and the hang is specifically `command_available("git")` in boot preflight `required_binaries`. **Required fix shape:** (a) same timeout-aware binary probe as #493, but cover `git` and `claw` too; (b) represent slow probes as timeout/degraded availability instead of blocking status/doctor; (c) prefer `which`/PATH existence plus optional bounded version string over mandatory `--version`; (d) add fake-binary regressions for slow `git --version`, slow `tmux --version`, and slow current-exe/version probe. **Why this matters:** status/doctor should explain missing or broken binaries. If the binary availability probe itself hangs, health checks fail before they can say `git` is unhealthy, forcing operators back to shell debugging. Source: gaebal-gajae dogfood response to Clawhip message `1506627582756651018` on 2026-05-20.
495. **The actual `PermissionEnforcer::check_bash` read-only heuristic still whitelists `tee`, so `tee file.txt` can write in read-only mode despite the richer `bash_validation` module correctly classifying `tee` as a write command** — dogfooded 2026-05-20 from the `#clawcode-building-in-public` 12:30 UTC nudge on `/home/bellman/Workspace/claw-code-pr2967` with branch/origin `docs/roadmap-workdir-provenance@916bf5f`. Code inspection shows two divergent validators: `rust/crates/runtime/src/bash_validation.rs` defines `WRITE_COMMANDS` including `tee` and would block it in read-only mode, but `PermissionEnforcer::check_bash` does not call that pipeline. It calls local `is_read_only_command` in `permission_enforcer.rs`, whose allowlist explicitly includes `tee` and only rejects commands containing `" > "`, `" >> "`, `"-i "`, or `"--in-place"`. Plain `tee out.txt` writes to `out.txt` without any redirection token, so `is_read_only_command("tee out.txt")` returns true and `check_bash` allows it under `PermissionMode::ReadOnly`. **Required fix shape:** (a) remove `tee` from `is_read_only_command` or, better, replace that local heuristic with the canonical `bash_validation::validate_command`/`classify_command` pipeline; (b) add regression tests proving `tee out.txt`, `tee -a out.txt`, `printf hi | tee out.txt`, and `cat a | tee b` are denied in read-only mode; (c) add a consistency test that every `WRITE_COMMANDS` entry in `bash_validation` is denied by `PermissionEnforcer::check_bash` in read-only mode. **Why this matters:** permission-mode enforcement is only as strong as the runtime path actually used. Having a stricter validator module sitting unused while `check_bash` allows a common write tool creates a real read-only bypass and contradicts the documented command semantics. Source: gaebal-gajae dogfood response to Clawhip message `1506635133468282950` on 2026-05-20.

49
SECURITY.md Normal file
View File

@@ -0,0 +1,49 @@
# Security Policy
## Supported versions
Security fixes target the current `main` branch and the latest published
release artifacts when available. Older experimental branches are not supported
unless a maintainer explicitly marks them as supported.
## Reporting a vulnerability
Please do **not** open a public issue for a suspected vulnerability. Use GitHub
private vulnerability reporting for `ultraworkers/claw-code` when available, or
contact a maintainer through the repository's published support channel with a
minimal, non-destructive reproduction.
Include:
- affected command, crate, or workflow;
- operating system and shell, especially for Windows/PowerShell path issues;
- whether live credentials, MCP servers, plugins, or workspace filesystem
access are involved;
- expected impact and any safe proof-of-concept steps.
Do not include real API keys, private prompts, session transcripts with secrets,
or exploit payloads that modify third-party systems.
## Scope
In scope:
- workspace path traversal or symlink escapes;
- permission bypasses, sandbox misreporting, or unsafe tool execution;
- credential disclosure in logs, JSON output, telemetry, docs, or examples;
- plugin, hook, MCP, provider, or config behavior that can unexpectedly execute
code or leak secrets.
Out of scope:
- social engineering;
- denial-of-service without a practical security impact;
- issues that require already-compromised local developer credentials;
- reports against third-party providers or upstream tools without a Claw Code
integration issue.
## Handling expectations
Maintainers will acknowledge valid private reports as soon as practical, keep
discussion private until a fix or mitigation is available, and credit reporters
when requested and appropriate.

24
SUPPORT.md Normal file
View File

@@ -0,0 +1,24 @@
# Support
Use the lightest support path that fits the request:
- **Usage questions:** start with [USAGE.md](./USAGE.md) and
[rust/README.md](./rust/README.md).
- **Bugs or regressions:** open a GitHub issue with the command, OS/shell,
expected behavior, actual behavior, and relevant non-secret output.
- **Security issues:** follow [SECURITY.md](./SECURITY.md) instead of opening a
public issue.
- **Community discussion:** use the UltraWorkers Discord linked from
[README.md](./README.md).
When asking for help, include:
```text
claw --version
claw doctor
operating system and shell
command you ran
```
Redact API keys, bearer tokens, private prompts, session transcripts, and local
paths that reveal sensitive information before sharing output.

View File

@@ -31,7 +31,7 @@ cd rust
cargo build --workspace
```
The CLI binary is available at `rust/target/debug/claw` after a debug build. Make the doctor check above your first post-build step.
The CLI binary is available at `rust/target/debug/claw` after a debug build (`rust\target\debug\claw.exe` on Windows). Make the doctor check above your first post-build step. For PowerShell-first install, release ZIP, PATH, provider-switching, and Windows/WSL notification examples, see [`docs/windows-install-release.md`](./docs/windows-install-release.md).
## Quick start
@@ -230,9 +230,37 @@ export ANTHROPIC_AUTH_TOKEN="anthropic-oauth-or-proxy-bearer-token"
**If you meant a different provider:** if `claw` reports missing Anthropic credentials but you already have `OPENAI_API_KEY`, `XAI_API_KEY`, or `DASHSCOPE_API_KEY` exported, you most likely forgot to prefix the model name with the provider's routing prefix. Use `--model openai/gpt-4.1-mini` (OpenAI-compat / OpenRouter / Ollama), `--model grok` (xAI), or `--model qwen-plus` (DashScope) and the prefix router will select the right backend regardless of the ambient credentials. The error message now includes a hint that names the detected env var.
### Windows PowerShell provider switching
The same provider rules work in PowerShell. Use placeholder values in docs and tests; put real keys only in your private environment. Remove unrelated provider env vars when validating a switch so failures are easy to diagnose.
`CLAUDE_CODE_PROVIDER` is not required for normal Claw routing; prefer explicit model prefixes such as `openai/` and provider-specific env vars so PowerShell examples stay portable.
```powershell
# Anthropic direct
$env:ANTHROPIC_API_KEY = "sk-ant-REPLACE_ME"
Remove-Item Env:\OPENAI_BASE_URL -ErrorAction SilentlyContinue
Remove-Item Env:\OPENAI_API_KEY -ErrorAction SilentlyContinue
.\target\debug\claw.exe --model "sonnet" prompt "reply with ready"
# OpenAI-compatible gateway / OpenRouter
Remove-Item Env:\ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
$env:OPENAI_BASE_URL = "https://openrouter.ai/api/v1"
$env:OPENAI_API_KEY = "sk-or-v1-REPLACE_ME"
.\target\debug\claw.exe --model "openai/gpt-4.1-mini" prompt "reply with ready"
# Local OpenAI-compatible server
$env:OPENAI_BASE_URL = "http://127.0.0.1:11434/v1"
Remove-Item Env:\OPENAI_API_KEY -ErrorAction SilentlyContinue
.\target\debug\claw.exe --model "llama3.2" prompt "reply with ready"
```
See the full [Windows install and release quickstart](./docs/windows-install-release.md) for release artifact setup, persistent `setx` usage, and WSL notes.
## Local Models
`claw` can talk to local servers and provider gateways through either Anthropic-compatible or OpenAI-compatible endpoints. Use `ANTHROPIC_BASE_URL` with `ANTHROPIC_AUTH_TOKEN` for Anthropic-compatible services, or `OPENAI_BASE_URL` with `OPENAI_API_KEY` for OpenAI-compatible services.
`claw` can talk to local servers and provider gateways through either Anthropic-compatible or OpenAI-compatible endpoints. Use `ANTHROPIC_BASE_URL` with `ANTHROPIC_AUTH_TOKEN` for Anthropic-compatible services, or `OPENAI_BASE_URL` with `OPENAI_API_KEY` for OpenAI-compatible services. For copyable Ollama, llama.cpp, vLLM, raw `/v1/chat/completions`, and local skills install examples, see [`docs/local-openai-compatible-providers.md`](./docs/local-openai-compatible-providers.md).
### Anthropic-compatible endpoint
@@ -306,7 +334,7 @@ Reasoning variants (`qwen-qwq-*`, `qwq-*`, `*-thinking`) automatically strip `te
The OpenAI-compatible backend also serves as the gateway for **OpenRouter**, **Ollama**, and any other service that speaks the OpenAI `/v1/chat/completions` wire format — just point `OPENAI_BASE_URL` at the service.
**Model-name prefix routing:** If a model name starts with `openai/`, `gpt-`, `qwen/`, or `qwen-`, the provider is selected by the prefix regardless of which env vars are set. This prevents accidental misrouting to Anthropic when multiple credentials exist in the environment.
**Model-name prefix routing:** If a model name starts with `openai/`, `gpt-`, `qwen/`, `qwen-`, `kimi/`, or `kimi-`, the provider is selected by the prefix regardless of which env vars are set. This prevents accidental misrouting to Anthropic when multiple credentials exist in the environment. For the default OpenAI API, `openai/` is a routing prefix and is stripped before the request hits the wire. For a custom `OPENAI_BASE_URL`, slash-containing OpenAI-compatible slugs (for example OpenRouter-style `openai/gpt-4.1-mini`) are preserved so the gateway receives the model ID it expects.
### Tested models and aliases
@@ -320,8 +348,11 @@ These are the models registered in the built-in alias table with known token lim
| `grok` / `grok-3` | `grok-3` | xAI | 64 000 | 131 072 |
| `grok-mini` / `grok-3-mini` | `grok-3-mini` | xAI | 64 000 | 131 072 |
| `grok-2` | `grok-2` | xAI | — | — |
| `kimi` | `kimi-k2.5` | DashScope | 16 384 | 256 000 |
| `gpt-4.1` / `gpt-4.1-mini` / `gpt-4.1-nano` | same | OpenAI-compatible | 32 768 | 1 047 576 |
| `gpt-5.4` / `gpt-5.4-mini` / `gpt-5.4-nano` | same | OpenAI-compatible | 128 000 | 1 000 000 / 400 000 |
Any model name that does not match an alias is passed through verbatim. This is how you use OpenRouter model slugs (`openai/gpt-4.1-mini`), Ollama tags (`llama3.2`), or full Anthropic model IDs (`claude-sonnet-4-20250514`).
Any model name that does not match an alias is passed through verbatim after provider routing is resolved. This is how you use OpenRouter model slugs (`openai/gpt-4.1-mini` with a custom `OPENAI_BASE_URL`), Ollama tags (`llama3.2`), or full Anthropic model IDs (`claude-sonnet-4-20250514`).
### User-defined aliases
@@ -343,11 +374,29 @@ Local project settings override user-level settings. Aliases resolve through the
1. If the resolved model name starts with `claude` → Anthropic.
2. If it starts with `grok` → xAI.
3. Otherwise, `claw` checks which credential is set: `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN` first, then `OPENAI_API_KEY`, then `XAI_API_KEY`.
4. If nothing matches, it defaults to Anthropic.
3. If it starts with `openai/` or `gpt-` → OpenAI-compatible.
4. If it starts with `qwen/`, `qwen-`, `kimi/`, or `kimi-` → DashScope-compatible OpenAI wire format.
5. If `OPENAI_BASE_URL` and `OPENAI_API_KEY` are set, unknown model names route to the OpenAI-compatible client for local/gateway servers.
6. Otherwise, `claw` checks which credential is set: Anthropic first, then OpenAI, then xAI. If only `OPENAI_BASE_URL` is set, it still routes to OpenAI-compatible for authless local servers.
7. If nothing matches, it defaults to Anthropic.
### Provider diagnostics and custom OpenAI-compatible parameters
The API layer exposes a provider diagnostics snapshot via `api::provider_diagnostics_for_model(model)`. It reports the resolved provider, auth/base-url environment variables, default base URL, whether the provider uses the OpenAI-compatible wire format, whether reasoning tuning parameters are stripped, whether DeepSeek V4 reasoning history is preserved, proxy support, extra-body support, and whether slash-containing model IDs are preserved for custom OpenAI-compatible gateways.
For gateway features that are not first-class request fields yet, `MessageRequest::extra_body` passes through provider-specific JSON parameters such as `web_search_options` or `parallel_tool_calls`. Core protocol fields (`model`, `messages`, `stream`, `tools`, `tool_choice`, `max_tokens`, and `max_completion_tokens`) are protected and cannot be overridden through `extra_body`.
## File context and navigation
Use `@path/to/file` in prompts to submit repository files as context, for example `Read @src/app.ts and explain the bug`, `Compare @old.md and @new.md`, or `Use @logs/error.txt as context and suggest a fix`. Prompt history, `Ctrl-r`, and long-output scrolling come from your shell, terminal, or tmux rather than from Claw itself. See [`docs/navigation-file-context.md`](./docs/navigation-file-context.md) for scrollback, attachment, and secret-redaction guidance.
## FAQ
### Is Claw Code Claude-only?
No. Claw Code is a Claude-Code-shaped workflow/runtime, not a Claude-only product. It can target Anthropic and OpenAI-compatible/provider-routed/local models depending on config. Non-Claude providers may require stricter response-shape and tool-call compatibility, so some workflows can be rougher than first-party Anthropic/OpenAI paths; provider-specific identity leaks are bugs, not product intent. See [`docs/local-openai-compatible-providers.md`](./docs/local-openai-compatible-providers.md) for local provider examples.
### What about Codex?
The name "codex" appears in the Claw Code ecosystem but it does **not** refer to OpenAI Codex (the code-generation model). Here is what it means in this project:
@@ -401,6 +450,18 @@ let client = build_http_client_with(&config).expect("proxy client");
- Empty values are treated as unset, so leaving `HTTPS_PROXY=""` in your shell will not enable a proxy.
- If a proxy URL cannot be parsed, `claw` falls back to a direct (no-proxy) client so existing workflows keep working; double-check the URL if you expected the request to be tunnelled.
## Skills
Use `/skills list` in the interactive REPL or `claw skills --output-format json` from the direct CLI to inspect installed skills. For offline/local installs, install the directory that contains `SKILL.md`, then verify the discovered name before invoking it:
```text
/skills install /absolute/path/to/my-skill
/skills list
/skills my-skill
```
If install succeeds but invocation fails with a provider HTTP error, treat provider setup separately: run `claw doctor` and a one-shot prompt smoke test before reinstalling the skill. See [`docs/local-openai-compatible-providers.md`](./docs/local-openai-compatible-providers.md#local-skills-install-from-disk) for the full checklist.
## Common operational commands
```bash

View File

@@ -9,7 +9,8 @@ This document describes model-specific handling in the OpenAI-compatible provide
- [Kimi Models (is_error Exclusion)](#kimi-models-is_error-exclusion)
- [Reasoning Models (Tuning Parameter Stripping)](#reasoning-models-tuning-parameter-stripping)
- [GPT-5 (max_completion_tokens)](#gpt-5-max_completion_tokens)
- [Qwen Models (DashScope Routing)](#qwen-models-dashscope-routing)
- [Qwen and Kimi Models (DashScope Routing)](#qwen-and-kimi-models-dashscope-routing)
- [Custom Gateway Slugs and Extra Body Parameters](#custom-gateway-slugs-and-extra-body-parameters)
- [Implementation Details](#implementation-details)
- [Adding New Models](#adding-new-models)
- [Testing](#testing)
@@ -22,6 +23,8 @@ The `openai_compat.rs` provider translates Claude Code's internal message format
- Sampling parameters (temperature, top_p, etc.)
- Token limit fields (`max_tokens` vs `max_completion_tokens`)
- Base URL routing
- Provider-specific extra body parameters (`web_search_options`, `parallel_tool_calls`, local-server switches, etc.)
- Provider diagnostics for status/doctor-style surfaces
## Model-Specific Handling
@@ -46,7 +49,7 @@ The `openai_compat.rs` provider translates Claude Code's internal message format
fn model_rejects_is_error_field(model: &str) -> bool {
let lowered = model.to_ascii_lowercase();
let canonical = lowered.rsplit('/').next().unwrap_or(lowered.as_str());
canonical.starts_with("kimi-")
canonical.starts_with("kimi")
}
```
@@ -120,13 +123,13 @@ let max_tokens_key = if wire_model.starts_with("gpt-5") {
---
### Qwen Models (DashScope Routing)
### Qwen and Kimi Models (DashScope Routing)
**Affected models:** All models with `qwen` prefix
**Affected models:** All models with `qwen` or `kimi` prefixes, including `qwen/`, `qwen-`, `kimi/`, and `kimi-` forms.
**Behavior:** Routed to DashScope (`https://dashscope.aliyuncs.com/compatible-mode/v1`) rather than default providers.
**Behavior:** Routed to DashScope (`https://dashscope.aliyuncs.com/compatible-mode/v1`) rather than ambient-credential fallback providers. Known routing prefixes are stripped before sending the wire model.
**Rationale:** Qwen models are hosted by Alibaba Cloud's DashScope service, not OpenAI or Anthropic.
**Rationale:** Qwen and Kimi compatible-mode models are hosted through Alibaba Cloud's DashScope service, not OpenAI or Anthropic.
**Configuration:**
```rust
@@ -137,6 +140,21 @@ pub const DEFAULT_DASHSCOPE_BASE_URL: &str = "https://dashscope.aliyuncs.com/com
**Note:** Some Qwen models are also reasoning models (see [Reasoning Models](#reasoning-models-tuning-parameter-stripping) above) and receive both treatments.
---
### Custom Gateway Slugs and Extra Body Parameters
**Affected models:** Slash-containing model IDs routed through the OpenAI-compatible provider, especially custom gateways configured with `OPENAI_BASE_URL` such as OpenRouter, local routers, or other `/v1/chat/completions` services.
**Behavior:**
- The default OpenAI API treats `openai/` as a routing prefix and sends the bare model name on the wire.
- Custom OpenAI-compatible base URLs preserve slash-containing slugs such as `openai/gpt-4.1-mini` so the gateway receives the exact model ID it expects.
- `MessageRequest::extra_body` passes through custom request JSON after core fields are populated. This supports provider-specific options such as `web_search_options` and `parallel_tool_calls`.
- Protected core fields (`model`, `messages`, `stream`, `tools`, `tool_choice`, `max_tokens`, `max_completion_tokens`) cannot be overridden through `extra_body`.
**Testing:** See `custom_openai_gateway_preserves_slash_model_ids_and_extra_body_params` in `openai_compat_integration.rs` and `extra_body_params_are_passed_through_without_overriding_core_fields` in `openai_compat.rs`.
## Implementation Details
### File Location
@@ -152,7 +170,8 @@ rust/crates/api/src/providers/openai_compat.rs
| `model_rejects_is_error_field()` | Detects models that don't support `is_error` in tool results |
| `is_reasoning_model()` | Detects reasoning models that need tuning param stripping |
| `translate_message()` | Converts internal messages to OpenAI format (applies `is_error` logic) |
| `build_chat_completion_request()` | Constructs full request payload (applies all model-specific logic) |
| `build_chat_completion_request()` | Constructs full request payload (applies all model-specific logic and safe `extra_body` passthrough) |
| `provider_diagnostics_for_model()` | Produces provider/status diagnostics including auth/base-url vars, reasoning behavior, proxy support, extra-body support, and slash-model preservation |
### Provider Prefix Handling
@@ -165,7 +184,7 @@ let canonical = model.to_ascii_lowercase()
.unwrap_or(model);
```
This ensures consistent detection regardless of whether models are referenced with or without provider prefixes.
This ensures consistent detection regardless of whether models are referenced with or without provider prefixes. Wire-model handling is more specific: known routing prefixes are stripped for provider-native defaults, while custom OpenAI-compatible base URLs preserve slash-containing gateway slugs.
## Adding New Models
@@ -183,11 +202,15 @@ When adding support for new models:
- Does it require `max_completion_tokens` instead of `max_tokens`?
- Update the `max_tokens_key` logic
4. **Add tests**
4. **Check custom gateway behavior**
- Should slash-containing IDs be preserved for custom `OPENAI_BASE_URL` gateways?
- Does the feature belong in a typed request field or `extra_body` passthrough?
5. **Add tests**
- Unit test for detection function
- Integration test in `build_chat_completion_request`
5. **Update this documentation**
6. **Update this documentation**
- Add the model to the affected lists
- Document any special behavior
@@ -204,6 +227,8 @@ cargo test --package api model_rejects_is_error_field
cargo test --package api reasoning_model
cargo test --package api gpt5
cargo test --package api qwen
cargo test --package api custom_openai_gateway_preserves_slash_model_ids_and_extra_body_params
cargo test --package api provider_diagnostics_explain_openai_compatible_capabilities
```
### Test Files
@@ -231,6 +256,6 @@ fn my_new_model_is_detected() {
---
*Last updated: 2026-04-16*
*Last updated: 2026-05-15*
For questions or updates, see the implementation in `rust/crates/api/src/providers/openai_compat.rs`.

44
docs/anti-slop-triage.md Normal file
View File

@@ -0,0 +1,44 @@
# Anti-slop issue and PR triage
Use this checklist before spending engineering time on low-signal issues, generated PRs, duplicate fixes, or broad unsolicited changes. The goal is not to reject community work by default; it is to make each merge, defer, or close recommendation evidence-backed and safe.
## Classifications
| Classification | Use when | Required evidence | Safe action |
| --- | --- | --- | --- |
| `actionable-bug` | The report has a reproducible product failure. | Repro steps, failing test, logs with secrets removed, or matching roadmap item. | Fix, assign, or link to an existing fix. |
| `actionable-docs` | The report identifies missing, stale, or confusing documentation. | Current doc path plus desired corrected source of truth. | Patch docs or link to the owning docs lane. |
| `actionable-feature` | The request matches Claw Code direction and has a concrete acceptance shape. | Issue/PR link plus roadmap or maintainer rationale. | Defer to planning or implement if already scoped. |
| `duplicate` | Another issue/PR already covers the same user-visible outcome. | Link the canonical issue/PR and note any extra evidence worth preserving. | Cross-link; close only with maintainer/owner policy. |
| `spam-or-promotion` | The content is promotional, irrelevant, or abusive. | URL/title/body excerpt summary, not a full repost. | Label/close per repository policy. |
| `generated-slop-or-hallucinated` | The change is broad, mechanically generated, unreviewable, or names APIs/files that do not exist. | Diff/path examples, missing symbols, or unverifiable claims. | Request a narrow repro or reject/defer with rationale. |
| `unsafe-or-security-sensitive` | The report includes secrets, exploit detail, or risky operational instructions. | Redacted summary and security policy link. | Move to the private/security path; do not expand public details. |
| `not-reproducible-yet` | The claim might be valid but lacks enough evidence to act. | Missing command, environment, expected/actual behavior, or version. | Ask for repro details; do not implement speculative fixes. |
| `externally-blocked` | Progress depends on upstream services, credentials, policy, or unavailable owner approval. | Blocking dependency and owner/gate. | Defer with a concrete unblock condition. |
## PR review gate
Every PR triage note should answer:
1. Is the PR a merge candidate, a request-changes candidate, a duplicate, unsafe, out-of-scope, or generated slop?
2. What exact evidence supports that classification?
3. Which tests/docs checks were run or intentionally skipped?
4. Which issue, roadmap row, or user problem does it resolve?
5. If it should not merge now, what is the minimal non-destructive next action?
Automation lanes must not merge or close remote PRs/issues. They may produce a ledger row, add local documentation/templates, and report recommended actions for a maintainer-owned final gate.
## Issue intake gate
Every issue triage note should answer:
1. Is the issue correct, duplicate, spam, invalid, externally blocked, or not reproducible yet?
2. If correct and resolvable, what fix path or already-merged commit resolves it?
3. If not currently resolvable, what evidence would change the classification?
4. Are secrets, private data, or security details present that require a private path?
## Template locations
- Issue intake form: `.github/ISSUE_TEMPLATE/anti_slop_triage.yml`
- PR review checklist: `.github/PULL_REQUEST_TEMPLATE.md`
- Final aggregate gate: `docs/pr-issue-resolution-gate.md`

View File

@@ -0,0 +1,185 @@
# G002 alpha security map and verification plan
Generated by `worker-4` for OMX team task 5 on 2026-05-14.
## Scope and coordination
- Active goal context: `G002-alpha-security` / Stream 6 day-one security and permissions gate.
- Worker ownership: `worker-1` owns minimal implementation changes for workspace/path enforcement. `worker-4` owns this repository map, integration verification plan, changed-file/commit report, and exact verification evidence.
- Boundary: this report does not mutate `.omx/ultragoal` and does not edit shared security/path tests.
- Parallel probe status: three native subagents were spawned for repository map, test probe, and change-slice probe, but all failed before returning findings with `429 Too Many Requests`; local mapping below is based on direct repository inspection.
## Current permission and path enforcement map
### Runtime permission policy and enforcer
- `rust/crates/runtime/src/permissions.rs`
- Owns the `PermissionMode` ordering and `PermissionPolicy` authorization contract.
- Existing tests cover read-only denial, workspace-write escalation, prompt approvals/denials, danger-full-access allowance, override recording, and required-mode reporting.
- Integration risk: any new dynamic file/path rule must preserve the existing `PermissionPolicy::authorize` semantics so prompt/override audit events remain stable.
- `rust/crates/runtime/src/permission_enforcer.rs`
- `PermissionEnforcer::check`, `check_with_required_mode`, `check_file_write`, and `check_bash` convert policy outcomes into structured `EnforcementResult` payloads.
- `check_file_write` currently has the direct write gate for workspace-write mode.
- `is_within_workspace` is a string-prefix boundary check after simple relative-path joining; it does not canonicalize symlinks, `..`, Windows drive prefixes, or case variants.
- Existing tests cover read-only denial, workspace-write inside/outside paths, trailing slashes, root equality, bash read-only heuristics, prompt-mode denial payloads, and structured denied fields.
### File tool path handling
- `rust/crates/runtime/src/file_ops.rs`
- `read_file`, `write_file`, and `edit_file` normalize paths before filesystem operations but do not themselves require a workspace root.
- `read_file_in_workspace`, `write_file_in_workspace`, and `edit_file_in_workspace` exist as boundary-enforced wrappers.
- `validate_workspace_boundary` canonicalizes through the caller-provided resolved path and checks `starts_with(workspace_root)`.
- `is_symlink_escape` detects direct symlink escapes by comparing canonical target to canonical workspace root.
- Search tools (`glob_search`, `grep_search`) derive walk roots and prune heavy directories, but they are separate from the write enforcement path.
- Existing tests cover oversized/binary reads, workspace-boundary read rejection, symlink escape detection, glob brace expansion, ignored directories, and grep/glob behavior.
### Bash command validation
- `rust/crates/runtime/src/bash_validation.rs`
- `validate_command` runs mode validation, sed validation, destructive warning checks, then path validation.
- `validate_read_only` blocks write-like commands, state-modifying commands, write redirects, and mutating git subcommands in read-only mode.
- `validate_mode` warns when workspace-write commands appear to target hard-coded system paths.
- `validate_paths` warns for `../`, `~/`, and `$HOME` references; it is intentionally heuristic and does not resolve shell expansion or canonical targets.
- Existing tests cover read-only blockers, destructive warnings, sed in-place blocking, path traversal/home warnings, command classification, and full pipeline allow/block/warn outcomes.
### Sandbox and diagnostics surfaces
- `rust/crates/runtime/src/sandbox.rs`
- Owns container/sandbox status detection and workspace-only sandbox command construction.
- Relevant for day-one security because sandbox status must not overstate filesystem isolation.
- `rust/crates/rusty-claude-cli/src/main.rs`
- Owns CLI permission-mode parsing, direct JSON/text diagnostic output, `/permissions`, `/status`, `/doctor`, and command dispatch paths.
- Existing CLI integration tests under `rust/crates/rusty-claude-cli/tests/` cover permission prompt scenarios and output-format contracts.
- `rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs`
- End-to-end harness includes `bash_permission_prompt_approved`, `bash_permission_prompt_denied`, read/write file allow/deny, and plugin workspace-write scenarios.
## Existing G002-adjacent coverage
- Unit-level permission coverage:
- `cargo test -p runtime permissions::tests`
- `cargo test -p runtime permission_enforcer::tests`
- `cargo test -p runtime bash_validation::tests`
- `cargo test -p runtime file_ops::tests`
- CLI and integration coverage:
- `cargo test -p rusty-claude-cli --test mock_parity_harness`
- `cargo test -p rusty-claude-cli --test output_format_contract`
- `cargo test -p rusty-claude-cli --test cli_flags_and_config_defaults`
- Board/report validation coverage:
- `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`
## Recommended safe work slices
### Implementation lane (owned by worker-1 unless re-scoped)
1. Replace string-prefix workspace boundary checks with canonical path comparison in the runtime enforcement path.
- Primary files: `rust/crates/runtime/src/permission_enforcer.rs`, possibly shared helper extraction from `rust/crates/runtime/src/file_ops.rs`.
- Regression cases: `../` traversal, symlink escape, root prefix collision (`/workspace` vs `/workspacex`), relative paths, trailing slash root equality.
2. Ensure direct file tools call workspace-aware wrappers when active permission mode is `workspace-write`.
- Primary files: likely `rust/crates/runtime/src/mcp_tool_bridge.rs` and/or the runtime tool execution bridge that calls `file_ops`.
- Regression cases: direct read/write paths, missing parent creation, symlink parent escape, and error payload stability.
3. Keep bash validation as a warning/classification layer unless a real shell-expansion resolver is introduced.
- Primary files: `rust/crates/runtime/src/bash_validation.rs`, `rust/crates/runtime/src/bash.rs`.
- Risk: heuristic parsing cannot faithfully resolve shell expansion, globs, aliases, or platform-specific path rules; avoid claiming hard enforcement unless execution sandbox or command resolver proves it.
### Test lane (coordinate with worker-3/worker-1 before editing)
1. Add unit regressions close to each enforcement function before changing behavior.
- `permission_enforcer.rs`: canonical path boundary and Windows-shaped path cases.
- `file_ops.rs`: write/edit workspace wrappers with symlink parent escapes and missing file parent canonicalization.
- `bash_validation.rs`: shell expansion/glob/path warnings remain warnings unless a resolver is introduced.
2. Add at least one integration test proving the runtime bridge actually routes file tools through workspace enforcement, not only helper functions.
- Candidate: `rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs` for direct write denial and no file created outside workspace.
3. Preserve existing prompt/event visibility tests.
- Candidate surfaces: permission prompt scenarios in `mock_parity_harness.rs`, status/doctor JSON in `output_format_contract.rs`.
### Docs/reporting lane (owned by worker-4)
1. Keep this file as the integration handoff artifact for G002 mapping and verification.
2. Report changed files and commits relative to `origin/main` so the leader can integrate worker branches deterministically.
3. Include exact command evidence in the task lifecycle result.
## Changed files relative to `origin/main` at map time
The worktree currently contains these files added relative to `origin/main` before this task report:
- `.omx/cc2/board.json`
- `.omx/cc2/board.md`
- `.omx/cc2/issue-parity-intake.json`
- `.omx/cc2/issue-parity-intake.md`
- `.omx/cc2/render_board_md.py`
- `.omx/cc2/validate_issue_parity_intake.py`
- `scripts/cc2_board.py`
- `scripts/generate_cc2_board.py`
- `scripts/validate_cc2_board.py`
This task adds:
- `docs/g002-security-verification-map.md`
## Commits relative to `origin/main` at map time
- `8311655``omx(team): auto-checkpoint worker-1 [1]`
- `c6e2a7d``omx(team): merge worker-1`
- `481585f``omx(team): auto-checkpoint worker-1 [1]`
- `74bbf4b``omx(team): auto-checkpoint worker-4 [unknown]`
- `5c77896``omx(team): auto-checkpoint worker-1 [1]`
- `07dad88``Classify issue and parity intake for CC2 board integration`
- `424825f``task: G001 human board and docs rendering`
- `d15268e``Create a canonical CC2 board so every frozen ROADMAP heading is verifiably mapped`
- `45b43b5``Make the CC2 board schema executable for G001`
## Verification checklist for leader integration
Run these from the repository root unless noted:
1. Python board/schema validation:
- `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`
2. Rust formatting and lint/type checks:
- `scripts/fmt.sh --check`
- `(cd rust && cargo check --workspace)`
- `(cd rust && cargo clippy --workspace --all-targets -- -D warnings)`
3. Targeted G002 security tests:
- `(cd rust && cargo test -p runtime permissions::tests permission_enforcer::tests bash_validation::tests file_ops::tests)`
- `(cd rust && cargo test -p rusty-claude-cli --test mock_parity_harness)`
4. Full regression:
- `(cd rust && cargo test --workspace)`
## Worker-4 verification evidence (2026-05-14)
PASS:
- `python3 scripts/validate_cc2_board.py --board .omx/cc2/board.json``PASS cc2 board validation`; 729 items; ROADMAP headings `124/124`; ROADMAP actions `542/542`.
- `python3 .omx/cc2/validate_issue_parity_intake.py .omx/cc2/issue-parity-intake.json``PASS issue/parity intake: 19 issue rows, 9 parity rows`.
- `scripts/fmt.sh --check` → no output and zero exit before Rust checks continued.
- `(cd rust && cargo check --workspace)``Finished dev profile` successfully.
- `(cd rust && cargo test -p runtime permissions::tests)` → 9 passed.
- `(cd rust && cargo test -p runtime permission_enforcer::tests)` → 21 passed.
- `(cd rust && cargo test -p runtime bash_validation::tests)` → 32 passed.
- `(cd rust && cargo test -p runtime file_ops::tests)` → 14 passed.
- `(cd rust && cargo test -p rusty-claude-cli --test mock_parity_harness)` → 1 passed.
FAIL / integration blockers observed on this worktree:
- `(cd rust && cargo clippy --workspace --all-targets -- -D warnings)` failed in existing runtime code, not this docs-only task:
- `rust/crates/runtime/src/compact.rs:215` / `:216`: `clippy::match_same_arms`.
- `rust/crates/runtime/src/policy_engine.rs:5`: `clippy::duration-suboptimal-units`.
- `rust/crates/runtime/src/sandbox.rs:295-302`: `clippy::map_unwrap_or`.
- `(cd rust && cargo test --workspace)` failed after broad success in API/commands/plugins/runtime tests because `rusty-claude-cli` unit test `tests::session_lifecycle_prefers_running_process_over_idle_shell` asserted `RunningProcess` but observed `IdleShell`.
- Rerun of the specific failing test confirmed deterministic failure: `(cd rust && cargo test -p rusty-claude-cli --bin claw tests::session_lifecycle_prefers_running_process_over_idle_shell -- --exact --nocapture)` → 0 passed, 1 failed with the same `IdleShell` vs `RunningProcess` assertion.
Recommended owner for failures: not `worker-4` unless re-scoped. These failures are outside the docs/report artifact and touch shared runtime/CLI implementation files.

View File

@@ -0,0 +1,96 @@
# G003 boot/session/preflight verification map
Generated by `worker-1` for OMX team task 2 on 2026-05-14.
## Scope and coordination
- Active goal context: `G003-boot-session` / Stream 1 reliable worker boot and session control.
- Boundary: this artifact is an audit/integration map only. It does not mutate `.omx/ultragoal` and it does not change shared implementation or tests.
- Current worker split from leader mailbox:
- `worker-1`: task 1 worker boot / prompt SLA plus this task 2 audit map.
- `worker-2`: default trusted roots / trust resolver.
- `worker-3`: startup-no-evidence classifier.
- `worker-4`: session control plus preflight/doctor JSON surfaces.
- Native subagent probes were attempted for Task 2 (`test probe` and `debug/root-cause probe`) but both failed before returning findings with `429 Too Many Requests`; the map below is based on direct repository inspection.
## Implementation surface map
### Worker boot lifecycle and prompt SLA
- `rust/crates/runtime/src/worker_boot.rs`
- Core state types: `WorkerStatus`, `WorkerFailureKind`, `WorkerEventKind`, `WorkerEventPayload`, `StartupFailureClassification`, `StartupEvidenceBundle`, `WorkerTaskReceipt`, and `WorkerReadySnapshot`.
- Control plane: `WorkerRegistry::{create,get,observe,resolve_trust,send_prompt,await_ready,restart,terminate,observe_completion,observe_startup_timeout}`.
- Lifecycle states currently covered in code: `spawning`, `trust_required`, `tool_permission_required`, `ready_for_prompt`, `running`, `finished`, and `failed`.
- Prompt delivery semantics currently use `Running` events and fields `prompt_in_flight`, `last_prompt`, `expected_receipt`, `replay_prompt`, and `prompt_delivery_attempts`.
- Startup-no-evidence surface: `observe_startup_timeout` builds `StartupEvidenceBundle` and classifies trust, tool permission, prompt acceptance timeout, prompt misdelivery, transport death, worker crash, or unknown.
- File observability surface: `emit_state_file` writes `.claw/worker-state.json` with status, readiness, trust state, prompt-in-flight flag, last event, and update age.
- `rust/crates/tools/src/lib.rs`
- Tool APIs expose the worker control plane through `WorkerCreate`, `WorkerGet`, `WorkerObserve`, `WorkerResolveTrust`, `WorkerAwaitReady`, `WorkerSendPrompt`, `WorkerRestart`, `WorkerTerminate`, and `WorkerObserveCompletion`.
- `WorkerCreate` merges `ConfigLoader::trusted_roots()` with per-call `trusted_roots` before calling `WorkerRegistry::create`.
- Tool-level tests exercise worker create/observe/send/restart/terminate/completion and state-file transitions.
### Trust resolver and default trusted roots
- `rust/crates/runtime/src/trust_resolver.rs`
- `TrustConfig`, `TrustAllowlistEntry`, and `TrustResolver` model trust prompts, allowlist/denylist policy, auto-trust, manual approval, and emitted trust events.
- `path_matches_trusted_root` and internal `path_matches` canonicalize paths when possible.
- Hazard: prefix matching must avoid accidental sibling matches such as `/tmp/work` matching `/tmp/work-evil`; worker-2 owns any changes here.
- `rust/crates/runtime/src/config.rs`
- `trustedRoots` is parsed by `parse_optional_trusted_roots` and exposed through `RuntimeConfig::trusted_roots()` / feature config accessors.
- Current default is empty when unset; any project default roots work belongs to worker-2.
### Session control
- `rust/crates/runtime/src/session_control.rs`
- `SessionStore` namespaces sessions by canonical workspace fingerprint.
- Key API: `from_cwd`, `from_data_dir`, `create_handle`, `resolve_reference`, `resolve_managed_path`, `list_sessions`, `latest_session`, `load_session`, and `fork_session`.
- Guardrail: `validate_loaded_session` rejects cross-workspace sessions and allows legacy sessions only when their path remains inside the current workspace.
- Worker-4 owns changes to this lane.
### CLI doctor/status/preflight and bootstrap-adjacent surfaces
- `rust/crates/commands/src/lib.rs`
- Slash command definitions include `/status`, `/sandbox`, and `/doctor`.
- JSON rendering for command surfaces exists through handler functions and tests in the same module.
- `rust/crates/tools/src/lib.rs`
- Bash and PowerShell tool runners include `workspace_test_branch_preflight`, which returns structured output with `return_code_interpretation: preflight_blocked:branch_divergence` for broad workspace tests on stale branches.
- Tests around `bash_workspace_tests_are_blocked_when_branch_is_behind_main` and targeted-test skipping protect this preflight behavior.
## Existing focused verification commands
Run from `rust/` unless noted.
- Worker boot runtime contract:
- `cargo test -p runtime worker_boot -- --nocapture`
- Worker tool API contract:
- `cargo test -p tools worker_ -- --nocapture`
- Session control contract:
- `cargo test -p runtime session_control -- --nocapture`
- Trust resolver/config trusted roots:
- `cargo test -p runtime trust_resolver -- --nocapture`
- `cargo test -p runtime config::tests::parses_trusted_roots_from_settings config::tests::trusted_roots_default_is_empty_when_unset -- --nocapture`
- Preflight/tool branch guardrails:
- `cargo test -p tools bash_workspace_tests_are_blocked_when_branch_is_behind_main bash_targeted_tests_skip_branch_preflight -- --nocapture`
- Formatting/type/lint baseline:
- `../scripts/fmt.sh --check`
- `cargo check -p runtime -p tools -p commands`
- `cargo clippy -p runtime -p tools -p commands --all-targets --no-deps -- -D warnings`
## Gaps and hazards for leader integration
- Prompt SLA event naming is partially implicit: `send_prompt` emits `WorkerEventKind::Running`; it does not expose separate `prompt.sent`, `prompt.accepted`, `prompt.acceptance_delayed`, or `prompt.acceptance_timeout` event names. The current equivalent evidence is `prompt_in_flight`, `Running`, `observe_completion`, and startup-timeout classification.
- `StartupFailureClassification::PromptAcceptanceTimeout` is covered in `worker_boot` tests; full terminal/transport integration should still be verified by the leader or worker-3 if a real pane watcher exists outside the in-memory registry.
- Default trusted roots are parsed and merged into `WorkerCreate`, but unset config currently means no default roots. Worker-2 owns any change to default root selection.
- Session control protects workspace fingerprints at load/fork time; worker-4 owns CLI/doctor/preflight JSON contract changes.
- Full-workspace clippy currently has known unrelated runtime findings observed during task 1 verification; do not block this docs-only map on those unless leader re-scopes cleanup.
## Recommended safe integration order
1. Integrate worker boot / prompt SLA changes first and run `cargo test -p runtime worker_boot -- --nocapture` plus `cargo test -p tools worker_ -- --nocapture`.
2. Integrate trust-root changes and rerun trust/config tests plus the worker create config merge test.
3. Integrate startup-no-evidence classifier changes and rerun `cargo test -p runtime worker_boot -- --nocapture`.
4. Integrate session control / preflight / doctor JSON changes and rerun session-control, commands JSON, and preflight tests.
5. Run final formatting, targeted cargo check/clippy, then broader workspace tests with known full-workspace failures documented separately.

View File

@@ -0,0 +1,67 @@
# G004 event and report contract guidance
Captured: 2026-05-14 during the Stream 2 `G004-events-reports` team run.
Purpose: keep the user/developer-facing contract guidance for ROADMAP Phase 2 in one tracked source that points back to the code and roadmap anchors. This document is intentionally not the implementation map for task 5; it describes the interoperability contract consumers should rely on as the lane-event, report-schema, approval-token, and capability-negotiation lanes land.
## Source-of-truth anchors
| Contract family | Roadmap anchor | Current implementation / owner-facing anchor | Consumer guidance |
| --- | --- | --- | --- |
| Canonical lane events | `ROADMAP.md` Phase 2 §4, §4.5, §4.6, §4.7 | `rust/crates/runtime/src/lane_events.rs` (`LaneEventName`, `LaneEventStatus`, `LaneEventMetadata`, terminal reconciliation helpers) | Consume `event`, `status`, `emittedAt`, and `metadata` fields as the canonical state stream; do not infer lane state from terminal text when a structured event is present. |
| Report schema v1 and projections | `ROADMAP.md` §4.25-§4.34 | Stream 2 report-schema lane / fixtures as they land | Treat a report as a versioned canonical payload plus derived projections. A projection may omit or transform fields only with explicit provenance: compatibility downgrade, redaction policy, truncation, or source absence. |
| Policy-blocked handoff and approval-token chain | `ROADMAP.md` §4.37-§4.39 | Stream 2 approval-token lane as it lands | Treat policy blocks and owner approvals as typed artifacts, not prose. Execute an exception only when the approval token matches actor, policy, action, repo/branch/commit scope, expiry, and one-time-use state. |
| Capability negotiation | `ROADMAP.md` §4.25, §4.26, §4.32, §4.34 | Report-schema/projection fixtures and consumer conformance cases as they land | Consumers must advertise supported schema versions, optional field families, projection views, redaction semantics, and downgrade handling before relying on reduced payloads. |
## Lane event contract
The lane-event stream is the first machine-trustworthy surface for Stream 2. Consumers should expect these invariants when reading `LaneEvent` payloads:
- `event` is a typed event name, currently including the core lane lifecycle (`lane.started`, `lane.ready`, `lane.blocked`, `lane.red`, `lane.green`, `lane.finished`, `lane.failed`), branch health (`branch.stale_against_main`, `branch.workspace_mismatch`), reconciliation (`lane.reconciled`, `lane.superseded`, `lane.closed`), and ship provenance (`ship.prepared`, `ship.commits_selected`, `ship.merged`, `ship.pushed_main`).
- `status` is the normalized state for the event; consumers should prefer it over freeform `detail` text for automation.
- `metadata.seq`, `metadata.timestamp_ms`, and terminal fingerprints are the ordering/deduplication hooks. Consumers should use terminal reconciliation output rather than double-reporting contradictory terminal bursts.
- `metadata.provenance`, `metadata.environment_label`, `metadata.emitter_identity`, and `metadata.confidence_level` tell consumers whether an event is live lane truth, test traffic, healthcheck/replay output, or transport-layer evidence.
- `metadata.session_identity` and `metadata.ownership` bind a lane event to the session, workspace, workflow scope, owner, and watcher action. A watcher should not act on events whose ownership says `observe` or `ignore`.
Minimal consumer rule: if a structured event exists, pane text is supporting evidence only. Pane scraping must not override a higher-confidence typed event with matching session/workflow ownership.
## Report schema v1 contract
A Stream 2 report should be treated as a canonical fact record with optional projections. Consumers should preserve these semantics even when they receive only a downgraded view:
- Every report payload declares a schema version and a stable report identity/content hash for the full-fidelity canonical payload.
- Assertions are labeled as `fact`, `hypothesis`, or another declared evidence class, with confidence and source references. Negative evidence is first-class: `not observed`, `checked and absent`, and `redacted` are distinct states.
- Field deltas name the field, previous value/state, new value/state, attribution, and whether the delta came from source content, projection, downgrade, or redaction policy.
- Projections carry lineage back to the canonical report id/content hash and name the projection view, capability set, schema version, redaction policy, and deterministic rendering inputs.
- Redaction provenance is explicit. A missing field without a redaction/downgrade/source-absence reason is not enough evidence for an automated consumer to conclude the underlying fact is absent.
Minimal consumer rule: store the canonical identity and projection metadata together. Do not compare two projections as state changes unless their canonical content hash or declared projection inputs differ.
## Approval-token and policy-blocked contract
Policy-blocked actions and owner-approved exceptions belong in the same structured event/report family:
- A policy block names the typed reason, policy source, actor scope, blocked action, and safe fallback path.
- An approval token names the approving actor, policy exception, action, repository/worktree/branch/commit scope, expiry, and allowed use count.
- Token consumption records the exact action and scope that spent the token. Replays, scope expansion, expired tokens, and revoked tokens should surface typed policy errors.
- Delegation traceability stays attached when another worker/lane executes the approved action; the executor must be able to prove which approval artifact authorized the exception.
Minimal consumer rule: prose such as "approved" is not an executable approval. Require the structured token and verify that it is unconsumed and scoped to the exact action before proceeding.
## Capability negotiation and conformance
Mixed-version consumers are expected during Stream 2 rollout. Producers and consumers should negotiate instead of silently dropping fields:
- Consumers advertise supported report schema versions, field families, projection views, redaction states, downgrade semantics, and fixture/conformance suite version.
- Producers preserve one canonical full-fidelity report and emit downgraded projections only with `downgraded_for_compatibility` metadata.
- Deterministic projection inputs include schema version, consumer capability set, projection policy version, redaction policy version, and canonical content hash.
- Consumer conformance should distinguish syntax acceptance from semantic correctness, especially for `redacted` vs `missing`, stale vs current projections, negative evidence, and approval-token replay states.
Minimal consumer rule: an older consumer may accept a downgraded projection, but it must surface the downgrade as a capability limitation rather than treating omitted fields as canonical absence.
## Documentation maintenance rules
- Keep ROADMAP Phase 2 as the product requirement source and this file as the contract-reading guide.
- Keep Rust type names and event names aligned with `rust/crates/runtime/src/lane_events.rs`; update this document in the same change when public event names or metadata semantics change.
- Keep report-schema examples/fixtures aligned with this guide once the schema lane lands; fixture updates should explain intentional schema or projection changes.
- Do not mutate `.omx/ultragoal` from worker lanes. Leader-owned Ultragoal checkpointing consumes commits and verification evidence from task results.

View File

@@ -0,0 +1,57 @@
# G004 events/reports verification map
Scope source: OMX team `g004-events-reports-u-e61d2271`, worker-1 tasks 1, 2, 4, 5. Workers must not mutate `.omx/ultragoal`; leader owns aggregate checkpoints.
## Ownership boundaries
- **Lane events / event identity / terminal reconciliation** — `rust/crates/runtime/src/lane_events.rs`, exported through `rust/crates/runtime/src/lib.rs`; tool-manifest consumers in `rust/crates/tools/src/lib.rs` write `LaneEvent` vectors.
- **Report schema v1 / projection / redaction / capability negotiation** — `rust/crates/runtime/src/report_schema.rs`, exported through `rust/crates/runtime/src/lib.rs`; fixture note at `rust/crates/runtime/tests/fixtures/report_schema_v1/README.md`.
- **Approval-token chain** — ROADMAP §§4.38-4.40; owned by worker-2 for this team split. Worker-1 did not edit it.
- **Pinpoint closure batch** — runtime hygiene across compact/search-parser/policy/sandbox/integration-test surfaces: `rust/crates/runtime/src/compact.rs`, `rust/crates/runtime/src/file_ops.rs`, `rust/crates/runtime/src/policy_engine.rs`, `rust/crates/runtime/src/sandbox.rs`, `rust/crates/runtime/tests/integration_tests.rs`.
- **Regression harness / docs alignment** — worker-3/worker-4 lanes per leader split. Coordinate before editing shared docs/tests.
## Relevant symbols and files
- `LaneEventName`, `LaneEventStatus`, `LaneEventMetadata`, `LaneEventBuilder`, `compute_event_fingerprint`, `dedupe_terminal_events`, `reconcile_terminal_events` in `runtime/src/lane_events.rs`.
- `CanonicalReportV1`, `ReportClaim`, `NegativeEvidence`, `FieldDelta`, `ConsumerCapabilities`, `ReportProjectionV1`, `canonicalize_report`, `project_report`, `report_schema_v1_registry` in `runtime/src/report_schema.rs`.
- `AgentOutput.lane_events`, `persist_agent_terminal_state`, `write_agent_manifest`, `maybe_commit_provenance` in `tools/src/lib.rs`.
- Search/parser closure helpers: `summarize_messages` in `compact.rs`, `grep_search_impl` / `build_grep_content_output` in `file_ops.rs`.
## Completed worker-1 commits
- `f45f05e` / task 1 auto-checkpoint — terminal event fingerprints use stable SHA-256-derived canonical JSON, and production convenience terminal events attach/refresh fingerprints after payload changes.
- `3989fc0` — report schema v1 contract, deterministic projection/redaction provenance, capability negotiation, and fixture note.
- `7fff4c4` / task 4 auto-checkpoint — strict runtime clippy closure batch across compact/file_ops/policy/sandbox/integration tests.
## Current verification evidence
Run from `rust/` unless noted:
- `cargo test -p runtime lane_events -- --nocapture` — PASS, 46 lane-event tests.
- `cargo test -p runtime report_schema -- --nocapture` — PASS, 4 report-schema tests.
- `cargo check -p runtime` — PASS.
- `cargo clippy -p runtime --all-targets -- -D warnings` — PASS after task 4 closure batch.
- `cargo test -p runtime -- --nocapture` — PASS, 531 unit tests, 12 integration tests, doc-tests pass.
- `cargo test -p tools lane_event_schema_serializes_to_canonical_names -- --nocapture` — PASS, 1 targeted tools contract test.
## Leader integration verification plan
1. Inspect worker commits: `git log --oneline --decorate --max-count=8`.
2. Re-run focused contracts:
- `cd rust && cargo test -p runtime lane_events -- --nocapture`
- `cd rust && cargo test -p runtime report_schema -- --nocapture`
- `cd rust && cargo test -p tools lane_event_schema_serializes_to_canonical_names -- --nocapture`
3. Re-run runtime quality gate:
- `cd rust && cargo check -p runtime`
- `cd rust && cargo clippy -p runtime --all-targets -- -D warnings`
- `cd rust && cargo test -p runtime -- --nocapture`
4. If merging with worker-2 approval-token work, additionally run the worker-2 focused approval-token tests and check for export conflicts in `runtime/src/lib.rs`.
5. If merging with worker-3/4 docs or harness work, re-run their named regression harnesses plus `git diff --check`.
## Integration hazards
- `runtime/src/lib.rs` export blocks are shared; resolve conflicts by keeping both lane-event and report-schema exports sorted enough to remain readable.
- `tools/src/lib.rs` serializes lane events into agent manifests; terminal fingerprint changes intentionally affect `metadata.event_fingerprint` for finished/failed/superseded/merged/closed events with payloads.
- `report_schema.rs` currently defines the reusable contract and in-code deterministic fixtures; it does not yet wire report emission into CLI/status surfaces.
- ROADMAP approval-token §§4.38-4.40 remain a separate lane; do not treat worker-1 report schema as an approval artifact.
- Full workspace checks may include unrelated slow/provider-dependent tests; the verified local gate for this stream is runtime + targeted tools tests above.

View File

@@ -0,0 +1,40 @@
# G005 Branch Recovery Verification Map
Scope: worker-1 follow-up map for G005 branch/test awareness and recovery. This file intentionally does not mutate leader-owned `.omx/ultragoal` state.
## Covered ROADMAP / PRD pinpoints
- `ROADMAP.md:912-921` — Phase 3 §7 stale-branch detection before broad verification: broad workspace test commands are preflighted before execution, stale/diverged branches emit `branch.stale_against_main`, and targeted tests bypass the broad-test gate.
- `ROADMAP.md:922-933` — Phase 3 §8 recovery recipes: stale-branch recovery remains represented by the `stale_branch` recipe, with one automatic attempt before escalation.
- `ROADMAP.md:935-949` — Phase 3 §8.5 recovery attempt ledger: `RecoveryContext` now exposes ledger entries with recipe id, attempt count, state, started/finished markers, last failure summary, and escalation reason.
- `ROADMAP.md:951-970` — Phase 3 §9 green-ness / hung-test reporting: timed-out test commands now classify as `test.hung` with structured provenance instead of generic timeout.
- `prd.json:37-44` — US-003 stale-branch detection before broad verification: verified through the `workspace_test_branch_preflight` broad-test block and targeted-test bypass tests.
- `prd.json:50-57` — US-004 recovery recipes with ledger: verified through recovery ledger unit coverage and serialization-compatible recovery structs.
## Implementation anchors
- `rust/crates/runtime/src/stale_branch.rs` — existing branch freshness model and policy actions for fresh, stale, and diverged branches.
- `rust/crates/tools/src/lib.rs``workspace_test_branch_preflight`, `branch_divergence_output`, Bash/PowerShell broad-test gating, and `test.hung` structured timeout provenance on tool-shell timeouts.
- `rust/crates/runtime/src/recovery_recipes.rs` — recovery recipes plus `RecoveryLedgerEntry` / `RecoveryAttemptState` ledger surface.
- `rust/crates/runtime/src/bash.rs` — runtime Bash timeout classification and structured provenance for hung test commands.
- `rust/crates/runtime/src/lib.rs` — public exports for the recovery ledger types.
## Verification evidence
- `cargo test -p runtime` → PASS: 538 unit tests, 2 G004 conformance tests, 12 integration tests, and doctests passed.
- `cargo test -p tools bash_tool_classifies_test_timeout_as_hung_with_provenance -- --nocapture` → PASS.
- `cargo test -p tools bash_workspace_tests_are_blocked_when_branch_is_behind_main -- --nocapture` → PASS.
- `cargo test -p tools bash_targeted_tests_skip_branch_preflight -- --nocapture` → PASS.
- `cargo check -p runtime -p tools` → PASS.
- `cargo clippy -p runtime --all-targets -- -D warnings` → PASS.
- `cargo clippy -p tools --lib --no-deps -- -D warnings` → PASS.
## Known unresolved / out-of-scope items
- Full `cargo test -p tools` is still red on six permission-enforcer expectation tests unrelated to G005 branch freshness, recovery ledger, or hung-test classification. The failing tests assert old permission wording/read-only behavior and pre-existed this follow-up scope.
- ROADMAP stale-base JSON/doctor/status pinpoints remain broader CLI diagnostic-surface work, especially `ROADMAP.md:2425-2489`, `ROADMAP.md:4346-4431`, and `ROADMAP.md:5061-5086`. They are related to branch freshness, but task 1 only required the broad-test freshness gate and narrow reporting surfaces.
- No `.omx/ultragoal` files were changed; leader-owned Ultragoal checkpointing remains outside worker scope.
## Delegation evidence
Subagent spawn evidence: 1, Repository map probe `019e25d5-9be9-7193-8a33-f21450beb62c`; spawned before further serial task-2 mapping per contract, but errored with 429 Too Many Requests, so direct repo evidence was integrated instead.

View File

@@ -0,0 +1,34 @@
# G006 Task Policy Board Verification Map
Goal: `G006-task-policy-board` — Stream 4 task packets, executable policy engine, lane board/status JSON, and running-state liveness heartbeat.
## Prompt-to-artifact checklist
| Requirement | Artifact/evidence |
| --- | --- |
| Typed task packet schema with objective, scope, files/resources, acceptance criteria, model/provider, permission profile, recovery policy, verification plan, reporting targets | `rust/crates/runtime/src/task_packet.rs` extends `TaskPacket` with `acceptance_criteria`, `resources`, `model`, `provider`, `permission_profile`, `recovery_policy`, `verification_plan`, and `reporting_targets`; tests cover legacy defaulted JSON and rich CC2 roundtrip. |
| Backwards compatibility for existing task packets and tool callers | `serde(default)`/optional fields in `task_packet.rs`; `rust/crates/tools/src/lib.rs` `run_task_packet_creates_packet_backed_task` updated for rich schema; legacy packet test keeps old JSON accepted. |
| Executable policy decisions for retry/rebase/merge/escalate/stale cleanup/approval token | `rust/crates/runtime/src/policy_engine.rs` adds `RetryAvailable`, `RebaseRequired`, `StaleCleanupRequired`, approval-token conditions/actions, `PolicyEvaluation`, `PolicyDecisionEvent`, and decision-table tests. |
| Policy decisions explainable and typed-event logged/emittable | `PolicyDecisionEvent` serializable typed event with `rule_name`, `priority`, `kind`, `explanation`, `approval_token_id`; `evaluate_with_events` emits event per flattened action. |
| Active lane board/dashboard/status JSON over canonical state | `rust/crates/runtime/src/task_registry.rs` adds `LaneBoard`, `LaneBoardEntry`, `LaneFreshness`, `lane_board_at`, and `lane_status_json_at`; CLI status JSON advertises lane board contract in `rust/crates/rusty-claude-cli/src/main.rs`. |
| Heartbeats independent of terminal rendering with healthy/stalled/transport-dead cases | `rust/crates/runtime/src/session.rs` adds `SessionHeartbeat`/`SessionLiveness` from persisted session health state; `task_registry.rs` heartbeat freshness is computed from canonical heartbeat timestamps and transport state. |
| Task/lane status JSON shows active/blocked/finished lanes with heartbeat freshness | `task_registry::tests::lane_board_groups_active_blocked_finished_and_reports_freshness`; `status_json_surfaces_session_lifecycle_for_clawhip`/status JSON surfaces lane board metadata. |
| Leader-owned ultragoal audit remains separate from workers | No worker changed `.omx/ultragoal`; leader will checkpoint with fresh `get_goal` only after terminal verification. |
## Verification run
- `git diff --check` — PASS
- `cargo fmt --manifest-path rust/Cargo.toml --all -- --check` — PASS
- `cargo check --manifest-path rust/Cargo.toml -p runtime -p tools -p rusty-claude-cli` — PASS
- `cargo test --manifest-path rust/Cargo.toml -p runtime task_packet -- --nocapture` — PASS (5 task packet tests)
- `cargo test --manifest-path rust/Cargo.toml -p runtime policy_engine -- --nocapture` — PASS (12 unit + 1 integration match)
- `cargo test --manifest-path rust/Cargo.toml -p runtime task_registry -- --nocapture` — PASS (17 task registry tests)
- `cargo test --manifest-path rust/Cargo.toml -p runtime session_heartbeat -- --nocapture` — PASS (1 heartbeat test)
- `cargo test --manifest-path rust/Cargo.toml -p tools run_task_packet_creates_packet_backed_task -- --nocapture` — PASS
- `cargo test --manifest-path rust/Cargo.toml -p tools lane_completion -- --nocapture` — PASS (6 tests)
- `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli status_json_surfaces -- --nocapture` — PASS
## Remaining gates
- G006 can be checkpointed after team lifecycle is reconciled terminal and this commit is pushed.
- Open PR/issue reconciliation remains explicitly deferred to G011/G012 via `docs/pr-issue-resolution-gate.md`.

View File

@@ -0,0 +1,55 @@
# G007 MCP Lifecycle Mapping
This map captures the current MCP/plugin lifecycle implementation surfaces for the
G007 plugin/MCP maturity lane. It is intentionally evidence-oriented: each row
names the runtime surface, the code owner boundary, and the current gap when the
surface is metadata-only.
## Degraded MCP startup
| Concern | Current surface | Notes |
| --- | --- | --- |
| Best-effort discovery | `rust/crates/runtime/src/mcp_stdio.rs` (`McpServerManager::discover_tools_best_effort`) | Discovers every configured stdio server, keeps tools from working servers, and records per-server failures without aborting the whole startup. |
| Failure payload | `rust/crates/runtime/src/mcp_stdio.rs` (`McpDiscoveryFailure`, `UnsupportedMcpServer`) | Failure records include `server_name`, lifecycle `phase`, `required`, `error`, `recoverable`, and structured `context`. Unsupported non-stdio servers keep `transport`, `required`, and `reason`. |
| Degraded report model | `rust/crates/runtime/src/mcp_lifecycle_hardened.rs` (`McpDegradedReport`, `McpFailedServer`, `McpErrorSurface`) | Normalizes degraded startup into working servers, failed servers, available tools, and missing tools. `McpErrorSurface` carries phase, server, message, context, and recoverability. |
| CLI runtime handoff | `rust/crates/rusty-claude-cli/src/main.rs` (`RuntimeMcpState::new`) | Converts discovery failures and unsupported servers into a runtime degraded report, including `required` in the error context. |
## Required vs. optional MCP servers
| Concern | Current surface | Notes |
| --- | --- | --- |
| Config contract | `rust/crates/runtime/src/config.rs` (`ScopedMcpServerConfig.required`) | `mcpServers.<name>.required` parses as a boolean and defaults to `false`; invalid non-boolean values are rejected by the shared optional-bool parser. |
| Scope merge | `rust/crates/runtime/src/config.rs` (`merge_mcp_servers`) | Requiredness is stored beside the scope and transport-specific config after normal user/project/local merging. |
| Inventory/reporting | `rust/crates/commands/src/lib.rs` (`mcp_server_json`, `render_mcp_server_report`) | JSON reports expose `server.required`; text `show` reports include `Required`. |
| Discovery propagation | `rust/crates/runtime/src/mcp_stdio.rs` | Requiredness is copied into managed stdio servers, unsupported server records, discovery failures, and degraded startup context. |
| Cache/signature identity | `rust/crates/runtime/src/mcp.rs` (`scoped_mcp_config_hash`) | The hash includes `required:<bool>` so required/optional changes affect MCP config identity. |
| Remaining policy gap | runtime behavior | The flag is currently surfaced and propagated as lifecycle metadata. It does not yet fail the whole runtime/session solely because a required server failed; consumers must inspect the degraded report context today. |
## Config interpolation and redaction surfaces
| Concern | Current surface | Notes |
| --- | --- | --- |
| Raw config parsing | `rust/crates/runtime/src/config.rs` (`parse_mcp_server_config`, `parse_mcp_remote_server_config`) | `command`, `args`, `url`, `headers`, and `headersHelper` are loaded as literal strings. No dedicated environment, tilde, or workspace-root interpolation pass is present in this parser. |
| Redacted key reporting | `rust/crates/commands/src/lib.rs` (`mcp_server_details_json`, `render_mcp_server_report`) | Stdio env and remote/websocket header values are not printed; only `env_keys` / `Header keys` are surfaced. |
| Unredacted reporting risk | `rust/crates/commands/src/lib.rs` (`mcp_server_summary`, `mcp_server_details_json`, text `show`) | Command, args, URL, `headers_helper`, OAuth metadata URL/client id, and managed proxy URL/id are currently emitted verbatim. Treat these fields as not-redacted unless a future policy layer classifies them safe. |
| OAuth exposure | `rust/crates/commands/src/lib.rs` (`mcp_oauth_json`, `format_mcp_oauth`) | OAuth secret-like values are mostly absent from the current config model, but client id and metadata URL are still reported directly. |
## Plugin lifecycle contract adjacency
| Concern | Current surface | Notes |
| --- | --- | --- |
| Manifest lifecycle | `rust/crates/plugins/src/lib.rs` (`PluginLifecycle`) | Plugin manifests support `lifecycle.Init` and `lifecycle.Shutdown` command arrays. |
| Registry summary | `rust/crates/plugins/src/lib.rs` (`PluginSummary::lifecycle_state`) | Installed summaries include enabled state, lifecycle commands, and derived lifecycle state (`ready` or `disabled`). Load failures remain first-class in registry reports. |
| CLI JSON output | `rust/crates/rusty-claude-cli/src/main.rs` (`plugin_command_json`) | Plugin command JSON emits top-level `status`, per-plugin `lifecycle_state` and lifecycle command counts, plus `load_failures` with `lifecycle_state: load_failed`. |
## Verification anchors
The current regression anchors for this map are:
- `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 test -p plugins installed_plugin_registry_report_collects_load_failures_from_install_root -- --nocapture`
- `cargo test -p rusty-claude-cli --test output_format_contract plugins_json_surfaces_lifecycle_contract_when_plugin_is_installed -- --nocapture`

View File

@@ -0,0 +1,54 @@
# G007 Plugin/MCP Lifecycle Verification Map
Goal: `G007-plugin-mcp` — Stream 5 plugin/MCP lifecycle maturity from ROADMAP Phase 5.
Scope: worker-2 follow-up map for W4 mock integration and regression verification. This file intentionally does not mutate leader-owned `.omx/ultragoal` state.
## Covered ROADMAP / CC2 anchors
- `ROADMAP.md:55-57` — Current pain point §6: plugin/MCP startup failures, handshake failures, config errors, partial startup, and degraded mode need clean classification.
- `ROADMAP.md:67` — Product principle §5: MCP partial success must be first-class and structurally report successful and failed servers.
- `ROADMAP.md:1033-1059` — Phase 5: first-class plugin/MCP lifecycle contract and MCP end-to-end lifecycle parity.
- `.omx/cc2/board.md` Stream 5 active headings: `CC2-RM-H0010`, `CC2-RM-H0080`, `CC2-RM-H0081`, and `CC2-RM-H0082` remain the goal-level source-of-truth anchors for plugin/MCP lifecycle maturity.
- `PARITY.md` harness checklist: mock parity scenarios are the executable regression surface for streamed model turns, plugin tool roundtrips, permissions, compaction metadata, and token/cost output.
## Mock integration anchors
| Area | Artifact/evidence |
| --- | --- |
| Deterministic model server | `rust/crates/mock-anthropic-service/src/lib.rs` implements the Anthropic-compatible mock server and scenario router used by CLI parity tests. |
| End-to-end CLI mock harness | `rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs` starts the mock server, runs clean-environment `claw` commands, asserts JSON output, and optionally writes a machine-readable report via `MOCK_PARITY_REPORT_PATH`. |
| Scenario manifest / docs parity guard | `rust/mock_parity_scenarios.json` is required to stay ordered with harness cases; `rust/scripts/run_mock_parity_diff.py --no-run` verifies every manifest `parity_refs[]` string exists in `PARITY.md`. |
| Convenience runner | `rust/scripts/run_mock_parity_harness.sh` runs `cargo test -p rusty-claude-cli --test mock_parity_harness -- --nocapture`. |
| Plugin-path regression | `plugin_tool_roundtrip` loads an external plugin fixture from isolated settings and executes `plugin_echo` through the runtime tool registry. |
| Lifecycle-adjacent regression | `auto_compact_triggered` and `token_cost_reporting` prove runtime JSON keeps compaction and usage/cost fields parseable under mock responses, preventing parity drift in machine-readable output. |
| MCP degraded-startup regression | `rust/crates/runtime/src/mcp_stdio.rs::manager_discovery_report_keeps_healthy_servers_when_one_server_fails` proves a healthy MCP server remains callable while a broken peer is surfaced in a structured degraded report. |
| Plugin lifecycle state regression | `rust/crates/runtime/src/plugin_lifecycle.rs` unit tests cover healthy, degraded, failed, and shutdown states plus startup-event mapping. |
## Regression verification commands
Use the smallest command that proves the changed or audited surface, then broaden only when integration risk requires it.
- Mock scenario/docs map only:
- `cd rust && python3 scripts/run_mock_parity_diff.py --no-run`
- Full mock integration:
- `cd rust && cargo test -p rusty-claude-cli --test mock_parity_harness -- --nocapture`
- `cd rust && python3 scripts/run_mock_parity_diff.py`
- Plugin/MCP lifecycle contract:
- `cd rust && cargo test -p runtime plugin_lifecycle -- --nocapture`
- `cd rust && cargo test -p runtime mcp_stdio::tests::manager_discovery_report_keeps_healthy_servers_when_one_server_fails -- --exact --nocapture`
- Standard Rust gates for implementation changes touching these surfaces:
- `cd rust && cargo fmt --all -- --check`
- `cd rust && cargo check -p runtime -p rusty-claude-cli -p mock-anthropic-service`
- `cd rust && cargo clippy -p runtime --all-targets -- -D warnings`
## Known gaps / follow-ups
- The mock parity harness validates plugin tool execution but does not yet spin up a real MCP stdio server through the CLI prompt path; MCP degraded-startup remains covered by runtime manager tests.
- Worker-4 owns the plugin command fallthrough regression implementation lane (`task-10`); this map records the verification/docs boundary and should not duplicate that parser work.
- Full `cargo clippy -p runtime --all-targets -- -D warnings` can be blocked by unrelated `policy_engine.rs` clippy violations in this worktree; when that happens, report the exact pre-existing diagnostics and keep focused lifecycle tests green.
- No `.omx/ultragoal` files were changed; leader-owned Ultragoal checkpointing remains outside worker scope.
## Delegation evidence
Subagent spawn evidence: Task 9 spawned repository map probe `019e291d-e700-7171-b7bc-27ec0f6c850f`, debug/root-cause probe `019e291d-e86f-78d0-a137-214ede03285c`, and test/docs probe `019e291e-135c-79e1-80d0-9fd82866bd6e` before deeper local inspection. The repository-map probe errored with 429; the remaining probes did not return before the local verification map was grounded from repo evidence, so direct findings above were integrated.

View File

@@ -0,0 +1,89 @@
# G009 Windows docs/release readiness verification map
## Scope and source
This map ties the Stream 8 acceptance target from `.omx/plans/claw-code-2-0-adaptive-plan.md` to repository artifacts and local verification. It is the worker-1 integration lane artifact; it does not mutate `.omx/ultragoal` and avoids duplicating peer implementation lanes for Windows CI, install/provider docs, and policy/link work.
Stream 8 source requirement summary:
- PowerShell-first docs and CLI examples.
- Safe provider switching examples.
- Staged packaging path: source-only alpha first, binary release matrix next, package managers later.
- Windows smoke CI for help/doctor/config/status without live credentials.
- License, contribution, security, and support policies.
- Command/link validation for adoption docs.
## Acceptance-to-evidence matrix
| Acceptance area | Repository artifact(s) | Verification command(s) | Notes |
|---|---|---|---|
| PowerShell-first Windows install/run path | `README.md` (`Windows setup`, post-build binary location, PowerShell `.exe` examples); `install.sh` (Unix/WSL installer guard) | `python3 .github/scripts/check_doc_source_of_truth.py`; `cargo run -p rusty-claude-cli -- --help` | Current docs explicitly present Windows as a supported PowerShell path for source builds and `claw.exe`; `install.sh` is Linux/macOS/WSL-oriented, so native PowerShell binary usage and WSL installer usage must stay clearly separated. |
| Safe provider switching examples | `USAGE.md` (`Auth`, `Local Models`, `Supported Providers & Models`); `docs/MODEL_COMPATIBILITY.md` | `cargo test -p api providers::`; `cargo test -p rusty-claude-cli --test output_format_contract provider_diagnostics_explain_openai_compatible_capabilities -- --nocapture` | Provider docs cover Anthropic API-key vs bearer-token shape, OpenAI-compatible routing, Ollama/OpenRouter/DashScope examples, and prefix routing to avoid ambient credential misrouting. |
| Release artifact quickstart and staged packaging path | `README.md` (`Quick start`, `Post-build: locate the binary and verify`); `.github/workflows/release.yml`; `docs/windows-install-release.md` | `cargo build --release -p rusty-claude-cli`; `cargo run -p rusty-claude-cli -- version --output-format json`; `python3 .github/scripts/check_release_readiness.py (release-readiness gate)` | Release workflow packages Linux, macOS, and `claw-windows-x64.exe` assets with `.sha256` checksum files. README remains source-build-first, and the Windows quickstart names the checksum verification path. |
| Windows smoke CI without live credentials | `.github/workflows/rust-ci.yml`; CLI local-only surfaces in `rust/crates/rusty-claude-cli/src/main.rs` (`help`, `doctor`, resumed `/config`, `status`) | `cargo run -p rusty-claude-cli -- --help`; `cargo run -p rusty-claude-cli -- doctor --output-format json`; `cargo run -p rusty-claude-cli -- status --output-format json`; `cargo run -p rusty-claude-cli -- config --output-format json` | The smoke target is local-only command execution with isolated config and no real provider credentials. If the Windows CI lane is not present in a branch, this map is the integration checklist for that lane. |
| License metadata | `rust/Cargo.toml` (`workspace.package.license = "MIT"`) | `grep -n '^license = "MIT"' rust/Cargo.toml` | Cargo metadata declares MIT. A root `LICENSE` file remains the user-facing policy artifact to add if not already present in the policy lane. |
| Contribution/security/support policies | Expected root policy docs: `CONTRIBUTING.md`, `SECURITY.md`, `SUPPORT.md`; existing support links in `README.md` | `test -f CONTRIBUTING.md`; `test -f SECURITY.md`; `test -f SUPPORT.md`; `python3 .github/scripts/check_doc_source_of_truth.py` | These files are policy-lane outputs. This map records the exact release gate so missing files fail visibly instead of being inferred from README links. |
| Command/link validation | `.github/scripts/check_doc_source_of_truth.py`; `README.md`; `USAGE.md`; `docs/**` | `python3 .github/scripts/check_doc_source_of_truth.py`; `python3 - <<'PY' ...` link/reference check listed below | Existing validation catches stale branding/assets/invites across adoption docs. The lightweight reference check below catches broken relative Markdown links without network access. |
## Windows/local smoke command contract
Use isolated config and no live credentials. These commands must not require `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, or `DASHSCOPE_API_KEY`:
```powershell
# From repository root on Windows PowerShell
$env:CLAW_CONFIG_HOME = Join-Path $env:TEMP "claw-smoke-config"
Remove-Item Env:\ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
Remove-Item Env:\ANTHROPIC_AUTH_TOKEN -ErrorAction SilentlyContinue
Remove-Item Env:\OPENAI_API_KEY -ErrorAction SilentlyContinue
Remove-Item Env:\XAI_API_KEY -ErrorAction SilentlyContinue
Remove-Item Env:\DASHSCOPE_API_KEY -ErrorAction SilentlyContinue
cd rust
cargo run -p rusty-claude-cli -- --help
cargo run -p rusty-claude-cli -- doctor --output-format json
cargo run -p rusty-claude-cli -- status --output-format json
cargo run -p rusty-claude-cli -- config --output-format json
```
Equivalent Unix smoke used by this worker:
```bash
env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN -u OPENAI_API_KEY -u XAI_API_KEY -u DASHSCOPE_API_KEY \
CLAW_CONFIG_HOME="$(mktemp -d)" cargo run -p rusty-claude-cli -- --help
```
## Offline Markdown reference check
```bash
python3 - <<'PY'
from pathlib import Path
import re, sys
root = Path.cwd()
errors = []
for path in [Path('README.md'), Path('USAGE.md'), Path('PARITY.md'), Path('PHILOSOPHY.md'), *Path('docs').glob('*.md')]:
if not path.exists():
continue
text = path.read_text(encoding='utf-8')
for match in re.finditer(r'\[[^\]]+\]\(([^)]+)\)', text):
target = match.group(1).split('#', 1)[0]
if not target or '://' in target or target.startswith('mailto:'):
continue
if not (root / path.parent / target).resolve().exists():
line = text.count('\n', 0, match.start()) + 1
errors.append(f'{path}:{line}: missing relative link target {match.group(1)}')
if errors:
print('\n'.join(errors))
sys.exit(1)
print('offline markdown reference check passed')
PY
```
## Release gate
A Stream 8 release candidate is ready when all of the following are true:
1. PowerShell examples in `README.md` build and run `claw.exe` from a clean Windows checkout.
2. Provider examples in `USAGE.md` show session-local/shell-local switching, include cleanup for conflicting ambient credentials (`unset` / `Remove-Item Env:`), and never instruct users to paste secrets into persistent config by default.
3. Windows smoke CI runs help/doctor/config/status without live credentials, separates native PowerShell `claw.exe` smoke from WSL `install.sh` smoke, and archives JSON output on failure.
4. Release artifacts include the documented platform matrix or the docs clearly state source-only alpha status.
5. `LICENSE`, `CONTRIBUTING.md`, `SECURITY.md`, and `SUPPORT.md` exist or the policy lane records an explicit release-blocking exception.
6. Doc source-of-truth and offline relative-link validation pass.

View File

@@ -0,0 +1,62 @@
# G010 clone disambiguation metadata and verification map
Scope: worker-2 task 5 for `G010-session-hygiene` / Stream 9 session hygiene, local state, and recovery UX. This artifact maps the clone/worktree disambiguation contract and the focused verification surface without mutating leader-owned `.omx/ultragoal` state.
## Contract summary
Claw session state is intentionally scoped to the current workspace clone/worktree. Operators and automation should treat the **session partition**, not a bare session id or the flat `.claw/sessions/` directory, as the identity boundary.
Required metadata and behaviors:
- **Workspace-bound partition**: managed sessions live under `.claw/sessions/<workspace_fingerprint>/`, where the fingerprint is a stable 16-character FNV-1a digest of the canonical workspace path.
- **Canonical path input**: `SessionStore::from_cwd` and `SessionStore::from_data_dir` canonicalize their workspace path before computing the partition, preventing `/tmp/foo` vs `/private/tmp/foo` and relative-vs-absolute spelling from creating two stores for the same clone.
- **Clone/worktree isolation**: two distinct clones or worktrees must get different session partitions, even if session ids collide.
- **Legacy safety**: flat legacy sessions under `.claw/sessions/` remain readable only when they are bound to the same workspace or are unbound but physically inside the current workspace; sessions whose persisted `workspace_root` points at another clone are rejected as `WorkspaceMismatch`.
- **Fork lineage stays local**: `/session fork` / managed session forking keeps the forked session in the same workspace partition and records parent id plus optional branch name.
- **User-facing disambiguation**: empty-session copy names the actual fingerprint directory and explains that sessions from other CWDs are intentionally invisible.
## Implementation anchors
| Contract area | Repo anchor | Evidence role |
| --- | --- | --- |
| Partition layout and canonical workspace root | `rust/crates/runtime/src/session_control.rs:10-18`, `:32-47`, `:54-71` | Documents and implements `.claw/sessions/<workspace_hash>/` for `from_cwd` and explicit data-dir stores. |
| Fingerprint algorithm | `rust/crates/runtime/src/session_control.rs:300-312` | Defines the 16-character FNV-1a workspace fingerprint used as the clone disambiguator. |
| Managed create/resolve/list/load/fork APIs | `rust/crates/runtime/src/session_control.rs:86-204` | Ensures handles, `latest`, load, and fork resolve inside the active partition. |
| Legacy/cross-workspace guard | `rust/crates/runtime/src/session_control.rs:213-233`, `:557-567` | Rejects mismatched persisted `workspace_root` and allows only same-workspace legacy files. |
| Empty partition copy | `rust/crates/runtime/src/session_control.rs:535-543` | Reports `.claw/sessions/<fingerprint>/` plus the workspace-partition note. |
| CLI wrapper | `rust/crates/rusty-claude-cli/src/main.rs:5952-6040` | Routes session CLI helpers through `current_session_store()`, so CLI list/latest/load uses the same partition. |
| CLI session-list lifecycle context | `rust/crates/rusty-claude-cli/src/main.rs:5991-6027`, `:12960-12990` | Renders saved-only/dirty/abandoned lifecycle context for the current partition. |
| CLI session resolution regression | `rust/crates/rusty-claude-cli/src/main.rs:13470-13579` | Covers JSONL default, legacy flat resolution, latest selection, and workspace mismatch rejection from CLI wrappers. |
## Covered roadmap and dogfood anchors
- `ROADMAP.md:1125-1129` — session files are namespaced by workspace fingerprint, and wrong-workspace session access is rejected.
- `ROADMAP.md:1419-1441` — empty/missing session messages must expose the fingerprint directory instead of implying a flat `.claw/sessions/` search.
- `ROADMAP.md:1453-1476` — the session partition boundary must be visible or shared deliberately; current contract is visible CWD/workspace partitioning.
- `ROADMAP.md:5797-5902` — canonicalization closes the symlink/path-equivalence split in workspace fingerprints.
- `ROADMAP.md:6342-6366` and `ROADMAP.md:6384-6411` — remaining Stream 9 risks around reported CWD form, failed-resume filesystem side effects, and broad-CWD resume guards are related UX/recovery lanes, not clone identity itself.
## Focused verification map
| Claim | Focused check |
| --- | --- |
| Same canonical workspace spellings share one partition | `cargo test --manifest-path rust/Cargo.toml -p runtime session_store_from_cwd_canonicalizes_equivalent_paths -- --nocapture` |
| Distinct clones/worktrees do not see each other's sessions | `cargo test --manifest-path rust/Cargo.toml -p runtime session_store_from_cwd_isolates_sessions_by_workspace -- --nocapture` |
| Explicit data-dir stores still namespace by workspace | `cargo test --manifest-path rust/Cargo.toml -p runtime session_store_from_data_dir_namespaces_by_workspace -- --nocapture` |
| Same-workspace legacy sessions are readable; cross-workspace ones are rejected | `cargo test --manifest-path rust/Cargo.toml -p runtime session_store_rejects_legacy_session_from_other_workspace session_store_loads_safe_legacy_session_from_same_workspace session_store_loads_unbound_legacy_session_from_same_workspace -- --nocapture` |
| `latest` and managed reference resolution stay inside the active partition | `cargo test --manifest-path rust/Cargo.toml -p runtime session_store_latest_and_resolve_reference -- --nocapture` and `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli latest_session_alias_resolves_most_recent_managed_session -- --nocapture` |
| Forks retain partition and lineage metadata | `cargo test --manifest-path rust/Cargo.toml -p runtime session_store_fork_stays_in_same_namespace -- --nocapture` |
| CLI wrapper rejects wrong-workspace files | `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli load_session_reference_rejects_workspace_mismatch -- --nocapture` |
| Docs-only map is syntactically clean | `git diff --check` |
| Broader type/test gate for the touched domain | `cargo check --manifest-path rust/Cargo.toml -p runtime -p rusty-claude-cli` plus `cargo test --manifest-path rust/Cargo.toml -p runtime session_control -- --nocapture` |
## Known boundaries and integration notes
- This worker intentionally did **not** edit `docs/g010-session-hygiene-verification-map.md` because worker-4 task 7 also names that final aggregate map. This file is the worker-2 clone-disambiguation map that worker-4/leader can link or merge into the aggregate map.
- The current `SessionStore::from_cwd` contract keys on the canonical current directory, not necessarily the git top-level. That is acceptable only if status/help surfaces keep the partition boundary visible; `ROADMAP.md:1453-1476` remains the product tradeoff record.
- Failed-resume directory creation and broad-CWD guards are related session hygiene hazards but are owned by the Stream 9 CLI/recovery lanes, not this docs-only clone-disambiguation task.
- No `.omx/ultragoal` files were changed; leader-owned aggregate checkpointing consumes this commit and task lifecycle evidence.
## Delegation evidence
Subagent spawn evidence: 1, repository map probe `019e295d-a3dc-7041-bc96-30ee52b95698`; spawned before deeper serial mapping per task contract, but it errored with `429 Too Many Requests`, so direct repo evidence above was integrated instead.

View File

@@ -0,0 +1,21 @@
# G010 Session Hygiene Verification Map
Stream 9 session hygiene is implemented in the Rust runtime/CLI as workspace-scoped session storage plus resume-safe recovery commands.
## Acceptance mapping
| Acceptance area | Code surface | Evidence |
| --- | --- | --- |
| Generated session files are not accidentally committed | `.gitignore`, `rust/.gitignore` ignore `.claw/sessions/` and `.claude/sessions/` | `git check-ignore .claw/sessions/example.jsonl rust/.claw/sessions/example.jsonl .claude/sessions/example.json` |
| Per-worktree session isolation | `rust/crates/runtime/src/session_control.rs` (`SessionStore`, `workspace_fingerprint`, workspace validation) | `cargo test -p runtime session_store_from_cwd_isolates_sessions_by_workspace` |
| List/resume/delete/exists contracts | `rust/crates/commands/src/lib.rs` parses `/session list`, `/session exists`, `/session delete`, `/resume`; `rust/crates/rusty-claude-cli/src/main.rs` renders text/JSON resume-safe session commands | `cargo test -p rusty-claude-cli session_exists_resume_command_reports_json_contract`; `cargo test -p rusty-claude-cli resume_report_uses_sectioned_layout` |
| Compact and provider context-window recovery | `rust/crates/runtime/src/compact.rs`; `rust/crates/rusty-claude-cli/src/main.rs` context-window error recovery guidance and resumed `/compact` | `cargo test -p rusty-claude-cli provider_context_window_errors_are_reframed_with_same_guidance`; `cargo test -p commands compacts_sessions_via_slash_command` |
| JSONL bloat safeguards | `rust/crates/runtime/src/session.rs` rotates oversized JSONL session files and keeps bounded rotated logs | `cargo test -p runtime rotates_and_cleans_up_large_session_logs` |
| Interrupt/recovery path | `rust/crates/rusty-claude-cli/src/main.rs` keeps `/clear --confirm`, `/compact`, `/status`, and `/resume latest` resume-safe for unusable threads | `cargo test -p rusty-claude-cli context_window_preflight_errors_render_recovery_steps`; `cargo test -p rusty-claude-cli parses_resume_flag_with_multiple_slash_commands` |
| Clone/session disambiguation | `Session` persists `workspace_root`; forks persist parent/branch metadata; session list shows lineage and lifecycle | `cargo test -p runtime persists_workspace_root_round_trip_and_forks_inherit_it`; `cargo test -p runtime forks_sessions_with_branch_metadata_and_persists_it` |
## Notes for leader audit
- Workers did not mutate `.omx/ultragoal`; this file is a repo-local verification map for team evidence only.
- Runtime-owned session state remains under ignored `.claw/sessions/<workspace-fingerprint>/` paths.
- Resume-safe JSON output uses stable `kind` fields (`restored`, `compact`, `session_list`, `session_exists`, etc.) so claws can route without scraping text.

View File

@@ -0,0 +1,68 @@
# G011 ACP/Zed and JSON-RPC status contract
Claw Code 2.0 keeps ACP/Zed and JSON-RPC serving behind the stable task,
session-control, and event/report contracts from the roadmap. The current public
surface is therefore a **truthful unsupported status**, not a hidden daemon.
## Supported status queries
The following commands are status queries and exit with code `0`:
```bash
claw acp
claw acp serve
claw --acp
claw -acp
claw acp --output-format json
claw acp serve --output-format json
```
`serve` is deliberately an alias for status today. It does not bind a socket,
start a daemon, or expose a JSON-RPC endpoint.
## JSON envelope
`claw acp --output-format json` returns a stable envelope for editor probes and
CI checks:
```json
{
"schema_version": "1.0",
"kind": "acp",
"status": "unsupported",
"phase": "discoverability_only",
"supported": false,
"exit_code": 0,
"serve_alias_only": true,
"protocol": {
"name": "ACP/Zed",
"json_rpc": false,
"daemon": false,
"endpoint": null,
"serve_starts_daemon": false
}
}
```
Consumers should check `kind == "acp"`, `supported == false`, and
`protocol.json_rpc == false` instead of inferring support from command presence.
## Unsupported invocations
Malformed ACP invocations, such as `claw acp start`, exit with code `1`. With
`--output-format json`, stderr uses the normal CLI error envelope and sets:
```json
{
"type": "error",
"kind": "unsupported_acp_invocation",
"exit_code": 1
}
```
## Deferral gate
Real ACP/Zed or JSON-RPC serve work remains deferred until the roadmap contracts
for task packets, session control, and event/report schemas are stable. This
keeps desktop, marketplace, and editor integrations from becoming alternate
sources of truth before the CLI/file/API contracts are ready.

View File

@@ -0,0 +1,62 @@
# G011 Ecosystem/Ops/UX Verification Map
G011 closes the laterals that were intentionally deferred from the earlier safety,
session, MCP, Windows, and docs streams. This map is the cross-lane gate for the
team run: it names the surfaces that can be verified locally, the exact checks to
rerun after worker integrations, and the UX deferrals that must remain explicit
until their product contracts are stable.
## Cross-lane acceptance matrix
| Lane | Owned surface | Regression evidence | Gate / gap |
| --- | --- | --- | --- |
| ACP/Zed status and JSON contracts | `rust/crates/rusty-claude-cli/src/main.rs` parses `claw acp`, `claw acp serve`, `--acp`, and `-acp`; `README.md` and `rust/README.md` document discoverability-only status | `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract acp_guidance_emits_json_when_requested -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli local_command_help_flags_stay_on_the_local_parser_path -- --nocapture` | Real ACP/Zed daemon support remains deferred; status output must not imply a running protocol endpoint. |
| Plugin/marketplace local routing | `rust/crates/rusty-claude-cli/src/main.rs` routes `claw plugins`, `claw plugin`, and `claw marketplace` to local plugin handling; `rust/crates/commands/src/lib.rs` keeps `/plugin` aliases in shared slash-command help | `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli removed_login_and_logout_subcommands_error_helpfully -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli direct_slash_commands_surface_shared_validation_errors -- --nocapture`; `python3 -m unittest tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_filter_excludes_plugin_sources tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_aliases_execute_as_local_commands tests.test_porting_workspace.PortingWorkspaceTests.test_route_plugin_slash_commands_match_commands tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_stream_emits_command_match tests.test_porting_workspace.PortingWorkspaceTests.test_turn_loop_plugin_commands_are_not_prompt_only` | Marketplace is an alias to local plugin management only; no remote marketplace browsing/install contract is claimed. |
| TUI/copy/paste/clickable path UX | `rust/crates/commands/src/lib.rs` advertises `/copy`, `/paste`, `/desktop`, and path-oriented commands; `rust/crates/rusty-claude-cli/src/main.rs` renders compact file/tool paths for terminal readability | `cargo test --manifest-path rust/Cargo.toml -p commands renders_help_with_grouped_categories_and_keyboard_shortcuts -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli tool_rendering_helpers_compact_output -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli tool_rendering_truncates_large_read_output_for_display_only -- --nocapture` | Clipboard integration, full-screen TUI mode, and clickable terminal hyperlinks are not stable product contracts yet; keep them as roadmap/UX follow-ups unless a targeted implementation lands. |
| Desktop integration deferral | `rust/crates/commands/src/lib.rs` includes `/desktop`; `rust/crates/rusty-claude-cli/src/main.rs` treats it as not implemented in the current build | `cargo test --manifest-path rust/Cargo.toml -p commands renders_help_from_shared_specs -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p commands renders_per_command_help_detail -- --nocapture` | `/desktop` must stay discoverable but non-committal until a desktop launch/API contract exists. |
| Navigation/file-context/local-provider docs | `README.md`, `USAGE.md`, `rust/README.md`, `docs/MODEL_COMPATIBILITY.md`, and worker-2 docs updates | `python3 .github/scripts/check_doc_source_of_truth.py`; `python3 .github/scripts/check_release_readiness.py`; `git diff --check` | Re-run after docs integrations; this lane should not alter Rust behavior unless docs expose a code contract gap. |
| Issue/PR ops gate | `docs/pr-issue-resolution-gate.md`, `docs/roadmap-pr-goals.md`, and issue/PR triage templates if present | `python3 .github/scripts/check_release_readiness.py`; `git diff --check`; optional `python3 scripts/validate_cc2_board.py` only when `.omx/cc2/board.md` changes | Worker lanes must not merge/close remote PRs or issues; final reconciliation remains leader-owned. |
## Task 5 UX/deferral support notes
- `/copy`, `/paste`, and `/desktop` are parsed slash-command names, but current
runtime handling still reports unimplemented commands rather than performing
clipboard or desktop side effects. That is safer than pretending support exists.
- `/marketplace` is intentionally a plugin alias; it should not be described as
a remote marketplace until install/search/update semantics and trust policy are
specified.
- Path readability is covered by terminal rendering helpers that compact long
tool outputs and preserve paths in read/write/edit summaries. Clickable OSC-8
links, if added later, need separate tests because terminal support varies.
- Full-screen TUI mode remains aspirational (`rust/TUI-ENHANCEMENT-PLAN.md`);
current verification should focus on the inline REPL/help/status surfaces.
## Final verification sequence
Run these after all G011 worker commits are integrated into the leader branch:
```bash
git diff --check
python3 .github/scripts/check_doc_source_of_truth.py
python3 .github/scripts/check_release_readiness.py
cargo check --manifest-path rust/Cargo.toml -p commands -p rusty-claude-cli
cargo test --manifest-path rust/Cargo.toml -p commands renders_help_from_shared_specs -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p commands renders_help_with_grouped_categories_and_keyboard_shortcuts -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p commands renders_per_command_help_detail -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli removed_login_and_logout_subcommands_error_helpfully -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli direct_slash_commands_surface_shared_validation_errors -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli local_command_help_flags_stay_on_the_local_parser_path -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli tool_rendering_helpers_compact_output -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli tool_rendering_truncates_large_read_output_for_display_only -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract acp_guidance_emits_json_when_requested -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract plugins_json_surfaces_lifecycle_contract_when_plugin_is_installed -- --nocapture
python3 -m unittest tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_filter_excludes_plugin_sources tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_aliases_execute_as_local_commands tests.test_porting_workspace.PortingWorkspaceTests.test_route_plugin_slash_commands_match_commands tests.test_porting_workspace.PortingWorkspaceTests.test_plugin_command_stream_emits_command_match tests.test_porting_workspace.PortingWorkspaceTests.test_turn_loop_plugin_commands_are_not_prompt_only
```
## Leader audit notes
- This map is repo-local evidence only; workers must not mutate `.omx/ultragoal`.
- If a check fails because another lane is still in progress, record the failing
command and rerun after that lane is integrated instead of weakening the gate.
- The minimum terminal condition is: docs checks pass, Rust targeted tests pass,
and any still-deferred UX surface is explicitly named above.

View File

@@ -0,0 +1,73 @@
# G012 Final Release Readiness Report
Snapshot: 2026-05-15T02:59:29Z on `origin/main` / `HEAD` `2e93264919f38835410668ff6ca588606bc629f0`.
This is the worker-1 roadmap/board audit and release-readiness evidence map for the
Claw Code 2.0 final gate. It is intentionally repo-local and non-destructive: it
references `.omx/ultragoal` evidence without modifying leader-owned ultragoal
state, and it does not merge PRs or close issues owned by the W3/W4 lanes.
## Release readiness summary
| Gate | Evidence | Result |
| --- | --- | --- |
| Ultragoal stream completion | `.omx/ultragoal/goals.json` shows G001-G011 complete and G012 pending at this snapshot. | PASS for pre-final stream completion; G012 remains the active final gate. |
| Roadmap board coverage | `python3 scripts/validate_cc2_board.py` -> `PASS cc2 board validation`; 729 board items; 124/124 ROADMAP headings mapped; 542/542 ROADMAP actions mapped. | PASS |
| Issue/parity intake coverage | `python3 .omx/cc2/validate_issue_parity_intake.py` -> `PASS issue/parity intake: 19 issue rows, 9 parity rows`. | PASS |
| Release docs/readiness script | `python3 .github/scripts/check_release_readiness.py` -> `release-readiness check passed`. | PASS |
| Documentation source-of-truth | `python3 .github/scripts/check_doc_source_of_truth.py` -> `doc source-of-truth check passed`. | PASS |
| Fresh open PR snapshot | `gh pr list --state open --limit 1000 --json number,title,state,updatedAt,url,isDraft,mergeable` -> 51 open PR records; newest #3040. | PASS for snapshot capture; W3 owns reconciliation/action. |
| Fresh open issue snapshot | `gh issue list --state open --limit 1000 --json number,title,state,updatedAt,url,labels` -> 1000 open issue records; newest returned #3036. | PASS for snapshot capture with limit caveat; W4 owns reconciliation/action. |
## Stream evidence index
| Goal | Status in local ultragoal state | Primary tracked evidence |
| --- | --- | --- |
| G001 Stream 0 board | complete | `.omx/cc2/board.json`, `.omx/cc2/board.md`, `scripts/validate_cc2_board.py` |
| G002 security | complete | `docs/g002-security-verification-map.md` |
| G003 boot/session | complete | `docs/g003-boot-session-verification-map.md` |
| G004 events/reports | complete | `docs/g004-events-reports-verification-map.md`, `docs/g004-events-reports-contract.md` |
| G005 branch/recovery | complete | `docs/g005-branch-recovery-verification-map.md` |
| G006 task/policy/board | complete | `docs/g006-task-policy-board-verification-map.md` |
| G007 plugin/MCP | complete | `docs/g007-plugin-mcp-verification-map.md`, `docs/g007-mcp-lifecycle-mapping.md` |
| G008 provider compatibility | complete | `docs/local-openai-compatible-providers.md` plus ultragoal quality-gate artifact |
| G009 Windows/docs/release | complete | `docs/g009-windows-docs-release-verification-map.md`, `docs/windows-install-release.md` |
| G010 session hygiene | complete | `docs/g010-session-hygiene-verification-map.md`, `docs/g010-clone-disambiguation-metadata.md` |
| G011 ecosystem/ops/UX | complete | `docs/g011-ecosystem-ops-ux-verification-map.md`, `docs/g011-acp-json-rpc-status-contract.md`, `docs/pr-issue-resolution-gate.md` |
| G012 final gate | pending | This report plus W2/W3/W4 final gate reports. |
## Roadmap PR audit snapshot
`docs/roadmap-pr-goals.md` lists 17 roadmap/product-fit PRs that must be merged
only when correct, resolvable, and safe. The fresh GitHub snapshot shows all 17
remain open. Sixteen roadmap-doc PRs are currently `CONFLICTING`, so they are not
safe direct-merge candidates from this worker lane. PR #2824 is `MERGEABLE`, but
it is explicitly product-fit review rather than a direct roadmap merge candidate.
| PR | Title | Mergeable | Draft | Updated | Worker-1 final-gate disposition |
| --- | --- | --- | --- | --- | --- |
| #2824 | docs: personal assistant roadmap | MERGEABLE | false | 2026-04-28T13:05:03Z | Defer to product-fit/leader decision; do not auto-merge as CC2 release gate evidence. |
| #2839 | docs(roadmap): add #330 — resume mode stats/cost always zero | CONFLICTING | false | 2026-04-29T12:36:19Z | Not mergeable without conflict resolution; mapped into completed session/status streams. |
| #2841 | docs(roadmap): add #332 — doctor json missing top-level status field | CONFLICTING | false | 2026-04-29T13:04:12Z | Not mergeable without conflict resolution; mapped into completed boot/doctor streams. |
| #2842 | docs(roadmap): add #334 — version json omits build_date and uses short sha only | CONFLICTING | false | 2026-04-29T13:35:01Z | Not mergeable without conflict resolution; release-readiness docs/scripts pass at HEAD. |
| #2844 | docs(roadmap): add #336 — session subcommand resume inconsistency and type/kind error mismatch | CONFLICTING | false | 2026-04-29T14:03:19Z | Not mergeable without conflict resolution; mapped into completed session hygiene streams. |
| #2846 | docs(roadmap): add #331 — export silently overwrites on repeated invocations | CONFLICTING | false | 2026-04-29T13:02:02Z | Not mergeable without conflict resolution; action remains W3/leader triage if still desired. |
| #2848 | docs(roadmap): add #333 — no in-session settings inspect command | CONFLICTING | false | 2026-04-29T13:32:01Z | Not mergeable without conflict resolution; action remains W3/leader triage if still desired. |
| #2850 | docs(roadmap): add #335 — session list omits created_at_ms field | CONFLICTING | false | 2026-04-29T14:01:29Z | Not mergeable without conflict resolution; mapped into completed session metadata streams. |
| #2858 | docs(roadmap): add #343 — session subcommand resume-safety inconsistently enforced | CONFLICTING | false | 2026-04-29T16:02:45Z | Not mergeable without conflict resolution; mapped into completed session/recovery streams. |
| #2862 | docs(roadmap): add #342 — status json omits active session ID, workspace counters ambiguous | CONFLICTING | false | 2026-04-29T19:04:31Z | Not mergeable without conflict resolution; mapped into completed status/session streams. |
| #2864 | docs(roadmap): add #364 — /cost returns no cost_usd; identical to /stats | CONFLICTING | false | 2026-04-29T22:32:52Z | Not mergeable without conflict resolution; mapped into completed UX/status contract review. |
| #2865 | docs(roadmap): add #362 — doctor auth false-positive: misses CLI session tokens | CONFLICTING | false | 2026-04-29T22:06:28Z | Not mergeable without conflict resolution; mapped into completed doctor/auth stream work. |
| #2867 | docs(roadmap): add #368 — export always appends .txt; response.file reflects mangled path | CONFLICTING | false | 2026-04-29T23:35:35Z | Not mergeable without conflict resolution; action remains W3/leader triage if still desired. |
| #2868 | docs(roadmap): add #356 — session list title always null; no rename command | CONFLICTING | false | 2026-04-29T20:36:43Z | Not mergeable without conflict resolution; mapped into completed session identity streams. |
| #2869 | docs(roadmap): add #358 — history entries missing role field, no pagination | CONFLICTING | false | 2026-04-29T21:02:55Z | Not mergeable without conflict resolution; mapped into completed session/history review. |
| #2872 | docs(roadmap): add #360 — /tokens, /stats, /cost identical output; no context-window or cost_usd | CONFLICTING | false | 2026-04-29T21:32:57Z | Not mergeable without conflict resolution; mapped into completed UX/status contract review. |
| #2876 | docs(roadmap): add #354 — /cwd suggests itself in did-you-mean; self-referential loop | CONFLICTING | false | 2026-04-29T20:01:22Z | Not mergeable without conflict resolution; mapped into completed command UX review. |
## Final-gate stop condition for worker-1
Worker-1's release-readiness lane is complete when this report is committed and
its checks pass. Overall G012 completion still requires the leader to integrate
W2 quality-gate classification and W3/W4 PR/issue reconciliation evidence. This
report does not claim the remote PR/issue backlog is resolved; it provides the
fresh roadmap/board/readiness audit that those lanes can reference.

View File

@@ -0,0 +1,150 @@
# Local OpenAI-compatible providers and skills setup
This guide covers two common offline/local workflows:
1. running Claw against an OpenAI-compatible local model server such as Ollama, llama.cpp, or vLLM; and
2. installing local skills from disk so Claw can discover them without network access.
## Claw is not Claude-only
Claw Code is a Claude-Code-shaped workflow/runtime, not a Claude-only product. It supports Anthropic directly and can target OpenAI-compatible, provider-routed, and local models depending on configuration. Non-Claude providers are supported honestly: they may require stricter tool-call and response-shape compatibility, and some slash/tool workflows can be rougher than first-party Anthropic/OpenAI paths. Provider-specific identity leaks are bugs, not intended product positioning.
If you need the most polished daily-driver experience for a specific non-Claude model today, compare that providers native tools. If you need runtime/provider hackability, Claws OpenAI-compatible route is the intended extension path.
## OpenAI-compatible routing basics
Set `OPENAI_BASE_URL` to the servers `/v1` endpoint and set `OPENAI_API_KEY` to either the required token or a harmless placeholder for local servers that expect an Authorization header. The model name must match what the server exposes.
```bash
export OPENAI_BASE_URL="http://127.0.0.1:11434/v1"
export OPENAI_API_KEY="local-dev-token"
claw --model "qwen3:latest" prompt "Reply exactly HELLO_WORLD_123"
```
Routing notes:
- Use the `openai/` prefix for OpenAI-compatible gateways when you need prefix routing to win over ambient Anthropic credentials, for example `--model "openai/gpt-4.1-mini"` with OpenRouter.
- For local servers, prefer the exact model ID reported by the server (`qwen3:latest`, `llama3.2`, `Qwen/Qwen2.5-Coder-7B-Instruct`, etc.). If your local gateway exposes slash-containing IDs, use that exact slug.
- If you have multiple provider keys in your environment, remove unrelated keys while smoke-testing a local route or choose a model prefix that unambiguously selects the intended provider.
- Tool workflows need model/server support for OpenAI-compatible tool calls. Plain prompt smoke tests can pass even when slash/tool workflows still fail because the server returns an incompatible tool-call shape.
## Raw `/v1/chat/completions` smoke test
Before debugging Claw, verify the local server speaks the expected wire format:
```bash
curl -sS "$OPENAI_BASE_URL/chat/completions" \
-H "Authorization: Bearer ${OPENAI_API_KEY:-local-dev-token}" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:latest",
"messages": [{"role": "user", "content": "Reply exactly HELLO_WORLD_123"}],
"stream": false
}'
```
Expected result: a JSON response with one assistant message containing `HELLO_WORLD_123`. If this fails, fix the local server, model name, or auth token before changing Claw settings.
## Ollama
Start Ollama and pull a model:
```bash
ollama pull qwen3:latest
ollama serve
```
In another shell:
```bash
export OPENAI_BASE_URL="http://127.0.0.1:11434/v1"
export OPENAI_API_KEY="local-dev-token"
claw --model "qwen3:latest" prompt "Reply exactly HELLO_WORLD_123"
```
If Ollama is running without auth and your build accepts authless local OpenAI-compatible servers, `unset OPENAI_API_KEY` is also acceptable. Use a placeholder token rather than a real cloud API key for local testing.
## llama.cpp server
Start a llama.cpp OpenAI-compatible server with the model name you want Claw to send:
```bash
llama-server -m ./models/qwen2.5-coder.gguf --host 127.0.0.1 --port 8080 --alias qwen2.5-coder
```
Then smoke-test through Claw:
```bash
export OPENAI_BASE_URL="http://127.0.0.1:8080/v1"
export OPENAI_API_KEY="local-dev-token"
claw --model "qwen2.5-coder" prompt "Reply exactly HELLO_WORLD_123"
```
## vLLM or another OpenAI-compatible server
Start vLLM with an OpenAI-compatible API server:
```bash
vllm serve Qwen/Qwen2.5-Coder-7B-Instruct --host 127.0.0.1 --port 8000
```
Then route Claw to it:
```bash
export OPENAI_BASE_URL="http://127.0.0.1:8000/v1"
export OPENAI_API_KEY="local-dev-token"
claw --model "Qwen/Qwen2.5-Coder-7B-Instruct" prompt "Reply exactly HELLO_WORLD_123"
```
## Local skills install from disk
Skills are discovered from Claw skill roots such as `.claw/skills/` in a workspace and `~/.claw/skills/` for user-level installs. Legacy `.codex/skills/` roots may also be scanned for compatibility, but new local Claw projects should prefer `.claw/skills/`.
A skill directory should contain a `SKILL.md` file with frontmatter:
```text
my-skill/
└── SKILL.md
```
```markdown
---
name: my-skill
description: Explain when this skill should be used.
---
# My Skill
Instructions for the agent go here.
```
Install a skill from a local path in the interactive REPL:
```text
/skills install /absolute/path/to/my-skill
/skills list
/skills my-skill
```
Or inspect skills from the direct CLI surface:
```bash
claw skills --output-format json
```
Offline install checklist:
- Install the specific skill directory, not only the repository root, unless that repository root itself contains `SKILL.md`.
- Keep the frontmatter `name` aligned with the directory name users will type.
- After installing, run `/skills list` or `claw skills --output-format json` to confirm the discovered name and source path.
- If a skill invocation fails with an HTTP/provider error, the skill may have installed correctly but the current model/provider call failed. Run `claw doctor`, verify provider credentials, and try a simple prompt smoke test before reinstalling the skill.
## Troubleshooting
| Symptom | Check |
|---|---|
| Claw still asks for Anthropic credentials | Use an explicit OpenAI-compatible model route or remove unrelated Anthropic env vars during local smoke tests. |
| `model not found` from local server | Use the exact model ID exposed by Ollama/llama.cpp/vLLM. |
| Plain prompt works but tools fail | Confirm the model/server supports OpenAI-compatible tool calls and response shapes. |
| Skill says installed but `/skills <name>` fails | Check `/skills list` for the discovered name and source; verify provider credentials separately with `claw doctor`. |
| A local docs/log file contains secrets | Redact it before using `@path` file context or attaching it to an issue. |

View File

@@ -0,0 +1,69 @@
# Navigation and file context guide
This guide answers the common “how do I browse output?” and “how do I submit a file?” questions for Claw Code. Claw is an agent CLI, not a full file manager: terminal navigation comes from your shell or terminal, while file context is passed explicitly in prompts.
## Prompt and terminal navigation
Use your terminals normal controls for command history and long output:
- `Up` / `Down` usually move through shell or REPL prompt history.
- `Ctrl-r` searches shell history in most shells.
- Long command output is viewed with your terminal scrollback. In tmux, enter copy mode with `Ctrl-b [` then use arrows, PageUp/PageDown, search, or your mouse depending on tmux config.
- If output is too large to scroll comfortably, redirect it to a file and give that file to Claw as context:
```bash
cargo test --workspace 2>&1 | tee logs/test-output.txt
claw prompt "Use @logs/test-output.txt as context and summarize the failing tests."
```
Claw may provide slash commands that inspect workspace state, but those commands do not replace your terminals scrollback or shell history.
## Submit repository files with `@path`
Mention files from the current workspace with `@` paths. Use relative paths from the repository or current working directory:
```text
Read @src/app.ts and explain the bug.
Compare @old.md and @new.md.
Use @logs/error.txt as context and suggest a fix.
Review @README.md and @docs/navigation-file-context.md for consistency.
```
Tips:
- Prefer the smallest useful file set. Large directories or logs can consume context quickly.
- Use exact paths when possible (`@rust/crates/runtime/src/lib.rs`) instead of vague descriptions.
- For generated logs, save them under a temporary or ignored directory such as `logs/` and reference the file.
- If the file is outside the repository, copy it into a safe workspace location first or use an app/UI attachment feature if your Claw surface supports attachments.
## Browse or inspect files
Claw can answer questions about files you reference, and you can ask it to inspect likely locations:
```text
Find where provider routing is implemented and summarize the relevant files.
Read @USAGE.md and tell me where local model setup is documented.
Search for the command that handles skills install, then explain the control flow.
```
For deterministic shell-side browsing, ordinary commands still work:
```bash
find docs -maxdepth 2 -type f | sort
rg -n "OPENAI_BASE_URL|skills install" USAGE.md docs rust
sed -n '250,340p' USAGE.md
```
## Attach external files where supported
Some UI surfaces let you drag and drop or attach files directly. When that is available, use attachments for files that should not be committed to the repo. In terminal-only usage, copy the file into the workspace, reference it with `@path`, then remove it when finished if it was temporary.
## Secret and credential safety
Do not paste real API keys, OAuth tokens, private logs, or customer data into prompts, issue comments, screenshots, or committed docs. Before submitting a file:
- Replace live keys with placeholders such as `sk-ant-REPLACE_ME`, `sk-or-v1-REPLACE_ME`, or `local-dev-token`.
- Redact bearer tokens, cookies, session IDs, and private base URLs.
- Prefer minimal reproductions over full production logs.
- Keep `.env`, key files, and private logs out of git.
If a task requires credentials, describe the variable names and expected shapes instead of sharing the values.

View File

@@ -0,0 +1,67 @@
# Claw Code 2.0 PR and Issue Resolution Gate
This gate was added to the Claw Code 2.0 Ultragoal after the explicit requirement:
> all PRs should be merged and all issues should be resolved if resolvable and correct.
## Scope
Before the Claw Code 2.0 Ultragoal can be marked complete:
1. Every open GitHub PR at the current final-gate snapshot must be triaged.
2. PRs that are correct, compatible with Claw Code 2.0 direction, and pass required verification must be merged.
3. PRs that are stale, incorrect, duplicative, unsafe, spam, or outside Claw Code scope must not be merged; each needs a recorded rationale.
4. Every open GitHub issue at the current final-gate snapshot must be triaged.
5. Issues that are resolvable and correct must be fixed or explicitly linked to a merged fix.
6. Issues that are spam, duplicates, incorrect, unactionable, externally blocked, or not Claw Code work must be closed or labeled/commented with rationale when repository policy allows.
7. The final completion audit must use a fresh GitHub snapshot, not only the planning snapshot.
## Current live snapshot
A fresh non-destructive snapshot was captured locally during G011 W3 execution:
- Command: `gh pr list --state open --limit 1000 --json number,title,state,updatedAt,url`
- Command: `gh issue list --state open --limit 1000 --json number,title,state,updatedAt,url,labels`
- Captured on: 2026-05-15T02:39:41Z during the active Ultragoal run.
- Observed counts: 51 open PR records and 1000 open issue records from GitHub CLI list calls.
- Most recent open PR in the snapshot: #3040, `fix: recognize OPENAI_API_KEY as valid auth for OpenAI-compatible endpoints`, updated 2026-05-14T11:35:23Z.
- Most recent open issue in the snapshot: #3039, `How to install skills?`, updated 2026-05-14T08:14:36Z.
- The issue snapshot hit the configured `--limit 1000`, so the final gate must treat the issue count as at least 1000 unless a higher-limit export or paginated ledger is captured.
These command outputs are evidence inputs, not final proof. The final gate must refresh them and compare deltas before any completion claim.
## Anti-slop triage templates
Use `docs/anti-slop-triage.md` plus the repository templates before acting on the live snapshot:
- `.github/ISSUE_TEMPLATE/anti_slop_triage.yml` records the initial issue classification, evidence, and non-destructive next action.
- `.github/PULL_REQUEST_TEMPLATE.md` adds PR classification, verification, and resolution-gate checklist items.
The anti-slop classifications are: `actionable-bug`, `actionable-docs`, `actionable-feature`, `duplicate`, `spam-or-promotion`, `generated-slop-or-hallucinated`, `unsafe-or-security-sensitive`, `not-reproducible-yet`, and `externally-blocked`.
Automation lanes may recommend labels, comments, defer/close rationales, or merge candidates, but must not merge or close remote PRs/issues without maintainer-owned approval.
## G012 final PR reconciliation snapshot
Worker-3 captured a fresh PR ledger for the final Claw Code 2.0 gate in `docs/pr-triage-g012-final-gate.json`.
- Captured on: 2026-05-15T02:58:00Z during G012 final-gate execution.
- Commands: `gh pr list --state open --limit 100 ...` plus `gh pr view <number> ...` for per-PR file and merge-state evidence.
- Observed count: 51 open PR records.
- Merge action taken by worker-3: none. The safety policy requires correct, safe, non-conflicting, resolvable PRs with evidence; this snapshot found 32 PRs in `CONFLICTING`/`DIRTY` state and 19 `MERGEABLE` PRs that GitHub reported as `UNSTABLE` with no fresh check-rollup evidence in the live snapshot.
- Docs-only candidate-review PRs: #3021 and #2824 remain deferred until content/source-of-truth review and fresh verification are available.
## Required final evidence
The final report must include:
- Fresh `gh pr list --state open` and `gh issue list --state open` snapshots.
- A PR ledger with one row per PR: merge / reject / defer, reason, verification, commit/merge reference.
- An issue ledger with one row per issue: fixed / duplicate / spam / invalid / deferred-with-rationale / externally-blocked, reason, and linked evidence.
- Verification that no correct, mergeable PR remains unmerged without rationale.
- Verification that no resolvable, correct issue remains open without a fix or rationale.
## Non-goals
This gate does not require merging unsafe, unverified, incompatible, spam, or incorrect contributions. It requires explicit evidence-backed triage and action for everything that is correct and resolvable.

File diff suppressed because it is too large Load Diff

58
docs/roadmap-pr-goals.md Normal file
View File

@@ -0,0 +1,58 @@
# Roadmap PR goal intake
Captured: 2026-05-14 (Asia/Seoul) during the Claw Code 2.0 Ultragoal run.
Purpose: make the user's follow-up requirement durable: all roadmap PRs should be merged when correct/resolvable, and unresolved roadmap deltas should become Ultragoal work rather than being lost. This file is a tracked companion to the leader-owned `.omx/ultragoal/goals.json` and `.omx/ultragoal/ledger.jsonl` artifacts.
## Merge policy
- Merge only PRs that are still relevant to Claw Code 2.0, are non-draft, target `main`, and are conflict-free after a fresh mergeability refresh.
- Prefer squash merges with a Lore-style body when GitHub allows a direct PR merge.
- If a PR is documentation-only but adds a real roadmap gap, merging it is acceptable once checks/conflicts are clean.
- If a PR is stale, duplicated by already-landed work, or not product-aligned, do not force-merge; record the rationale and map any still-correct requirement into G011/G012.
- After merging roadmap PRs, refresh generated board artifacts (`.omx/cc2/board.json`, `.omx/cc2/board.md`) so Stream 0 coverage stays current.
## Open roadmap PRs with green historical checks
These are first-pass merge candidates, pending fresh mergeability and conflict checks against current `main`.
| PR | Title | Branch | Checks | Mergeable | URL |
| --- | --- | --- | --- | --- | --- |
| #2848 | docs(roadmap): add #333 — no in-session settings inspect command | `docs/roadmap-333-no-settings-inspect-command` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2848 |
| #2846 | docs(roadmap): add #331 — export silently overwrites on repeated invocations | `docs/roadmap-331-export-filename-collision` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2846 |
| #2869 | docs(roadmap): add #358 — history entries missing role field, no pagination | `docs/roadmap-348-history-entries-missing-role` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2869 |
| #2850 | docs(roadmap): add #335 — session list omits created_at_ms field | `docs/roadmap-335-session-list-no-created-at` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2850 |
| #2868 | docs(roadmap): add #356 — session list title always null; no rename command | `docs/roadmap-347-session-list-title-always-null` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2868 |
| #2865 | docs(roadmap): add #362 — doctor auth false-positive: misses CLI session tokens | `docs/roadmap-345-doctor-auth-check-incomplete` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2865 |
| #2864 | docs(roadmap): add #364 — /cost returns no cost_usd; identical to /stats | `docs/roadmap-344-cost-command-no-dollar-amount` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2864 |
| #2867 | docs(roadmap): add #368 — export always appends .txt; response.file reflects mangled path | `docs/roadmap-346-export-forces-txt-extension` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2867 |
| #2862 | docs(roadmap): add #342 — status json omits active session ID, workspace counters ambiguous | `docs/roadmap-342-v2` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2862 |
| #2876 | docs(roadmap): add #354 — /cwd suggests itself in did-you-mean; self-referential loop | `docs/roadmap-354-cwd-self-referential-suggestion` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2876 |
| #2872 | docs(roadmap): add #360 — /tokens, /stats, /cost identical output; no context-window or cost_usd | `docs/roadmap-349-tokens-stats-cost-identical` -> `main` | 4/4 checks successful | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2872 |
## Open roadmap PRs needing local validation or CI refresh
These have no check rollup in the live snapshot; validate locally or refresh CI before merging.
| PR | Title | Branch | Checks | Mergeable | URL |
| --- | --- | --- | --- | --- | --- |
| #2858 | docs(roadmap): add #343 — session subcommand resume-safety inconsistently enforced | `docs/roadmap-340-session-resume-safe-inconsistent` -> `main` | no checks reported | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2858 |
| #2839 | docs(roadmap): add #330 — resume mode stats/cost always zero | `docs/roadmap-324-resume-stats-zero` -> `main` | no checks reported | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2839 |
| #2841 | docs(roadmap): add #332 — doctor json missing top-level status field | `docs/roadmap-325-doctor-no-status-field` -> `main` | no checks reported | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2841 |
| #2844 | docs(roadmap): add #336 — session subcommand resume inconsistency and type/kind error mismatch | `docs/roadmap-329-session-subcommand-resume-inconsistency` -> `main` | no checks reported | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2844 |
| #2842 | docs(roadmap): add #334 — version json omits build_date and uses short sha only | `docs/roadmap-328-version-json-incomplete` -> `main` | no checks reported | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2842 |
## Product-fit review before merge
These may be broader than the Claw Code 2.0 roadmap scope and need a product-fit decision before merge.
| PR | Title | Branch | Checks | Mergeable | URL |
| --- | --- | --- | --- | --- | --- |
| #2824 | docs: personal assistant roadmap | `pr/docs-personal-assistant-roadmap` -> `main` | no checks reported | UNKNOWN | https://github.com/ultraworkers/claw-code/pull/2824 |
## Ultragoal mapping
- G003-G010: close implementation gaps that overlap a roadmap PR title if the requirement belongs to the active stream.
- G011: reconcile ecosystem/ops/UX roadmap PRs and unresolved correct issues that do not fit earlier streams.
- G012: final release gate must prove that every open roadmap PR was merged, closed as duplicate/obsolete, or converted into an explicit remaining goal with evidence.

View File

@@ -0,0 +1,195 @@
# Windows install and release quickstart
This page is the PowerShell-first path for installing, verifying, and safely switching providers on Windows. It is intentionally copyable without embedding live secrets.
## Choose an install path
### Option A: build from source in PowerShell
Use this when you are developing Claw Code or testing a local checkout.
```powershell
git clone https://github.com/ultraworkers/claw-code
Set-Location .\claw-code\rust
cargo build --workspace
.\target\debug\claw.exe --help
.\target\debug\claw.exe doctor
```
For an optimized local binary:
```powershell
Set-Location .\claw-code\rust
cargo build --workspace --release
.\target\release\claw.exe --help
```
### Option B: use a release artifact
Use this when a GitHub release publishes a Windows artifact. The release workflow publishes `claw-windows-x64.exe` plus `claw-windows-x64.exe.sha256`; if a future release wraps the binary in a ZIP, prefer the `windows-x86_64` / `pc-windows-msvc` asset and its matching checksum file.
```powershell
$Asset = "claw-windows-x64.exe"
$InstallRoot = "$env:LOCALAPPDATA\Programs\claw"
New-Item -ItemType Directory -Force $InstallRoot | Out-Null
# Download $Asset and $Asset.sha256 from the release page, then verify them:
$Actual = (Get-FileHash ".\$Asset" -Algorithm SHA256).Hash.ToLowerInvariant()
$Expected = (Get-Content ".\$Asset.sha256" | Select-Object -First 1).Split()[0].ToLowerInvariant()
if ($Actual -ne $Expected) { throw "checksum mismatch for $Asset" }
Copy-Item ".\$Asset" "$InstallRoot\claw.exe" -Force
& "$InstallRoot\claw.exe" --help
& "$InstallRoot\claw.exe" doctor
```
To make that binary available in new PowerShell windows:
```powershell
$InstallRoot = "$env:LOCALAPPDATA\Programs\claw"
[Environment]::SetEnvironmentVariable(
"Path",
[Environment]::GetEnvironmentVariable("Path", "User") + ";$InstallRoot",
"User"
)
```
Open a new terminal before running `claw --help` from another directory.
### Option C: WSL
The repository `install.sh` path is for Linux, macOS, and Windows via WSL. Run it from inside your WSL distribution, not from native PowerShell:
```powershell
wsl --install
wsl
```
Then inside WSL:
```bash
git clone https://github.com/ultraworkers/claw-code
cd claw-code
./install.sh
```
## First-run health checks
Run these before using live prompts:
```powershell
Set-Location .\claw-code\rust
.\target\debug\claw.exe --help
.\target\debug\claw.exe doctor
.\target\debug\claw.exe status --output-format json
.\target\debug\claw.exe config --output-format json
```
`doctor`, `status`, `config`, and `version` support `--output-format json`; do not use a separate `--json` suffix.
## Safe credential setup
Set keys only in your local environment or a private `.env` file. Do not paste real keys into shell history shared with others, issue trackers, or documentation.
Current PowerShell session only:
```powershell
$env:ANTHROPIC_API_KEY = "sk-ant-REPLACE_ME"
```
Persist for future PowerShell windows:
```powershell
setx ANTHROPIC_API_KEY "sk-ant-REPLACE_ME"
```
Open a new terminal after `setx`. To remove a session-local key while testing provider switching:
```powershell
Remove-Item Env:\ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
```
## Safe provider switching examples
Provider routing is model-prefix first. When multiple credentials exist, choose an explicit model prefix so `claw` does not infer the wrong backend.
### Anthropic direct
```powershell
$env:ANTHROPIC_API_KEY = "sk-ant-REPLACE_ME"
Remove-Item Env:\OPENAI_BASE_URL -ErrorAction SilentlyContinue
Remove-Item Env:\OPENAI_API_KEY -ErrorAction SilentlyContinue
.\target\debug\claw.exe --model "sonnet" prompt "reply with ready"
```
### OpenAI-compatible gateway or OpenRouter
```powershell
Remove-Item Env:\ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
$env:OPENAI_BASE_URL = "https://openrouter.ai/api/v1"
$env:OPENAI_API_KEY = "sk-or-v1-REPLACE_ME"
.\target\debug\claw.exe --model "openai/gpt-4.1-mini" prompt "reply with ready"
```
For the default OpenAI-compatible API, omit `OPENAI_BASE_URL` or set it to `https://api.openai.com/v1`, and keep the `openai/` or `gpt-` model prefix explicit.
### Local OpenAI-compatible server
Use a loopback URL and a placeholder token unless your local server requires a real one:
```powershell
Remove-Item Env:\ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
$env:OPENAI_BASE_URL = "http://127.0.0.1:11434/v1"
$env:OPENAI_API_KEY = "local-dev-token"
.\target\debug\claw.exe --model "llama3.2" prompt "reply with ready"
```
If the local server is authless, remove `OPENAI_API_KEY` instead of putting a real cloud key into local testing:
```powershell
Remove-Item Env:\OPENAI_API_KEY -ErrorAction SilentlyContinue
```
### DashScope / Qwen
```powershell
Remove-Item Env:\ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
$env:DASHSCOPE_API_KEY = "sk-REPLACE_ME"
.\target\debug\claw.exe --model "qwen-plus" prompt "reply with ready"
```
## Windows and WSL notifications
Notification support is exposed through the `notifications` slash command in the interactive REPL. Use JSON/status commands first to confirm the CLI runs, then configure notifications from the REPL if your workflow needs them.
Native PowerShell smoke path:
```powershell
Set-Location .\claw-code\rust
.\target\debug\claw.exe
# inside the REPL:
/notifications
```
WSL smoke path:
```bash
cd claw-code/rust
./target/debug/claw
# inside the REPL:
/notifications
```
When moving between PowerShell and WSL, keep provider keys in the environment where `claw` is actually running; Windows user env vars set with `setx` are not automatically the same as WSL shell exports.
## Troubleshooting checklist
- `claw` not found: use `claw.exe` on Windows or run the binary by full path (`.\target\debug\claw.exe`).
- `cargo` not found: reopen PowerShell after installing Rust from <https://rustup.rs/>.
- `401 Invalid bearer token`: put `sk-ant-*` values in `ANTHROPIC_API_KEY`, not `ANTHROPIC_AUTH_TOKEN`.
- Wrong provider selected: add an explicit model prefix such as `openai/gpt-4.1-mini`, `qwen-plus`, or `grok`.
- Release ZIP extracted but command still fails: open a new terminal after updating the user `Path`, or call `& "$env:LOCALAPPDATA\Programs\claw\claw.exe"` directly.

View File

@@ -16,7 +16,7 @@ unsafe_code = "forbid"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
pedantic = { level = "allow", priority = -1 }
module_name_repetitions = "allow"
missing_panics_doc = "allow"
missing_errors_doc = "allow"

View File

@@ -22,6 +22,8 @@ The harness runs these scripted scenarios against a fresh workspace and isolated
8. `bash_permission_prompt_approved`
9. `bash_permission_prompt_denied`
10. `plugin_tool_roundtrip`
11. `auto_compact_triggered`
12. `token_cost_reporting`
## Run
@@ -37,7 +39,7 @@ cd rust/
python3 scripts/run_mock_parity_diff.py
```
Scenario-to-PARITY mappings live in `mock_parity_scenarios.json`.
Scenario-to-PARITY mappings live in `mock_parity_scenarios.json`; keep this manifest aligned with `rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs` and `PARITY.md` via `python3 scripts/run_mock_parity_diff.py --no-run`.
## Manual mock server

View File

@@ -145,7 +145,7 @@ Top-level commands:
init
```
`claw acp` is a local discoverability surface for editor-first users: it reports the current ACP/Zed status without starting the runtime. As of April 16, 2026, claw-code does **not** ship an ACP/Zed daemon entrypoint yet, and `claw acp serve` is only a status alias until the real protocol surface lands.
`claw acp` is a local discoverability surface for editor-first users: it reports the current ACP/Zed status without starting the runtime. As of April 16, 2026, claw-code does **not** ship an ACP/Zed daemon or JSON-RPC entrypoint yet, and `claw acp serve` is only a status alias until the real protocol surface lands. Status queries exit 0 and expose the same machine-readable contract via `--output-format json`; malformed ACP invocations exit 1 with `kind: unsupported_acp_invocation`.
The command surface is moving quickly. For the canonical live help text, run:

View File

@@ -76,6 +76,7 @@ fn create_sample_request(message_count: usize) -> MessageRequest {
presence_penalty: None,
stop: None,
reasoning_effort: None,
extra_body: std::collections::BTreeMap::new(),
}
}

View File

@@ -20,13 +20,15 @@ pub use prompt_cache::{
};
pub use providers::anthropic::{AnthropicClient, AnthropicClient as ApiClient, AuthSource};
pub use providers::openai_compat::{
build_chat_completion_request, flatten_tool_result_content, is_reasoning_model,
model_rejects_is_error_field, model_requires_reasoning_content_in_history, translate_message,
OpenAiCompatClient, OpenAiCompatConfig,
build_chat_completion_request, check_request_body_size, estimate_request_body_size,
flatten_tool_result_content, is_reasoning_model, model_rejects_is_error_field,
model_requires_reasoning_content_in_history, translate_message, OpenAiCompatClient,
OpenAiCompatConfig,
};
pub use providers::{
detect_provider_kind, max_tokens_for_model, max_tokens_for_model_with_override,
model_family_identity_for, model_family_identity_for_kind, resolve_model_alias, ProviderKind,
model_family_identity_for, model_family_identity_for_kind, provider_diagnostics_for_model,
resolve_model_alias, ProviderDiagnostics, ProviderKind,
};
pub use sse::{parse_frame, SseParser};
pub use types::{

View File

@@ -600,8 +600,9 @@ fn jitter_for_base(base: Duration) -> Duration {
}
let raw_nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0);
.map_or(0, |elapsed| {
u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX)
});
let tick = JITTER_COUNTER.fetch_add(1, Ordering::Relaxed);
// splitmix64 finalizer — mixes the low bits so large bases still see
// jitter across their full range instead of being clamped to subsec nanos.
@@ -844,19 +845,17 @@ impl MessageStream {
StreamEvent::MessageDelta(MessageDeltaEvent { usage, .. }) => {
self.latest_usage = Some(usage.clone());
}
StreamEvent::MessageStop(_) => {
if !self.usage_recorded {
if let (Some(prompt_cache), Some(usage)) =
(&self.prompt_cache, self.latest_usage.as_ref())
{
let record = prompt_cache.record_usage(&self.request, usage);
*self
.last_prompt_cache_record
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(record);
}
self.usage_recorded = true;
StreamEvent::MessageStop(_) if !self.usage_recorded => {
if let (Some(prompt_cache), Some(usage)) =
(&self.prompt_cache, self.latest_usage.as_ref())
{
let record = prompt_cache.record_usage(&self.request, usage);
*self
.last_prompt_cache_record
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(record);
}
self.usage_recorded = true;
}
_ => {}
}

View File

@@ -1,4 +1,5 @@
#![allow(clippy::cast_possible_truncation)]
#![allow(dead_code)]
use std::future::Future;
use std::pin::Pin;
@@ -28,7 +29,7 @@ pub trait Provider {
) -> ProviderFuture<'a, Self::Stream>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ProviderKind {
Anthropic,
Xai,
@@ -49,6 +50,74 @@ pub struct ModelTokenLimit {
pub context_window_tokens: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderWireProtocol {
AnthropicMessages,
OpenAiChatCompletions,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderFeatureSupport {
Supported,
Unsupported,
PassthroughAsTool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProviderCapabilityReport {
pub provider: ProviderKind,
pub wire_protocol: ProviderWireProtocol,
pub auth_env: &'static str,
pub base_url_env: &'static str,
pub default_base_url: &'static str,
pub tool_calls: ProviderFeatureSupport,
pub streaming: ProviderFeatureSupport,
pub streaming_usage: ProviderFeatureSupport,
pub prompt_cache: ProviderFeatureSupport,
pub custom_parameters: ProviderFeatureSupport,
pub reasoning_effort: ProviderFeatureSupport,
pub reasoning_content_history: ProviderFeatureSupport,
pub fixed_sampling_reasoning_models: ProviderFeatureSupport,
pub web_search: ProviderFeatureSupport,
pub web_fetch: ProviderFeatureSupport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderDiagnosticSeverity {
Info,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProviderDiagnostic {
pub code: &'static str,
pub severity: ProviderDiagnosticSeverity,
pub message: String,
pub action: String,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProviderDiagnostics {
pub requested_model: String,
pub resolved_model: String,
pub provider: ProviderKind,
pub auth_env: &'static str,
pub base_url_env: &'static str,
pub default_base_url: &'static str,
pub openai_compatible: bool,
pub reasoning_model: bool,
pub preserves_reasoning_content_in_history: bool,
pub strips_tuning_params: bool,
pub supports_stream_usage: bool,
pub honors_proxy_env: bool,
pub supports_extra_body_params: bool,
pub preserves_slash_model_ids_on_custom_base_url: bool,
}
const MODEL_REGISTRY: &[(&str, ProviderMetadata)] = &[
(
"opus",
@@ -219,6 +288,55 @@ pub fn metadata_for_model(model: &str) -> Option<ProviderMetadata> {
None
}
#[must_use]
pub fn provider_diagnostics_for_model(model: &str) -> ProviderDiagnostics {
let resolved_model = resolve_model_alias(model);
let metadata =
metadata_for_model(&resolved_model).unwrap_or_else(|| {
match detect_provider_kind(&resolved_model) {
ProviderKind::Anthropic => ProviderMetadata {
provider: ProviderKind::Anthropic,
auth_env: "ANTHROPIC_API_KEY",
base_url_env: "ANTHROPIC_BASE_URL",
default_base_url: anthropic::DEFAULT_BASE_URL,
},
ProviderKind::Xai => ProviderMetadata {
provider: ProviderKind::Xai,
auth_env: "XAI_API_KEY",
base_url_env: "XAI_BASE_URL",
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
},
ProviderKind::OpenAi => ProviderMetadata {
provider: ProviderKind::OpenAi,
auth_env: "OPENAI_API_KEY",
base_url_env: "OPENAI_BASE_URL",
default_base_url: openai_compat::DEFAULT_OPENAI_BASE_URL,
},
}
});
let openai_compatible = matches!(metadata.provider, ProviderKind::OpenAi | ProviderKind::Xai);
let reasoning_model = openai_compatible && openai_compat::is_reasoning_model(&resolved_model);
ProviderDiagnostics {
requested_model: model.to_string(),
resolved_model: resolved_model.clone(),
provider: metadata.provider,
auth_env: metadata.auth_env,
base_url_env: metadata.base_url_env,
default_base_url: metadata.default_base_url,
openai_compatible,
reasoning_model,
preserves_reasoning_content_in_history: openai_compatible
&& openai_compat::model_requires_reasoning_content_in_history(&resolved_model),
strips_tuning_params: reasoning_model,
supports_stream_usage: metadata.provider == ProviderKind::OpenAi
&& metadata.default_base_url == openai_compat::DEFAULT_OPENAI_BASE_URL,
honors_proxy_env: true,
supports_extra_body_params: openai_compatible,
preserves_slash_model_ids_on_custom_base_url: metadata.provider == ProviderKind::OpenAi,
}
}
#[must_use]
pub fn detect_provider_kind(model: &str) -> ProviderKind {
if let Some(metadata) = metadata_for_model(model) {
@@ -263,6 +381,208 @@ pub fn model_family_identity_for(model: &str) -> runtime::ModelFamilyIdentity {
model_family_identity_for_kind(detect_provider_kind(model))
}
#[must_use]
pub fn provider_capabilities_for_model(model: &str) -> ProviderCapabilityReport {
let metadata = metadata_for_model(model).unwrap_or_else(|| {
let provider = detect_provider_kind(model);
metadata_for_provider_kind(provider)
});
let (
wire_protocol,
streaming_usage,
prompt_cache,
custom_parameters,
reasoning_effort,
reasoning_content_history,
fixed_sampling_reasoning_models,
) = match metadata.provider {
ProviderKind::Anthropic => (
ProviderWireProtocol::AnthropicMessages,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Supported,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Unsupported,
),
ProviderKind::Xai => (
ProviderWireProtocol::OpenAiChatCompletions,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Supported,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Supported,
),
ProviderKind::OpenAi => (
ProviderWireProtocol::OpenAiChatCompletions,
ProviderFeatureSupport::Supported,
ProviderFeatureSupport::Unsupported,
ProviderFeatureSupport::Supported,
ProviderFeatureSupport::Supported,
if openai_compat::model_requires_reasoning_content_in_history(model) {
ProviderFeatureSupport::Supported
} else {
ProviderFeatureSupport::Unsupported
},
ProviderFeatureSupport::Supported,
),
};
ProviderCapabilityReport {
provider: metadata.provider,
wire_protocol,
auth_env: metadata.auth_env,
base_url_env: metadata.base_url_env,
default_base_url: metadata.default_base_url,
tool_calls: ProviderFeatureSupport::Supported,
streaming: ProviderFeatureSupport::Supported,
streaming_usage,
prompt_cache,
custom_parameters,
reasoning_effort,
reasoning_content_history,
fixed_sampling_reasoning_models,
web_search: ProviderFeatureSupport::PassthroughAsTool,
web_fetch: ProviderFeatureSupport::PassthroughAsTool,
}
}
#[must_use]
pub fn provider_diagnostics_for_request(request: &MessageRequest) -> Vec<ProviderDiagnostic> {
let capabilities = provider_capabilities_for_model(&request.model);
let mut diagnostics = Vec::new();
if request.reasoning_effort.is_some()
&& capabilities.reasoning_effort == ProviderFeatureSupport::Unsupported
{
diagnostics.push(ProviderDiagnostic {
code: "reasoning_effort_unsupported",
severity: ProviderDiagnosticSeverity::Warning,
message: format!(
"{} does not map `reasoning_effort` for model `{}`.",
provider_label(capabilities.provider),
request.model
),
action: "Remove `reasoning_effort` or route to an OpenAI-compatible reasoning model such as `openai/o4-mini`.".to_string(),
});
}
if openai_compat::is_reasoning_model(&request.model)
&& has_openai_tuning_parameters(request)
&& capabilities.fixed_sampling_reasoning_models == ProviderFeatureSupport::Supported
{
diagnostics.push(ProviderDiagnostic {
code: "reasoning_model_fixed_sampling",
severity: ProviderDiagnosticSeverity::Info,
message: format!(
"Model `{}` is treated as a fixed-sampling reasoning model; tuning parameters are omitted before the provider call.",
request.model
),
action: "Leave temperature/top_p/frequency_penalty/presence_penalty unset for reasoning models to match provider validation rules.".to_string(),
});
}
if openai_compat::model_requires_reasoning_content_in_history(&request.model) {
diagnostics.push(ProviderDiagnostic {
code: "deepseek_v4_reasoning_history",
severity: ProviderDiagnosticSeverity::Info,
message: format!(
"Model `{}` requires assistant thinking history to be echoed as `reasoning_content`.",
request.model
),
action: "Keep prior assistant Thinking blocks in history; the OpenAI-compatible serializer will emit `reasoning_content` for DeepSeek V4 models.".to_string(),
});
}
if declares_tool(request, "web_search") {
diagnostics.push(web_passthrough_diagnostic(
"web_search_passthrough_tool",
"web_search",
capabilities.provider,
));
}
if declares_tool(request, "web_fetch") {
diagnostics.push(web_passthrough_diagnostic(
"web_fetch_passthrough_tool",
"web_fetch",
capabilities.provider,
));
}
diagnostics
}
#[must_use]
fn metadata_for_provider_kind(provider: ProviderKind) -> ProviderMetadata {
match provider {
ProviderKind::Anthropic => ProviderMetadata {
provider,
auth_env: "ANTHROPIC_API_KEY",
base_url_env: "ANTHROPIC_BASE_URL",
default_base_url: anthropic::DEFAULT_BASE_URL,
},
ProviderKind::Xai => ProviderMetadata {
provider,
auth_env: "XAI_API_KEY",
base_url_env: "XAI_BASE_URL",
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
},
ProviderKind::OpenAi => ProviderMetadata {
provider,
auth_env: "OPENAI_API_KEY",
base_url_env: "OPENAI_BASE_URL",
default_base_url: openai_compat::DEFAULT_OPENAI_BASE_URL,
},
}
}
#[must_use]
const fn provider_label(provider: ProviderKind) -> &'static str {
match provider {
ProviderKind::Anthropic => "Anthropic",
ProviderKind::Xai => "xAI",
ProviderKind::OpenAi => "OpenAI-compatible",
}
}
#[must_use]
fn has_openai_tuning_parameters(request: &MessageRequest) -> bool {
request.temperature.is_some()
|| request.top_p.is_some()
|| request.frequency_penalty.is_some()
|| request.presence_penalty.is_some()
}
#[must_use]
fn declares_tool(request: &MessageRequest, tool_name: &str) -> bool {
request.tools.as_ref().is_some_and(|tools| {
tools
.iter()
.any(|tool| tool.name.eq_ignore_ascii_case(tool_name))
})
}
#[must_use]
fn web_passthrough_diagnostic(
code: &'static str,
tool_name: &'static str,
provider: ProviderKind,
) -> ProviderDiagnostic {
ProviderDiagnostic {
code,
severity: ProviderDiagnosticSeverity::Info,
message: format!(
"`{tool_name}` is exposed to {} as a normal function tool, not as a provider-native web capability.",
provider_label(provider)
),
action: format!(
"Provide a local `{tool_name}` tool implementation or route through a provider adapter that explicitly supports native web tools."
),
}
}
#[must_use]
pub fn max_tokens_for_model(model: &str) -> u32 {
let canonical = resolve_model_alias(model);
@@ -272,9 +592,7 @@ pub fn max_tokens_for_model(model: &str) -> u32 {
64_000
};
model_token_limit(model)
.map(|limit| heuristic.min(limit.max_output_tokens))
.unwrap_or(heuristic)
model_token_limit(model).map_or(heuristic, |limit| heuristic.min(limit.max_output_tokens))
}
/// Returns the effective max output tokens for a model, preferring a plugin
@@ -498,7 +816,9 @@ mod tests {
anthropic_missing_credentials, anthropic_missing_credentials_hint, detect_provider_kind,
load_dotenv_file, max_tokens_for_model, max_tokens_for_model_with_override,
model_family_identity_for, model_family_identity_for_kind, model_token_limit, parse_dotenv,
preflight_message_request, resolve_model_alias, ProviderKind,
preflight_message_request, provider_capabilities_for_model,
provider_diagnostics_for_request, resolve_model_alias, ProviderFeatureSupport,
ProviderKind, ProviderWireProtocol,
};
/// Serializes every test in this module that mutates process-wide
@@ -593,6 +913,105 @@ mod tests {
assert_eq!(xai_identity, runtime::ModelFamilyIdentity::Generic);
}
#[test]
fn provider_capability_matrix_snapshots_openai_compat_differences() {
let openai = provider_capabilities_for_model("openai/gpt-4.1-mini");
assert_eq!(openai.provider, ProviderKind::OpenAi);
assert_eq!(
openai.wire_protocol,
ProviderWireProtocol::OpenAiChatCompletions
);
assert_eq!(openai.auth_env, "OPENAI_API_KEY");
assert_eq!(openai.streaming_usage, ProviderFeatureSupport::Supported);
assert_eq!(openai.reasoning_effort, ProviderFeatureSupport::Supported);
assert_eq!(openai.web_search, ProviderFeatureSupport::PassthroughAsTool);
assert_eq!(openai.web_fetch, ProviderFeatureSupport::PassthroughAsTool);
let deepseek = provider_capabilities_for_model("openai/deepseek-v4-pro");
assert_eq!(
deepseek.reasoning_content_history,
ProviderFeatureSupport::Supported
);
let xai = provider_capabilities_for_model("grok-3");
assert_eq!(xai.provider, ProviderKind::Xai);
assert_eq!(xai.auth_env, "XAI_API_KEY");
assert_eq!(xai.reasoning_effort, ProviderFeatureSupport::Unsupported);
assert_eq!(xai.streaming_usage, ProviderFeatureSupport::Unsupported);
let anthropic = provider_capabilities_for_model("claude-sonnet-4-6");
assert_eq!(anthropic.provider, ProviderKind::Anthropic);
assert_eq!(
anthropic.wire_protocol,
ProviderWireProtocol::AnthropicMessages
);
assert_eq!(anthropic.prompt_cache, ProviderFeatureSupport::Supported);
assert_eq!(
anthropic.custom_parameters,
ProviderFeatureSupport::Unsupported
);
}
#[test]
fn provider_diagnostics_explain_deepseek_reasoning_and_web_tool_passthrough() {
let request = MessageRequest {
model: "openai/deepseek-v4-pro".to_string(),
max_tokens: 1024,
messages: vec![InputMessage::user_text("research this")],
tools: Some(vec![
ToolDefinition {
name: "web_search".to_string(),
description: Some("Search the web".to_string()),
input_schema: json!({"type": "object"}),
},
ToolDefinition {
name: "web_fetch".to_string(),
description: Some("Fetch a URL".to_string()),
input_schema: json!({"type": "object"}),
},
]),
stream: true,
..Default::default()
};
let diagnostics = provider_diagnostics_for_request(&request);
let codes = diagnostics
.iter()
.map(|diagnostic| diagnostic.code)
.collect::<Vec<_>>();
assert!(codes.contains(&"deepseek_v4_reasoning_history"));
assert!(codes.contains(&"web_search_passthrough_tool"));
assert!(codes.contains(&"web_fetch_passthrough_tool"));
assert!(diagnostics
.iter()
.any(|diagnostic| diagnostic.action.contains("provider adapter")));
}
#[test]
fn provider_diagnostics_warn_for_unsupported_reasoning_effort() {
let request = MessageRequest {
model: "grok-3-mini".to_string(),
max_tokens: 1024,
messages: vec![InputMessage::user_text("think")],
reasoning_effort: Some("high".to_string()),
temperature: Some(0.7),
..Default::default()
};
let diagnostics = provider_diagnostics_for_request(&request);
let codes = diagnostics
.iter()
.map(|diagnostic| diagnostic.code)
.collect::<Vec<_>>();
assert!(codes.contains(&"reasoning_effort_unsupported"));
assert!(codes.contains(&"reasoning_model_fixed_sampling"));
assert!(diagnostics.iter().any(|diagnostic| diagnostic
.message
.contains("does not map `reasoning_effort`")));
}
#[test]
fn openai_namespaced_model_routes_to_openai_not_anthropic() {
// Regression: "openai/gpt-4.1-mini" was misrouted to Anthropic when
@@ -673,6 +1092,19 @@ mod tests {
assert_eq!(super::resolve_model_alias("KIMI"), "kimi-k2.5"); // case insensitive
}
#[test]
fn provider_diagnostics_explain_openai_compatible_capabilities() {
let diagnostics = super::provider_diagnostics_for_model("openai/deepseek-v4-pro");
assert_eq!(diagnostics.provider, ProviderKind::OpenAi);
assert_eq!(diagnostics.auth_env, "OPENAI_API_KEY");
assert!(diagnostics.openai_compatible);
assert!(diagnostics.preserves_reasoning_content_in_history);
assert!(diagnostics.supports_extra_body_params);
assert!(diagnostics.honors_proxy_env);
assert!(diagnostics.preserves_slash_model_ids_on_custom_base_url);
}
#[test]
fn keeps_existing_max_token_heuristic() {
assert_eq!(max_tokens_for_model("opus"), 32_000);

View File

@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, VecDeque};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
@@ -145,6 +146,12 @@ impl OpenAiCompatClient {
self
}
#[must_use]
pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
self.http = http;
self
}
#[must_use]
pub fn with_retry_policy(
mut self,
@@ -267,14 +274,18 @@ impl OpenAiCompatClient {
request: &MessageRequest,
) -> Result<reqwest::Response, ApiError> {
// Pre-flight check: verify request body size against provider limits
check_request_body_size(request, self.config())?;
check_request_body_size_for_base_url(request, self.config(), &self.base_url)?;
let request_url = chat_completions_endpoint(&self.base_url);
self.http
.post(&request_url)
.header("content-type", "application/json")
.bearer_auth(&self.api_key)
.json(&build_chat_completion_request(request, self.config()))
.json(&build_chat_completion_request_for_base_url(
request,
self.config(),
&self.base_url,
))
.send()
.await
.map_err(ApiError::from)
@@ -327,8 +338,9 @@ fn jitter_for_base(base: Duration) -> Duration {
}
let raw_nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0);
.map_or(0, |elapsed| {
u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX)
});
let tick = JITTER_COUNTER.fetch_add(1, Ordering::Relaxed);
let mut mixed = raw_nanos
.wrapping_add(tick)
@@ -463,6 +475,7 @@ impl StreamState {
}
}
#[allow(clippy::too_many_lines)]
fn ingest_chunk(&mut self, chunk: ChatCompletionChunk) -> Result<Vec<StreamEvent>, ApiError> {
let mut events = Vec::new();
if !self.message_started {
@@ -488,12 +501,7 @@ impl StreamState {
}
if let Some(usage) = chunk.usage {
self.usage = Some(Usage {
input_tokens: usage.prompt_tokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
output_tokens: usage.completion_tokens,
});
self.usage = Some(usage.normalized());
}
for choice in chunk.choices {
@@ -771,6 +779,29 @@ struct OpenAiUsage {
prompt_tokens: u32,
#[serde(default)]
completion_tokens: u32,
#[serde(default)]
prompt_tokens_details: Option<OpenAiPromptTokensDetails>,
}
#[derive(Debug, Deserialize)]
struct OpenAiPromptTokensDetails {
#[serde(default)]
cached_tokens: u32,
}
impl OpenAiUsage {
fn normalized(&self) -> Usage {
let cached_tokens = self
.prompt_tokens_details
.as_ref()
.map_or(0, |details| details.cached_tokens);
Usage {
input_tokens: self.prompt_tokens.saturating_sub(cached_tokens),
cache_creation_input_tokens: 0,
cache_read_input_tokens: cached_tokens,
output_tokens: self.completion_tokens,
}
}
}
#[derive(Debug, Deserialize)]
@@ -855,7 +886,7 @@ pub fn is_reasoning_model(model: &str) -> bool {
|| canonical.contains("thinking")
}
/// Returns true for OpenAI-compatible DeepSeek V4 models that require prior
/// Returns true for OpenAI-compatible `DeepSeek` V4 models that require prior
/// assistant reasoning to be echoed back as `reasoning_content` in history.
#[must_use]
pub fn model_requires_reasoning_content_in_history(model: &str) -> bool {
@@ -867,6 +898,7 @@ pub fn model_requires_reasoning_content_in_history(model: &str) -> bool {
/// Strip routing prefix (e.g., "openai/gpt-4" → "gpt-4") for the wire.
/// The prefix is used only to select transport; the backend expects the
/// bare model id.
#[allow(dead_code)]
fn strip_routing_prefix(model: &str) -> &str {
if let Some(pos) = model.find('/') {
let prefix = &model[..pos];
@@ -882,10 +914,51 @@ fn strip_routing_prefix(model: &str) -> &str {
}
}
fn wire_model_for_base_url<'a>(
model: &'a str,
config: OpenAiCompatConfig,
base_url: &str,
) -> Cow<'a, str> {
let Some(pos) = model.find('/') else {
return Cow::Borrowed(model);
};
let prefix = &model[..pos];
let lowered_prefix = prefix.to_ascii_lowercase();
if lowered_prefix == "openai" {
let trimmed_base_url = base_url.trim_end_matches('/');
let default_openai = DEFAULT_OPENAI_BASE_URL.trim_end_matches('/');
if config.provider_name == "OpenAI" && trimmed_base_url != default_openai {
// OpenAI-compatible gateways such as OpenRouter commonly use
// slash-containing model slugs (for example `openai/gpt-4.1-mini`).
// Preserve the slug when the user configured a non-default OpenAI
// base URL; the prefix still routed to the OpenAI-compatible client,
// but the gateway owns the final model namespace.
return Cow::Borrowed(model);
}
return Cow::Borrowed(&model[pos + 1..]);
}
if matches!(lowered_prefix.as_str(), "xai" | "grok" | "qwen" | "kimi") {
return Cow::Borrowed(&model[pos + 1..]);
}
Cow::Borrowed(model)
}
/// Estimate the serialized JSON size of a request payload in bytes.
/// This is a pre-flight check to avoid hitting provider-specific size limits.
#[must_use]
pub fn estimate_request_body_size(request: &MessageRequest, config: OpenAiCompatConfig) -> usize {
let payload = build_chat_completion_request(request, config);
estimate_request_body_size_for_base_url(request, config, &read_base_url(config))
}
fn estimate_request_body_size_for_base_url(
request: &MessageRequest,
config: OpenAiCompatConfig,
base_url: &str,
) -> usize {
let payload = build_chat_completion_request_for_base_url(request, config, base_url);
// serde_json::to_vec gives us the exact byte size of the serialized JSON
serde_json::to_vec(&payload).map_or(0, |v| v.len())
}
@@ -897,7 +970,15 @@ pub fn check_request_body_size(
request: &MessageRequest,
config: OpenAiCompatConfig,
) -> Result<(), ApiError> {
let estimated_bytes = estimate_request_body_size(request, config);
check_request_body_size_for_base_url(request, config, &read_base_url(config))
}
fn check_request_body_size_for_base_url(
request: &MessageRequest,
config: OpenAiCompatConfig,
base_url: &str,
) -> Result<(), ApiError> {
let estimated_bytes = estimate_request_body_size_for_base_url(request, config, base_url);
let max_bytes = config.max_request_body_bytes;
if estimated_bytes > max_bytes {
@@ -913,9 +994,18 @@ pub fn check_request_body_size(
/// Builds a chat completion request payload from a `MessageRequest`.
/// Public for benchmarking purposes.
#[must_use]
pub fn build_chat_completion_request(
request: &MessageRequest,
config: OpenAiCompatConfig,
) -> Value {
build_chat_completion_request_for_base_url(request, config, &read_base_url(config))
}
fn build_chat_completion_request_for_base_url(
request: &MessageRequest,
config: OpenAiCompatConfig,
base_url: &str,
) -> Value {
let mut messages = Vec::new();
if let Some(system) = request.system.as_ref().filter(|value| !value.is_empty()) {
@@ -924,8 +1014,10 @@ pub fn build_chat_completion_request(
"content": system,
}));
}
// Strip routing prefix (e.g., "openai/gpt-4" → "gpt-4") for the wire.
let wire_model = strip_routing_prefix(&request.model);
// Resolve the transport routing prefix into the wire model. Custom
// OpenAI-compatible gateways may require slash-containing slugs intact.
let wire_model = wire_model_for_base_url(&request.model, config, base_url);
let wire_model = wire_model.as_ref();
for message in &request.messages {
messages.extend(translate_message(message, wire_model));
}
@@ -994,9 +1086,29 @@ pub fn build_chat_completion_request(
payload["reasoning_effort"] = json!(effort);
}
for (key, value) in &request.extra_body {
if is_protected_extra_body_key(key) {
continue;
}
payload[key] = value.clone();
}
payload
}
fn is_protected_extra_body_key(key: &str) -> bool {
matches!(
key,
"model"
| "messages"
| "stream"
| "tools"
| "tool_choice"
| "max_tokens"
| "max_completion_tokens"
)
}
/// Returns true for models that do NOT support the `is_error` field in tool results.
/// kimi models (via Moonshot AI/Dashscope) reject this field with 400 Bad Request.
/// Returns true for models that do NOT support the `is_error` field in tool results.
@@ -1083,8 +1195,7 @@ pub fn translate_message(message: &InputMessage, model: &str) -> Vec<Value> {
}
Some(msg)
}
InputContentBlock::Thinking { .. } => None,
InputContentBlock::ToolUse { .. } => None,
InputContentBlock::Thinking { .. } | InputContentBlock::ToolUse { .. } => None,
})
.collect(),
}
@@ -1294,18 +1405,10 @@ fn normalize_response(
.finish_reason
.map(|value| normalize_finish_reason(&value)),
stop_sequence: None,
usage: Usage {
input_tokens: response
.usage
.as_ref()
.map_or(0, |usage| usage.prompt_tokens),
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
output_tokens: response
.usage
.as_ref()
.map_or(0, |usage| usage.completion_tokens),
},
usage: response
.usage
.as_ref()
.map_or_else(Usage::default, OpenAiUsage::normalized),
request_id: None,
})
}
@@ -1515,6 +1618,7 @@ mod tests {
ToolChoice, ToolDefinition, ToolResultContentBlock,
};
use serde_json::json;
use std::collections::BTreeMap;
use std::sync::{Mutex, OnceLock};
#[test]
@@ -1949,6 +2053,7 @@ mod tests {
presence_penalty: Some(0.3),
stop: Some(vec!["\n".to_string()]),
reasoning_effort: None,
extra_body: BTreeMap::new(),
};
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
assert_eq!(payload["temperature"], 0.7);
@@ -1958,6 +2063,39 @@ mod tests {
assert_eq!(payload["stop"], json!(["\n"]));
}
#[test]
fn extra_body_params_are_passed_through_without_overriding_core_fields() {
let mut extra_body = BTreeMap::new();
extra_body.insert(
"web_search_options".to_string(),
json!({"search_context_size": "medium"}),
);
extra_body.insert("parallel_tool_calls".to_string(), json!(false));
extra_body.insert("model".to_string(), json!("bad-override"));
extra_body.insert("messages".to_string(), json!([]));
extra_body.insert("max_tokens".to_string(), json!(1));
let payload = build_chat_completion_request(
&MessageRequest {
model: "gpt-4o".to_string(),
max_tokens: 1024,
messages: vec![InputMessage::user_text("hello")],
extra_body,
..Default::default()
},
OpenAiCompatConfig::openai(),
);
assert_eq!(payload["model"], json!("gpt-4o"));
assert_eq!(payload["max_tokens"], json!(1024));
assert_eq!(payload["messages"].as_array().map(Vec::len), Some(1));
assert_eq!(
payload["web_search_options"],
json!({"search_context_size": "medium"})
);
assert_eq!(payload["parallel_tool_calls"], json!(false));
}
#[test]
fn reasoning_model_strips_tuning_params() {
let request = MessageRequest {

View File

@@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use runtime::{pricing_for_model, TokenUsage, UsageCostEstimate};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -31,6 +33,14 @@ pub struct MessageRequest {
/// Silently ignored by backends that do not support it.
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
/// Provider-specific OpenAI-compatible request body parameters. These are
/// copied into the final JSON payload after core fields are populated so
/// users can opt into gateway features such as `web_search_options`,
/// `parallel_tool_calls`, or custom local-server switches without waiting
/// for first-class typed fields. Core protocol keys are protected and cannot
/// be overridden through this map.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub extra_body: BTreeMap<String, Value>,
}
impl MessageRequest {

View File

@@ -2,12 +2,13 @@ use std::collections::HashMap;
use std::ffi::OsString;
use std::sync::Arc;
use std::sync::{Mutex as StdMutex, OnceLock};
use std::time::Duration;
use api::{
ApiError, ContentBlockDelta, ContentBlockDeltaEvent, ContentBlockStartEvent,
ContentBlockStopEvent, InputContentBlock, InputMessage, MessageDeltaEvent, MessageRequest,
OpenAiCompatClient, OpenAiCompatConfig, OutputContentBlock, ProviderClient, StreamEvent,
ToolChoice, ToolDefinition,
build_http_client_with, ApiError, ContentBlockDelta, ContentBlockDeltaEvent,
ContentBlockStartEvent, ContentBlockStopEvent, InputContentBlock, InputMessage,
MessageDeltaEvent, MessageRequest, OpenAiCompatClient, OpenAiCompatConfig, OutputContentBlock,
ProviderClient, ProxyConfig, StreamEvent, ToolChoice, ToolDefinition,
};
use serde_json::json;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -25,7 +26,7 @@ async fn send_message_uses_openai_compatible_endpoint_and_auth() {
"\"message\":{\"role\":\"assistant\",\"content\":\"Hello from Grok\",\"tool_calls\":[]},",
"\"finish_reason\":\"stop\"",
"}],",
"\"usage\":{\"prompt_tokens\":11,\"completion_tokens\":5}",
"\"usage\":{\"prompt_tokens\":11,\"completion_tokens\":5,\"prompt_tokens_details\":{\"cached_tokens\":3}}",
"}"
);
let server = spawn_server(
@@ -42,6 +43,9 @@ async fn send_message_uses_openai_compatible_endpoint_and_auth() {
.expect("request should succeed");
assert_eq!(response.model, "grok-3");
assert_eq!(response.usage.input_tokens, 8);
assert_eq!(response.usage.cache_read_input_tokens, 3);
assert_eq!(response.usage.output_tokens, 5);
assert_eq!(response.total_tokens(), 16);
assert_eq!(
response.content,
@@ -63,6 +67,56 @@ async fn send_message_uses_openai_compatible_endpoint_and_auth() {
assert_eq!(body["tools"][0]["type"], json!("function"));
}
#[tokio::test]
async fn send_message_passes_optional_openai_compatible_parameters_on_wire() {
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
let body = concat!(
"{",
"\"id\":\"chatcmpl_params\",",
"\"model\":\"gpt-4o\",",
"\"choices\":[{",
"\"message\":{\"role\":\"assistant\",\"content\":\"Parameters preserved\",\"tool_calls\":[]},",
"\"finish_reason\":\"stop\"",
"}],",
"\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":2}",
"}"
);
let server = spawn_server(
state.clone(),
vec![http_response("200 OK", "application/json", body)],
)
.await;
let client = OpenAiCompatClient::new("openai-test-key", OpenAiCompatConfig::openai())
.with_base_url(server.base_url());
let response = client
.send_message(&MessageRequest {
model: "gpt-4o".to_string(),
temperature: Some(0.2),
top_p: Some(0.8),
frequency_penalty: Some(0.15),
presence_penalty: Some(0.25),
stop: Some(vec!["END".to_string()]),
reasoning_effort: Some("low".to_string()),
..sample_request(false)
})
.await
.expect("request should succeed");
assert_eq!(response.total_tokens(), 5);
let captured = state.lock().await;
let request = captured.first().expect("server should capture request");
let body: serde_json::Value = serde_json::from_str(&request.body).expect("json body");
assert_eq!(body["model"], json!("gpt-4o"));
assert_eq!(body["temperature"], json!(0.2));
assert_eq!(body["top_p"], json!(0.8));
assert_eq!(body["frequency_penalty"], json!(0.15));
assert_eq!(body["presence_penalty"], json!(0.25));
assert_eq!(body["stop"], json!(["END"]));
assert_eq!(body["reasoning_effort"], json!("low"));
}
#[tokio::test]
async fn send_message_preserves_deepseek_reasoning_content_before_text() {
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
@@ -107,6 +161,59 @@ async fn send_message_preserves_deepseek_reasoning_content_before_text() {
);
}
#[tokio::test]
async fn custom_openai_gateway_preserves_slash_model_ids_and_extra_body_params() {
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
let body = concat!(
"{",
"\"id\":\"chatcmpl_slash_model\",",
"\"model\":\"openai/gpt-4.1-mini\",",
"\"choices\":[{",
"\"message\":{\"role\":\"assistant\",\"content\":\"Gateway accepted slug\",\"tool_calls\":[]},",
"\"finish_reason\":\"stop\"",
"}],",
"\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":2}",
"}"
);
let server = spawn_server(
state.clone(),
vec![http_response("200 OK", "application/json", body)],
)
.await;
let mut extra_body = std::collections::BTreeMap::new();
extra_body.insert(
"web_search_options".to_string(),
json!({"search_context_size": "low"}),
);
extra_body.insert("parallel_tool_calls".to_string(), json!(false));
extra_body.insert("model".to_string(), json!("malicious-override"));
let client = OpenAiCompatClient::new("openai-test-key", OpenAiCompatConfig::openai())
.with_base_url(server.base_url());
let response = client
.send_message(&MessageRequest {
model: "openai/gpt-4.1-mini".to_string(),
extra_body,
..sample_request(false)
})
.await
.expect("gateway request should succeed");
assert_eq!(response.model, "openai/gpt-4.1-mini");
assert_eq!(response.total_tokens(), 5);
let captured = state.lock().await;
let request = captured.first().expect("captured request");
let body: serde_json::Value = serde_json::from_str(&request.body).expect("json body");
assert_eq!(body["model"], json!("openai/gpt-4.1-mini"));
assert_eq!(
body["web_search_options"],
json!({"search_context_size": "low"})
);
assert_eq!(body["parallel_tool_calls"], json!(false));
}
#[tokio::test]
async fn send_message_blocks_oversized_xai_requests_before_the_http_call() {
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
@@ -277,6 +384,65 @@ async fn stream_message_normalizes_text_and_multiple_tool_calls() {
assert!(request.body.contains("\"stream\":true"));
}
#[allow(clippy::await_holding_lock)]
#[tokio::test]
async fn stream_message_retries_retryable_sse_handshake_failures() {
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
let sse = concat!(
"data: {\"id\":\"chatcmpl_stream_retry\",\"model\":\"gpt-4o\",\"choices\":[{\"delta\":{\"content\":\"Recovered\"}}]}\n\n",
"data: {\"id\":\"chatcmpl_stream_retry\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n",
"data: [DONE]\n\n"
);
let server = spawn_server(
state.clone(),
vec![
http_response(
"500 Internal Server Error",
"application/json",
"{\"error\":{\"message\":\"try again\",\"type\":\"server_error\",\"code\":500}}",
),
http_response_with_headers(
"200 OK",
"text/event-stream",
sse,
&[("x-request-id", "req_stream_retry")],
),
],
)
.await;
let client = OpenAiCompatClient::new("openai-test-key", OpenAiCompatConfig::openai())
.with_base_url(server.base_url())
.with_retry_policy(1, Duration::ZERO, Duration::ZERO);
let mut stream = client
.stream_message(&MessageRequest {
model: "gpt-4o".to_string(),
..sample_request(false)
})
.await
.expect("stream should retry once then start");
assert_eq!(stream.request_id(), Some("req_stream_retry"));
let mut events = Vec::new();
while let Some(event) = stream.next_event().await.expect("event should parse") {
events.push(event);
}
assert!(events.iter().any(|event| matches!(
event,
StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
delta: ContentBlockDelta::TextDelta { text },
..
}) if text == "Recovered"
)));
let captured = state.lock().await;
assert_eq!(captured.len(), 2, "one original request plus one retry");
for request in captured.iter() {
let body: serde_json::Value = serde_json::from_str(&request.body).expect("json body");
assert_eq!(body["stream"], json!(true));
}
}
#[allow(clippy::await_holding_lock)]
#[tokio::test]
async fn openai_streaming_requests_opt_into_usage_chunks() {
@@ -284,7 +450,7 @@ async fn openai_streaming_requests_opt_into_usage_chunks() {
let sse = concat!(
"data: {\"id\":\"chatcmpl_openai_stream\",\"model\":\"gpt-5\",\"choices\":[{\"delta\":{\"content\":\"Hi\"}}]}\n\n",
"data: {\"id\":\"chatcmpl_openai_stream\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n",
"data: {\"id\":\"chatcmpl_openai_stream\",\"choices\":[],\"usage\":{\"prompt_tokens\":9,\"completion_tokens\":4}}\n\n",
"data: {\"id\":\"chatcmpl_openai_stream\",\"choices\":[],\"usage\":{\"prompt_tokens\":9,\"completion_tokens\":4,\"prompt_tokens_details\":{\"cached_tokens\":2}}}\n\n",
"data: [DONE]\n\n"
);
let server = spawn_server(
@@ -339,8 +505,10 @@ async fn openai_streaming_requests_opt_into_usage_chunks() {
match &events[4] {
StreamEvent::MessageDelta(MessageDeltaEvent { usage, .. }) => {
assert_eq!(usage.input_tokens, 9);
assert_eq!(usage.input_tokens, 7);
assert_eq!(usage.cache_read_input_tokens, 2);
assert_eq!(usage.output_tokens, 4);
assert_eq!(usage.total_tokens(), 13);
}
other => panic!("expected message delta, got {other:?}"),
}
@@ -353,6 +521,44 @@ async fn openai_streaming_requests_opt_into_usage_chunks() {
assert_eq!(body["stream_options"], json!({"include_usage": true}));
}
#[allow(clippy::await_holding_lock)]
#[tokio::test]
async fn openai_compatible_client_honors_http_proxy_for_requests() {
let _lock = env_lock();
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
let proxy = spawn_server(
state.clone(),
vec![http_response(
"200 OK",
"application/json",
"{\"id\":\"chatcmpl_proxy\",\"model\":\"gpt-4o\",\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"Via proxy\",\"tool_calls\":[]},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":4,\"completion_tokens\":3}}",
)],
)
.await;
let proxied_http = build_http_client_with(&ProxyConfig::from_proxy_url(proxy.base_url()))
.expect("proxy client should build");
let client = OpenAiCompatClient::new("openai-test-key", OpenAiCompatConfig::openai())
.with_http_client(proxied_http)
.with_base_url("http://origin.invalid/v1");
let response = client
.send_message(&MessageRequest {
model: "gpt-4o".to_string(),
..sample_request(false)
})
.await
.expect("proxy should return the OpenAI-compatible response");
assert_eq!(response.total_tokens(), 7);
let captured = state.lock().await;
let request = captured.first().expect("proxy should capture request");
assert_eq!(request.path, "http://origin.invalid/v1/chat/completions");
assert_eq!(
request.headers.get("authorization").map(String::as_str),
Some("Bearer openai-test-key")
);
}
#[allow(clippy::await_holding_lock)]
#[tokio::test]
async fn provider_client_dispatches_xai_requests_from_env() {

View File

@@ -221,11 +221,11 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
SlashCommandSpec {
name: "session",
aliases: &[],
summary: "List, switch, fork, or delete managed local sessions",
summary: "List, check, switch, fork, or delete managed local sessions",
argument_hint: Some(
"[list|switch <session-id>|fork [branch-name]|delete <session-id> [--force]]",
"[list|exists <session-id>|switch <session-id>|fork [branch-name]|delete <session-id> [--force]]",
),
resume_supported: false,
resume_supported: true,
},
SlashCommandSpec {
name: "plugin",
@@ -1590,7 +1590,17 @@ fn parse_session_command(args: &[&str]) -> Result<SlashCommand, SlashCommandPars
action: Some("list".to_string()),
target: None,
}),
["list", ..] => Err(usage_error("session", "[list|switch <session-id>|fork [branch-name]|delete <session-id> [--force]]")),
["list", ..] => Err(usage_error("session", "[list|exists <session-id>|switch <session-id>|fork [branch-name]|delete <session-id> [--force]]")),
["exists"] => Err(usage_error("session exists", "<session-id>")),
["exists", target] => Ok(SlashCommand::Session {
action: Some("exists".to_string()),
target: Some((*target).to_string()),
}),
["exists", ..] => Err(command_error(
"Unexpected arguments for /session exists.",
"session",
"/session exists <session-id>",
)),
["switch"] => Err(usage_error("session switch", "<session-id>")),
["switch", target] => Ok(SlashCommand::Session {
action: Some("switch".to_string()),
@@ -1637,10 +1647,10 @@ fn parse_session_command(args: &[&str]) -> Result<SlashCommand, SlashCommandPars
)),
[action, ..] => Err(command_error(
&format!(
"Unknown /session action '{action}'. Use list, switch <session-id>, fork [branch-name], or delete <session-id> [--force]."
"Unknown /session action '{action}'. Use list, exists <session-id>, switch <session-id>, fork [branch-name], or delete <session-id> [--force]."
),
"session",
"/session [list|switch <session-id>|fork [branch-name]|delete <session-id> [--force]]",
"/session [list|exists <session-id>|switch <session-id>|fork [branch-name]|delete <session-id> [--force]]",
)),
}
}
@@ -2392,8 +2402,8 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|| args.starts_with("describe ") =>
{
let name = args
.splitn(2, ' ')
.nth(1)
.split_once(' ')
.map(|(_, name)| name)
.unwrap_or_default()
.trim()
.to_lowercase();
@@ -2457,8 +2467,8 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|| args.starts_with("describe ") =>
{
let name = args
.splitn(2, ' ')
.nth(1)
.split_once(' ')
.map(|(_, name)| name)
.unwrap_or_default()
.trim()
.to_lowercase();
@@ -2622,6 +2632,7 @@ pub fn resolve_skill_path(cwd: &Path, skill: &str) -> std::io::Result<PathBuf> {
))
}
#[allow(clippy::unnecessary_wraps)]
fn render_mcp_report_for(
loader: &ConfigLoader,
cwd: &Path,
@@ -2719,6 +2730,7 @@ fn render_mcp_unsupported_action_json(action: &str, hint: &str) -> Value {
})
}
#[allow(clippy::unnecessary_wraps)]
fn render_mcp_report_json_for(
loader: &ConfigLoader,
cwd: &Path,
@@ -3790,6 +3802,7 @@ fn render_mcp_server_report(
format!(" Working directory {}", cwd.display()),
format!(" Name {server_name}"),
format!(" Scope {}", config_source_label(server.scope)),
format!(" Required {}", server.required),
format!(
" Transport {}",
mcp_transport_label(&server.config)
@@ -4188,6 +4201,7 @@ fn mcp_server_details_json(config: &McpServerConfig) -> Value {
fn mcp_server_json(name: &str, server: &ScopedMcpServerConfig) -> Value {
json!({
"name": name,
"required": server.required,
"scope": config_source_json(server.scope),
"transport": mcp_transport_json(&server.config),
"summary": mcp_server_summary(&server.config),
@@ -4315,8 +4329,8 @@ mod tests {
DefinitionSource, SkillOrigin, SkillRoot, SkillSlashDispatch, SlashCommand,
};
use plugins::{
PluginError, PluginKind, PluginLoadFailure, PluginManager, PluginManagerConfig,
PluginMetadata, PluginSummary,
PluginError, PluginKind, PluginLifecycle, PluginLoadFailure, PluginManager,
PluginManagerConfig, PluginMetadata, PluginSummary,
};
use runtime::{
CompactionConfig, ConfigLoader, ContentBlock, ConversationMessage, MessageRole, Session,
@@ -4590,6 +4604,13 @@ mod tests {
target: Some("abc123".to_string())
}))
);
assert_eq!(
SlashCommand::parse("/session exists abc123"),
Ok(Some(SlashCommand::Session {
action: Some("exists".to_string()),
target: Some("abc123".to_string())
}))
);
assert_eq!(
SlashCommand::parse("/plugins install demo"),
Ok(Some(SlashCommand::Plugins {
@@ -5127,6 +5148,7 @@ mod tests {
root: None,
},
enabled: true,
lifecycle: PluginLifecycle::default(),
},
PluginSummary {
metadata: PluginMetadata {
@@ -5140,6 +5162,7 @@ mod tests {
root: None,
},
enabled: false,
lifecycle: PluginLifecycle::default(),
},
]);
@@ -5166,6 +5189,7 @@ mod tests {
root: None,
},
enabled: true,
lifecycle: PluginLifecycle::default(),
}],
&[PluginLoadFailure::new(
PathBuf::from("/tmp/broken-plugin"),
@@ -5580,6 +5604,7 @@ mod tests {
"command": "uvx",
"args": ["alpha-server"],
"env": {"ALPHA_TOKEN": "secret"},
"required": true,
"toolCallTimeoutMs": 1200
},
"remote": {
@@ -5625,6 +5650,7 @@ mod tests {
let show = super::render_mcp_report_for(&loader, &workspace, Some("show alpha"))
.expect("mcp show report should render");
assert!(show.contains("Name alpha"));
assert!(show.contains("Required true"));
assert!(show.contains("Command uvx"));
assert!(show.contains("Args alpha-server"));
assert!(show.contains("Env keys ALPHA_TOKEN"));
@@ -5657,6 +5683,7 @@ mod tests {
"command": "uvx",
"args": ["alpha-server"],
"env": {"ALPHA_TOKEN": "secret"},
"required": true,
"toolCallTimeoutMs": 1200
},
"remote": {
@@ -5693,6 +5720,7 @@ mod tests {
assert_eq!(list["action"], "list");
assert_eq!(list["configured_servers"], 2);
assert_eq!(list["servers"][0]["name"], "alpha");
assert_eq!(list["servers"][0]["required"], true);
assert_eq!(list["servers"][0]["transport"]["id"], "stdio");
assert_eq!(list["servers"][0]["details"]["command"], "uvx");
assert_eq!(list["servers"][1]["name"], "remote");
@@ -5708,6 +5736,7 @@ mod tests {
assert_eq!(show["action"], "show");
assert_eq!(show["found"], true);
assert_eq!(show["server"]["name"], "alpha");
assert_eq!(show["server"]["required"], true);
assert_eq!(show["server"]["details"]["env_keys"][0], "ALPHA_TOKEN");
assert_eq!(show["server"]["details"]["tool_call_timeout_ms"], 1200);

View File

@@ -248,7 +248,6 @@ fn detect_scenario(request: &MessageRequest) -> Option<Scenario> {
.split_whitespace()
.find_map(|token| token.strip_prefix(SCENARIO_PREFIX))
.and_then(Scenario::parse),
InputContentBlock::Thinking { .. } => None,
_ => None,
})
})

View File

@@ -648,6 +648,7 @@ impl RegisteredPlugin {
PluginSummary {
metadata: self.metadata().clone(),
enabled: self.enabled,
lifecycle: self.definition.lifecycle().clone(),
}
}
}
@@ -656,6 +657,18 @@ impl RegisteredPlugin {
pub struct PluginSummary {
pub metadata: PluginMetadata,
pub enabled: bool,
pub lifecycle: PluginLifecycle,
}
impl PluginSummary {
#[must_use]
pub fn lifecycle_state(&self) -> &'static str {
if self.enabled {
"ready"
} else {
"disabled"
}
}
}
#[derive(Debug)]
@@ -3319,7 +3332,7 @@ mod tests {
let config_home = temp_dir("installed-report-home");
let bundled_root = temp_dir("installed-report-bundled");
let install_root = config_home.join("plugins").join("installed");
write_external_plugin(&install_root.join("valid"), "installed-valid", "1.0.0");
write_lifecycle_plugin(&install_root.join("valid"), "installed-valid", "1.0.0");
write_broken_plugin(&install_root.join("broken"), "installed-broken");
let mut config = PluginManagerConfig::new(&config_home);
@@ -3334,6 +3347,14 @@ mod tests {
// then
assert!(report.registry().contains("installed-valid@external"));
let summaries = report.summaries();
let valid = summaries
.iter()
.find(|summary| summary.metadata.id == "installed-valid@external")
.expect("valid plugin summary should be present");
assert_eq!(valid.lifecycle_state(), "disabled");
assert_eq!(valid.lifecycle.init.len(), 1);
assert_eq!(valid.lifecycle.shutdown.len(), 1);
assert_eq!(report.failures().len(), 1);
assert!(report.failures()[0]
.plugin_root

View File

@@ -0,0 +1,502 @@
use std::collections::BTreeMap;
/// Machine-readable policy exception scope that an approval token may override.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalScope {
pub policy: String,
pub action: String,
pub repository: Option<String>,
pub branch: Option<String>,
}
impl ApprovalScope {
#[must_use]
pub fn new(policy: impl Into<String>, action: impl Into<String>) -> Self {
Self {
policy: policy.into(),
action: action.into(),
repository: None,
branch: None,
}
}
#[must_use]
pub fn with_repository(mut self, repository: impl Into<String>) -> Self {
self.repository = Some(repository.into());
self
}
#[must_use]
pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
self.branch = Some(branch.into());
self
}
}
/// Actor/session hop recorded when an approval is delegated or consumed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalDelegationHop {
pub actor: String,
pub session_id: Option<String>,
pub reason: String,
}
impl ApprovalDelegationHop {
#[must_use]
pub fn new(actor: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
actor: actor.into(),
session_id: None,
reason: reason.into(),
}
}
#[must_use]
pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
}
/// Current lifecycle state for a policy-exception approval token.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalTokenStatus {
Pending,
Granted,
Consumed,
Expired,
Revoked,
}
impl ApprovalTokenStatus {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "approval_pending",
Self::Granted => "approval_granted",
Self::Consumed => "approval_consumed",
Self::Expired => "approval_expired",
Self::Revoked => "approval_revoked",
}
}
}
/// Typed policy errors returned when a token cannot authorize a blocked action.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApprovalTokenError {
NoApproval,
ApprovalPending,
ApprovalExpired,
ApprovalRevoked,
ApprovalAlreadyConsumed,
ScopeMismatch {
expected: Box<ApprovalScope>,
actual: Box<ApprovalScope>,
},
UnauthorizedDelegate {
expected: String,
actual: String,
},
}
impl ApprovalTokenError {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::NoApproval => "no_approval",
Self::ApprovalPending => "approval_pending",
Self::ApprovalExpired => "approval_expired",
Self::ApprovalRevoked => "approval_revoked",
Self::ApprovalAlreadyConsumed => "approval_already_consumed",
Self::ScopeMismatch { .. } => "approval_scope_mismatch",
Self::UnauthorizedDelegate { .. } => "approval_unauthorized_delegate",
}
}
}
/// Approval grant bound to a policy/action scope, approving owner, and executor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalTokenGrant {
pub token: String,
pub scope: ApprovalScope,
pub approving_actor: String,
pub approved_executor: String,
pub status: ApprovalTokenStatus,
pub expires_at_epoch_seconds: Option<u64>,
pub max_uses: u32,
pub uses: u32,
delegation_chain: Vec<ApprovalDelegationHop>,
}
impl ApprovalTokenGrant {
#[must_use]
pub fn pending(
token: impl Into<String>,
scope: ApprovalScope,
approving_actor: impl Into<String>,
approved_executor: impl Into<String>,
) -> Self {
Self {
token: token.into(),
scope,
approving_actor: approving_actor.into(),
approved_executor: approved_executor.into(),
status: ApprovalTokenStatus::Pending,
expires_at_epoch_seconds: None,
max_uses: 1,
uses: 0,
delegation_chain: Vec::new(),
}
}
#[must_use]
pub fn granted(
token: impl Into<String>,
scope: ApprovalScope,
approving_actor: impl Into<String>,
approved_executor: impl Into<String>,
) -> Self {
Self::pending(token, scope, approving_actor, approved_executor).approve()
}
#[must_use]
pub fn approve(mut self) -> Self {
self.status = ApprovalTokenStatus::Granted;
self
}
#[must_use]
pub fn expires_at(mut self, epoch_seconds: u64) -> Self {
self.expires_at_epoch_seconds = Some(epoch_seconds);
self
}
#[must_use]
pub fn with_max_uses(mut self, max_uses: u32) -> Self {
self.max_uses = max_uses.max(1);
self
}
#[must_use]
pub fn with_delegation_hop(mut self, hop: ApprovalDelegationHop) -> Self {
self.delegation_chain.push(hop);
self
}
#[must_use]
pub fn delegation_chain(&self) -> &[ApprovalDelegationHop] {
&self.delegation_chain
}
}
/// Auditable result of verifying or consuming an approval token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalTokenAudit {
pub token: String,
pub scope: ApprovalScope,
pub approving_actor: String,
pub executing_actor: String,
pub status: ApprovalTokenStatus,
pub delegated_execution: bool,
pub delegation_chain: Vec<ApprovalDelegationHop>,
pub uses: u32,
pub max_uses: u32,
}
/// In-memory approval-token ledger with one-time-use and replay protection.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ApprovalTokenLedger {
grants: BTreeMap<String, ApprovalTokenGrant>,
}
impl ApprovalTokenLedger {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, grant: ApprovalTokenGrant) {
self.grants.insert(grant.token.clone(), grant);
}
#[must_use]
pub fn get(&self, token: &str) -> Option<&ApprovalTokenGrant> {
self.grants.get(token)
}
pub fn revoke(&mut self, token: &str) -> Result<ApprovalTokenAudit, ApprovalTokenError> {
let grant = self
.grants
.get_mut(token)
.ok_or(ApprovalTokenError::NoApproval)?;
grant.status = ApprovalTokenStatus::Revoked;
Ok(Self::audit_for(grant, &grant.approved_executor))
}
pub fn verify(
&self,
token: &str,
scope: &ApprovalScope,
executing_actor: &str,
now_epoch_seconds: u64,
) -> Result<ApprovalTokenAudit, ApprovalTokenError> {
let grant = self
.grants
.get(token)
.ok_or(ApprovalTokenError::NoApproval)?;
Self::validate_grant(grant, scope, executing_actor, now_epoch_seconds)?;
Ok(Self::audit_for(grant, executing_actor))
}
pub fn consume(
&mut self,
token: &str,
scope: &ApprovalScope,
executing_actor: &str,
now_epoch_seconds: u64,
) -> Result<ApprovalTokenAudit, ApprovalTokenError> {
let grant = self
.grants
.get_mut(token)
.ok_or(ApprovalTokenError::NoApproval)?;
Self::validate_grant(grant, scope, executing_actor, now_epoch_seconds)?;
grant.uses += 1;
if grant.uses >= grant.max_uses {
grant.status = ApprovalTokenStatus::Consumed;
}
Ok(Self::audit_for(grant, executing_actor))
}
fn validate_grant(
grant: &ApprovalTokenGrant,
scope: &ApprovalScope,
executing_actor: &str,
now_epoch_seconds: u64,
) -> Result<(), ApprovalTokenError> {
match grant.status {
ApprovalTokenStatus::Pending => return Err(ApprovalTokenError::ApprovalPending),
ApprovalTokenStatus::Consumed => {
return Err(ApprovalTokenError::ApprovalAlreadyConsumed)
}
ApprovalTokenStatus::Expired => return Err(ApprovalTokenError::ApprovalExpired),
ApprovalTokenStatus::Revoked => return Err(ApprovalTokenError::ApprovalRevoked),
ApprovalTokenStatus::Granted => {}
}
if grant
.expires_at_epoch_seconds
.is_some_and(|expires_at| now_epoch_seconds > expires_at)
{
return Err(ApprovalTokenError::ApprovalExpired);
}
if grant.uses >= grant.max_uses {
return Err(ApprovalTokenError::ApprovalAlreadyConsumed);
}
if grant.scope != *scope {
return Err(ApprovalTokenError::ScopeMismatch {
expected: Box::new(grant.scope.clone()),
actual: Box::new(scope.clone()),
});
}
if grant.approved_executor != executing_actor {
return Err(ApprovalTokenError::UnauthorizedDelegate {
expected: grant.approved_executor.clone(),
actual: executing_actor.to_string(),
});
}
Ok(())
}
fn audit_for(grant: &ApprovalTokenGrant, executing_actor: &str) -> ApprovalTokenAudit {
let mut delegation_chain = grant.delegation_chain.clone();
if delegation_chain.is_empty() {
delegation_chain.push(ApprovalDelegationHop::new(
grant.approving_actor.clone(),
"approval granted",
));
}
if grant.approving_actor != executing_actor
&& !delegation_chain
.iter()
.any(|hop| hop.actor == executing_actor)
{
delegation_chain.push(ApprovalDelegationHop::new(
executing_actor.to_string(),
"delegated execution",
));
}
ApprovalTokenAudit {
token: grant.token.clone(),
scope: grant.scope.clone(),
approving_actor: grant.approving_actor.clone(),
executing_actor: executing_actor.to_string(),
status: grant.status,
delegated_execution: grant.approving_actor != executing_actor,
delegation_chain,
uses: grant.uses,
max_uses: grant.max_uses,
}
}
}
#[cfg(test)]
mod tests {
use super::{
ApprovalDelegationHop, ApprovalScope, ApprovalTokenError, ApprovalTokenGrant,
ApprovalTokenLedger, ApprovalTokenStatus,
};
#[test]
fn approval_token_blocks_until_owner_grants_policy_exception() {
let mut ledger = ApprovalTokenLedger::new();
let scope = ApprovalScope::new("main_push_forbidden", "git push")
.with_repository("sisyphus/claw-code")
.with_branch("main");
ledger.insert(ApprovalTokenGrant::pending(
"tok-pending",
scope.clone(),
"repo-owner",
"release-bot",
));
assert!(matches!(
ledger.verify("tok-missing", &scope, "release-bot", 10),
Err(ApprovalTokenError::NoApproval)
));
assert!(matches!(
ledger.verify("tok-pending", &scope, "release-bot", 10),
Err(ApprovalTokenError::ApprovalPending)
));
ledger.insert(ApprovalTokenGrant::granted(
"tok-granted",
scope.clone(),
"repo-owner",
"release-bot",
));
let audit = ledger
.verify("tok-granted", &scope, "release-bot", 10)
.expect("owner approval should verify");
assert_eq!(audit.status, ApprovalTokenStatus::Granted);
assert_eq!(audit.approving_actor, "repo-owner");
assert_eq!(audit.executing_actor, "release-bot");
assert!(audit.delegated_execution);
}
#[test]
fn approval_token_is_one_time_use_and_rejects_replay() {
let mut ledger = ApprovalTokenLedger::new();
let scope = ApprovalScope::new("release_requires_owner", "release publish")
.with_repository("sisyphus/claw-code");
ledger.insert(ApprovalTokenGrant::granted(
"tok-once",
scope.clone(),
"owner",
"release-bot",
));
let first = ledger
.consume("tok-once", &scope, "release-bot", 10)
.expect("first use should consume token");
assert_eq!(first.status, ApprovalTokenStatus::Consumed);
assert_eq!(first.uses, 1);
assert!(matches!(
ledger.consume("tok-once", &scope, "release-bot", 11),
Err(ApprovalTokenError::ApprovalAlreadyConsumed)
));
assert_eq!(
ledger.get("tok-once").map(|grant| grant.status),
Some(ApprovalTokenStatus::Consumed)
);
}
#[test]
fn approval_token_rejects_scope_expansion_expiry_and_revocation() {
let mut ledger = ApprovalTokenLedger::new();
let scope = ApprovalScope::new("main_push_forbidden", "git push")
.with_repository("sisyphus/claw-code")
.with_branch("main");
let dev_scope = ApprovalScope::new("main_push_forbidden", "git push")
.with_repository("sisyphus/claw-code")
.with_branch("dev");
ledger.insert(
ApprovalTokenGrant::granted("tok-expiring", scope.clone(), "owner", "bot")
.expires_at(20),
);
assert!(matches!(
ledger.verify("tok-expiring", &dev_scope, "bot", 10),
Err(ApprovalTokenError::ScopeMismatch { .. })
));
assert!(matches!(
ledger.verify("tok-expiring", &scope, "bot", 21),
Err(ApprovalTokenError::ApprovalExpired)
));
ledger.insert(ApprovalTokenGrant::granted(
"tok-revoked",
scope.clone(),
"owner",
"bot",
));
let revoked = ledger
.revoke("tok-revoked")
.expect("revocation should be audited");
assert_eq!(revoked.status, ApprovalTokenStatus::Revoked);
assert!(matches!(
ledger.verify("tok-revoked", &scope, "bot", 10),
Err(ApprovalTokenError::ApprovalRevoked)
));
}
#[test]
fn approval_token_preserves_delegation_traceability() {
let mut ledger = ApprovalTokenLedger::new();
let scope = ApprovalScope::new("deploy_requires_owner", "deploy prod");
ledger.insert(
ApprovalTokenGrant::granted("tok-delegated", scope.clone(), "owner", "deploy-bot")
.with_delegation_hop(
ApprovalDelegationHop::new("owner", "owner approval")
.with_session_id("session-owner"),
)
.with_delegation_hop(
ApprovalDelegationHop::new("lead-agent", "handoff to deploy bot")
.with_session_id("session-lead"),
),
);
assert!(matches!(
ledger.verify("tok-delegated", &scope, "unexpected-bot", 10),
Err(ApprovalTokenError::UnauthorizedDelegate { expected, actual })
if expected == "deploy-bot" && actual == "unexpected-bot"
));
let audit = ledger
.consume("tok-delegated", &scope, "deploy-bot", 10)
.expect("approved delegate should consume token");
let actors = audit
.delegation_chain
.iter()
.map(|hop| hop.actor.as_str())
.collect::<Vec<_>>();
assert!(audit.delegated_execution);
assert_eq!(actors, vec!["owner", "lead-agent", "deploy-bot"]);
assert_eq!(
audit.delegation_chain[0].session_id.as_deref(),
Some("session-owner")
);
assert_eq!(
audit.delegation_chain[1].session_id.as_deref(),
Some("session-lead")
);
}
}

View File

@@ -4,6 +4,7 @@ use std::process::{Command, Stdio};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tokio::process::Command as TokioCommand;
use tokio::runtime::Builder;
use tokio::time::timeout;
@@ -176,27 +177,10 @@ async fn execute_bash_async(
let mut command = prepare_tokio_command(&input.command, &cwd, &sandbox_status, true);
let output_result = if let Some(timeout_ms) = input.timeout {
match timeout(Duration::from_millis(timeout_ms), command.output()).await {
Ok(result) => (result?, false),
Err(_) => {
return Ok(BashCommandOutput {
stdout: String::new(),
stderr: format!("Command exceeded timeout of {timeout_ms} ms"),
raw_output_path: None,
interrupted: true,
is_image: None,
background_task_id: None,
backgrounded_by_user: None,
assistant_auto_backgrounded: None,
dangerously_disable_sandbox: input.dangerously_disable_sandbox,
return_code_interpretation: Some(String::from("timeout")),
no_output_expected: Some(true),
structured_content: None,
persisted_output_path: None,
persisted_output_size: None,
sandbox_status: Some(sandbox_status),
});
}
if let Ok(result) = timeout(Duration::from_millis(timeout_ms), command.output()).await {
(result?, false)
} else {
return Ok(timeout_output(&input, timeout_ms, sandbox_status));
}
} else {
(command.output().await?, false)
@@ -233,6 +217,67 @@ async fn execute_bash_async(
})
}
fn timeout_output(
input: &BashCommandInput,
timeout_ms: u64,
sandbox_status: SandboxStatus,
) -> BashCommandOutput {
let is_test = is_test_command(&input.command);
let return_code_interpretation = if is_test { "test.hung" } else { "timeout" };
BashCommandOutput {
stdout: String::new(),
stderr: format!("Command exceeded timeout of {timeout_ms} ms"),
raw_output_path: None,
interrupted: true,
is_image: None,
background_task_id: None,
backgrounded_by_user: None,
assistant_auto_backgrounded: None,
dangerously_disable_sandbox: input.dangerously_disable_sandbox,
return_code_interpretation: Some(String::from(return_code_interpretation)),
no_output_expected: Some(true),
structured_content: Some(vec![test_timeout_provenance(
&input.command,
timeout_ms,
is_test,
)]),
persisted_output_path: None,
persisted_output_size: None,
sandbox_status: Some(sandbox_status),
}
}
fn is_test_command(command: &str) -> bool {
let normalized = command
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase();
normalized.contains("cargo test")
|| normalized.contains("cargo nextest")
|| normalized.contains("npm test")
|| normalized.contains("pnpm test")
|| normalized.contains("yarn test")
|| normalized.contains("pytest")
}
fn test_timeout_provenance(
command: &str,
timeout_ms: u64,
classified_as_test_hang: bool,
) -> serde_json::Value {
json!({
"event": if classified_as_test_hang { "test.hung" } else { "command.timeout" },
"failureClass": if classified_as_test_hang { "test_hang" } else { "timeout" },
"data": {
"command": command,
"timeoutMs": timeout_ms,
"provenance": "bash.timeout",
"classification": if classified_as_test_hang { "test.hung" } else { "timeout" }
}
})
}
fn sandbox_status_for_input(input: &BashCommandInput, cwd: &std::path::Path) -> SandboxStatus {
let config = ConfigLoader::default_for(cwd).load().map_or_else(
|_| SandboxConfig::default(),
@@ -349,6 +394,31 @@ mod tests {
assert!(!output.sandbox_status.expect("sandbox status").enabled);
}
#[test]
fn timed_out_test_command_is_classified_as_hung_test_with_provenance() {
let output = execute_bash(BashCommandInput {
command: String::from("sleep 1 # cargo test slow_case"),
timeout: Some(1),
description: None,
run_in_background: Some(false),
dangerously_disable_sandbox: Some(false),
namespace_restrictions: Some(false),
isolate_network: Some(false),
filesystem_mode: Some(FilesystemIsolationMode::WorkspaceOnly),
allowed_mounts: None,
})
.expect("bash command should return structured timeout");
assert!(output.interrupted);
assert_eq!(
output.return_code_interpretation.as_deref(),
Some("test.hung")
);
let structured = output.structured_content.expect("structured content");
assert_eq!(structured[0]["event"], "test.hung");
assert_eq!(structured[0]["data"]["provenance"], "bash.timeout");
}
}
/// Maximum output bytes before truncation (16 KiB, matching upstream).

View File

@@ -212,8 +212,7 @@ fn summarize_messages(messages: &[ConversationMessage]) -> String {
.filter_map(|block| match block {
ContentBlock::ToolUse { name, .. } => Some(name.as_str()),
ContentBlock::ToolResult { tool_name, .. } => Some(tool_name.as_str()),
ContentBlock::Text { .. } => None,
ContentBlock::Thinking { .. } => None,
ContentBlock::Text { .. } | ContentBlock::Thinking { .. } => None,
})
.collect::<Vec<_>>();
tool_names.sort_unstable();

View File

@@ -101,6 +101,7 @@ pub struct McpConfigCollection {
/// MCP server config paired with the scope that defined it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScopedMcpServerConfig {
pub required: bool,
pub scope: ConfigSource,
pub config: McpServerConfig,
}
@@ -414,6 +415,17 @@ impl RuntimeConfig {
pub fn trusted_roots(&self) -> &[String] {
&self.feature_config.trusted_roots
}
/// Merge config-level default trusted roots with per-call roots.
///
/// Config roots are defaults and are kept first; per-call roots extend the
/// allowlist for a specific worker/session creation request. Duplicates are
/// removed without reordering the first occurrence so evidence remains
/// deterministic while avoiding repeated trust checks.
#[must_use]
pub fn trusted_roots_with_overrides(&self, per_call_roots: &[String]) -> Vec<String> {
merge_trusted_roots(self.trusted_roots(), per_call_roots)
}
}
impl RuntimeFeatureConfig {
@@ -483,6 +495,22 @@ impl RuntimeFeatureConfig {
pub fn trusted_roots(&self) -> &[String] {
&self.trusted_roots
}
/// Merge this config's default trusted roots with per-call roots.
#[must_use]
pub fn trusted_roots_with_overrides(&self, per_call_roots: &[String]) -> Vec<String> {
merge_trusted_roots(self.trusted_roots(), per_call_roots)
}
}
fn merge_trusted_roots(config_roots: &[String], per_call_roots: &[String]) -> Vec<String> {
let mut merged = Vec::with_capacity(config_roots.len() + per_call_roots.len());
for root in config_roots.iter().chain(per_call_roots.iter()) {
if !merged.contains(root) {
merged.push(root.clone());
}
}
merged
}
impl ProviderFallbackConfig {
@@ -725,6 +753,12 @@ fn merge_mcp_servers(
target.insert(
name.clone(),
ScopedMcpServerConfig {
required: optional_bool(
expect_object(value, &format!("{}: mcpServers.{name}", path.display()))?,
"required",
&format!("{}: mcpServers.{name}", path.display()),
)?
.unwrap_or(false),
scope: source,
config: parsed,
},
@@ -1245,8 +1279,8 @@ fn push_unique(target: &mut Vec<String>, value: String) {
mod tests {
use super::{
deep_merge_objects, parse_permission_mode_label, ConfigLoader, ConfigSource,
McpServerConfig, McpTransport, ResolvedPermissionMode, RuntimeHookConfig,
RuntimePluginConfig, CLAW_SETTINGS_SCHEMA_NAME,
McpServerConfig, McpTransport, ResolvedPermissionMode, RuntimeFeatureConfig,
RuntimeHookConfig, RuntimePluginConfig, CLAW_SETTINGS_SCHEMA_NAME,
};
use crate::json::JsonValue;
use crate::sandbox::FilesystemIsolationMode;
@@ -1502,6 +1536,51 @@ mod tests {
fs::remove_dir_all(root).expect("cleanup temp dir");
}
#[test]
fn trusted_roots_with_overrides_preserves_config_defaults_and_adds_per_call_roots() {
// given
let root = temp_dir();
let cwd = root.join("project");
let home = root.join("home").join(".claw");
fs::create_dir_all(&home).expect("home config dir");
fs::create_dir_all(&cwd).expect("project dir");
fs::write(
home.join("settings.json"),
r#"{"trustedRoots": ["/tmp/config-default", "/tmp/shared"]}"#,
)
.expect("write settings");
// when
let loaded = ConfigLoader::new(&cwd, &home)
.load()
.expect("config should load");
let merged = loaded.trusted_roots_with_overrides(&[
"/tmp/per-call".to_string(),
"/tmp/shared".to_string(),
]);
// then
assert_eq!(
merged,
["/tmp/config-default", "/tmp/shared", "/tmp/per-call"]
);
fs::remove_dir_all(root).expect("cleanup temp dir");
}
#[test]
fn runtime_feature_trusted_roots_with_overrides_matches_runtime_config_merge() {
let config = RuntimeFeatureConfig {
trusted_roots: vec!["/tmp/config".to_string()],
..RuntimeFeatureConfig::default()
};
assert_eq!(
config.trusted_roots_with_overrides(&["/tmp/per-call".to_string()]),
["/tmp/config", "/tmp/per-call"]
);
}
#[test]
fn trusted_roots_default_is_empty_when_unset() {
// given
@@ -1538,7 +1617,8 @@ mod tests {
"stdio-server": {
"command": "uvx",
"args": ["mcp-server"],
"env": {"TOKEN": "secret"}
"env": {"TOKEN": "secret"},
"required": true
},
"remote-server": {
"type": "http",
@@ -1587,6 +1667,7 @@ mod tests {
.get("stdio-server")
.expect("stdio server should exist");
assert_eq!(stdio_server.scope, ConfigSource::User);
assert!(stdio_server.required);
assert_eq!(stdio_server.transport(), McpTransport::Stdio);
let remote_server = loaded
@@ -1594,6 +1675,7 @@ mod tests {
.get("remote-server")
.expect("remote server should exist");
assert_eq!(remote_server.scope, ConfigSource::Local);
assert!(!remote_server.required);
assert_eq!(remote_server.transport(), McpTransport::Ws);
match &remote_server.config {
McpServerConfig::Ws(config) => {

View File

@@ -307,11 +307,23 @@ pub fn edit_file(
/// Expands a glob pattern and returns matching filenames.
pub fn glob_search(pattern: &str, path: Option<&str>) -> io::Result<GlobSearchOutput> {
glob_search_impl(pattern, path, None)
}
fn glob_search_impl(
pattern: &str,
path: Option<&str>,
workspace_root: Option<&Path>,
) -> io::Result<GlobSearchOutput> {
let started = Instant::now();
let base_dir = path
.map(normalize_path)
.transpose()?
.unwrap_or(std::env::current_dir()?);
let canonical_root = workspace_root.map(canonicalize_workspace_root);
if let Some(root) = canonical_root.as_deref() {
validate_workspace_boundary(&base_dir, root)?;
}
let search_pattern = if Path::new(pattern).is_absolute() {
pattern.to_owned()
} else {
@@ -329,6 +341,12 @@ pub fn glob_search(pattern: &str, path: Option<&str>) -> io::Result<GlobSearchOu
let compiled = Pattern::new(pat)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error.to_string()))?;
let walk_root = derive_glob_walk_root(pat);
if let Some(root) = canonical_root.as_deref() {
let canonical_walk_root = walk_root
.canonicalize()
.unwrap_or_else(|_| walk_root.clone());
validate_workspace_boundary(&canonical_walk_root, root)?;
}
let entries = WalkDir::new(&walk_root)
.into_iter()
.filter_entry(|entry| !should_skip_glob_dir(entry));
@@ -338,6 +356,10 @@ pub fn glob_search(pattern: &str, path: Option<&str>) -> io::Result<GlobSearchOu
&& compiled.matches_path(candidate)
&& seen.insert(candidate.to_path_buf())
{
if let Some(root) = canonical_root.as_deref() {
let canonical_candidate = candidate.canonicalize()?;
validate_workspace_boundary(&canonical_candidate, root)?;
}
matches.push(candidate.to_path_buf());
}
}
@@ -367,12 +389,23 @@ pub fn glob_search(pattern: &str, path: Option<&str>) -> io::Result<GlobSearchOu
/// Runs a regex search over workspace files with optional context lines.
pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
grep_search_impl(input, None)
}
fn grep_search_impl(
input: &GrepSearchInput,
workspace_root: Option<&Path>,
) -> io::Result<GrepSearchOutput> {
let base_path = input
.path
.as_deref()
.map(normalize_path)
.transpose()?
.unwrap_or(std::env::current_dir()?);
let canonical_root = workspace_root.map(canonicalize_workspace_root);
if let Some(root) = canonical_root.as_deref() {
validate_workspace_boundary(&base_path, root)?;
}
let regex = RegexBuilder::new(&input.pattern)
.case_insensitive(input.case_insensitive.unwrap_or(false))
@@ -398,6 +431,10 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
let mut total_matches = 0usize;
for file_path in collect_search_files(&base_path)? {
if let Some(root) = canonical_root.as_deref() {
let canonical_file = file_path.canonicalize()?;
validate_workspace_boundary(&canonical_file, root)?;
}
if !matches_optional_filters(&file_path, glob_filter.as_ref(), file_type) {
continue;
}
@@ -447,27 +484,21 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
let (filenames, applied_limit, applied_offset) =
apply_limit(filenames, input.head_limit, input.offset);
let content_output = if output_mode == "content" {
let (lines, limit, offset) = apply_limit(content_lines, input.head_limit, input.offset);
return Ok(GrepSearchOutput {
mode: Some(output_mode),
num_files: filenames.len(),
if output_mode == "content" {
return Ok(build_grep_content_output(
output_mode,
filenames,
num_lines: Some(lines.len()),
content: Some(lines.join("\n")),
num_matches: None,
applied_limit: limit,
applied_offset: offset,
});
} else {
None
};
content_lines,
input.head_limit,
input.offset,
));
}
Ok(GrepSearchOutput {
mode: Some(output_mode.clone()),
num_files: filenames.len(),
filenames,
content: content_output,
content: None,
num_lines: None,
num_matches: (output_mode == "count").then_some(total_matches),
applied_limit,
@@ -475,6 +506,32 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
})
}
fn build_grep_content_output(
output_mode: String,
filenames: Vec<String>,
content_lines: Vec<String>,
head_limit: Option<usize>,
offset: Option<usize>,
) -> GrepSearchOutput {
let (lines, limit, offset) = apply_limit(content_lines, head_limit, offset);
GrepSearchOutput {
mode: Some(output_mode),
num_files: filenames.len(),
filenames,
num_lines: Some(lines.len()),
content: Some(lines.join("\n")),
num_matches: None,
applied_limit: limit,
applied_offset: offset,
}
}
fn canonicalize_workspace_root(workspace_root: &Path) -> PathBuf {
workspace_root
.canonicalize()
.unwrap_or_else(|_| workspace_root.to_path_buf())
}
fn should_skip_glob_dir(entry: &DirEntry) -> bool {
entry.file_type().is_dir()
&& entry
@@ -625,9 +682,7 @@ pub fn read_file_in_workspace(
workspace_root: &Path,
) -> io::Result<ReadFileOutput> {
let absolute_path = normalize_path(path)?;
let canonical_root = workspace_root
.canonicalize()
.unwrap_or_else(|_| workspace_root.to_path_buf());
let canonical_root = canonicalize_workspace_root(workspace_root);
validate_workspace_boundary(&absolute_path, &canonical_root)?;
read_file(path, offset, limit)
}
@@ -640,9 +695,7 @@ pub fn write_file_in_workspace(
workspace_root: &Path,
) -> io::Result<WriteFileOutput> {
let absolute_path = normalize_path_allow_missing(path)?;
let canonical_root = workspace_root
.canonicalize()
.unwrap_or_else(|_| workspace_root.to_path_buf());
let canonical_root = canonicalize_workspace_root(workspace_root);
validate_workspace_boundary(&absolute_path, &canonical_root)?;
write_file(path, content)
}
@@ -657,13 +710,30 @@ pub fn edit_file_in_workspace(
workspace_root: &Path,
) -> io::Result<EditFileOutput> {
let absolute_path = normalize_path(path)?;
let canonical_root = workspace_root
.canonicalize()
.unwrap_or_else(|_| workspace_root.to_path_buf());
let canonical_root = canonicalize_workspace_root(workspace_root);
validate_workspace_boundary(&absolute_path, &canonical_root)?;
edit_file(path, old_string, new_string, replace_all)
}
/// Expand a glob pattern with workspace boundary enforcement.
#[allow(dead_code)]
pub fn glob_search_in_workspace(
pattern: &str,
path: Option<&str>,
workspace_root: &Path,
) -> io::Result<GlobSearchOutput> {
glob_search_impl(pattern, path, Some(workspace_root))
}
/// Search file contents with workspace boundary enforcement.
#[allow(dead_code)]
pub fn grep_search_in_workspace(
input: &GrepSearchInput,
workspace_root: &Path,
) -> io::Result<GrepSearchOutput> {
grep_search_impl(input, Some(workspace_root))
}
/// Check whether a path is a symlink that resolves outside the workspace.
#[allow(dead_code)]
pub fn is_symlink_escape(path: &Path, workspace_root: &Path) -> io::Result<bool> {
@@ -708,7 +778,7 @@ mod tests {
use super::{
component_contains_glob, derive_glob_walk_root, edit_file, expand_braces, glob_search,
grep_search, is_symlink_escape, read_file, read_file_in_workspace, write_file,
GrepSearchInput, MAX_WRITE_SIZE,
write_file_in_workspace, GrepSearchInput, MAX_WRITE_SIZE,
};
fn temp_path(name: &str) -> std::path::PathBuf {
@@ -808,6 +878,68 @@ mod tests {
assert!(!is_symlink_escape(&normal, &workspace).expect("check should succeed"));
}
#[test]
#[cfg(unix)]
fn workspace_read_rejects_symlink_escape_regression_3007_class() {
let workspace = temp_path("workspace-read-symlink-escape");
let outside = temp_path("workspace-read-symlink-target");
std::fs::create_dir_all(&workspace).expect("workspace dir should be created");
std::fs::create_dir_all(&outside).expect("outside dir should be created");
let outside_file = outside.join("secret.txt");
std::fs::write(&outside_file, "outside secret").expect("outside file should write");
let link_path = workspace.join("linked-secret.txt");
std::os::unix::fs::symlink(&outside_file, &link_path).expect("symlink should create");
let result =
read_file_in_workspace(link_path.to_string_lossy().as_ref(), None, None, &workspace);
assert!(result.is_err(), "symlink escape must be rejected");
let error = result.unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
assert!(
error.to_string().contains("escapes workspace"),
"error should explain workspace escape: {error}"
);
let _ = std::fs::remove_dir_all(&workspace);
let _ = std::fs::remove_dir_all(&outside);
}
#[test]
#[cfg(unix)]
fn workspace_write_rejects_parent_symlink_escape_regression_3007_class() {
let workspace = temp_path("workspace-write-symlink-escape");
let outside = temp_path("workspace-write-symlink-target");
std::fs::create_dir_all(&workspace).expect("workspace dir should be created");
std::fs::create_dir_all(&outside).expect("outside dir should be created");
let link_dir = workspace.join("linked-outside");
std::os::unix::fs::symlink(&outside, &link_dir).expect("symlink dir should create");
let escaped_child = link_dir.join("created.txt");
let result = write_file_in_workspace(
escaped_child.to_string_lossy().as_ref(),
"must not escape",
&workspace,
);
assert!(result.is_err(), "parent symlink escape must be rejected");
let error = result.unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
assert!(
error.to_string().contains("escapes workspace"),
"error should explain workspace escape: {error}"
);
assert!(
!outside.join("created.txt").exists(),
"write should not create through an escaping symlink"
);
let _ = std::fs::remove_dir_all(&workspace);
let _ = std::fs::remove_dir_all(&outside);
}
#[test]
fn globs_and_greps_directory() {
let dir = temp_path("search-dir");

View File

@@ -0,0 +1,399 @@
//! Machine-checkable conformance helpers for G004 event/report contract bundles.
//!
//! The harness intentionally validates JSON-shaped artifacts instead of owning the
//! lane-event, report, or approval-token implementations. This keeps it usable by
//! independent implementation lanes and by golden fixtures produced outside the
//! runtime crate.
use serde_json::Value;
const BUNDLE_SCHEMA_VERSION: &str = "g004.contract.bundle.v1";
const REPORT_SCHEMA_VERSION: &str = "g004.report.v1";
/// A single conformance validation failure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct G004ConformanceError {
/// JSON pointer-ish path to the invalid field.
pub path: String,
/// Human-readable reason the field failed validation.
pub message: String,
}
impl G004ConformanceError {
fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
Self {
path: path.into(),
message: message.into(),
}
}
}
/// Validate a G004 golden contract bundle.
///
/// The bundle shape is deliberately small and cross-lane:
/// - `laneEvents[]` must expose stable event identity, ordering/provenance, and
/// terminal dedupe fingerprints.
/// - `reports[]` must expose schema identity, content hash, projection/redaction
/// provenance, capability negotiation, fact/hypothesis/negative-evidence
/// labels, confidence, and field-level delta attribution.
/// - `approvalTokens[]` must expose owner/scope, delegation chain, one-time-use,
/// and replay-prevention fields.
#[must_use]
pub fn validate_g004_contract_bundle(bundle: &Value) -> Vec<G004ConformanceError> {
let mut errors = Vec::new();
require_string_eq(bundle, "/schemaVersion", BUNDLE_SCHEMA_VERSION, &mut errors);
validate_lane_events(bundle.get("laneEvents"), "/laneEvents", &mut errors);
validate_reports(bundle.get("reports"), "/reports", &mut errors);
validate_approval_tokens(bundle.get("approvalTokens"), "/approvalTokens", &mut errors);
errors
}
#[must_use]
pub fn is_g004_contract_bundle_valid(bundle: &Value) -> bool {
validate_g004_contract_bundle(bundle).is_empty()
}
fn validate_lane_events(value: Option<&Value>, path: &str, errors: &mut Vec<G004ConformanceError>) {
let Some(events) = non_empty_array(value, path, errors) else {
return;
};
let mut previous_seq = None;
for (index, event) in events.iter().enumerate() {
let base = format!("{path}/{index}");
require_non_empty_string_at(event, "/event", &format!("{base}/event"), errors);
require_non_empty_string_at(event, "/status", &format!("{base}/status"), errors);
require_non_empty_string_at(event, "/emittedAt", &format!("{base}/emittedAt"), errors);
require_non_empty_string_at(
event,
"/metadata/provenance",
&format!("{base}/metadata/provenance"),
errors,
);
require_non_empty_string_at(
event,
"/metadata/emitterIdentity",
&format!("{base}/metadata/emitterIdentity"),
errors,
);
require_non_empty_string_at(
event,
"/metadata/environmentLabel",
&format!("{base}/metadata/environmentLabel"),
errors,
);
match get_path(event, "/metadata/seq").and_then(Value::as_u64) {
Some(seq) => {
if let Some(previous) = previous_seq {
if seq <= previous {
errors.push(G004ConformanceError::new(
format!("{base}/metadata/seq"),
"sequence must be strictly increasing",
));
}
}
previous_seq = Some(seq);
}
None => errors.push(G004ConformanceError::new(
format!("{base}/metadata/seq"),
"required u64 field missing",
)),
}
if is_terminal_event_value(event.get("event")) {
require_non_empty_string_at(
event,
"/metadata/eventFingerprint",
&format!("{base}/metadata/eventFingerprint"),
errors,
);
}
}
}
fn validate_reports(value: Option<&Value>, path: &str, errors: &mut Vec<G004ConformanceError>) {
let Some(reports) = non_empty_array(value, path, errors) else {
return;
};
for (index, report) in reports.iter().enumerate() {
let base = format!("{path}/{index}");
require_string_eq_at(
report,
"/schemaVersion",
&format!("{base}/schemaVersion"),
REPORT_SCHEMA_VERSION,
errors,
);
require_non_empty_string_at(report, "/reportId", &format!("{base}/reportId"), errors);
require_non_empty_string_at(
report,
"/identity/contentHash",
&format!("{base}/identity/contentHash"),
errors,
);
require_non_empty_string_at(
report,
"/projection/provenance",
&format!("{base}/projection/provenance"),
errors,
);
require_non_empty_string_at(
report,
"/redaction/provenance",
&format!("{base}/redaction/provenance"),
errors,
);
non_empty_array(
get_path(report, "/consumerCapabilities"),
&format!("{base}/consumerCapabilities"),
errors,
);
validate_findings(
get_path(report, "/findings"),
&format!("{base}/findings"),
errors,
);
validate_field_deltas(
get_path(report, "/fieldDeltas"),
&format!("{base}/fieldDeltas"),
errors,
);
}
}
fn validate_findings(value: Option<&Value>, path: &str, errors: &mut Vec<G004ConformanceError>) {
let Some(findings) = non_empty_array(value, path, errors) else {
return;
};
for (index, finding) in findings.iter().enumerate() {
let base = format!("{path}/{index}");
require_one_of_at(
finding,
"/kind",
&format!("{base}/kind"),
&["fact", "hypothesis", "negative_evidence"],
errors,
);
require_one_of_at(
finding,
"/confidence",
&format!("{base}/confidence"),
&["low", "medium", "high"],
errors,
);
require_non_empty_string_at(finding, "/statement", &format!("{base}/statement"), errors);
}
}
fn validate_field_deltas(
value: Option<&Value>,
path: &str,
errors: &mut Vec<G004ConformanceError>,
) {
let Some(deltas) = non_empty_array(value, path, errors) else {
return;
};
for (index, delta) in deltas.iter().enumerate() {
let base = format!("{path}/{index}");
require_non_empty_string_at(delta, "/field", &format!("{base}/field"), errors);
require_non_empty_string_at(
delta,
"/previousHash",
&format!("{base}/previousHash"),
errors,
);
require_non_empty_string_at(
delta,
"/currentHash",
&format!("{base}/currentHash"),
errors,
);
require_non_empty_string_at(
delta,
"/attribution",
&format!("{base}/attribution"),
errors,
);
}
}
fn validate_approval_tokens(
value: Option<&Value>,
path: &str,
errors: &mut Vec<G004ConformanceError>,
) {
let Some(tokens) = non_empty_array(value, path, errors) else {
return;
};
for (index, token) in tokens.iter().enumerate() {
let base = format!("{path}/{index}");
require_non_empty_string_at(token, "/tokenId", &format!("{base}/tokenId"), errors);
require_non_empty_string_at(token, "/owner", &format!("{base}/owner"), errors);
require_non_empty_string_at(token, "/scope", &format!("{base}/scope"), errors);
require_non_empty_string_at(token, "/issuedAt", &format!("{base}/issuedAt"), errors);
require_bool_true_at(token, "/oneTimeUse", &format!("{base}/oneTimeUse"), errors);
require_non_empty_string_at(
token,
"/replayPreventionNonce",
&format!("{base}/replayPreventionNonce"),
errors,
);
validate_delegation_chain(
get_path(token, "/delegationChain"),
&format!("{base}/delegationChain"),
errors,
);
}
}
fn validate_delegation_chain(
value: Option<&Value>,
path: &str,
errors: &mut Vec<G004ConformanceError>,
) {
let Some(chain) = non_empty_array(value, path, errors) else {
return;
};
for (index, hop) in chain.iter().enumerate() {
let base = format!("{path}/{index}");
require_non_empty_string_at(hop, "/from", &format!("{base}/from"), errors);
require_non_empty_string_at(hop, "/to", &format!("{base}/to"), errors);
require_non_empty_string_at(hop, "/action", &format!("{base}/action"), errors);
require_non_empty_string_at(hop, "/at", &format!("{base}/at"), errors);
}
}
fn non_empty_array<'a>(
value: Option<&'a Value>,
path: &str,
errors: &mut Vec<G004ConformanceError>,
) -> Option<&'a Vec<Value>> {
match value.and_then(Value::as_array) {
Some(array) if !array.is_empty() => Some(array),
Some(_) => {
errors.push(G004ConformanceError::new(path, "array must not be empty"));
None
}
None => {
errors.push(G004ConformanceError::new(
path,
"required array field missing",
));
None
}
}
}
fn require_string_eq(
root: &Value,
path: &str,
expected: &str,
errors: &mut Vec<G004ConformanceError>,
) {
require_string_eq_at(root, path, path, expected, errors);
}
fn require_string_eq_at(
root: &Value,
pointer: &str,
error_path: &str,
expected: &str,
errors: &mut Vec<G004ConformanceError>,
) {
match get_path(root, pointer).and_then(Value::as_str) {
Some(actual) if actual == expected => {}
Some(actual) => errors.push(G004ConformanceError::new(
error_path,
format!("expected '{expected}', got '{actual}'"),
)),
None => errors.push(G004ConformanceError::new(
error_path,
"required string field missing",
)),
}
}
fn require_non_empty_string_at(
root: &Value,
pointer: &str,
error_path: &str,
errors: &mut Vec<G004ConformanceError>,
) {
match get_path(root, pointer).and_then(Value::as_str) {
Some(value) if !value.trim().is_empty() => {}
Some(_) => errors.push(G004ConformanceError::new(
error_path,
"string must not be empty",
)),
None => errors.push(G004ConformanceError::new(
error_path,
"required string field missing",
)),
}
}
fn require_one_of_at(
root: &Value,
pointer: &str,
error_path: &str,
allowed: &[&str],
errors: &mut Vec<G004ConformanceError>,
) {
match get_path(root, pointer).and_then(Value::as_str) {
Some(value) if allowed.contains(&value) => {}
Some(value) => errors.push(G004ConformanceError::new(
error_path,
format!("'{value}' is not one of {}", allowed.join(", ")),
)),
None => errors.push(G004ConformanceError::new(
error_path,
"required string field missing",
)),
}
}
fn require_bool_true_at(
root: &Value,
pointer: &str,
error_path: &str,
errors: &mut Vec<G004ConformanceError>,
) {
match get_path(root, pointer).and_then(Value::as_bool) {
Some(true) => {}
Some(false) => errors.push(G004ConformanceError::new(error_path, "must be true")),
None => errors.push(G004ConformanceError::new(
error_path,
"required boolean field missing",
)),
}
}
fn is_terminal_event_value(value: Option<&Value>) -> bool {
matches!(
value.and_then(Value::as_str),
Some("lane.finished" | "lane.failed" | "lane.merged" | "lane.superseded" | "lane.closed")
)
}
fn get_path<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
if let Some(value) = root.pointer(path) {
return Some(value);
}
let segments = path.trim_start_matches('/').split('/').collect::<Vec<_>>();
for index in 1..segments.len() {
let relative = format!("/{}", segments[index..].join("/"));
if let Some(value) = root.pointer(&relative) {
return Some(value);
}
}
None
}

View File

@@ -27,19 +27,38 @@ impl std::fmt::Display for GreenLevel {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GreenContract {
pub required_level: GreenLevel,
pub requirements: Vec<GreenContractRequirement>,
pub block_known_flakes: bool,
}
impl GreenContract {
#[must_use]
pub fn new(required_level: GreenLevel) -> Self {
Self { required_level }
Self {
required_level,
requirements: Vec::new(),
block_known_flakes: false,
}
}
#[must_use]
pub fn evaluate(self, observed_level: Option<GreenLevel>) -> GreenContractOutcome {
pub fn merge_ready(required_level: GreenLevel) -> Self {
Self {
required_level,
requirements: vec![
GreenContractRequirement::TestCommandProvenance,
GreenContractRequirement::BaseBranchFreshness,
GreenContractRequirement::RecoveryAttemptContext,
],
block_known_flakes: true,
}
}
#[must_use]
pub fn evaluate(&self, observed_level: Option<GreenLevel>) -> GreenContractOutcome {
match observed_level {
Some(level) if level >= self.required_level => GreenContractOutcome::Satisfied {
required_level: self.required_level,
@@ -53,11 +72,170 @@ impl GreenContract {
}
#[must_use]
pub fn is_satisfied_by(self, observed_level: GreenLevel) -> bool {
pub fn evaluate_evidence(&self, evidence: &GreenEvidence) -> GreenEvidenceOutcome {
let mut missing = Vec::new();
let mut blocking_flakes = Vec::new();
if evidence.observed_level < self.required_level {
missing.push(GreenContractRequirement::RequiredLevel);
}
for requirement in &self.requirements {
match requirement {
GreenContractRequirement::TestCommandProvenance
if !evidence.has_passing_test_command() =>
{
missing.push(*requirement);
}
GreenContractRequirement::BaseBranchFreshness if !evidence.base_branch_fresh => {
missing.push(*requirement);
}
GreenContractRequirement::RecoveryAttemptContext
if !evidence.recovery_attempt_context_recorded =>
{
missing.push(*requirement);
}
_ => {}
}
}
if self.block_known_flakes {
blocking_flakes = evidence
.known_flakes
.iter()
.filter(|flake| flake.blocks_green)
.cloned()
.collect();
}
if missing.is_empty() && blocking_flakes.is_empty() {
GreenEvidenceOutcome::Satisfied {
required_level: self.required_level,
observed_level: evidence.observed_level,
}
} else {
GreenEvidenceOutcome::Unsatisfied {
required_level: self.required_level,
observed_level: evidence.observed_level,
missing,
blocking_flakes,
}
}
}
#[must_use]
pub fn is_satisfied_by(&self, observed_level: GreenLevel) -> bool {
observed_level >= self.required_level
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GreenEvidence {
pub observed_level: GreenLevel,
pub test_commands: Vec<TestCommandProvenance>,
pub base_branch_fresh: bool,
pub known_flakes: Vec<KnownFlake>,
pub recovery_attempt_context_recorded: bool,
}
impl GreenEvidence {
#[must_use]
pub fn new(observed_level: GreenLevel) -> Self {
Self {
observed_level,
test_commands: Vec::new(),
base_branch_fresh: false,
known_flakes: Vec::new(),
recovery_attempt_context_recorded: false,
}
}
#[must_use]
pub fn with_test_command(mut self, command: impl Into<String>, exit_code: i32) -> Self {
self.test_commands.push(TestCommandProvenance {
command: command.into(),
exit_code,
});
self
}
#[must_use]
pub fn with_base_branch_fresh(mut self, is_fresh: bool) -> Self {
self.base_branch_fresh = is_fresh;
self
}
#[must_use]
pub fn with_known_flake(mut self, test_name: impl Into<String>, blocks_green: bool) -> Self {
self.known_flakes.push(KnownFlake {
test_name: test_name.into(),
blocks_green,
});
self
}
#[must_use]
pub fn with_recovery_attempt_context(mut self, recorded: bool) -> Self {
self.recovery_attempt_context_recorded = recorded;
self
}
#[must_use]
pub fn has_passing_test_command(&self) -> bool {
self.test_commands.iter().any(TestCommandProvenance::passed)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TestCommandProvenance {
pub command: String,
pub exit_code: i32,
}
impl TestCommandProvenance {
#[must_use]
pub fn passed(&self) -> bool {
self.exit_code == 0 && !self.command.trim().is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KnownFlake {
pub test_name: String,
pub blocks_green: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GreenContractRequirement {
RequiredLevel,
TestCommandProvenance,
BaseBranchFreshness,
RecoveryAttemptContext,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum GreenEvidenceOutcome {
Satisfied {
required_level: GreenLevel,
observed_level: GreenLevel,
},
Unsatisfied {
required_level: GreenLevel,
observed_level: GreenLevel,
missing: Vec<GreenContractRequirement>,
blocking_flakes: Vec<KnownFlake>,
},
}
impl GreenEvidenceOutcome {
#[must_use]
pub fn is_satisfied(&self) -> bool {
matches!(self, Self::Satisfied { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum GreenContractOutcome {
@@ -149,4 +327,83 @@ mod tests {
}
);
}
#[test]
fn merge_ready_contract_requires_provenance_beyond_test_level() {
// given
let contract = GreenContract::merge_ready(GreenLevel::Workspace);
let evidence = GreenEvidence::new(GreenLevel::Workspace)
.with_test_command("cargo test --manifest-path rust/Cargo.toml", 0);
// when
let outcome = contract.evaluate_evidence(&evidence);
// then
assert_eq!(
outcome,
GreenEvidenceOutcome::Unsatisfied {
required_level: GreenLevel::Workspace,
observed_level: GreenLevel::Workspace,
missing: vec![
GreenContractRequirement::BaseBranchFreshness,
GreenContractRequirement::RecoveryAttemptContext,
],
blocking_flakes: vec![],
}
);
assert!(!outcome.is_satisfied());
}
#[test]
fn merge_ready_contract_accepts_complete_test_provenance_context() {
// given
let contract = GreenContract::merge_ready(GreenLevel::Workspace);
let evidence = GreenEvidence::new(GreenLevel::MergeReady)
.with_test_command("cargo test --manifest-path rust/Cargo.toml", 0)
.with_base_branch_fresh(true)
.with_recovery_attempt_context(true);
// when
let outcome = contract.evaluate_evidence(&evidence);
// then
assert_eq!(
outcome,
GreenEvidenceOutcome::Satisfied {
required_level: GreenLevel::Workspace,
observed_level: GreenLevel::MergeReady,
}
);
}
#[test]
fn known_blocking_flake_prevents_green_contract_satisfaction() {
// given
let contract = GreenContract::merge_ready(GreenLevel::Workspace);
let evidence = GreenEvidence::new(GreenLevel::MergeReady)
.with_test_command("cargo test --manifest-path rust/Cargo.toml", 0)
.with_base_branch_fresh(true)
.with_recovery_attempt_context(true)
.with_known_flake(
"session_lifecycle_prefers_running_process_over_idle_shell",
true,
);
// when
let outcome = contract.evaluate_evidence(&evidence);
// then
assert_eq!(
outcome,
GreenEvidenceOutcome::Unsatisfied {
required_level: GreenLevel::Workspace,
observed_level: GreenLevel::MergeReady,
missing: vec![],
blocking_flakes: vec![KnownFlake {
test_name: "session_lifecycle_prefers_running_process_over_idle_shell"
.to_string(),
blocks_green: true,
}],
}
);
}
}

View File

@@ -449,18 +449,21 @@ pub fn compute_event_fingerprint(
status: &LaneEventStatus,
data: Option<&serde_json::Value>,
) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use sha2::{Digest, Sha256};
let mut hasher = DefaultHasher::new();
format!("{event:?}").hash(&mut hasher);
format!("{status:?}").hash(&mut hasher);
if let Some(d) = data {
serde_json::to_string(d)
.unwrap_or_default()
.hash(&mut hasher);
let payload = serde_json::json!({
"event": event,
"status": status,
"data": data,
});
let canonical = serde_json::to_vec(&payload).unwrap_or_default();
let digest = Sha256::digest(canonical);
let mut fingerprint = String::with_capacity(16);
for byte in &digest[..8] {
use std::fmt::Write as _;
write!(&mut fingerprint, "{byte:02x}").expect("writing to String should not fail");
}
format!("{:016x}", hasher.finish())
fingerprint
}
/// Classification of event terminality for reconciliation.
@@ -1045,6 +1048,7 @@ impl LaneEvent {
emitted_at,
)
.with_optional_detail(detail)
.with_terminal_fingerprint()
}
#[must_use]
@@ -1098,7 +1102,7 @@ impl LaneEvent {
event =
event.with_data(serde_json::to_value(subphase).expect("subphase should serialize"));
}
event
event.with_terminal_fingerprint()
}
/// Ship prepared — §4.44.5
@@ -1170,6 +1174,21 @@ impl LaneEvent {
#[must_use]
pub fn with_data(mut self, data: Value) -> Self {
self.data = Some(data);
if is_terminal_event(self.event) {
self = self.with_terminal_fingerprint();
}
self
}
#[must_use]
fn with_terminal_fingerprint(mut self) -> Self {
if is_terminal_event(self.event) {
self.metadata.event_fingerprint = Some(compute_event_fingerprint(
&self.event,
&self.status,
self.data.as_ref(),
));
}
self
}
}
@@ -1375,6 +1394,39 @@ mod tests {
assert_eq!(round_trip.event, LaneEventName::ShipPushedMain);
}
#[test]
fn convenience_terminal_events_attach_and_refresh_fingerprints() {
let finished = LaneEvent::finished("2026-04-04T00:00:00Z", Some("done".to_string()));
let initial_fingerprint = finished
.metadata
.event_fingerprint
.clone()
.expect("finished events should carry terminal fingerprint");
let with_payload = finished.with_data(json!({"result": "ok", "attempt": 1}));
assert!(with_payload.metadata.event_fingerprint.is_some());
assert_ne!(
Some(initial_fingerprint),
with_payload.metadata.event_fingerprint,
"payload changes must refresh the actionable terminal fingerprint"
);
}
#[test]
fn tool_style_finished_events_dedupe_after_payload_is_added() {
let first = LaneEvent::finished("2026-04-04T00:00:00Z", Some("done".to_string()))
.with_data(json!({"result": "ok"}));
let duplicate = LaneEvent::finished("2026-04-04T00:00:01Z", Some("done again".to_string()))
.with_data(json!({"result": "ok"}));
assert_eq!(
first.metadata.event_fingerprint,
duplicate.metadata.event_fingerprint
);
let deduped = dedupe_terminal_events(&[first, duplicate]);
assert_eq!(deduped.len(), 1);
}
#[test]
fn commit_events_can_carry_worktree_and_supersession_metadata() {
let event = LaneEvent::commit_created(

View File

@@ -4,6 +4,7 @@
//! MCP plumbing, tool-facing file operations, and the core conversation loop
//! that drives interactive and one-shot turns.
mod approval_tokens;
mod bash;
pub mod bash_validation;
mod bootstrap;
@@ -13,6 +14,7 @@ mod config;
pub mod config_validate;
mod conversation;
mod file_ops;
pub mod g004_conformance;
mod git_context;
pub mod green_contract;
mod hooks;
@@ -33,6 +35,7 @@ mod policy_engine;
mod prompt;
pub mod recovery_recipes;
mod remote;
mod report_schema;
pub mod sandbox;
mod session;
pub mod session_control;
@@ -49,6 +52,10 @@ mod trust_resolver;
mod usage;
pub mod worker_boot;
pub use approval_tokens::{
ApprovalDelegationHop, ApprovalScope, ApprovalTokenAudit, ApprovalTokenError,
ApprovalTokenGrant, ApprovalTokenLedger, ApprovalTokenStatus,
};
pub use bash::{execute_bash, BashCommandInput, BashCommandOutput};
pub use bootstrap::{BootstrapPhase, BootstrapPlan};
pub use branch_lock::{detect_branch_lock_collisions, BranchLockCollision, BranchLockIntent};
@@ -74,9 +81,10 @@ pub use conversation::{
ToolExecutor, TurnSummary,
};
pub use file_ops::{
edit_file, glob_search, grep_search, read_file, write_file, EditFileOutput, GlobSearchOutput,
GrepSearchInput, GrepSearchOutput, ReadFileOutput, StructuredPatchHunk, TextFilePayload,
WriteFileOutput,
edit_file, edit_file_in_workspace, glob_search, glob_search_in_workspace, grep_search,
grep_search_in_workspace, read_file, read_file_in_workspace, write_file,
write_file_in_workspace, EditFileOutput, GlobSearchOutput, GrepSearchInput, GrepSearchOutput,
ReadFileOutput, StructuredPatchHunk, TextFilePayload, WriteFileOutput,
};
pub use git_context::{GitCommitEntry, GitContext};
pub use hooks::{
@@ -127,22 +135,31 @@ pub use plugin_lifecycle::{
PluginState, ResourceInfo, ServerHealth, ServerStatus, ToolInfo,
};
pub use policy_engine::{
evaluate, DiffScope, GreenLevel, LaneBlocker, LaneContext, PolicyAction, PolicyCondition,
PolicyEngine, PolicyRule, ReconcileReason, ReviewStatus,
evaluate, evaluate_with_events, ApprovalToken, DiffScope, GreenLevel, LaneBlocker, LaneContext,
PolicyAction, PolicyCondition, PolicyDecisionEvent, PolicyDecisionKind, PolicyEngine,
PolicyEvaluation, PolicyRule, ReconcileReason, ReviewStatus,
};
pub use prompt::{
load_system_prompt, prepend_bullets, ContextFile, ModelFamilyIdentity, ProjectContext,
PromptBuildError, SystemPromptBuilder, FRONTIER_MODEL_NAME, SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
};
pub use recovery_recipes::{
attempt_recovery, recipe_for, EscalationPolicy, FailureScenario, RecoveryContext,
RecoveryEvent, RecoveryRecipe, RecoveryResult, RecoveryStep,
attempt_recovery, recipe_for, EscalationPolicy, FailureScenario, RecoveryAttemptState,
RecoveryAttemptType, RecoveryCommandResult, RecoveryContext, RecoveryEvent,
RecoveryLedgerEntry, RecoveryRecipe, RecoveryResult, RecoveryStatusReport, RecoveryStep,
};
pub use remote::{
inherited_upstream_proxy_env, no_proxy_list, read_token, upstream_proxy_ws_url,
RemoteSessionContext, UpstreamProxyBootstrap, UpstreamProxyState, DEFAULT_REMOTE_BASE_URL,
DEFAULT_SESSION_TOKEN_PATH, DEFAULT_SYSTEM_CA_BUNDLE, NO_PROXY_HOSTS, UPSTREAM_PROXY_ENV_KEYS,
};
pub use report_schema::{
canonicalize_report, project_report, report_content_hash, report_schema_v1_registry,
CanonicalReportV1, ClaimKind, ConsumerCapabilities, FieldDelta, FieldDeltaState,
NegativeEvidence, NegativeFindingStatus, ProjectionProvenance, RedactionProvenance,
ReportClaim, ReportConfidence, ReportIdentity, ReportProjectionV1, ReportSchemaField,
ReportSchemaRegistry, SensitivityClass, DEFAULT_PROJECTION_POLICY_V1, REPORT_SCHEMA_V1,
};
pub use sandbox::{
build_linux_sandbox_command, detect_container_environment, detect_container_environment_from,
resolve_sandbox_status, resolve_sandbox_status_for_request, ContainerEnvironment,
@@ -151,7 +168,7 @@ pub use sandbox::{
};
pub use session::{
ContentBlock, ConversationMessage, MessageRole, Session, SessionCompaction, SessionError,
SessionFork, SessionPromptEntry,
SessionFork, SessionHeartbeat, SessionLiveness, SessionPromptEntry,
};
pub use sse::{IncrementalSseParser, SseEvent};
pub use stale_base::{
@@ -162,7 +179,10 @@ pub use stale_branch::{
apply_policy, check_freshness, BranchFreshness, StaleBranchAction, StaleBranchEvent,
StaleBranchPolicy,
};
pub use task_packet::{validate_packet, TaskPacket, TaskPacketValidationError, ValidatedPacket};
pub use task_packet::{
validate_packet, TaskPacket, TaskPacketValidationError, TaskResource, ValidatedPacket,
};
pub use task_registry::{LaneBoard, LaneBoardEntry, LaneFreshness, LaneHeartbeat};
#[cfg(test)]
pub use trust_resolver::{TrustConfig, TrustDecision, TrustEvent, TrustPolicy, TrustResolver};
pub use usage::{

View File

@@ -117,7 +117,7 @@ pub fn scoped_mcp_config_hash(config: &ScopedMcpServerConfig) -> String {
format!("claudeai-proxy|{}|{}", proxy.url, proxy.id)
}
};
stable_hex_hash(&rendered)
stable_hex_hash(&format!("required:{}|{rendered}", config.required))
}
fn render_command_signature(command: &[String]) -> String {
@@ -275,10 +275,12 @@ mod tests {
oauth: None,
});
let user = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::User,
config: base_config.clone(),
};
let local = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: base_config,
};
@@ -288,6 +290,7 @@ mod tests {
);
let changed = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Http(McpRemoteServerConfig {
url: "https://vendor.example/v2/mcp".to_string(),

View File

@@ -143,6 +143,7 @@ mod tests {
#[test]
fn bootstraps_stdio_servers_into_transport_targets() {
let config = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::User,
config: McpServerConfig::Stdio(McpStdioServerConfig {
command: "uvx".to_string(),
@@ -176,6 +177,7 @@ mod tests {
#[test]
fn bootstraps_remote_servers_with_oauth_auth() {
let config = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Project,
config: McpServerConfig::Http(McpRemoteServerConfig {
url: "https://vendor.example/mcp".to_string(),
@@ -213,6 +215,7 @@ mod tests {
#[test]
fn bootstraps_websocket_and_sdk_transports_without_oauth() {
let ws = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Ws(McpWebSocketServerConfig {
url: "wss://vendor.example/mcp".to_string(),
@@ -221,6 +224,7 @@ mod tests {
}),
};
let sdk = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Sdk(McpSdkServerConfig {
name: "sdk-server".to_string(),

View File

@@ -230,6 +230,7 @@ pub struct ManagedMcpTool {
pub struct UnsupportedMcpServer {
pub server_name: String,
pub transport: McpTransport,
pub required: bool,
pub reason: String,
}
@@ -237,6 +238,7 @@ pub struct UnsupportedMcpServer {
pub struct McpDiscoveryFailure {
pub server_name: String,
pub phase: McpLifecyclePhase,
pub required: bool,
pub error: String,
pub recoverable: bool,
pub context: BTreeMap<String, String>,
@@ -366,7 +368,7 @@ impl McpServerManagerError {
) && matches!(self, Self::Transport { .. } | Self::Timeout { .. })
}
fn discovery_failure(&self, server_name: &str) -> McpDiscoveryFailure {
fn discovery_failure(&self, server_name: &str, required: bool) -> McpDiscoveryFailure {
let phase = self.lifecycle_phase();
let recoverable = self.recoverable();
let context = self.error_context();
@@ -374,6 +376,7 @@ impl McpServerManagerError {
McpDiscoveryFailure {
server_name: server_name.to_string(),
phase,
required,
error: self.to_string(),
recoverable,
context,
@@ -447,7 +450,10 @@ fn unsupported_server_failed_server(server: &UnsupportedMcpServer) -> McpFailedS
McpLifecyclePhase::ServerRegistration,
Some(server.server_name.clone()),
server.reason.clone(),
BTreeMap::from([("transport".to_string(), format!("{:?}", server.transport))]),
BTreeMap::from([
("transport".to_string(), format!("{:?}", server.transport)),
("required".to_string(), server.required.to_string()),
]),
false,
),
}
@@ -464,14 +470,16 @@ struct ManagedMcpServer {
bootstrap: McpClientBootstrap,
process: Option<McpStdioProcess>,
initialized: bool,
required: bool,
}
impl ManagedMcpServer {
fn new(bootstrap: McpClientBootstrap) -> Self {
fn new(bootstrap: McpClientBootstrap, required: bool) -> Self {
Self {
bootstrap,
process: None,
initialized: false,
required,
}
}
}
@@ -498,11 +506,15 @@ impl McpServerManager {
for (server_name, server_config) in servers {
if server_config.transport() == McpTransport::Stdio {
let bootstrap = McpClientBootstrap::from_scoped_config(server_name, server_config);
managed_servers.insert(server_name.clone(), ManagedMcpServer::new(bootstrap));
managed_servers.insert(
server_name.clone(),
ManagedMcpServer::new(bootstrap, server_config.required),
);
} else {
unsupported_servers.push(UnsupportedMcpServer {
server_name: server_name.clone(),
transport: server_config.transport(),
required: server_config.required,
reason: format!(
"transport {:?} is not supported by McpServerManager",
server_config.transport()
@@ -576,7 +588,11 @@ impl McpServerManager {
}
Err(error) => {
self.clear_routes_for_server(&server_name);
failed_servers.push(error.discovery_failure(&server_name));
let required = self
.servers
.get(&server_name)
.is_some_and(|server| server.required);
failed_servers.push(error.discovery_failure(&server_name, required));
}
}
}
@@ -590,7 +606,11 @@ impl McpServerManager {
failure.phase,
Some(failure.server_name.clone()),
failure.error.clone(),
failure.context.clone(),
{
let mut context = failure.context.clone();
context.insert("required".to_string(), failure.required.to_string());
context
},
failure.recoverable,
),
})
@@ -1765,6 +1785,7 @@ mod tests {
fn sample_bootstrap(script_path: &Path) -> McpClientBootstrap {
let config = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Stdio(McpStdioServerConfig {
command: "/bin/sh".to_string(),
@@ -1832,6 +1853,7 @@ mod tests {
]);
env.extend(extra_env);
ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Stdio(McpStdioServerConfig {
command: "python3".to_string(),
@@ -1874,6 +1896,7 @@ mod tests {
#[test]
fn rejects_non_stdio_bootstrap() {
let config = ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Sdk(crate::config::McpSdkServerConfig {
name: "sdk-server".to_string(),
@@ -2310,6 +2333,7 @@ mod tests {
let servers = BTreeMap::from([(
"slow".to_string(),
ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Stdio(McpStdioServerConfig {
command: "python3".to_string(),
@@ -2363,6 +2387,7 @@ mod tests {
let servers = BTreeMap::from([(
"broken".to_string(),
ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Stdio(McpStdioServerConfig {
command: "python3".to_string(),
@@ -2701,6 +2726,7 @@ mod tests {
(
"broken".to_string(),
ScopedMcpServerConfig {
required: true,
scope: ConfigSource::Local,
config: McpServerConfig::Stdio(McpStdioServerConfig {
command: broken_script_path.display().to_string(),
@@ -2722,6 +2748,7 @@ mod tests {
);
assert_eq!(report.failed_servers.len(), 1);
assert_eq!(report.failed_servers[0].server_name, "broken");
assert!(report.failed_servers[0].required);
assert_eq!(
report.failed_servers[0].phase,
McpLifecyclePhase::InitializeHandshake
@@ -2742,6 +2769,14 @@ mod tests {
assert_eq!(degraded.working_servers, vec!["alpha".to_string()]);
assert_eq!(degraded.failed_servers.len(), 1);
assert_eq!(degraded.failed_servers[0].server_name, "broken");
assert_eq!(
degraded.failed_servers[0]
.error
.context
.get("required")
.map(String::as_str),
Some("true")
);
assert_eq!(
degraded.failed_servers[0].phase,
McpLifecyclePhase::InitializeHandshake
@@ -2777,6 +2812,7 @@ mod tests {
(
"http".to_string(),
ScopedMcpServerConfig {
required: true,
scope: ConfigSource::Local,
config: McpServerConfig::Http(McpRemoteServerConfig {
url: "https://example.test/mcp".to_string(),
@@ -2789,6 +2825,7 @@ mod tests {
(
"sdk".to_string(),
ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Sdk(McpSdkServerConfig {
name: "sdk-server".to_string(),
@@ -2798,6 +2835,7 @@ mod tests {
(
"ws".to_string(),
ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Ws(McpWebSocketServerConfig {
url: "wss://example.test/mcp".to_string(),
@@ -2813,11 +2851,14 @@ mod tests {
assert_eq!(unsupported.len(), 3);
assert_eq!(unsupported[0].server_name, "http");
assert!(unsupported[0].required);
assert_eq!(unsupported[1].server_name, "sdk");
assert_eq!(unsupported[2].server_name, "ws");
let failed = unsupported_server_failed_server(&unsupported[0]);
assert_eq!(failed.phase, McpLifecyclePhase::ServerRegistration);
assert_eq!(
unsupported_server_failed_server(&unsupported[0]).phase,
McpLifecyclePhase::ServerRegistration
failed.error.context.get("required").map(String::as_str),
Some("true")
);
}

View File

@@ -442,6 +442,7 @@ mod tests {
log_path: &Path,
) -> ScopedMcpServerConfig {
ScopedMcpServerConfig {
required: false,
scope: ConfigSource::Local,
config: McpServerConfig::Stdio(McpStdioServerConfig {
command: "python3".to_string(),

View File

@@ -61,6 +61,25 @@ pub enum PluginState {
}
impl PluginState {
#[must_use]
pub fn startup_event(&self) -> Option<PluginLifecycleEvent> {
match self {
Self::Healthy => Some(PluginLifecycleEvent::StartupHealthy),
Self::Degraded { .. } => Some(PluginLifecycleEvent::StartupDegraded),
Self::Failed { .. } => Some(PluginLifecycleEvent::StartupFailed),
Self::Unconfigured
| Self::Validated
| Self::Starting
| Self::ShuttingDown
| Self::Stopped => None,
}
}
#[must_use]
pub fn is_startup_terminal(&self) -> bool {
self.startup_event().is_some()
}
#[must_use]
pub fn from_servers(servers: &[ServerHealth]) -> Self {
if servers.is_empty() {
@@ -122,6 +141,11 @@ pub struct PluginHealthcheck {
}
impl PluginHealthcheck {
#[must_use]
pub fn startup_event(&self) -> Option<PluginLifecycleEvent> {
self.state.startup_event()
}
#[must_use]
pub fn new(plugin_name: impl Into<String>, servers: Vec<ServerHealth>) -> Self {
let state = PluginState::from_servers(&servers);
@@ -343,6 +367,41 @@ mod tests {
}
}
#[test]
fn startup_event_maps_terminal_health_states() {
// given
let healthy =
PluginHealthcheck::new("healthy-plugin", vec![healthy_server("alpha", &["search"])]);
let degraded = PluginHealthcheck::new(
"degraded-plugin",
vec![
healthy_server("alpha", &["search"]),
failed_server("beta", &["write"], "connection refused"),
],
);
let failed = PluginHealthcheck::new(
"failed-plugin",
vec![failed_server("beta", &["write"], "connection refused")],
);
// then
assert_eq!(
healthy.startup_event(),
Some(PluginLifecycleEvent::StartupHealthy)
);
assert_eq!(
degraded.startup_event(),
Some(PluginLifecycleEvent::StartupDegraded)
);
assert_eq!(
failed.startup_event(),
Some(PluginLifecycleEvent::StartupFailed)
);
assert!(healthy.state.is_startup_terminal());
assert_eq!(PluginState::Starting.startup_event(), None);
assert!(!PluginState::Starting.is_startup_terminal());
}
#[test]
fn full_lifecycle_happy_path() {
// given

View File

@@ -1,8 +1,10 @@
use std::time::Duration;
use serde::{Deserialize, Serialize};
pub type GreenLevel = u8;
const STALE_BRANCH_THRESHOLD: Duration = Duration::from_secs(60 * 60);
const STALE_BRANCH_THRESHOLD: Duration = Duration::from_hours(1);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyRule {
@@ -46,6 +48,11 @@ pub enum PolicyCondition {
ReviewPassed,
ScopedDiff,
TimedOut { duration: Duration },
RetryAvailable,
RebaseRequired,
StaleCleanupRequired,
ApprovalTokenPresent,
ApprovalTokenMissing,
}
impl PolicyCondition {
@@ -58,7 +65,9 @@ impl PolicyCondition {
Self::Or(conditions) => conditions
.iter()
.any(|condition| condition.matches(context)),
Self::GreenAt { level } => context.green_level >= *level,
Self::GreenAt { level } => {
context.green_contract_satisfied && context.green_level >= *level
}
Self::StaleBranch => context.branch_freshness >= STALE_BRANCH_THRESHOLD,
Self::StartupBlocked => context.blocker == LaneBlocker::Startup,
Self::LaneCompleted => context.completed,
@@ -66,6 +75,11 @@ impl PolicyCondition {
Self::ReviewPassed => context.review_status == ReviewStatus::Approved,
Self::ScopedDiff => context.diff_scope == DiffScope::Scoped,
Self::TimedOut { duration } => context.branch_freshness >= *duration,
Self::RetryAvailable => context.retry_count < context.retry_limit,
Self::RebaseRequired => context.rebase_required,
Self::StaleCleanupRequired => context.stale_cleanup_required,
Self::ApprovalTokenPresent => context.approval_token.is_some(),
Self::ApprovalTokenMissing => context.approval_token.is_none(),
}
}
}
@@ -75,11 +89,15 @@ pub enum PolicyAction {
MergeToDev,
MergeForward,
RecoverOnce,
Retry { reason: String },
Rebase { reason: String },
Escalate { reason: String },
CloseoutLane,
CleanupSession,
CleanupStale { reason: String },
Reconcile { reason: ReconcileReason },
Notify { channel: String },
RequireApprovalToken { operation: String },
Block { reason: String },
Chain(Vec<PolicyAction>),
}
@@ -130,16 +148,61 @@ pub enum DiffScope {
Scoped,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovalToken {
pub token_id: String,
pub operation: String,
pub granted_by: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyDecisionKind {
Retry,
Rebase,
Merge,
Escalate,
StaleCleanup,
ApprovalRequired,
Notify,
Block,
Closeout,
Reconcile,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyDecisionEvent {
pub lane_id: String,
pub rule_name: String,
pub priority: u32,
pub kind: PolicyDecisionKind,
pub explanation: String,
pub approval_token_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyEvaluation {
pub actions: Vec<PolicyAction>,
pub events: Vec<PolicyDecisionEvent>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LaneContext {
pub lane_id: String,
pub green_level: GreenLevel,
pub green_contract_satisfied: bool,
pub branch_freshness: Duration,
pub blocker: LaneBlocker,
pub review_status: ReviewStatus,
pub diff_scope: DiffScope,
pub completed: bool,
pub reconciled: bool,
pub retry_count: u32,
pub retry_limit: u32,
pub rebase_required: bool,
pub stale_cleanup_required: bool,
pub approval_token: Option<ApprovalToken>,
}
impl LaneContext {
@@ -156,12 +219,18 @@ impl LaneContext {
Self {
lane_id: lane_id.into(),
green_level,
green_contract_satisfied: false,
branch_freshness,
blocker,
review_status,
diff_scope,
completed,
reconciled: false,
retry_count: 0,
retry_limit: 1,
rebase_required: false,
stale_cleanup_required: false,
approval_token: None,
}
}
@@ -171,14 +240,51 @@ impl LaneContext {
Self {
lane_id: lane_id.into(),
green_level: 0,
green_contract_satisfied: false,
branch_freshness: Duration::from_secs(0),
blocker: LaneBlocker::None,
review_status: ReviewStatus::Pending,
diff_scope: DiffScope::Full,
completed: true,
reconciled: true,
retry_count: 0,
retry_limit: 1,
rebase_required: false,
stale_cleanup_required: false,
approval_token: None,
}
}
#[must_use]
pub fn with_green_contract_satisfied(mut self, satisfied: bool) -> Self {
self.green_contract_satisfied = satisfied;
self
}
#[must_use]
pub fn with_retry_state(mut self, retry_count: u32, retry_limit: u32) -> Self {
self.retry_count = retry_count;
self.retry_limit = retry_limit;
self
}
#[must_use]
pub fn with_rebase_required(mut self, required: bool) -> Self {
self.rebase_required = required;
self
}
#[must_use]
pub fn with_stale_cleanup_required(mut self, required: bool) -> Self {
self.stale_cleanup_required = required;
self
}
#[must_use]
pub fn with_approval_token(mut self, token: ApprovalToken) -> Self {
self.approval_token = Some(token);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -202,17 +308,119 @@ impl PolicyEngine {
pub fn evaluate(&self, context: &LaneContext) -> Vec<PolicyAction> {
evaluate(self, context)
}
#[must_use]
pub fn evaluate_with_events(&self, context: &LaneContext) -> PolicyEvaluation {
evaluate_with_events(self, context)
}
}
#[must_use]
pub fn evaluate(engine: &PolicyEngine, context: &LaneContext) -> Vec<PolicyAction> {
evaluate_with_events(engine, context).actions
}
#[must_use]
pub fn evaluate_with_events(engine: &PolicyEngine, context: &LaneContext) -> PolicyEvaluation {
let mut actions = Vec::new();
let mut events = Vec::new();
for rule in &engine.rules {
if rule.matches(context) {
let before = actions.len();
rule.action.flatten_into(&mut actions);
for action in &actions[before..] {
events.push(decision_event(rule, context, action));
}
}
}
actions
PolicyEvaluation { actions, events }
}
fn decision_event(
rule: &PolicyRule,
context: &LaneContext,
action: &PolicyAction,
) -> PolicyDecisionEvent {
let (kind, explanation) = match action {
PolicyAction::MergeToDev | PolicyAction::MergeForward => (
PolicyDecisionKind::Merge,
format!(
"rule '{}' allows merge action for lane {}",
rule.name, context.lane_id
),
),
PolicyAction::RecoverOnce | PolicyAction::Retry { reason: _ } => (
PolicyDecisionKind::Retry,
format!(
"rule '{}' allows retry {}/{} for lane {}",
rule.name, context.retry_count, context.retry_limit, context.lane_id
),
),
PolicyAction::Rebase { reason } => (
PolicyDecisionKind::Rebase,
format!("rule '{}' requires rebase: {reason}", rule.name),
),
PolicyAction::Escalate { reason } => (
PolicyDecisionKind::Escalate,
format!(
"rule '{}' escalates lane {}: {reason}",
rule.name, context.lane_id
),
),
PolicyAction::CleanupStale { reason } => (
PolicyDecisionKind::StaleCleanup,
format!("rule '{}' requests cleanup: {reason}", rule.name),
),
PolicyAction::CleanupSession => (
PolicyDecisionKind::StaleCleanup,
format!("rule '{}' requests session cleanup", rule.name),
),
PolicyAction::CloseoutLane => (
PolicyDecisionKind::Closeout,
format!("rule '{}' closes out lane {}", rule.name, context.lane_id),
),
PolicyAction::Reconcile { reason } => (
PolicyDecisionKind::Reconcile,
format!(
"rule '{}' reconciles lane {}: {reason:?}",
rule.name, context.lane_id
),
),
PolicyAction::Notify { channel } => (
PolicyDecisionKind::Notify,
format!("rule '{}' notifies {channel}", rule.name),
),
PolicyAction::RequireApprovalToken { operation } => (
PolicyDecisionKind::ApprovalRequired,
format!(
"rule '{}' requires approval token for {operation}",
rule.name
),
),
PolicyAction::Block { reason } => (
PolicyDecisionKind::Block,
format!(
"rule '{}' blocks lane {}: {reason}",
rule.name, context.lane_id
),
),
PolicyAction::Chain(_) => (
PolicyDecisionKind::Notify,
format!("rule '{}' expanded a chained action", rule.name),
),
};
PolicyDecisionEvent {
lane_id: context.lane_id.clone(),
rule_name: rule.name.clone(),
priority: rule.priority,
kind,
explanation,
approval_token_id: context
.approval_token
.as_ref()
.map(|token| token.token_id.clone()),
}
}
#[cfg(test)]
@@ -220,8 +428,9 @@ mod tests {
use std::time::Duration;
use super::{
evaluate, DiffScope, LaneBlocker, LaneContext, PolicyAction, PolicyCondition, PolicyEngine,
PolicyRule, ReconcileReason, ReviewStatus, STALE_BRANCH_THRESHOLD,
evaluate, ApprovalToken, DiffScope, LaneBlocker, LaneContext, PolicyAction,
PolicyCondition, PolicyDecisionKind, PolicyEngine, PolicyRule, ReconcileReason,
ReviewStatus, STALE_BRANCH_THRESHOLD,
};
fn default_context() -> LaneContext {
@@ -257,7 +466,8 @@ mod tests {
ReviewStatus::Approved,
DiffScope::Scoped,
false,
);
)
.with_green_contract_satisfied(true);
// when
let actions = engine.evaluate(&context);
@@ -266,6 +476,36 @@ mod tests {
assert_eq!(actions, vec![PolicyAction::MergeToDev]);
}
#[test]
fn merge_rule_blocks_when_green_tests_lack_contract_provenance() {
// given
let engine = PolicyEngine::new(vec![PolicyRule::new(
"merge-to-dev",
PolicyCondition::And(vec![
PolicyCondition::GreenAt { level: 2 },
PolicyCondition::ScopedDiff,
PolicyCondition::ReviewPassed,
]),
PolicyAction::MergeToDev,
20,
)]);
let context = LaneContext::new(
"lane-7",
3,
Duration::from_secs(5),
LaneBlocker::None,
ReviewStatus::Approved,
DiffScope::Scoped,
false,
);
// when
let actions = engine.evaluate(&context);
// then
assert!(actions.is_empty());
}
#[test]
fn stale_branch_rule_fires_at_threshold() {
// given
@@ -468,7 +708,8 @@ mod tests {
ReviewStatus::Pending,
DiffScope::Full,
false,
);
)
.with_green_contract_satisfied(true);
// when
let actions = engine.evaluate(&context);
@@ -489,6 +730,121 @@ mod tests {
);
}
#[test]
#[allow(clippy::duration_suboptimal_units, clippy::too_many_lines)]
fn executable_decision_table_emits_retry_rebase_merge_escalate_cleanup_and_approval_events() {
let engine = PolicyEngine::new(vec![
PolicyRule::new(
"retry-available",
PolicyCondition::RetryAvailable,
PolicyAction::Retry {
reason: "transient failure".to_string(),
},
1,
),
PolicyRule::new(
"rebase-required",
PolicyCondition::RebaseRequired,
PolicyAction::Rebase {
reason: "base branch moved".to_string(),
},
2,
),
PolicyRule::new(
"stale-cleanup",
PolicyCondition::StaleCleanupRequired,
PolicyAction::CleanupStale {
reason: "lease expired".to_string(),
},
3,
),
PolicyRule::new(
"approval-required",
PolicyCondition::ApprovalTokenMissing,
PolicyAction::RequireApprovalToken {
operation: "merge".to_string(),
},
4,
),
PolicyRule::new(
"merge-approved",
PolicyCondition::And(vec![
PolicyCondition::ApprovalTokenPresent,
PolicyCondition::GreenAt { level: 2 },
PolicyCondition::ScopedDiff,
PolicyCondition::ReviewPassed,
]),
PolicyAction::MergeToDev,
5,
),
PolicyRule::new(
"retry-exhausted",
PolicyCondition::TimedOut {
duration: Duration::from_secs(60),
},
PolicyAction::Escalate {
reason: "lane timed out".to_string(),
},
6,
),
]);
let missing_token_context = LaneContext::new(
"lane-cc2",
2,
Duration::from_secs(90),
LaneBlocker::None,
ReviewStatus::Approved,
DiffScope::Scoped,
false,
)
.with_green_contract_satisfied(true)
.with_retry_state(0, 1)
.with_rebase_required(true)
.with_stale_cleanup_required(true);
let missing = engine.evaluate_with_events(&missing_token_context);
assert!(missing.actions.contains(&PolicyAction::Retry {
reason: "transient failure".to_string()
}));
assert!(missing.actions.contains(&PolicyAction::Rebase {
reason: "base branch moved".to_string()
}));
assert!(missing.actions.contains(&PolicyAction::CleanupStale {
reason: "lease expired".to_string()
}));
assert!(missing
.actions
.contains(&PolicyAction::RequireApprovalToken {
operation: "merge".to_string()
}));
assert!(missing.actions.contains(&PolicyAction::Escalate {
reason: "lane timed out".to_string()
}));
assert!(missing
.events
.iter()
.any(|event| event.kind == PolicyDecisionKind::ApprovalRequired
&& event.explanation.contains("approval token")));
let approved_context = missing_token_context.with_approval_token(ApprovalToken {
token_id: "approval-123".to_string(),
operation: "merge".to_string(),
granted_by: "leader".to_string(),
});
let approved = engine.evaluate_with_events(&approved_context);
assert!(approved.actions.contains(&PolicyAction::MergeToDev));
let merge_event = approved
.events
.iter()
.find(|event| event.kind == PolicyDecisionKind::Merge)
.expect("merge event should be emitted");
assert_eq!(
merge_event.approval_token_id.as_deref(),
Some("approval-123")
);
}
#[test]
fn reconciled_lane_emits_reconcile_and_cleanup() {
// given — a lane where branch is already merged, no PR needed, session stale

View File

@@ -121,6 +121,21 @@ pub enum RecoveryResult {
},
}
/// Type of recovery execution represented in the ledger.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryAttemptType {
Automatic,
}
/// Result for one executable recovery command/step.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoveryCommandResult {
pub command: RecoveryStep,
pub status: RecoveryAttemptState,
pub result: String,
}
/// Structured event emitted during recovery.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -135,14 +150,59 @@ pub enum RecoveryEvent {
Escalated,
}
/// Machine-readable recovery progress for one failure scenario.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoveryLedgerEntry {
pub recipe_id: String,
pub attempt_type: RecoveryAttemptType,
pub trigger: FailureScenario,
pub attempt_count: u32,
pub retry_limit: u32,
pub attempts_remaining: u32,
pub state: RecoveryAttemptState,
pub started_at: Option<String>,
pub finished_at: Option<String>,
pub command_results: Vec<RecoveryCommandResult>,
pub result: Option<RecoveryResult>,
pub last_failure_summary: Option<String>,
pub escalation_reason: Option<String>,
}
/// Current state of a recovery recipe attempt.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryAttemptState {
Queued,
Running,
Succeeded,
Failed,
Exhausted,
}
/// Machine-readable status projection for callers that need to
/// distinguish an untouched scenario from an exhausted recovery.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoveryStatusReport {
pub scenario: FailureScenario,
pub attempted: bool,
pub state: Option<RecoveryAttemptState>,
pub attempt_count: u32,
pub retry_limit: Option<u32>,
pub attempts_remaining: Option<u32>,
pub escalation_reason: Option<String>,
}
/// Minimal context for tracking recovery state and emitting events.
///
/// Holds per-scenario attempt counts, a structured event log, and an
/// optional simulation knob for controlling step outcomes during tests.
/// Holds per-scenario attempt counts, a structured event log, a recovery
/// attempt ledger, and an optional simulation knob for controlling step
/// outcomes during tests.
#[derive(Debug, Clone, Default)]
pub struct RecoveryContext {
attempts: HashMap<FailureScenario, u32>,
events: Vec<RecoveryEvent>,
ledger: HashMap<FailureScenario, RecoveryLedgerEntry>,
clock_tick: u64,
/// Optional step index at which simulated execution fails.
/// `None` means all steps succeed.
fail_at_step: Option<usize>,
@@ -172,6 +232,51 @@ impl RecoveryContext {
pub fn attempt_count(&self, scenario: &FailureScenario) -> u32 {
self.attempts.get(scenario).copied().unwrap_or(0)
}
/// Returns the machine-readable recovery ledger entry for a scenario.
#[must_use]
pub fn ledger_entry(&self, scenario: &FailureScenario) -> Option<&RecoveryLedgerEntry> {
self.ledger.get(scenario)
}
/// Returns all recovery ledger entries currently tracked by this context.
#[must_use]
pub fn ledger_entries(&self) -> Vec<&RecoveryLedgerEntry> {
let mut entries: Vec<_> = self.ledger.values().collect();
entries.sort_by(|left, right| left.recipe_id.cmp(&right.recipe_id));
entries
}
/// Returns a compact machine-readable recovery status for a scenario,
/// including `attempted = false` when no ledger entry exists yet.
#[must_use]
pub fn status_report(&self, scenario: &FailureScenario) -> RecoveryStatusReport {
self.ledger_entry(scenario).map_or(
RecoveryStatusReport {
scenario: *scenario,
attempted: false,
state: None,
attempt_count: 0,
retry_limit: None,
attempts_remaining: None,
escalation_reason: None,
},
|entry| RecoveryStatusReport {
scenario: *scenario,
attempted: entry.attempt_count > 0,
state: Some(entry.state),
attempt_count: entry.attempt_count,
retry_limit: Some(entry.retry_limit),
attempts_remaining: Some(entry.attempts_remaining),
escalation_reason: entry.escalation_reason.clone(),
},
)
}
fn next_timestamp(&mut self) -> String {
self.clock_tick += 1;
format!("recovery-ledger-tick-{}", self.clock_tick)
}
}
/// Returns the known recovery recipe for the given failure scenario.
@@ -233,18 +338,51 @@ pub fn recipe_for(scenario: &FailureScenario) -> RecoveryRecipe {
/// Looks up the recipe, enforces the one-attempt-before-escalation
/// policy, simulates step execution (controlled by the context), and
/// emits structured [`RecoveryEvent`]s for every attempt.
#[allow(clippy::too_many_lines)]
pub fn attempt_recovery(scenario: &FailureScenario, ctx: &mut RecoveryContext) -> RecoveryResult {
let recipe = recipe_for(scenario);
let attempt_count = ctx.attempts.entry(*scenario).or_insert(0);
let recipe_id = scenario.to_string();
ctx.ledger
.entry(*scenario)
.or_insert_with(|| RecoveryLedgerEntry {
recipe_id: recipe_id.clone(),
attempt_type: RecoveryAttemptType::Automatic,
trigger: *scenario,
attempt_count: 0,
retry_limit: recipe.max_attempts,
attempts_remaining: recipe.max_attempts,
state: RecoveryAttemptState::Queued,
started_at: None,
finished_at: None,
command_results: Vec::new(),
result: None,
last_failure_summary: None,
escalation_reason: None,
});
let current_attempts = ctx.attempt_count(scenario);
// Enforce one automatic recovery attempt before escalation.
if *attempt_count >= recipe.max_attempts {
if current_attempts >= recipe.max_attempts {
let result = RecoveryResult::EscalationRequired {
reason: format!(
"max recovery attempts ({}) exceeded for {}",
recipe.max_attempts, scenario
),
};
let finished_at = ctx.next_timestamp();
if let Some(entry) = ctx.ledger.get_mut(scenario) {
entry.attempt_count = current_attempts;
entry.attempts_remaining = 0;
entry.state = RecoveryAttemptState::Exhausted;
entry.finished_at = Some(finished_at);
entry.result = Some(result.clone());
let RecoveryResult::EscalationRequired { reason } = &result else {
unreachable!("exhaustion always produces escalation");
};
entry.last_failure_summary = Some(reason.clone());
entry.escalation_reason = Some(reason.clone());
}
ctx.events.push(RecoveryEvent::RecoveryAttempted {
scenario: *scenario,
recipe,
@@ -254,19 +392,44 @@ pub fn attempt_recovery(scenario: &FailureScenario, ctx: &mut RecoveryContext) -
return result;
}
*attempt_count += 1;
let updated_attempts = ctx.attempts.entry(*scenario).or_insert(0);
*updated_attempts += 1;
let updated_attempts = *updated_attempts;
let started_at = ctx.next_timestamp();
if let Some(entry) = ctx.ledger.get_mut(scenario) {
entry.attempt_count = updated_attempts;
entry.attempts_remaining = recipe.max_attempts.saturating_sub(updated_attempts);
entry.state = RecoveryAttemptState::Running;
entry.started_at = Some(started_at);
entry.finished_at = None;
entry.command_results.clear();
entry.result = None;
entry.last_failure_summary = None;
entry.escalation_reason = None;
}
// Execute steps, honoring the optional fail_at_step simulation.
let fail_index = ctx.fail_at_step;
let mut executed = Vec::new();
let mut command_results = Vec::new();
let mut failed = false;
for (i, step) in recipe.steps.iter().enumerate() {
if fail_index == Some(i) {
command_results.push(RecoveryCommandResult {
command: step.clone(),
status: RecoveryAttemptState::Failed,
result: format!("step {i} failed for {scenario}"),
});
failed = true;
break;
}
executed.push(step.clone());
command_results.push(RecoveryCommandResult {
command: step.clone(),
status: RecoveryAttemptState::Succeeded,
result: format!("step {i} succeeded for {scenario}"),
});
}
let result = if failed {
@@ -288,6 +451,29 @@ pub fn attempt_recovery(scenario: &FailureScenario, ctx: &mut RecoveryContext) -
};
// Emit the attempt as structured event data.
let finished_at = ctx.next_timestamp();
if let Some(entry) = ctx.ledger.get_mut(scenario) {
entry.finished_at = Some(finished_at);
entry.command_results = command_results;
entry.result = Some(result.clone());
match &result {
RecoveryResult::Recovered { .. } => {
entry.state = RecoveryAttemptState::Succeeded;
}
RecoveryResult::PartialRecovery { remaining, .. } => {
entry.state = RecoveryAttemptState::Failed;
entry.last_failure_summary = Some(format!(
"{} step(s) remaining after partial recovery",
remaining.len()
));
}
RecoveryResult::EscalationRequired { reason } => {
entry.state = RecoveryAttemptState::Exhausted;
entry.last_failure_summary = Some(reason.clone());
entry.escalation_reason = Some(reason.clone());
}
}
}
ctx.events.push(RecoveryEvent::RecoveryAttempted {
scenario: *scenario,
recipe,
@@ -499,6 +685,126 @@ mod tests {
assert_eq!(ctx.attempt_count(&FailureScenario::PromptMisdelivery), 0);
}
#[test]
fn recovery_context_exposes_machine_readable_ledger() {
// given
let mut ctx = RecoveryContext::new();
// when
let result = attempt_recovery(&FailureScenario::StaleBranch, &mut ctx);
// then
assert_eq!(result, RecoveryResult::Recovered { steps_taken: 2 });
let entry = ctx
.ledger_entry(&FailureScenario::StaleBranch)
.expect("stale branch ledger entry");
assert_eq!(entry.recipe_id, "stale_branch");
assert_eq!(entry.attempt_type, RecoveryAttemptType::Automatic);
assert_eq!(entry.trigger, FailureScenario::StaleBranch);
assert_eq!(entry.attempt_count, 1);
assert_eq!(entry.retry_limit, 1);
assert_eq!(entry.attempts_remaining, 0);
assert_eq!(entry.state, RecoveryAttemptState::Succeeded);
assert!(entry.started_at.is_some());
assert!(entry.finished_at.is_some());
assert_eq!(
entry.result,
Some(RecoveryResult::Recovered { steps_taken: 2 })
);
assert_eq!(entry.command_results.len(), 2);
assert_eq!(entry.command_results[0].command, RecoveryStep::RebaseBranch);
assert_eq!(
entry.command_results[0].status,
RecoveryAttemptState::Succeeded
);
assert_eq!(entry.last_failure_summary, None);
assert_eq!(entry.escalation_reason, None);
}
#[test]
fn recovery_ledger_records_exhausted_escalation_reason() {
// given
let mut ctx = RecoveryContext::new();
let scenario = FailureScenario::PromptMisdelivery;
// when
let _ = attempt_recovery(&scenario, &mut ctx);
let result = attempt_recovery(&scenario, &mut ctx);
// then
assert!(matches!(result, RecoveryResult::EscalationRequired { .. }));
let entry = ctx.ledger_entry(&scenario).expect("ledger entry");
assert_eq!(entry.state, RecoveryAttemptState::Exhausted);
assert_eq!(entry.attempt_count, 1);
assert_eq!(entry.attempts_remaining, 0);
assert!(matches!(
entry.result,
Some(RecoveryResult::EscalationRequired { .. })
));
assert!(entry
.escalation_reason
.as_deref()
.expect("escalation reason")
.contains("max recovery attempts"));
}
#[test]
fn recovery_status_report_distinguishes_not_attempted_from_exhausted() {
// given
let mut ctx = RecoveryContext::new();
let scenario = FailureScenario::PromptMisdelivery;
// then — no ledger entry is not the same as exhausted.
let not_attempted = ctx.status_report(&scenario);
assert!(!not_attempted.attempted);
assert_eq!(not_attempted.state, None);
assert_eq!(not_attempted.attempt_count, 0);
assert_eq!(not_attempted.retry_limit, None);
// when — one allowed attempt then one extra attempt.
let _ = attempt_recovery(&scenario, &mut ctx);
let _ = attempt_recovery(&scenario, &mut ctx);
// then
let exhausted = ctx.status_report(&scenario);
assert!(exhausted.attempted);
assert_eq!(exhausted.state, Some(RecoveryAttemptState::Exhausted));
assert_eq!(exhausted.attempt_count, 1);
assert_eq!(exhausted.retry_limit, Some(1));
assert_eq!(exhausted.attempts_remaining, Some(0));
assert!(exhausted
.escalation_reason
.as_deref()
.is_some_and(|reason| reason.contains("max recovery attempts")));
}
#[test]
fn recovery_ledger_records_failed_command_result() {
// given
let mut ctx = RecoveryContext::new().with_fail_at_step(1);
let scenario = FailureScenario::PartialPluginStartup;
// when
let result = attempt_recovery(&scenario, &mut ctx);
// then
assert!(matches!(result, RecoveryResult::PartialRecovery { .. }));
let entry = ctx.ledger_entry(&scenario).expect("ledger entry");
assert_eq!(entry.state, RecoveryAttemptState::Failed);
assert_eq!(entry.command_results.len(), 2);
assert_eq!(
entry.command_results[0].status,
RecoveryAttemptState::Succeeded
);
assert_eq!(
entry.command_results[1].status,
RecoveryAttemptState::Failed
);
assert!(entry.command_results[1]
.result
.contains("partial_plugin_startup"));
}
#[test]
fn stale_branch_recipe_has_rebase_then_clean_build() {
// given

View File

@@ -0,0 +1,552 @@
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
pub const REPORT_SCHEMA_V1: &str = "claw.report.v1";
pub const DEFAULT_PROJECTION_POLICY_V1: &str = "claw.report.projection.v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClaimKind {
ObservedFact,
Inference,
Hypothesis,
Recommendation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReportConfidence {
High,
Medium,
Low,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SensitivityClass {
Public,
Internal,
OperatorOnly,
Secret,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FieldDeltaState {
Changed,
Unchanged,
Cleared,
CarriedForward,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NegativeFindingStatus {
NotObservedInCheckedScope,
UnknownNotChecked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReportClaim {
pub id: String,
pub kind: ClaimKind,
pub text: String,
pub confidence: ReportConfidence,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<String>,
pub sensitivity: SensitivityClass,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NegativeEvidence {
pub id: String,
pub status: NegativeFindingStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checked_surfaces: Vec<String>,
pub query: String,
pub window: String,
pub sensitivity: SensitivityClass,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldDelta {
pub field: String,
pub state: FieldDeltaState,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_hash: Option<String>,
pub attribution: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReportIdentity {
pub report_id: String,
pub content_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CanonicalReportV1 {
pub schema_version: String,
pub identity: ReportIdentity,
pub generated_at: String,
pub producer: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub claims: Vec<ReportClaim>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub negative_evidence: Vec<NegativeEvidence>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub field_deltas: Vec<FieldDelta>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsumerCapabilities {
pub consumer: String,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub schema_versions: BTreeSet<String>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub field_families: BTreeSet<String>,
pub max_sensitivity: SensitivityClass,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RedactionProvenance {
pub field_path: String,
pub reason: String,
pub policy_id: String,
pub original_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectionProvenance {
pub policy_id: String,
pub source_schema_version: String,
pub source_report_id: String,
pub source_content_hash: String,
pub consumer: String,
pub downgraded: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omitted_field_families: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub redactions: Vec<RedactionProvenance>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReportProjectionV1 {
pub schema_version: String,
pub projection_id: String,
pub view: String,
pub provenance: ProjectionProvenance,
pub payload: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReportSchemaField {
pub id: String,
pub description: String,
pub required: bool,
pub field_family: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReportSchemaRegistry {
pub schema_version: String,
pub compatibility: String,
pub fields: Vec<ReportSchemaField>,
}
#[must_use]
pub fn report_schema_v1_registry() -> ReportSchemaRegistry {
ReportSchemaRegistry {
schema_version: REPORT_SCHEMA_V1.to_string(),
compatibility: "additive fields are compatible; missing required fields are breaking"
.to_string(),
fields: vec![
field(
"identity.report_id",
"stable canonical report identity",
true,
"identity",
),
field(
"identity.content_hash",
"hash of canonical payload excluding identity",
true,
"identity",
),
field(
"claims[].kind",
"fact/inference/hypothesis/recommendation label",
true,
"claims",
),
field(
"claims[].confidence",
"confidence bucket for the claim",
true,
"claims",
),
field(
"claims[].evidence",
"evidence ids supporting a claim",
false,
"claims",
),
field(
"negative_evidence[]",
"searched-and-not-found findings with checked scope",
false,
"negative_evidence",
),
field(
"field_deltas[]",
"field-level changed/unchanged/cleared/carried-forward attribution",
false,
"field_deltas",
),
field(
"projection.provenance.redactions[]",
"redaction policy provenance for projected fields",
false,
"projection",
),
],
}
}
#[must_use]
pub fn canonicalize_report(mut report: CanonicalReportV1) -> CanonicalReportV1 {
report.schema_version = REPORT_SCHEMA_V1.to_string();
report.claims.sort_by(|a, b| a.id.cmp(&b.id));
report.negative_evidence.sort_by(|a, b| a.id.cmp(&b.id));
report.field_deltas.sort_by(|a, b| a.field.cmp(&b.field));
let content_hash = report_content_hash(&report);
if report.identity.report_id.is_empty() {
report.identity.report_id = format!("report-{content_hash}");
}
report.identity.content_hash = content_hash;
report
}
#[must_use]
pub fn report_content_hash(report: &CanonicalReportV1) -> String {
let mut hashable = report.clone();
hashable.identity.report_id.clear();
hashable.identity.content_hash.clear();
stable_json_hash(&serde_json::to_value(hashable).expect("report should serialize"))
}
#[must_use]
pub fn project_report(
report: &CanonicalReportV1,
capabilities: &ConsumerCapabilities,
view: impl Into<String>,
) -> ReportProjectionV1 {
let view = view.into();
let supports_schema = capabilities.schema_versions.contains(REPORT_SCHEMA_V1);
let mut omitted_field_families = Vec::new();
let mut redactions = Vec::new();
let mut payload = serde_json::Map::new();
payload.insert(
"identity".to_string(),
serde_json::to_value(&report.identity).expect("identity serializes"),
);
payload.insert(
"generated_at".to_string(),
Value::String(report.generated_at.clone()),
);
payload.insert(
"producer".to_string(),
Value::String(report.producer.clone()),
);
if supports_family(capabilities, "claims") {
let claims = report
.claims
.iter()
.enumerate()
.filter_map(|(index, claim)| redact_claim(index, claim, capabilities, &mut redactions))
.collect::<Vec<_>>();
payload.insert("claims".to_string(), Value::Array(claims));
} else {
omitted_field_families.push("claims".to_string());
}
if supports_family(capabilities, "negative_evidence") {
payload.insert(
"negative_evidence".to_string(),
serde_json::to_value(&report.negative_evidence).expect("negative evidence serializes"),
);
} else {
omitted_field_families.push("negative_evidence".to_string());
}
if supports_family(capabilities, "field_deltas") {
payload.insert(
"field_deltas".to_string(),
serde_json::to_value(&report.field_deltas).expect("field deltas serialize"),
);
} else {
omitted_field_families.push("field_deltas".to_string());
}
let downgraded =
!supports_schema || !omitted_field_families.is_empty() || !redactions.is_empty();
let provenance = ProjectionProvenance {
policy_id: DEFAULT_PROJECTION_POLICY_V1.to_string(),
source_schema_version: report.schema_version.clone(),
source_report_id: report.identity.report_id.clone(),
source_content_hash: report.identity.content_hash.clone(),
consumer: capabilities.consumer.clone(),
downgraded,
omitted_field_families,
redactions,
};
let mut projection = ReportProjectionV1 {
schema_version: REPORT_SCHEMA_V1.to_string(),
projection_id: String::new(),
view,
provenance,
payload: Value::Object(payload),
};
projection.projection_id = stable_json_hash(&serde_json::json!({
"view": projection.view,
"provenance": projection.provenance,
"payload": projection.payload,
}));
projection
}
fn field(id: &str, description: &str, required: bool, field_family: &str) -> ReportSchemaField {
ReportSchemaField {
id: id.to_string(),
description: description.to_string(),
required,
field_family: field_family.to_string(),
}
}
fn supports_family(capabilities: &ConsumerCapabilities, family: &str) -> bool {
capabilities.field_families.is_empty() || capabilities.field_families.contains(family)
}
fn redact_claim(
index: usize,
claim: &ReportClaim,
capabilities: &ConsumerCapabilities,
redactions: &mut Vec<RedactionProvenance>,
) -> Option<Value> {
if claim.sensitivity <= capabilities.max_sensitivity {
return Some(serde_json::to_value(claim).expect("claim serializes"));
}
if claim.sensitivity == SensitivityClass::Secret {
redactions.push(RedactionProvenance {
field_path: format!("claims[{index}]"),
reason: "omitted: sensitivity exceeds consumer policy".to_string(),
policy_id: DEFAULT_PROJECTION_POLICY_V1.to_string(),
original_hash: stable_json_hash(
&serde_json::to_value(claim).expect("claim serializes"),
),
});
return None;
}
let mut redacted = claim.clone();
let original_hash = stable_json_hash(&serde_json::to_value(claim).expect("claim serializes"));
redacted.text = "<redacted>".to_string();
redacted.evidence.clear();
redactions.push(RedactionProvenance {
field_path: format!("claims[{index}].text"),
reason: "transformed: sensitivity exceeds consumer policy".to_string(),
policy_id: DEFAULT_PROJECTION_POLICY_V1.to_string(),
original_hash,
});
Some(serde_json::to_value(redacted).expect("redacted claim serializes"))
}
fn stable_json_hash(value: &Value) -> String {
let normalized = normalize_json(value);
let bytes = serde_json::to_vec(&normalized).expect("normalized json should serialize");
let digest = Sha256::digest(bytes);
let mut hash = String::with_capacity(16);
for byte in &digest[..8] {
use std::fmt::Write as _;
write!(&mut hash, "{byte:02x}").expect("writing to String should not fail");
}
hash
}
fn normalize_json(value: &Value) -> Value {
match value {
Value::Array(values) => Value::Array(values.iter().map(normalize_json).collect()),
Value::Object(map) => {
let sorted = map
.iter()
.map(|(key, value)| (key.clone(), normalize_json(value)))
.collect::<BTreeMap<_, _>>();
serde_json::to_value(sorted).expect("sorted map should serialize")
}
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::{
canonicalize_report, project_report, report_schema_v1_registry, CanonicalReportV1,
ClaimKind, ConsumerCapabilities, FieldDelta, FieldDeltaState, NegativeEvidence,
NegativeFindingStatus, ReportClaim, ReportConfidence, ReportIdentity, SensitivityClass,
REPORT_SCHEMA_V1,
};
fn fixture_report() -> CanonicalReportV1 {
canonicalize_report(CanonicalReportV1 {
schema_version: String::new(),
identity: ReportIdentity {
report_id: String::new(),
content_hash: String::new(),
},
generated_at: "2026-05-14T00:00:00Z".to_string(),
producer: "worker-1".to_string(),
claims: vec![
ReportClaim {
id: "claim-secret".to_string(),
kind: ClaimKind::ObservedFact,
text: "secret token appeared in logs".to_string(),
confidence: ReportConfidence::High,
evidence: vec!["log:secret".to_string()],
sensitivity: SensitivityClass::Secret,
},
ReportClaim {
id: "claim-hypothesis".to_string(),
kind: ClaimKind::Hypothesis,
text: "transport restart likely caused the retry".to_string(),
confidence: ReportConfidence::Medium,
evidence: vec!["event:transport".to_string()],
sensitivity: SensitivityClass::Internal,
},
ReportClaim {
id: "claim-fact".to_string(),
kind: ClaimKind::ObservedFact,
text: "lane finished once".to_string(),
confidence: ReportConfidence::High,
evidence: vec!["event:lane.finished".to_string()],
sensitivity: SensitivityClass::Public,
},
],
negative_evidence: vec![NegativeEvidence {
id: "neg-blocker".to_string(),
status: NegativeFindingStatus::NotObservedInCheckedScope,
checked_surfaces: vec!["lane_events".to_string(), "worker_status".to_string()],
query: "current blocker".to_string(),
window: "2026-05-14T00:00:00Z/2026-05-14T00:05:00Z".to_string(),
sensitivity: SensitivityClass::Public,
}],
field_deltas: vec![FieldDelta {
field: "blocker".to_string(),
state: FieldDeltaState::Cleared,
previous_hash: Some("prev123".to_string()),
current_hash: None,
attribution: "lane.failed reconciled to lane.finished".to_string(),
}],
})
}
fn capabilities(families: &[&str], max_sensitivity: SensitivityClass) -> ConsumerCapabilities {
ConsumerCapabilities {
consumer: "clawhip".to_string(),
schema_versions: [REPORT_SCHEMA_V1.to_string()].into_iter().collect(),
field_families: families
.iter()
.map(|family| (*family).to_string())
.collect(),
max_sensitivity,
}
}
#[test]
fn report_schema_registry_is_self_describing() {
let registry = report_schema_v1_registry();
assert_eq!(registry.schema_version, REPORT_SCHEMA_V1);
assert!(registry
.fields
.iter()
.any(|field| field.id == "claims[].kind"));
assert!(registry
.fields
.iter()
.any(|field| field.id == "negative_evidence[]"));
assert!(registry
.fields
.iter()
.any(|field| field.id == "projection.provenance.redactions[]"));
}
#[test]
fn canonical_report_labels_claims_negative_evidence_and_deltas() {
let report = fixture_report();
assert_eq!(report.schema_version, REPORT_SCHEMA_V1);
assert!(report.identity.report_id.starts_with("report-"));
assert_eq!(report.identity.content_hash.len(), 16);
assert_eq!(report.claims[0].id, "claim-fact");
assert_eq!(report.claims[1].kind, ClaimKind::Hypothesis);
assert_eq!(report.claims[1].confidence, ReportConfidence::Medium);
assert_eq!(
report.negative_evidence[0].status,
NegativeFindingStatus::NotObservedInCheckedScope
);
assert_eq!(report.field_deltas[0].state, FieldDeltaState::Cleared);
}
#[test]
fn projections_are_deterministic_and_record_redaction_provenance() {
let report = fixture_report();
let capabilities = capabilities(
&["claims", "negative_evidence", "field_deltas"],
SensitivityClass::Public,
);
let first = project_report(&report, &capabilities, "delta_brief");
let second = project_report(&report, &capabilities, "delta_brief");
assert_eq!(first, second);
assert_eq!(first.provenance.source_report_id, report.identity.report_id);
assert_eq!(
first.provenance.source_content_hash,
report.identity.content_hash
);
assert!(first.provenance.downgraded);
assert_eq!(first.provenance.redactions.len(), 2);
assert!(first
.provenance
.redactions
.iter()
.any(|redaction| redaction.field_path == "claims[1].text"));
assert!(first
.provenance
.redactions
.iter()
.any(|redaction| redaction.field_path == "claims[2]"));
}
#[test]
fn capability_negotiation_omits_unsupported_field_families() {
let report = fixture_report();
let capabilities = capabilities(&["claims"], SensitivityClass::Internal);
let projection = project_report(&report, &capabilities, "legacy_clawhip");
assert!(projection.provenance.downgraded);
assert_eq!(
projection.provenance.omitted_field_families,
vec!["negative_evidence".to_string(), "field_deltas".to_string()]
);
assert!(projection.payload.get("claims").is_some());
assert!(projection.payload.get("negative_evidence").is_none());
assert!(projection.payload.get("field_deltas").is_none());
}
}

View File

@@ -298,8 +298,7 @@ fn unshare_user_namespace_works() -> bool {
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
.is_ok_and(|status| status.success())
})
}

View File

@@ -8,10 +8,14 @@ use std::time::{SystemTime, UNIX_EPOCH};
use crate::json::{JsonError, JsonValue};
use crate::usage::TokenUsage;
use serde::{Deserialize, Serialize};
const SESSION_VERSION: u32 = 1;
const ROTATE_AFTER_BYTES: u64 = 256 * 1024;
const MAX_ROTATED_FILES: usize = 3;
const MAX_JSONL_FIELD_CHARS: usize = 16 * 1024;
const JSONL_TRUNCATION_MARKER: &str = "… [truncated for session JSONL]";
const JSONL_REDACTION_MARKER: &str = "[redacted]";
static SESSION_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
static LAST_TIMESTAMP_MS: AtomicU64 = AtomicU64::new(0);
@@ -82,6 +86,25 @@ struct SessionPersistence {
path: PathBuf,
}
/// Running-state liveness classification for a session heartbeat.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionLiveness {
Healthy,
Stalled,
TransportDead,
Unknown,
}
/// Heartbeat emitted from canonical session state, independent of terminal rendering.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionHeartbeat {
pub session_id: String,
pub observed_at_ms: u64,
pub transport_alive: bool,
pub liveness: SessionLiveness,
}
/// Persisted conversational state for the runtime and CLI session manager.
///
/// `workspace_root` binds the session to the worktree it was created in. The
@@ -250,6 +273,35 @@ impl Session {
self.push_message(ConversationMessage::user_text(text))
}
pub fn record_health_check(&mut self, timestamp_ms: u64) {
self.last_health_check_ms = Some(timestamp_ms);
self.touch();
}
#[must_use]
pub fn heartbeat_at(
&self,
now_ms: u64,
stalled_after_ms: u64,
transport_alive: bool,
) -> SessionHeartbeat {
let liveness = match (transport_alive, self.last_health_check_ms) {
(false, _) => SessionLiveness::TransportDead,
(true, Some(last)) if now_ms.saturating_sub(last) <= stalled_after_ms => {
SessionLiveness::Healthy
}
(true, Some(_)) => SessionLiveness::Stalled,
(true, None) => SessionLiveness::Unknown,
};
SessionHeartbeat {
session_id: self.session_id.clone(),
observed_at_ms: now_ms,
transport_alive,
liveness,
}
}
pub fn record_compaction(&mut self, summary: impl Into<String>, removed_message_count: usize) {
self.touch();
let count = self.compaction.as_ref().map_or(1, |value| value.count + 1);
@@ -871,7 +923,7 @@ impl SessionCompaction {
);
object.insert(
"summary".to_string(),
JsonValue::String(self.summary.clone()),
JsonValue::String(sanitize_jsonl_field(&self.summary)),
);
Ok(JsonValue::Object(object))
}
@@ -931,7 +983,10 @@ impl SessionPromptEntry {
"timestamp_ms".to_string(),
JsonValue::Number(i64::try_from(self.timestamp_ms).unwrap_or(i64::MAX)),
);
object.insert("text".to_string(), JsonValue::String(self.text.clone()));
object.insert(
"text".to_string(),
JsonValue::String(sanitize_jsonl_field(&self.text)),
);
JsonValue::Object(object)
}
@@ -949,10 +1004,165 @@ impl SessionPromptEntry {
fn message_record(message: &ConversationMessage) -> JsonValue {
let mut object = BTreeMap::new();
object.insert("type".to_string(), JsonValue::String("message".to_string()));
object.insert("message".to_string(), message.to_json());
object.insert("message".to_string(), persisted_message_json(message));
JsonValue::Object(object)
}
fn persisted_message_json(message: &ConversationMessage) -> JsonValue {
let mut object = BTreeMap::new();
object.insert(
"role".to_string(),
JsonValue::String(
match message.role {
MessageRole::System => "system",
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::Tool => "tool",
}
.to_string(),
),
);
object.insert(
"blocks".to_string(),
JsonValue::Array(message.blocks.iter().map(persisted_block_json).collect()),
);
if let Some(usage) = message.usage {
object.insert("usage".to_string(), usage_to_json(usage));
}
JsonValue::Object(object)
}
fn persisted_block_json(block: &ContentBlock) -> JsonValue {
let mut object = BTreeMap::new();
match block {
ContentBlock::Text { text } => {
object.insert("type".to_string(), JsonValue::String("text".to_string()));
object.insert(
"text".to_string(),
JsonValue::String(sanitize_jsonl_field(text)),
);
}
ContentBlock::Thinking {
thinking,
signature,
} => {
object.insert(
"type".to_string(),
JsonValue::String("thinking".to_string()),
);
object.insert(
"thinking".to_string(),
JsonValue::String(sanitize_jsonl_field(thinking)),
);
if let Some(signature) = signature {
object.insert(
"signature".to_string(),
JsonValue::String(sanitize_jsonl_field(signature)),
);
}
}
ContentBlock::ToolUse { id, name, input } => {
object.insert(
"type".to_string(),
JsonValue::String("tool_use".to_string()),
);
object.insert(
"id".to_string(),
JsonValue::String(sanitize_jsonl_field(id)),
);
object.insert("name".to_string(), JsonValue::String(name.clone()));
object.insert(
"input".to_string(),
JsonValue::String(sanitize_jsonl_field(input)),
);
}
ContentBlock::ToolResult {
tool_use_id,
tool_name,
output,
is_error,
} => {
object.insert(
"type".to_string(),
JsonValue::String("tool_result".to_string()),
);
object.insert(
"tool_use_id".to_string(),
JsonValue::String(sanitize_jsonl_field(tool_use_id)),
);
object.insert(
"tool_name".to_string(),
JsonValue::String(tool_name.clone()),
);
object.insert(
"output".to_string(),
JsonValue::String(sanitize_jsonl_field(output)),
);
object.insert("is_error".to_string(), JsonValue::Bool(*is_error));
}
}
JsonValue::Object(object)
}
fn sanitize_jsonl_field(value: &str) -> String {
truncate_jsonl_field(&redact_jsonl_secrets(value))
}
fn truncate_jsonl_field(value: &str) -> String {
let char_count = value.chars().count();
if char_count <= MAX_JSONL_FIELD_CHARS {
return value.to_string();
}
let keep = MAX_JSONL_FIELD_CHARS.saturating_sub(JSONL_TRUNCATION_MARKER.chars().count());
let mut truncated = value.chars().take(keep).collect::<String>();
truncated.push_str(JSONL_TRUNCATION_MARKER);
truncated
}
fn redact_jsonl_secrets(value: &str) -> String {
let mut redacted = value.to_string();
for marker in [
"ANTHROPIC_API_KEY=",
"ANTHROPIC_AUTH_TOKEN=",
"OPENAI_API_KEY=",
"DASHSCOPE_API_KEY=",
"XAI_API_KEY=",
"Authorization: Bearer ",
"authorization: Bearer ",
"Bearer sk-",
"sk-ant-",
] {
redacted = redact_after_marker(&redacted, marker);
}
redacted
}
fn redact_after_marker(value: &str, marker: &str) -> String {
let mut output = String::with_capacity(value.len());
let mut rest = value;
while let Some(index) = rest.find(marker) {
let (before, after_before) = rest.split_at(index);
output.push_str(before);
output.push_str(marker);
output.push_str(JSONL_REDACTION_MARKER);
let secret_start = marker.len();
let after_marker = &after_before[secret_start..];
let secret_end = after_marker
.char_indices()
.find_map(|(idx, ch)| {
(ch.is_whitespace() || matches!(ch, '\'' | '"' | ',' | '}' | ']')).then_some(idx)
})
.unwrap_or(after_marker.len());
rest = &after_marker[secret_end..];
}
output.push_str(rest);
output
}
fn usage_to_json(usage: TokenUsage) -> JsonValue {
let mut object = BTreeMap::new();
object.insert(
@@ -1315,6 +1525,54 @@ mod tests {
assert_eq!(restored.messages[0], ConversationMessage::user_text("hi"));
}
#[test]
fn jsonl_persistence_redacts_and_truncates_oversized_payload_fields() {
let path = temp_session_path("jsonl-safeguards");
let secret = "sk-live-secret-should-not-persist";
let oversized_output = format!(
"OPENAI_API_KEY={secret}\n{}",
"tool-output ".repeat(super::MAX_JSONL_FIELD_CHARS)
);
let mut session = Session::new();
session
.push_message(ConversationMessage::assistant(vec![
ContentBlock::ToolUse {
id: "tool-1".to_string(),
name: "bash".to_string(),
input: format!("Authorization: Bearer {secret}"),
},
]))
.expect("tool use should append");
session
.push_message(ConversationMessage::tool_result(
"tool-1",
"bash",
oversized_output,
false,
))
.expect("tool result should append");
session.save_to_path(&path).expect("session should save");
let persisted = fs::read_to_string(&path).expect("session jsonl should read");
let restored = Session::load_from_path(&path).expect("session should load");
fs::remove_file(&path).expect("temp file should be removable");
assert!(
!persisted.contains(secret),
"secret leaked into JSONL: {persisted}"
);
assert!(persisted.contains(super::JSONL_REDACTION_MARKER));
assert!(persisted.contains(super::JSONL_TRUNCATION_MARKER));
let ContentBlock::ToolResult { output, .. } = &restored.messages[1].blocks[0] else {
panic!("restored second message should be a tool result");
};
assert!(!output.contains(secret));
assert!(output.contains(super::JSONL_REDACTION_MARKER));
assert!(output.ends_with(super::JSONL_TRUNCATION_MARKER));
assert!(output.chars().count() <= super::MAX_JSONL_FIELD_CHARS);
}
#[test]
fn persists_compaction_metadata() {
let path = temp_session_path("compaction");
@@ -1599,4 +1857,26 @@ mod workspace_sessions_dir_tests {
fs::remove_dir_all(&tmp_a).ok();
fs::remove_dir_all(&tmp_b).ok();
}
#[test]
fn session_heartbeat_classifies_healthy_stalled_transport_dead_and_unknown() {
let mut session = Session::new();
assert_eq!(
session.heartbeat_at(1_000, 500, true).liveness,
SessionLiveness::Unknown
);
session.record_health_check(800);
assert_eq!(
session.heartbeat_at(1_000, 500, true).liveness,
SessionLiveness::Healthy
);
assert_eq!(
session.heartbeat_at(2_000, 500, true).liveness,
SessionLiveness::Stalled
);
assert_eq!(
session.heartbeat_at(1_000, 500, false).liveness,
SessionLiveness::TransportDead
);
}
}

View File

@@ -163,6 +163,17 @@ impl SessionStore {
})
}
#[must_use]
pub fn session_exists(&self, reference: &str) -> bool {
self.resolve_reference(reference).is_ok()
}
pub fn delete_session(&self, reference: &str) -> Result<SessionHandle, SessionControlError> {
let handle = self.resolve_reference(reference)?;
fs::remove_file(&handle.path)?;
Ok(handle)
}
pub fn load_session(
&self,
reference: &str,
@@ -480,6 +491,30 @@ pub fn load_managed_session(reference: &str) -> Result<LoadedManagedSession, Ses
load_managed_session_for(env::current_dir()?, reference)
}
pub fn managed_session_exists(reference: &str) -> Result<bool, SessionControlError> {
managed_session_exists_for(env::current_dir()?, reference)
}
pub fn managed_session_exists_for(
base_dir: impl AsRef<Path>,
reference: &str,
) -> Result<bool, SessionControlError> {
let store = SessionStore::from_cwd(base_dir)?;
Ok(store.session_exists(reference))
}
pub fn delete_managed_session(reference: &str) -> Result<SessionHandle, SessionControlError> {
delete_managed_session_for(env::current_dir()?, reference)
}
pub fn delete_managed_session_for(
base_dir: impl AsRef<Path>,
reference: &str,
) -> Result<SessionHandle, SessionControlError> {
let store = SessionStore::from_cwd(base_dir)?;
store.delete_session(reference)
}
pub fn load_managed_session_for(
base_dir: impl AsRef<Path>,
reference: &str,
@@ -569,10 +604,10 @@ fn path_is_within_workspace(path: &Path, workspace_root: &Path) -> bool {
#[cfg(test)]
mod tests {
use super::{
create_managed_session_handle_for, fork_managed_session_for, is_session_reference_alias,
list_managed_sessions_for, load_managed_session_for, resolve_session_reference_for,
workspace_fingerprint, ManagedSessionSummary, SessionControlError, SessionStore,
LATEST_SESSION_REFERENCE,
create_managed_session_handle_for, delete_managed_session_for, fork_managed_session_for,
is_session_reference_alias, list_managed_sessions_for, load_managed_session_for,
managed_session_exists_for, resolve_session_reference_for, workspace_fingerprint,
ManagedSessionSummary, SessionControlError, SessionStore, LATEST_SESSION_REFERENCE,
};
use crate::session::Session;
use std::fs;
@@ -996,6 +1031,32 @@ mod tests {
fs::remove_dir_all(base).expect("temp dir should clean up");
}
#[test]
fn session_exists_and_delete_are_scoped_to_workspace_store() {
// given
let base = temp_dir();
fs::create_dir_all(&base).expect("base dir should exist");
let store = SessionStore::from_cwd(&base).expect("store should build");
let session = persist_session_via_store(&store, "delete me");
// when
assert!(
managed_session_exists_for(&base, &session.session_id).expect("exists should run"),
"persisted session should exist before deletion"
);
let deleted =
delete_managed_session_for(&base, &session.session_id).expect("delete should succeed");
// then
assert_eq!(deleted.id, session.session_id);
assert!(!deleted.path.exists(), "session file should be removed");
assert!(
!managed_session_exists_for(&base, &session.session_id).expect("exists should run"),
"deleted session should not exist"
);
fs::remove_dir_all(base).expect("temp dir should clean up");
}
#[test]
fn session_store_fork_stays_in_same_namespace() {
// given

View File

@@ -38,10 +38,38 @@ pub struct TaskPacket {
#[serde(skip_serializing_if = "Option::is_none")]
pub worktree: Option<String>,
pub branch_policy: String,
/// Legacy verification commands kept for compatibility with existing task packets.
#[serde(default)]
pub acceptance_tests: Vec<String>,
/// Human-readable acceptance criteria for the task objective.
#[serde(default)]
pub acceptance_criteria: Vec<String>,
/// Files, directories, services, or other resources the task is allowed to touch.
#[serde(default)]
pub resources: Vec<TaskResource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_profile: Option<String>,
pub commit_policy: String,
/// Legacy reporting contract kept for compatibility with existing task packets.
pub reporting_contract: String,
#[serde(default)]
pub reporting_targets: Vec<String>,
/// Legacy escalation policy kept for compatibility with existing task packets.
pub escalation_policy: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recovery_policy: Option<String>,
#[serde(default)]
pub verification_plan: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaskResource {
pub kind: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -91,16 +119,25 @@ pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacket
validate_required("repo", &packet.repo, &mut errors);
validate_required("branch_policy", &packet.branch_policy, &mut errors);
validate_required("commit_policy", &packet.commit_policy, &mut errors);
validate_required(
"reporting_contract",
&packet.reporting_contract,
&mut errors,
);
validate_required("escalation_policy", &packet.escalation_policy, &mut errors);
if packet.reporting_contract.trim().is_empty() && packet.reporting_targets.is_empty() {
errors.push("reporting_contract or reporting_targets must not be empty".to_string());
}
if packet.escalation_policy.trim().is_empty()
&& packet
.recovery_policy
.as_ref()
.is_none_or(|policy| policy.trim().is_empty())
{
errors.push("escalation_policy or recovery_policy must not be empty".to_string());
}
// Validate scope-specific requirements
validate_scope_requirements(&packet, &mut errors);
if packet.acceptance_tests.is_empty() && packet.acceptance_criteria.is_empty() {
errors.push("acceptance_tests or acceptance_criteria must not be empty".to_string());
}
for (index, test) in packet.acceptance_tests.iter().enumerate() {
if test.trim().is_empty() {
errors.push(format!(
@@ -109,6 +146,43 @@ pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacket
}
}
for (index, criterion) in packet.acceptance_criteria.iter().enumerate() {
if criterion.trim().is_empty() {
errors.push(format!(
"acceptance_criteria contains an empty value at index {index}"
));
}
}
for (index, resource) in packet.resources.iter().enumerate() {
if resource.kind.trim().is_empty() || resource.value.trim().is_empty() {
errors.push(format!(
"resources contains an incomplete entry at index {index}"
));
}
}
validate_optional("model", packet.model.as_deref(), &mut errors);
validate_optional("provider", packet.provider.as_deref(), &mut errors);
validate_optional(
"permission_profile",
packet.permission_profile.as_deref(),
&mut errors,
);
validate_optional(
"recovery_policy",
packet.recovery_policy.as_deref(),
&mut errors,
);
for (index, step) in packet.verification_plan.iter().enumerate() {
if step.trim().is_empty() {
errors.push(format!(
"verification_plan contains an empty value at index {index}"
));
}
}
if errors.is_empty() {
Ok(ValidatedPacket(packet))
} else {
@@ -142,6 +216,12 @@ fn validate_required(field: &str, value: &str, errors: &mut Vec<String>) {
}
}
fn validate_optional(field: &str, value: Option<&str>, errors: &mut Vec<String>) {
if value.is_some_and(|value| value.trim().is_empty()) {
errors.push(format!("{field} must not be empty when present"));
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -158,9 +238,20 @@ mod tests {
"cargo build --workspace".to_string(),
"cargo test --workspace".to_string(),
],
acceptance_criteria: vec!["packet can launch without pane scraping".to_string()],
resources: vec![TaskResource {
kind: "file".to_string(),
value: "rust/crates/runtime/src/task_packet.rs".to_string(),
}],
model: Some("gpt-5.5".to_string()),
provider: Some("openai".to_string()),
permission_profile: Some("workspace-write".to_string()),
commit_policy: "single verified commit".to_string(),
reporting_contract: "print build result, test result, commit sha".to_string(),
reporting_targets: vec!["leader".to_string()],
escalation_policy: "stop only on destructive ambiguity".to_string(),
recovery_policy: Some("retry once then escalate".to_string()),
verification_plan: vec!["cargo test -p runtime task_packet".to_string()],
}
}
@@ -183,9 +274,20 @@ mod tests {
repo: String::new(),
branch_policy: "\t".to_string(),
acceptance_tests: vec!["ok".to_string(), " ".to_string()],
acceptance_criteria: vec![" ".to_string()],
resources: vec![TaskResource {
kind: " ".to_string(),
value: "resource".to_string(),
}],
model: Some(" ".to_string()),
provider: Some("openai".to_string()),
permission_profile: Some("workspace-write".to_string()),
commit_policy: String::new(),
reporting_contract: String::new(),
reporting_targets: Vec::new(),
escalation_policy: String::new(),
recovery_policy: None,
verification_plan: vec![" ".to_string()],
};
let error = validate_packet(packet).expect_err("packet should be rejected");
@@ -202,6 +304,51 @@ mod tests {
.contains(&"acceptance_tests contains an empty value at index 1".to_string()));
}
#[test]
fn legacy_packet_json_deserializes_with_defaulted_cc2_fields() {
let legacy = r#"{
"objective": "Legacy packet",
"scope": "workspace",
"repo": "claw-code",
"branch_policy": "origin/main only",
"acceptance_tests": ["cargo test"],
"commit_policy": "single commit",
"reporting_contract": "report sha",
"escalation_policy": "ask leader"
}"#;
let packet: TaskPacket = serde_json::from_str(legacy).expect("legacy packet should load");
assert_eq!(packet.objective, "Legacy packet");
assert!(packet.acceptance_criteria.is_empty());
assert!(packet.resources.is_empty());
assert_eq!(packet.model, None);
validate_packet(packet).expect("legacy packet remains valid through aliases");
}
#[test]
fn rich_cc2_packet_fields_roundtrip_and_validate() {
let packet = sample_packet();
let json = serde_json::to_value(&packet).expect("packet should serialize");
assert_eq!(
json["acceptance_criteria"][0],
"packet can launch without pane scraping"
);
assert_eq!(json["resources"][0]["kind"], "file");
assert_eq!(json["model"], "gpt-5.5");
assert_eq!(json["provider"], "openai");
assert_eq!(json["permission_profile"], "workspace-write");
assert_eq!(json["recovery_policy"], "retry once then escalate");
assert_eq!(
json["verification_plan"][0],
"cargo test -p runtime task_packet"
);
let roundtrip: TaskPacket = serde_json::from_value(json).expect("rich packet roundtrips");
validate_packet(roundtrip).expect("rich packet validates");
}
#[test]
fn serialization_roundtrip_preserves_packet() {
let packet = sample_packet();
@@ -210,4 +357,96 @@ mod tests {
serde_json::from_str(&serialized).expect("packet should deserialize");
assert_eq!(deserialized, packet);
}
#[test]
fn legacy_packet_json_deserializes_with_defaults() {
let legacy = r#"{
"objective": "Ship legacy task packet",
"scope": "module",
"scope_path": "runtime/task system",
"repo": "claw-code-parity",
"worktree": "/tmp/wt-legacy",
"branch_policy": "origin/main only",
"acceptance_tests": ["cargo test --workspace"],
"commit_policy": "single verified commit",
"reporting_contract": "print build result, test result, commit sha",
"escalation_policy": "manual escalation"
}"#;
let packet: TaskPacket = serde_json::from_str(legacy).expect("legacy packet should parse");
assert_eq!(packet.acceptance_criteria, Vec::<String>::new());
assert_eq!(packet.permission_profile, None);
assert_eq!(packet.model, None);
assert_eq!(packet.provider, None);
assert_eq!(packet.recovery_policy, None);
assert_eq!(packet.commit_policy, "single verified commit");
assert_eq!(
packet.reporting_contract,
"print build result, test result, commit sha"
);
assert_eq!(packet.escalation_policy, "manual escalation");
validate_packet(packet).expect("legacy packet should remain valid");
}
#[test]
fn new_schema_fields_validate_without_legacy_acceptance_tests() {
let mut packet = sample_packet();
packet.acceptance_tests.clear();
packet.reporting_contract.clear();
packet.escalation_policy.clear();
validate_packet(packet).expect("new schema fields should be sufficient");
}
#[test]
fn scoped_packets_require_scope_path() {
for scope in [TaskScope::Module, TaskScope::SingleFile, TaskScope::Custom] {
let mut packet = sample_packet();
packet.scope = scope;
packet.scope_path = Some(" ".to_string());
let error = validate_packet(packet).expect_err("scoped packet should require path");
assert!(error
.errors()
.contains(&format!("scope_path is required for scope '{scope}'")));
}
}
#[test]
fn modern_required_groups_report_missing_fallbacks() {
let mut packet = sample_packet();
packet.acceptance_criteria.clear();
packet.acceptance_tests.clear();
packet.recovery_policy = None;
packet.escalation_policy.clear();
packet.reporting_targets.clear();
packet.reporting_contract.clear();
let error = validate_packet(packet).expect_err("packet should require task policies");
for expected in [
"acceptance_tests or acceptance_criteria must not be empty",
"escalation_policy or recovery_policy must not be empty",
"reporting_contract or reporting_targets must not be empty",
] {
assert!(error.errors().contains(&expected.to_string()));
}
}
#[test]
fn permission_profile_serializes_as_optional_string() {
let mut packet = sample_packet();
packet.permission_profile = Some("danger-full-access".to_string());
let json = serde_json::to_value(&packet).expect("packet should serialize");
assert_eq!(json["permission_profile"], "danger-full-access");
let roundtrip: TaskPacket =
serde_json::from_value(json).expect("packet should deserialize");
assert_eq!(
roundtrip.permission_profile.as_deref(),
Some("danger-full-access")
);
}
}

View File

@@ -14,6 +14,7 @@ use crate::{validate_packet, TaskPacket, TaskPacketValidationError};
pub enum TaskStatus {
Created,
Running,
Blocked,
Completed,
Failed,
Stopped,
@@ -24,6 +25,7 @@ impl std::fmt::Display for TaskStatus {
match self {
Self::Created => write!(f, "created"),
Self::Running => write!(f, "running"),
Self::Blocked => write!(f, "blocked"),
Self::Completed => write!(f, "completed"),
Self::Failed => write!(f, "failed"),
Self::Stopped => write!(f, "stopped"),
@@ -43,6 +45,54 @@ pub struct Task {
pub messages: Vec<TaskMessage>,
pub output: String,
pub team_id: Option<String>,
pub heartbeat: Option<LaneHeartbeat>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LaneFreshness {
Healthy,
Stalled,
TransportDead,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LaneHeartbeat {
pub observed_at: u64,
pub transport_alive: bool,
pub status: String,
}
impl LaneHeartbeat {
#[must_use]
pub fn freshness_at(&self, now: u64, stalled_after_secs: u64) -> LaneFreshness {
if !self.transport_alive {
return LaneFreshness::TransportDead;
}
if now.saturating_sub(self.observed_at) > stalled_after_secs {
return LaneFreshness::Stalled;
}
LaneFreshness::Healthy
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LaneBoardEntry {
pub task_id: String,
pub prompt: String,
pub status: TaskStatus,
pub team_id: Option<String>,
pub heartbeat: Option<LaneHeartbeat>,
pub freshness: LaneFreshness,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LaneBoard {
pub generated_at: u64,
pub active: Vec<LaneBoardEntry>,
pub blocked: Vec<LaneBoardEntry>,
pub finished: Vec<LaneBoardEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -114,6 +164,7 @@ impl TaskRegistry {
messages: Vec::new(),
output: String::new(),
team_id: None,
heartbeat: None,
};
inner.tasks.insert(task_id, task.clone());
task
@@ -134,6 +185,67 @@ impl TaskRegistry {
.collect()
}
pub fn update_heartbeat(&self, task_id: &str, heartbeat: LaneHeartbeat) -> Result<(), String> {
let mut inner = self.inner.lock().expect("registry lock poisoned");
let task = inner
.tasks
.get_mut(task_id)
.ok_or_else(|| format!("task not found: {task_id}"))?;
task.heartbeat = Some(heartbeat);
task.updated_at = now_secs();
Ok(())
}
#[must_use]
pub fn lane_board(&self, stalled_after_secs: u64) -> LaneBoard {
let now = now_secs();
self.lane_board_at(now, stalled_after_secs)
}
#[must_use]
pub fn lane_board_at(&self, now: u64, stalled_after_secs: u64) -> LaneBoard {
let inner = self.inner.lock().expect("registry lock poisoned");
let mut board = LaneBoard {
generated_at: now,
active: Vec::new(),
blocked: Vec::new(),
finished: Vec::new(),
};
for task in inner.tasks.values() {
let freshness = task
.heartbeat
.as_ref()
.map_or(LaneFreshness::Unknown, |heartbeat| {
heartbeat.freshness_at(now, stalled_after_secs)
});
let entry = LaneBoardEntry {
task_id: task.task_id.clone(),
prompt: task.prompt.clone(),
status: task.status,
team_id: task.team_id.clone(),
heartbeat: task.heartbeat.clone(),
freshness,
};
match task.status {
TaskStatus::Running | TaskStatus::Created => board.active.push(entry),
TaskStatus::Blocked => board.blocked.push(entry),
TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Stopped => {
board.finished.push(entry);
}
}
}
board
}
#[must_use]
pub fn lane_status_json_at(&self, now: u64, stalled_after_secs: u64) -> serde_json::Value {
serde_json::to_value(self.lane_board_at(now, stalled_after_secs))
.expect("lane board should serialize")
}
pub fn stop(&self, task_id: &str) -> Result<Task, String> {
let mut inner = self.inner.lock().expect("registry lock poisoned");
let task = inner
@@ -260,9 +372,20 @@ mod tests {
repo: "claw-code-parity".to_string(),
branch_policy: "origin/main only".to_string(),
acceptance_tests: vec!["cargo test --workspace".to_string()],
acceptance_criteria: vec!["task is inspectable".to_string()],
resources: vec![crate::TaskResource {
kind: "module".to_string(),
value: "runtime/task system".to_string(),
}],
model: Some("gpt-5.5".to_string()),
provider: Some("openai".to_string()),
permission_profile: Some("workspace-write".to_string()),
commit_policy: "single commit".to_string(),
reporting_contract: "print commit sha".to_string(),
reporting_targets: vec!["leader".to_string()],
escalation_policy: "manual escalation".to_string(),
recovery_policy: Some("retry once".to_string()),
verification_plan: vec!["cargo test --workspace".to_string()],
};
let task = registry
@@ -340,6 +463,68 @@ mod tests {
assert_eq!(output, "line 1\nline 2\n");
}
#[test]
fn lane_board_groups_active_blocked_finished_and_reports_freshness() {
let registry = TaskRegistry::new();
let active = registry.create("active", None);
let blocked = registry.create("blocked", None);
let finished = registry.create("finished", None);
registry
.set_status(&active.task_id, TaskStatus::Running)
.expect("running status");
registry
.set_status(&blocked.task_id, TaskStatus::Blocked)
.expect("blocked status");
registry
.set_status(&finished.task_id, TaskStatus::Completed)
.expect("completed status");
registry
.update_heartbeat(
&active.task_id,
LaneHeartbeat {
observed_at: 100,
transport_alive: true,
status: "running".to_string(),
},
)
.expect("heartbeat");
registry
.update_heartbeat(
&blocked.task_id,
LaneHeartbeat {
observed_at: 10,
transport_alive: true,
status: "waiting".to_string(),
},
)
.expect("heartbeat");
registry
.update_heartbeat(
&finished.task_id,
LaneHeartbeat {
observed_at: 100,
transport_alive: false,
status: "done".to_string(),
},
)
.expect("heartbeat");
let board = registry.lane_board_at(110, 30);
assert_eq!(board.active.len(), 1);
assert_eq!(board.active[0].freshness, LaneFreshness::Healthy);
assert_eq!(board.blocked.len(), 1);
assert_eq!(board.blocked[0].freshness, LaneFreshness::Stalled);
assert_eq!(board.finished.len(), 1);
assert_eq!(board.finished[0].freshness, LaneFreshness::TransportDead);
let json = registry.lane_status_json_at(110, 30);
assert_eq!(json["active"][0]["status"], "running");
assert_eq!(json["blocked"][0]["freshness"], "stalled");
assert_eq!(json["finished"][0]["freshness"], "transport_dead");
}
#[test]
fn assigns_team_and_removes_task() {
let registry = TaskRegistry::new();
@@ -375,6 +560,7 @@ mod tests {
let cases = [
(TaskStatus::Created, "created"),
(TaskStatus::Running, "running"),
(TaskStatus::Blocked, "blocked"),
(TaskStatus::Completed, "completed"),
(TaskStatus::Failed, "failed"),
(TaskStatus::Stopped, "stopped"),
@@ -392,6 +578,7 @@ mod tests {
vec![
("created".to_string(), "created"),
("running".to_string(), "running"),
("blocked".to_string(), "blocked"),
("completed".to_string(), "completed"),
("failed".to_string(), "failed"),
("stopped".to_string(), "stopped"),
@@ -478,6 +665,7 @@ mod tests {
assert!(task.messages.is_empty());
assert!(task.output.is_empty());
assert_eq!(task.team_id, None);
assert_eq!(task.heartbeat, None);
}
#[test]

View File

@@ -122,13 +122,37 @@ pub enum StartupFailureClassification {
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StartupHealthSummary {
/// Whether this subsystem appeared healthy at timeout.
pub healthy: bool,
/// Stable placeholder/source string until deeper transport and MCP probes are wired in.
pub summary: String,
}
impl StartupHealthSummary {
fn observed(name: &str, healthy: bool) -> Self {
let status = if healthy { "healthy" } else { "unhealthy" };
Self {
healthy,
summary: format!("{name}_{status}_placeholder"),
}
}
}
/// Evidence bundle collected when worker startup times out without clear evidence.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StartupEvidenceBundle {
/// Last known worker lifecycle state before timeout
pub last_lifecycle_state: WorkerStatus,
/// Timestamp of the last lifecycle state transition, unix epoch seconds
pub last_lifecycle_at: u64,
/// The pane/command that was being executed
pub pane_command: String,
/// Timestamp when the pane/command snapshot was observed, unix epoch seconds
pub pane_observed_at: u64,
/// Timestamp when the worker command was started, unix epoch seconds
pub command_started_at: u64,
/// Timestamp when prompt was sent (if any), unix epoch seconds
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_sent_at: Option<u64>,
@@ -146,8 +170,12 @@ pub struct StartupEvidenceBundle {
pub tool_permission_allow_scope: Option<ToolPermissionAllowScope>,
/// Transport health summary (true = healthy/responsive)
pub transport_healthy: bool,
/// Typed transport health placeholder for future concrete probes
pub transport_health: StartupHealthSummary,
/// MCP health summary (true = all servers healthy)
pub mcp_healthy: bool,
/// Typed MCP health placeholder for future concrete probes
pub mcp_health: StartupHealthSummary,
/// Seconds since worker creation
pub elapsed_seconds: u64,
}
@@ -225,6 +253,7 @@ pub struct Worker {
pub auto_recover_prompt_misdelivery: bool,
pub prompt_delivery_attempts: u32,
pub prompt_in_flight: bool,
pub prompt_sent_at: Option<u64>,
pub last_prompt: Option<String>,
pub expected_receipt: Option<WorkerTaskReceipt>,
pub replay_prompt: Option<String>,
@@ -274,6 +303,7 @@ impl WorkerRegistry {
auto_recover_prompt_misdelivery,
prompt_delivery_attempts: 0,
prompt_in_flight: false,
prompt_sent_at: None,
last_prompt: None,
expected_receipt: None,
replay_prompt: None,
@@ -528,6 +558,7 @@ impl WorkerRegistry {
worker.prompt_delivery_attempts += 1;
worker.prompt_in_flight = true;
worker.prompt_sent_at = Some(now_secs());
worker.last_prompt = Some(next_prompt.clone());
worker.expected_receipt = task_receipt;
worker.replay_prompt = None;
@@ -579,6 +610,7 @@ impl WorkerRegistry {
worker.last_error = None;
worker.prompt_delivery_attempts = 0;
worker.prompt_in_flight = false;
worker.prompt_sent_at = None;
push_event(
worker,
WorkerEventKind::Restarted,
@@ -696,12 +728,11 @@ impl WorkerRegistry {
// Build evidence bundle
let evidence = StartupEvidenceBundle {
last_lifecycle_state: worker.status,
last_lifecycle_at: worker.updated_at,
pane_command: pane_command.to_string(),
prompt_sent_at: if worker.prompt_delivery_attempts > 0 {
Some(worker.updated_at)
} else {
None
},
pane_observed_at: now,
command_started_at: worker.created_at,
prompt_sent_at: worker.prompt_sent_at,
prompt_acceptance_state: worker.status == WorkerStatus::Running
&& !worker.prompt_in_flight,
trust_prompt_detected: worker
@@ -716,7 +747,9 @@ impl WorkerRegistry {
.map(|event| now.saturating_sub(event.timestamp)),
tool_permission_allow_scope,
transport_healthy,
transport_health: StartupHealthSummary::observed("transport", transport_healthy),
mcp_healthy,
mcp_health: StartupHealthSummary::observed("mcp", mcp_healthy),
elapsed_seconds: elapsed,
};
@@ -1840,8 +1873,16 @@ mod tests {
"last state should be spawning"
);
assert_eq!(evidence.pane_command, "cargo test");
assert!(evidence.command_started_at <= evidence.pane_observed_at);
assert!(evidence.last_lifecycle_at <= evidence.pane_observed_at);
assert!(!evidence.transport_healthy);
assert!(!evidence.transport_health.healthy);
assert!(evidence
.transport_health
.summary
.contains("transport_unhealthy"));
assert!(evidence.mcp_healthy);
assert!(evidence.mcp_health.healthy);
assert_eq!(*classification, StartupFailureClassification::TransportDead);
}
_ => panic!(
@@ -1932,11 +1973,53 @@ mod tests {
}
}
#[test]
fn startup_timeout_preserves_original_prompt_sent_timestamp() {
let registry = WorkerRegistry::new();
let worker = registry.create("/tmp/repo-prompt-timestamp", &[], true);
registry
.observe(&worker.worker_id, "Ready for input\n>")
.expect("ready observe should succeed");
let prompted = registry
.send_prompt(
&worker.worker_id,
Some("Run timestamp-sensitive work"),
None,
)
.expect("prompt send should succeed");
let sent_at = prompted
.prompt_sent_at
.expect("prompt send should record a prompt timestamp");
let timed_out = registry
.observe_startup_timeout(&worker.worker_id, "claw worker", true, true)
.expect("startup timeout observe should succeed");
let event = timed_out
.events
.iter()
.find(|e| e.kind == WorkerEventKind::StartupNoEvidence)
.expect("startup no evidence event should exist");
match event.payload.as_ref() {
Some(WorkerEventPayload::StartupNoEvidence { evidence, .. }) => {
assert_eq!(evidence.prompt_sent_at, Some(sent_at));
assert!(evidence.last_lifecycle_at <= evidence.pane_observed_at);
assert!(evidence.command_started_at <= sent_at);
}
_ => panic!("expected StartupNoEvidence payload"),
}
}
#[test]
fn startup_evidence_bundle_serializes_correctly() {
let bundle = StartupEvidenceBundle {
last_lifecycle_state: WorkerStatus::Running,
last_lifecycle_at: 1_234_567_889,
pane_command: "test command".to_string(),
pane_observed_at: 1_234_567_891,
command_started_at: 1_234_567_800,
prompt_sent_at: Some(1_234_567_890),
prompt_acceptance_state: false,
trust_prompt_detected: true,
@@ -1944,7 +2027,9 @@ mod tests {
tool_permission_prompt_age_seconds: None,
tool_permission_allow_scope: None,
transport_healthy: true,
transport_health: StartupHealthSummary::observed("transport", true),
mcp_healthy: false,
mcp_health: StartupHealthSummary::observed("mcp", false),
elapsed_seconds: 60,
};
@@ -1953,8 +2038,13 @@ mod tests {
assert!(json.contains("\"pane_command\""));
assert!(json.contains("\"prompt_sent_at\":1234567890"));
assert!(json.contains("\"trust_prompt_detected\":true"));
assert!(json.contains("\"last_lifecycle_at\":1234567889"));
assert!(json.contains("\"pane_observed_at\":1234567891"));
assert!(json.contains("\"command_started_at\":1234567800"));
assert!(json.contains("\"transport_healthy\":true"));
assert!(json.contains("\"transport_health\""));
assert!(json.contains("\"mcp_healthy\":false"));
assert!(json.contains("\"mcp_health\""));
let deserialized: StartupEvidenceBundle =
serde_json::from_str(&json).expect("should deserialize");
@@ -1966,7 +2056,10 @@ mod tests {
fn classify_startup_failure_detects_transport_dead() {
let evidence = StartupEvidenceBundle {
last_lifecycle_state: WorkerStatus::Spawning,
last_lifecycle_at: 10,
pane_command: "test".to_string(),
pane_observed_at: 40,
command_started_at: 1,
prompt_sent_at: None,
prompt_acceptance_state: false,
trust_prompt_detected: false,
@@ -1974,7 +2067,9 @@ mod tests {
tool_permission_prompt_age_seconds: None,
tool_permission_allow_scope: None,
transport_healthy: false,
transport_health: StartupHealthSummary::observed("transport", false),
mcp_healthy: true,
mcp_health: StartupHealthSummary::observed("mcp", true),
elapsed_seconds: 30,
};
@@ -1986,7 +2081,10 @@ mod tests {
fn classify_startup_failure_defaults_to_unknown() {
let evidence = StartupEvidenceBundle {
last_lifecycle_state: WorkerStatus::Spawning,
last_lifecycle_at: 10,
pane_command: "test".to_string(),
pane_observed_at: 40,
command_started_at: 1,
prompt_sent_at: None,
prompt_acceptance_state: false,
trust_prompt_detected: false,
@@ -1994,7 +2092,9 @@ mod tests {
tool_permission_prompt_age_seconds: None,
tool_permission_allow_scope: None,
transport_healthy: true,
transport_health: StartupHealthSummary::observed("transport", true),
mcp_healthy: true,
mcp_health: StartupHealthSummary::observed("mcp", true),
elapsed_seconds: 10,
};
@@ -2002,13 +2102,44 @@ mod tests {
assert_eq!(classification, StartupFailureClassification::Unknown);
}
#[test]
fn classify_startup_failure_detects_prompt_misdelivery_after_timeout() {
let evidence = StartupEvidenceBundle {
last_lifecycle_state: WorkerStatus::ReadyForPrompt,
last_lifecycle_at: 10,
pane_command: "test".to_string(),
pane_observed_at: 45,
command_started_at: 1,
prompt_sent_at: Some(10),
prompt_acceptance_state: false,
trust_prompt_detected: false,
tool_permission_prompt_detected: false,
tool_permission_prompt_age_seconds: None,
tool_permission_allow_scope: None,
transport_healthy: true,
transport_health: StartupHealthSummary::observed("transport", true),
mcp_healthy: true,
mcp_health: StartupHealthSummary::observed("mcp", true),
elapsed_seconds: 31,
};
let classification = classify_startup_failure(&evidence);
assert_eq!(
classification,
StartupFailureClassification::PromptMisdelivery
);
}
#[test]
fn classify_startup_failure_detects_worker_crashed() {
// Worker crashed scenario: transport healthy but MCP unhealthy
// Don't have prompt in flight (no prompt_sent_at) to avoid matching PromptAcceptanceTimeout
let evidence = StartupEvidenceBundle {
last_lifecycle_state: WorkerStatus::Spawning,
last_lifecycle_at: 10,
pane_command: "test".to_string(),
pane_observed_at: 40,
command_started_at: 1,
prompt_sent_at: None, // No prompt sent yet
prompt_acceptance_state: false,
trust_prompt_detected: false,
@@ -2016,7 +2147,9 @@ mod tests {
tool_permission_prompt_age_seconds: None,
tool_permission_allow_scope: None,
transport_healthy: true,
mcp_healthy: false, // MCP unhealthy but transport healthy suggests crash
transport_health: StartupHealthSummary::observed("transport", true),
mcp_healthy: false,
mcp_health: StartupHealthSummary::observed("mcp", false), // MCP unhealthy but transport healthy suggests crash
elapsed_seconds: 45,
};

View File

@@ -0,0 +1,81 @@
{
"schemaVersion": "g004.contract.bundle.v1",
"laneEvents": [
{
"event": "lane.started",
"status": "running",
"emittedAt": "2026-05-14T00:00:00Z",
"metadata": {
"seq": 1,
"provenance": "live_lane",
"emitterIdentity": "worker-1",
"environmentLabel": "team-g004"
}
},
{
"event": "lane.finished",
"status": "completed",
"emittedAt": "2026-05-14T00:00:10Z",
"metadata": {
"seq": 2,
"provenance": "live_lane",
"emitterIdentity": "worker-1",
"environmentLabel": "team-g004",
"eventFingerprint": "terminal-fp-001"
}
}
],
"reports": [
{
"schemaVersion": "g004.report.v1",
"reportId": "report-g004-fixture",
"identity": { "contentHash": "sha256:report-content" },
"projection": { "provenance": "runtime.event_projection.v1" },
"redaction": { "provenance": "runtime.redaction_policy.v1" },
"consumerCapabilities": ["facts", "field_deltas", "redaction_provenance"],
"findings": [
{
"kind": "fact",
"confidence": "high",
"statement": "lane event reached terminal state"
},
{
"kind": "hypothesis",
"confidence": "medium",
"statement": "consumer can reconcile the terminal fingerprint"
},
{
"kind": "negative_evidence",
"confidence": "high",
"statement": "no duplicate terminal event appears in this fixture"
}
],
"fieldDeltas": [
{
"field": "/laneEvents/1/status",
"previousHash": "sha256:running",
"currentHash": "sha256:completed",
"attribution": "worker-1 terminal reconciliation"
}
]
}
],
"approvalTokens": [
{
"tokenId": "approval-token-fixture",
"owner": "leader-fixed",
"scope": "g004.contract.bundle.fixture",
"issuedAt": "2026-05-14T00:00:01Z",
"oneTimeUse": true,
"replayPreventionNonce": "nonce-fixture-001",
"delegationChain": [
{
"from": "leader-fixed",
"to": "worker-3",
"action": "validate-g004-contract-fixture",
"at": "2026-05-14T00:00:02Z"
}
]
}
]
}

View File

@@ -0,0 +1,11 @@
# Report schema v1 fixture set
Validated by `cargo test -p runtime report_schema -- --nocapture`.
The in-code fixture in `runtime::report_schema::tests::fixture_report` covers:
- fact / hypothesis / confidence labels
- negative evidence with checked surfaces and query window
- field-level delta attribution
- canonical report id plus content hash
- deterministic projection/redaction provenance
- consumer capability negotiation and downgraded projections

View File

@@ -0,0 +1,80 @@
use runtime::g004_conformance::{is_g004_contract_bundle_valid, validate_g004_contract_bundle};
use serde_json::{json, Value};
fn valid_bundle() -> Value {
serde_json::from_str(include_str!("fixtures/g004_contract_bundle.valid.json"))
.expect("valid fixture JSON should parse")
}
#[test]
fn valid_g004_contract_bundle_fixture_passes_conformance() {
let fixture = valid_bundle();
let errors = validate_g004_contract_bundle(&fixture);
assert!(
errors.is_empty(),
"unexpected conformance errors: {errors:?}"
);
assert!(is_g004_contract_bundle_valid(&fixture));
}
#[test]
fn g004_conformance_reports_machine_readable_paths_for_contract_gaps() {
let invalid = json!({
"schemaVersion": "g004.contract.bundle.v1",
"laneEvents": [
{
"event": "lane.finished",
"status": "completed",
"emittedAt": "2026-05-14T00:00:10Z",
"metadata": {
"seq": 1,
"provenance": "live_lane",
"emitterIdentity": "worker-1",
"environmentLabel": "team-g004"
}
}
],
"reports": [
{
"schemaVersion": "g004.report.v1",
"reportId": "report-with-gaps",
"identity": { "contentHash": "sha256:report-content" },
"projection": { "provenance": "runtime.event_projection.v1" },
"redaction": { "provenance": "runtime.redaction_policy.v1" },
"consumerCapabilities": [],
"findings": [
{
"kind": "guess",
"confidence": "certain",
"statement": "bad labels should be rejected"
}
],
"fieldDeltas": []
}
],
"approvalTokens": [
{
"tokenId": "approval-token-fixture",
"owner": "leader-fixed",
"scope": "g004.contract.bundle.fixture",
"issuedAt": "2026-05-14T00:00:01Z",
"oneTimeUse": false,
"replayPreventionNonce": "nonce-fixture-001",
"delegationChain": []
}
]
});
let errors = validate_g004_contract_bundle(&invalid);
let paths: Vec<&str> = errors.iter().map(|error| error.path.as_str()).collect();
assert!(paths.contains(&"/laneEvents/0/metadata/eventFingerprint"));
assert!(paths.contains(&"/reports/0/consumerCapabilities"));
assert!(paths.contains(&"/reports/0/findings/0/kind"));
assert!(paths.contains(&"/reports/0/findings/0/confidence"));
assert!(paths.contains(&"/reports/0/fieldDeltas"));
assert!(paths.contains(&"/approvalTokens/0/oneTimeUse"));
assert!(paths.contains(&"/approvalTokens/0/delegationChain"));
}

View File

@@ -22,7 +22,7 @@ fn stale_branch_detection_flows_into_policy_engine() {
let stale_context = LaneContext::new(
"stale-lane",
0,
Duration::from_secs(2 * 60 * 60), // 2 hours stale
Duration::from_hours(2), // 2 hours stale
LaneBlocker::None,
ReviewStatus::Pending,
DiffScope::Full,
@@ -49,7 +49,7 @@ fn fresh_branch_does_not_trigger_stale_policy() {
let fresh_context = LaneContext::new(
"fresh-lane",
0,
Duration::from_secs(30 * 60), // 30 min stale — under 1 hour threshold
Duration::from_mins(30), // 30 min stale — under 1 hour threshold
LaneBlocker::None,
ReviewStatus::Pending,
DiffScope::Full,
@@ -96,9 +96,7 @@ fn green_contract_unsatisfied_blocks_merge() {
false,
);
// This is a conceptual test — we need a way to express "requires workspace green"
// Currently LaneContext has raw green_level: u8, not a contract
// For now we just verify the policy condition works
// The context has a test level but lacks the full green contract, so merge stays blocked.
let engine = PolicyEngine::new(vec![PolicyRule::new(
"workspace-green-required",
PolicyCondition::GreenAt { level: 3 }, // GreenLevel::Workspace
@@ -212,8 +210,8 @@ fn end_to_end_stale_lane_gets_merge_forward_action() {
// when: build context and evaluate policy
let context = LaneContext::new(
"lane-9411",
3, // Workspace green
Duration::from_secs(5 * 60 * 60), // 5 hours stale, definitely over threshold
3, // Workspace green
Duration::from_hours(5), // 5 hours stale, definitely over threshold
LaneBlocker::None,
ReviewStatus::Approved,
DiffScope::Scoped,
@@ -261,13 +259,14 @@ fn end_to_end_stale_lane_gets_merge_forward_action() {
fn fresh_approved_lane_gets_merge_action() {
let context = LaneContext::new(
"fresh-approved-lane",
3, // Workspace green
Duration::from_secs(30 * 60), // 30 min — under 1 hour threshold = fresh
3, // Workspace green
Duration::from_mins(30), // 30 min — under 1 hour threshold = fresh
LaneBlocker::None,
ReviewStatus::Approved,
DiffScope::Scoped,
false,
);
)
.with_green_contract_satisfied(true);
let engine = PolicyEngine::new(vec![PolicyRule::new(
"merge-if-green-approved-not-stale",
@@ -347,7 +346,7 @@ fn worker_provider_failure_flows_through_recovery_to_policy() {
// (Simulating the policy check that would happen after successful recovery)
let recovery_success = matches!(result, RecoveryResult::Recovered { .. });
let green_level = 3; // Workspace green
let not_stale = Duration::from_secs(30 * 60); // 30 min — fresh
let not_stale = Duration::from_mins(30); // 30 min — fresh
let post_recovery_context = LaneContext::new(
"recovered-lane",
@@ -357,7 +356,8 @@ fn worker_provider_failure_flows_through_recovery_to_policy() {
ReviewStatus::Approved,
DiffScope::Scoped,
false,
);
)
.with_green_contract_satisfied(true);
let policy_engine = PolicyEngine::new(vec![
// Rule: if recovered from failure + green + approved -> merge

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More