mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-05-30 20:04:17 +08:00
feat: sweep
This commit is contained in:
parent
1003510a75
commit
c613e8e676
20
.github/hooks/pre-push
vendored
Executable file
20
.github/hooks/pre-push
vendored
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Claw Code local pre-push safety gate.
|
||||
#
|
||||
# Install with:
|
||||
# git config core.hooksPath .github/hooks
|
||||
#
|
||||
# This intentionally mirrors the CI build gate so stale field/enum references are
|
||||
# caught before pushing to main or PR branches.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null)"
|
||||
cd "$repo_root"
|
||||
|
||||
if [[ ! -f rust/Cargo.toml ]]; then
|
||||
echo "pre-push: rust/Cargo.toml not found; skipping cargo workspace build" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "pre-push: cargo build --manifest-path rust/Cargo.toml --workspace" >&2
|
||||
cargo build --manifest-path rust/Cargo.toml --workspace
|
||||
@ -3,11 +3,11 @@
|
||||
"duplicate_roadmap_heading_lines": [],
|
||||
"roadmap_actions_mapped": 542,
|
||||
"roadmap_actions_total": 542,
|
||||
"roadmap_headings_mapped": 124,
|
||||
"roadmap_headings_total": 124,
|
||||
"roadmap_headings_mapped": 127,
|
||||
"roadmap_headings_total": 127,
|
||||
"unmapped_roadmap_heading_lines": []
|
||||
},
|
||||
"generated_at": "2026-05-14T08:13:45+00:00",
|
||||
"generated_at": "2026-05-25T04:30:33+00:00",
|
||||
"generation_policy": {
|
||||
"release_buckets": [
|
||||
"2.x_intake",
|
||||
@ -14823,6 +14823,69 @@
|
||||
"status": "context",
|
||||
"title": "Parity source metadata: openai/codex",
|
||||
"verification_required": "none_context_only"
|
||||
},
|
||||
{
|
||||
"category": "boot",
|
||||
"deferral_rationale": "",
|
||||
"dependencies": [
|
||||
"stream_0_governance"
|
||||
],
|
||||
"id": "CC2-RM-H0125-pinpoint-693-claw-analog-bootstrap-plan",
|
||||
"lifecycle_status": "done_verify",
|
||||
"owner_lane": "stream_1_worker_boot_session_control",
|
||||
"release_bucket": "alpha_blocker",
|
||||
"source_anchor": "ROADMAP.md:L7528",
|
||||
"source_context": "Clawable Coding Harness Roadmap > Pinpoint follow-up intake",
|
||||
"source_level": 2,
|
||||
"source_line": 7528,
|
||||
"source_ordinal": null,
|
||||
"source_path": "ROADMAP.md",
|
||||
"source_type": "roadmap_heading",
|
||||
"status": "done_verify",
|
||||
"title": "Pinpoint #693. `claw-analog` bootstrap-plan phase parser silently falls back to `\"unknown\"` \u2014 `lib.rs:1114` uses `.unwrap_or(\"unknown\")` for phase field; unrecognized phases emit opaque kind instead of typed error",
|
||||
"verification_required": "targeted_regression_or_acceptance_test_required"
|
||||
},
|
||||
{
|
||||
"category": "branch_recovery",
|
||||
"deferral_rationale": "",
|
||||
"dependencies": [
|
||||
"stream_0_governance"
|
||||
],
|
||||
"id": "CC2-RM-H0126-pinpoint-694-no-pre-push-cargo-build-gat",
|
||||
"lifecycle_status": "done_verify",
|
||||
"owner_lane": "stream_3_branch_test_recovery",
|
||||
"release_bucket": "alpha_blocker",
|
||||
"source_anchor": "ROADMAP.md:L7538",
|
||||
"source_context": "Clawable Coding Harness Roadmap > Pinpoint follow-up intake",
|
||||
"source_level": 2,
|
||||
"source_line": 7538,
|
||||
"source_ordinal": null,
|
||||
"source_path": "ROADMAP.md",
|
||||
"source_type": "roadmap_heading",
|
||||
"status": "done_verify",
|
||||
"title": "Pinpoint #694. No pre-push `cargo build` gate \u2014 stale field refs (`retry_after`, `Team` variant, `config_load_error_kind`) broke main build undetected until CI",
|
||||
"verification_required": "git_fixture_or_recovery_recipe_test"
|
||||
},
|
||||
{
|
||||
"category": "boot",
|
||||
"deferral_rationale": "",
|
||||
"dependencies": [
|
||||
"stream_0_governance"
|
||||
],
|
||||
"id": "CC2-RM-H0127-pinpoint-695-agent-starts-in-stale-wrong",
|
||||
"lifecycle_status": "done_verify",
|
||||
"owner_lane": "stream_1_worker_boot_session_control",
|
||||
"release_bucket": "alpha_blocker",
|
||||
"source_anchor": "ROADMAP.md:L7548",
|
||||
"source_context": "Clawable Coding Harness Roadmap > Pinpoint follow-up intake",
|
||||
"source_level": 2,
|
||||
"source_line": 7548,
|
||||
"source_ordinal": null,
|
||||
"source_path": "ROADMAP.md",
|
||||
"source_type": "roadmap_heading",
|
||||
"status": "done_verify",
|
||||
"title": "Pinpoint #695. Agent starts in stale/wrong worktree and burns a full turn before noticing \u2014 no pre-flight check for \"file exists on current branch\" or \"this .git is writable from sandbox\"",
|
||||
"verification_required": "worker_boot_state_machine_or_cli_json_contract_test"
|
||||
}
|
||||
],
|
||||
"schema_version": "cc2.board.v1",
|
||||
@ -14839,7 +14902,7 @@
|
||||
"root": "/Users/bellman/Documents/Workspace/claw-code/.omx/research"
|
||||
},
|
||||
"roadmap": {
|
||||
"heading_count": 124,
|
||||
"heading_count": 127,
|
||||
"ordered_action_count": 542,
|
||||
"path": "ROADMAP.md",
|
||||
"sha256_prefix": "2aba3315e52f3079"
|
||||
@ -14850,15 +14913,15 @@
|
||||
"adoption_overlay": 357,
|
||||
"parity_overlay": 20,
|
||||
"stream_0_governance": 221,
|
||||
"stream_1_worker_boot_session_control": 15,
|
||||
"stream_1_worker_boot_session_control": 17,
|
||||
"stream_2_event_reporting_contracts": 73,
|
||||
"stream_3_branch_test_recovery": 16,
|
||||
"stream_3_branch_test_recovery": 17,
|
||||
"stream_4_claws_first_execution": 5,
|
||||
"stream_5_plugin_mcp_lifecycle": 22
|
||||
},
|
||||
"by_release_bucket": {
|
||||
"2.x_intake": 30,
|
||||
"alpha_blocker": 240,
|
||||
"alpha_blocker": 243,
|
||||
"beta_adoption": 417,
|
||||
"context": 15,
|
||||
"ga_ecosystem": 22,
|
||||
@ -14870,13 +14933,13 @@
|
||||
"latest_open_issue": 30,
|
||||
"parity_repo_context": 2,
|
||||
"roadmap_action": 542,
|
||||
"roadmap_heading": 124
|
||||
"roadmap_heading": 127
|
||||
},
|
||||
"by_status": {
|
||||
"active": 73,
|
||||
"context": 15,
|
||||
"deferred_with_rationale": 9,
|
||||
"done_verify": 313,
|
||||
"done_verify": 316,
|
||||
"open": 285,
|
||||
"rejected_not_claw": 2,
|
||||
"stale_done": 31,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Claw Code 2.0 Canonical Board
|
||||
|
||||
Generated from board schema: `2026-05-14T08:13:45+00:00`
|
||||
Generated from board schema: `2026-05-25T04:30:33+00:00`
|
||||
Schema version: `cc2.board.v1`
|
||||
Ultragoal mutation policy: `.omx/ultragoal` is leader-owned and was not modified by this rendering task.
|
||||
|
||||
@ -8,7 +8,7 @@ Ultragoal mutation policy: `.omx/ultragoal` is leader-owned and was not modified
|
||||
|
||||
| Source | Frozen evidence |
|
||||
| --- | --- |
|
||||
| Roadmap | `ROADMAP.md` sha256 prefix `2aba3315e52f3079`; 124 headings; 542 ordered actions |
|
||||
| Roadmap | `ROADMAP.md` sha256 prefix `2aba3315e52f3079`; 127 headings; 542 ordered actions |
|
||||
| Approved plan | `.omx/plans/claw-code-2-0-adaptive-plan.md` sha256 prefix `e7ef6faf23bfc16b` |
|
||||
| Research bundle | root `/Users/bellman/Documents/Workspace/claw-code/.omx/research`; latest open issues 30; issue corpus 1000; codex/opencode clone metadata included |
|
||||
|
||||
@ -16,11 +16,11 @@ Ultragoal mutation policy: `.omx/ultragoal` is leader-owned and was not modified
|
||||
|
||||
| Coverage gate | Mapped | Total | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| ROADMAP headings | 124 | 124 | PASS |
|
||||
| ROADMAP headings | 127 | 127 | PASS |
|
||||
| ROADMAP ordered actions | 542 | 542 | PASS |
|
||||
| Duplicate heading lines | 0 | 0 | PASS |
|
||||
|
||||
Total canonical board items: **729**
|
||||
Total canonical board items: **732**
|
||||
|
||||
## Lifecycle Enum Reference
|
||||
|
||||
@ -29,7 +29,7 @@ Total canonical board items: **729**
|
||||
| `active` | 73 | Current Claw Code 2.0 implementation surface that should remain visible on the board. |
|
||||
| `context` | 15 | Context-only heading or evidence anchor; not an implementation work item. |
|
||||
| `deferred_with_rationale` | 9 | Intentionally deferred; rationale must be present in the board item. |
|
||||
| `done_verify` | 313 | Marked as done upstream but retained for verification against current CC2 behavior. |
|
||||
| `done_verify` | 316 | Marked as done upstream but retained for verification against current CC2 behavior. |
|
||||
| `open` | 285 | Actionable unresolved work that needs implementation or acceptance evidence. |
|
||||
| `rejected_not_claw` | 2 | Excluded because it is not Claw Code product work. |
|
||||
| `stale_done` | 31 | Historically completed or merged work that may be stale and needs freshness checks before relying on it. |
|
||||
@ -40,7 +40,7 @@ Total canonical board items: **729**
|
||||
| Bucket | Count | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `2.x_intake` | 30 | Post-2.0 intake or follow-up candidate retained for sequencing. |
|
||||
| `alpha_blocker` | 240 | Must be resolved before alpha-quality autonomous coding lanes are dependable. |
|
||||
| `alpha_blocker` | 243 | Must be resolved before alpha-quality autonomous coding lanes are dependable. |
|
||||
| `beta_adoption` | 417 | Important for broader dogfood/adoption once alpha blockers are controlled. |
|
||||
| `context` | 15 | Non-actionable roadmap context. |
|
||||
| `ga_ecosystem` | 22 | Required for mature plugin/MCP/provider ecosystem behavior. |
|
||||
@ -54,9 +54,9 @@ Total canonical board items: **729**
|
||||
| Adoption overlay — user-visible parity and release polish | 357 | 329 | `deferred_with_rationale` 3, `done_verify` 237, `open` 92, `rejected_not_claw` 2, `stale_done` 23 |
|
||||
| Parity overlay — opencode/codex comparison context | 20 | 16 | `context` 2, `deferred_with_rationale` 1, `done_verify` 5, `open` 11, `stale_done` 1 |
|
||||
| Stream 0 — Governance, intake, and cross-cutting roadmap triage | 221 | 198 | `active` 6, `context` 13, `deferred_with_rationale` 4, `done_verify` 45, `open` 147, `stale_done` 5, `superseded` 1 |
|
||||
| Stream 1 — Worker boot and session control | 15 | 14 | `active` 8, `deferred_with_rationale` 1, `open` 6 |
|
||||
| Stream 1 — Worker boot and session control | 17 | 16 | `active` 8, `deferred_with_rationale` 1, `done_verify` 2, `open` 6 |
|
||||
| Stream 2 — Event/reporting contracts | 73 | 73 | `active` 45, `done_verify` 20, `open` 8 |
|
||||
| Stream 3 — Branch/test recovery | 16 | 14 | `active` 6, `done_verify` 1, `open` 7, `stale_done` 2 |
|
||||
| Stream 3 — Branch/test recovery | 17 | 15 | `active` 6, `done_verify` 2, `open` 7, `stale_done` 2 |
|
||||
| Stream 4 — Claws-first task execution | 5 | 5 | `active` 4, `done_verify` 1 |
|
||||
| Stream 5 — Plugin/MCP lifecycle | 22 | 22 | `active` 4, `done_verify` 4, `open` 14 |
|
||||
|
||||
@ -68,7 +68,7 @@ Total canonical board items: **729**
|
||||
| `latest_open_issue` | 30 |
|
||||
| `parity_repo_context` | 2 |
|
||||
| `roadmap_action` | 542 |
|
||||
| `roadmap_heading` | 124 |
|
||||
| `roadmap_heading` | 127 |
|
||||
|
||||
## Board Items by Stream
|
||||
|
||||
@ -704,6 +704,8 @@ Total canonical board items: **729**
|
||||
| `CC2-RM-A0363-surface-inconsistency-cluster-of-3-after` | **Surface inconsistency (cluster of 3)**: after #143 Phase 1, the behavior matrix is: | `ROADMAP.md:L5515` / `roadmap_action` | `alpha_blocker` | `open` | `plugin_mcp_lifecycle_contract_test` | `stream_1_worker_boot_session_control` | — |
|
||||
| `CC2-RM-A0391-remove-the-error-prefix-from-format-unkn` | Remove the "error:" prefix from format_unknown_verb_option (already added by top-level handler) | `ROADMAP.md:L5916` / `roadmap_action` | `alpha_blocker` | `open` | `worker_boot_state_machine_or_cli_json_contract_test` | none | — |
|
||||
| `CC2-RM-A0512-system-prompt-output-format-json-exposes` | **`system-prompt --output-format json` exposes `"__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__"` as a literal element in the `sections` array — an internal split delimiter leaked into the public structured output** — dogfooded 2026-04-30 by Jobdori on `e939777f`. Running `claw system-prompt --output-format json` returns `{"kind":"system-prompt","message":"<full prose>","sections":["You are an interactive agent...", "# System\n...", "# Doing tasks\n...", "# Executing actions with care\n...", "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__", "# Environment context\n...", "# Project context\n...", "# Claude instructions\n...", "# Runtime config\n..."]}`. The `sections` array has 9 elements; element index 4 is the raw string `"__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__"`. This internal sentinel marks the boundary between the static and dynamic sections of the compiled system prompt, used during assembly to split the prompt at injection time. It appears in the public JSON output verbatim as a first-class section, indistinguishable from real sections by type alone. Automation that iterates `sections[]` must special-case this sentinel or it will process an internal implementation string as if it were a real system prompt section. **Required fix shape:** (a) strip `"__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__"` and any similar internal delimiters from the `sections` array before serializing to JSON; (b) if the static/dynamic boundary is semantically meaningful for callers, expose it as a structured metadata field such as `boundary_index:4` or as a `section_type:"static"\|"dynamic"` field on each section entry, not as a raw sentinel string in the array; (c) rename the `sections` type from `string[]` to `[{id, type, content}]` to enable this without breaking the boundary signal; (d) add regression coverage proving the `system-prompt --output-format json` output's `sections` array contains no elements whose value equals `"__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__"` or matches `/__[A-Z_]+__/`. **Why this matters:** internal sentinel strings in public JSON are a contract liability — they couple the wire format to internal implementation details. Any refactor that renames or removes the sentinel breaks callers that don't special-case it, and automation that doesn't know to filter it will miscount, misparse, or misrender the system prompt. Source: Jobdori live dogfood, `e939777f`, 2026-04-30. | `ROADMAP.md:L6333` / `roadmap_action` | `beta_adoption` | `open` | `worker_boot_state_machine_or_cli_json_contract_test` | none | — |
|
||||
| `CC2-RM-H0125-pinpoint-693-claw-analog-bootstrap-plan` | Pinpoint #693. `claw-analog` bootstrap-plan phase parser silently falls back to `"unknown"` — `lib.rs:1114` uses `.unwrap_or("unknown")` for phase field; unrecognized phases emit opaque kind instead of typed error | `ROADMAP.md:L7528` / `roadmap_heading` | `alpha_blocker` | `done_verify` | `targeted_regression_or_acceptance_test_required` | `stream_0_governance` | — |
|
||||
| `CC2-RM-H0127-pinpoint-695-agent-starts-in-stale-wrong` | Pinpoint #695. Agent starts in stale/wrong worktree and burns a full turn before noticing — no pre-flight check for "file exists on current branch" or "this .git is writable from sandbox" | `ROADMAP.md:L7548` / `roadmap_heading` | `alpha_blocker` | `done_verify` | `worker_boot_state_machine_or_cli_json_contract_test` | `stream_0_governance` | — |
|
||||
|
||||
### Stream 2 — Event/reporting contracts
|
||||
|
||||
@ -803,6 +805,7 @@ Total canonical board items: **729**
|
||||
| `CC2-RM-A0410-remediation-registry-a-function-remediat` | **Remediation registry:** A function `remediation_for(kind: &str, operation: &str) -> Remediation` that maps `(error_kind, operation_context)` pairs to stable remediation structs: | `ROADMAP.md:L6041` / `roadmap_action` | `alpha_blocker` | `open` | `targeted_regression_or_acceptance_test_required` | `stream_2_event_reporting_contracts` | — |
|
||||
| `CC2-RM-A0411-stable-hint-outputs-per-class-each-error` | **Stable hint outputs per class:** Each `error_kind` maps to exactly one remediation shape. No more prose splitting. | `ROADMAP.md:L6049` / `roadmap_action` | `alpha_blocker` | `open` | `targeted_regression_or_acceptance_test_required` | `stream_2_event_reporting_contracts` | — |
|
||||
| `CC2-RM-A0412-golden-fixture-tests-test-each-kind-oper` | **Golden fixture tests:** Test each `(kind, operation)` pair against expected remediation output as golden fixtures instead of the current `split_error_hint()` string hacks. | `ROADMAP.md:L6050` / `roadmap_action` | `alpha_blocker` | `open` | `targeted_regression_or_acceptance_test_required` | `stream_2_event_reporting_contracts` | — |
|
||||
| `CC2-RM-H0126-pinpoint-694-no-pre-push-cargo-build-gat` | Pinpoint #694. No pre-push `cargo build` gate — stale field refs (`retry_after`, `Team` variant, `config_load_error_kind`) broke main build undetected until CI | `ROADMAP.md:L7538` / `roadmap_heading` | `alpha_blocker` | `done_verify` | `git_fixture_or_recovery_recipe_test` | `stream_0_governance` | — |
|
||||
|
||||
### Stream 4 — Claws-first task execution
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"createdAt": "2026-05-14T07:53:46.061Z",
|
||||
"updatedAt": "2026-05-15T04:38:54.887Z",
|
||||
"updatedAt": "2026-05-25T04:18:52.711Z",
|
||||
"briefPath": ".omx/ultragoal/brief.md",
|
||||
"goalsPath": ".omx/ultragoal/goals.json",
|
||||
"ledgerPath": ".omx/ultragoal/ledger.jsonl",
|
||||
@ -148,7 +148,19 @@
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"id": "G013-implement-roadmap-pinpoints-693-695",
|
||||
"title": "Implement ROADMAP pinpoints #693-#695",
|
||||
"objective": "Map and implement the newly appended ROADMAP.md pinpoints #693, #694, and #695 after reset to origin/main: typed claw-analog bootstrap phase errors, a local pre-push cargo build gate, and startup/worktree preflight diagnostics; update CC2 board/coverage and verify with targeted and workspace checks.",
|
||||
"status": "in_progress",
|
||||
"attempt": 1,
|
||||
"createdAt": "2026-05-25T04:18:43.420Z",
|
||||
"updatedAt": "2026-05-25T04:18:52.711Z",
|
||||
"evidence": "Current-head verification after reset: python3 scripts/validate_cc2_board.py --board .omx/cc2/board.json failed with unmapped ROADMAP headings [7528,7538,7548], corresponding to Pinpoints #693-#695.",
|
||||
"startedAt": "2026-05-25T04:18:52.711Z"
|
||||
}
|
||||
],
|
||||
"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."
|
||||
"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.",
|
||||
"activeGoalId": "G013-implement-roadmap-pinpoints-693-695"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
47
docs/g013-roadmap-pinpoints-693-695-verification-map.md
Normal file
47
docs/g013-roadmap-pinpoints-693-695-verification-map.md
Normal file
@ -0,0 +1,47 @@
|
||||
# G013 ROADMAP pinpoints #693-#695 verification map
|
||||
|
||||
This map records the current-head follow-up that was discovered after resetting
|
||||
`main` to `origin/main`: ROADMAP.md contained three new Pinpoint headings not
|
||||
covered by the Claw Code 2.0 board.
|
||||
|
||||
## Pinpoint #693 — typed phase error instead of silent `unknown`
|
||||
|
||||
- Code: `rust/crates/claw-analog/src/lib.rs`
|
||||
- Behavior: `format_rag_query_json_for_model` now rejects missing, empty, or
|
||||
literal `"unknown"` phase values with a structured error envelope containing
|
||||
`kind:"unknown_bootstrap_phase"`, `field:"phase"`, and `received_value`.
|
||||
- Regression tests: `rag_response_missing_phase_returns_typed_error` and
|
||||
`rag_response_unknown_phase_returns_typed_error`.
|
||||
|
||||
## Pinpoint #694 — local pre-push build gate
|
||||
|
||||
- Hook: `.github/hooks/pre-push`
|
||||
- Install command: `git config core.hooksPath .github/hooks`
|
||||
- Gate: `cargo build --manifest-path rust/Cargo.toml --workspace`
|
||||
- Purpose: mirror the CI build job locally so stale field/variant references are
|
||||
caught before push.
|
||||
|
||||
## Pinpoint #695 — startup/worktree preflight diagnostics
|
||||
|
||||
- Code: `rust/crates/runtime/src/worker_boot.rs`
|
||||
- Behavior: `startup_preflight_warnings` and
|
||||
`WorkerRegistry::observe_startup_preflight` emit structured warnings before
|
||||
the first model turn when a task mentions a path not tracked on the current
|
||||
branch (`file_absent_on_branch`) or git metadata is not writable
|
||||
(`git_metadata_not_writable`).
|
||||
- Regression tests:
|
||||
- `startup_preflight_warns_when_task_file_is_absent_on_branch`
|
||||
- `startup_preflight_records_structured_warning_event`
|
||||
|
||||
## Verification commands
|
||||
|
||||
```bash
|
||||
python3 scripts/generate_cc2_board.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
|
||||
bash -n .github/hooks/pre-push
|
||||
cargo fmt --manifest-path rust/Cargo.toml --all -- --check
|
||||
cargo test --manifest-path rust/Cargo.toml -p claw-analog rag_response_ -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p runtime startup_preflight -- --nocapture
|
||||
cargo build --manifest-path rust/Cargo.toml --workspace
|
||||
```
|
||||
@ -1111,7 +1111,24 @@ enum BlockKind {
|
||||
|
||||
pub(crate) fn format_rag_query_json_for_model(body: &str) -> Result<String, String> {
|
||||
let v: Value = serde_json::from_str(body).map_err(|e| format!("invalid JSON: {e}"))?;
|
||||
let phase = v.get("phase").and_then(|x| x.as_str()).unwrap_or("unknown");
|
||||
let phase = v.get("phase").and_then(|x| x.as_str()).ok_or_else(|| {
|
||||
json!({
|
||||
"kind": "unknown_bootstrap_phase",
|
||||
"field": "phase",
|
||||
"received_value": v.get("phase").cloned().unwrap_or(Value::Null),
|
||||
"message": "RAG response is missing a string phase; refusing to silently render phase as unknown"
|
||||
})
|
||||
.to_string()
|
||||
})?;
|
||||
if phase.trim().is_empty() || phase == "unknown" {
|
||||
return Err(json!({
|
||||
"kind": "unknown_bootstrap_phase",
|
||||
"field": "phase",
|
||||
"received_value": phase,
|
||||
"message": "RAG response phase must be a concrete phase name"
|
||||
})
|
||||
.to_string());
|
||||
}
|
||||
let hits = v
|
||||
.get("hits")
|
||||
.and_then(|h| h.as_array())
|
||||
@ -2557,6 +2574,20 @@ mod tests {
|
||||
assert!(out.contains("score="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rag_response_missing_phase_returns_typed_error() {
|
||||
let err = format_rag_query_json_for_model(r#"{"hits":[]}"#).unwrap_err();
|
||||
assert!(err.contains(r#""kind":"unknown_bootstrap_phase""#));
|
||||
assert!(err.contains(r#""field":"phase""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rag_response_unknown_phase_returns_typed_error() {
|
||||
let err = format_rag_query_json_for_model(r#"{"hits":[],"phase":"unknown"}"#).unwrap_err();
|
||||
assert!(err.contains(r#""kind":"unknown_bootstrap_phase""#));
|
||||
assert!(err.contains(r#""received_value":"unknown""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_rag_base_url_toml_beats_env() {
|
||||
let _g = mock_env_lock();
|
||||
|
||||
@ -438,13 +438,24 @@ fn normalize_path(path: &Path) -> PathBuf {
|
||||
/// Extract repository name from a path for event context.
|
||||
fn extract_repo_name(cwd: &str) -> Option<String> {
|
||||
let path = Path::new(cwd);
|
||||
// Try to find a .git directory to identify repo root
|
||||
let mut current = Some(path);
|
||||
while let Some(p) = current {
|
||||
if p.join(".git").is_dir() {
|
||||
return p.file_name().map(|n| n.to_string_lossy().to_string());
|
||||
// Ask git from the cwd itself. Walking ancestors manually can accidentally
|
||||
// classify synthetic/nonexistent paths as an unrelated parent repo (for
|
||||
// example `/tmp/.git`), which makes trust events point at the wrong repo.
|
||||
if path.is_dir() {
|
||||
if let Ok(output) = std::process::Command::new("git")
|
||||
.args(["rev-parse", "--show-toplevel"])
|
||||
.current_dir(path)
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !root.is_empty() {
|
||||
return Path::new(&root)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
current = p.parent();
|
||||
}
|
||||
// Fallback: use the last component of the path
|
||||
path.file_name().map(|n| n.to_string_lossy().to_string())
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@ -73,6 +74,7 @@ pub struct WorkerFailure {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkerEventKind {
|
||||
Spawning,
|
||||
StartupPreflightWarning,
|
||||
TrustRequired,
|
||||
ToolPermissionRequired,
|
||||
TrustResolved,
|
||||
@ -102,6 +104,21 @@ pub enum WorkerPromptTarget {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkerStartupPreflightWarningKind {
|
||||
FileAbsentOnBranch,
|
||||
GitMetadataNotWritable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WorkerStartupPreflightWarning {
|
||||
pub kind: WorkerStartupPreflightWarningKind,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
/// Classification of startup failure when no evidence is available.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@ -212,6 +229,12 @@ pub enum WorkerEventPayload {
|
||||
evidence: StartupEvidenceBundle,
|
||||
classification: StartupFailureClassification,
|
||||
},
|
||||
StartupPreflightWarning {
|
||||
kind: WorkerStartupPreflightWarningKind,
|
||||
message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
path: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@ -329,6 +352,34 @@ impl WorkerRegistry {
|
||||
inner.workers.get(worker_id).cloned()
|
||||
}
|
||||
|
||||
pub fn observe_startup_preflight(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
task_prompt: &str,
|
||||
) -> Result<Worker, String> {
|
||||
let mut inner = self.inner.lock().expect("worker registry lock poisoned");
|
||||
let worker = inner
|
||||
.workers
|
||||
.get_mut(worker_id)
|
||||
.ok_or_else(|| format!("worker not found: {worker_id}"))?;
|
||||
|
||||
for warning in startup_preflight_warnings(Path::new(&worker.cwd), task_prompt) {
|
||||
push_event(
|
||||
worker,
|
||||
WorkerEventKind::StartupPreflightWarning,
|
||||
worker.status,
|
||||
Some(warning.message.clone()),
|
||||
Some(WorkerEventPayload::StartupPreflightWarning {
|
||||
kind: warning.kind,
|
||||
message: warning.message,
|
||||
path: warning.path,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(worker.clone())
|
||||
}
|
||||
|
||||
pub fn observe(&self, worker_id: &str, screen_text: &str) -> Result<Worker, String> {
|
||||
let mut inner = self.inner.lock().expect("worker registry lock poisoned");
|
||||
let worker = inner
|
||||
@ -1064,6 +1115,118 @@ fn extract_server_from_qualified_tool(tool: &str) -> Option<String> {
|
||||
(!server.is_empty()).then(|| server.to_string())
|
||||
}
|
||||
|
||||
pub fn startup_preflight_warnings(
|
||||
cwd: &Path,
|
||||
task_prompt: &str,
|
||||
) -> Vec<WorkerStartupPreflightWarning> {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
if let Some(git_path) = git_metadata_path(cwd) {
|
||||
if !path_is_writable(&git_path) {
|
||||
warnings.push(WorkerStartupPreflightWarning {
|
||||
kind: WorkerStartupPreflightWarningKind::GitMetadataNotWritable,
|
||||
message: format!(
|
||||
"git metadata is not writable; commits or pushes may fail: {}",
|
||||
git_path.display()
|
||||
),
|
||||
path: Some(git_path.display().to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for path in mentioned_repo_paths(task_prompt) {
|
||||
if !git_tracks_path(cwd, &path) {
|
||||
warnings.push(WorkerStartupPreflightWarning {
|
||||
kind: WorkerStartupPreflightWarningKind::FileAbsentOnBranch,
|
||||
message: format!(
|
||||
"task mentions {path}, but git does not track it on the current branch"
|
||||
),
|
||||
path: Some(path),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
warnings
|
||||
}
|
||||
|
||||
fn mentioned_repo_paths(task_prompt: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for raw in task_prompt.split_whitespace() {
|
||||
let token = raw.trim_matches(|ch: char| {
|
||||
matches!(
|
||||
ch,
|
||||
'`' | '"' | '\'' | '(' | ')' | '[' | ']' | '{' | '}' | ',' | ';' | ':'
|
||||
)
|
||||
});
|
||||
if !token.contains('/') || token.contains("://") || token.starts_with('/') {
|
||||
continue;
|
||||
}
|
||||
let token = token.trim_start_matches("./");
|
||||
if token.contains("..") {
|
||||
continue;
|
||||
}
|
||||
if token
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '_' | '-' | '.'))
|
||||
&& token
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.is_some_and(|name| name.contains('.'))
|
||||
&& !out.iter().any(|seen| seen == token)
|
||||
{
|
||||
out.push(token.to_string());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn git_tracks_path(cwd: &Path, path: &str) -> bool {
|
||||
Command::new("git")
|
||||
.arg("ls-files")
|
||||
.arg("--error-unmatch")
|
||||
.arg("--")
|
||||
.arg(path)
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.is_ok_and(|output| output.status.success())
|
||||
}
|
||||
|
||||
fn git_metadata_path(cwd: &Path) -> Option<PathBuf> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "--git-path", "."])
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let path = PathBuf::from(text);
|
||||
Some(if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
cwd.join(path)
|
||||
})
|
||||
}
|
||||
|
||||
fn path_is_writable(path: &Path) -> bool {
|
||||
let probe_dir = if path.is_dir() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
path.parent().unwrap_or(path).to_path_buf()
|
||||
};
|
||||
let probe = probe_dir.join(format!(".claw-write-probe-{}", now_secs()));
|
||||
std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&probe)
|
||||
.and_then(|_| std::fs::remove_file(&probe))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn detect_trust_prompt(lowered: &str) -> bool {
|
||||
[
|
||||
"do you trust the files in this folder",
|
||||
@ -1285,6 +1448,8 @@ fn cwd_matches_observed_target(expected_cwd: &str, observed_cwd: &str) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn allowlisted_trust_prompt_auto_resolves_then_reaches_ready_state() {
|
||||
@ -1431,6 +1596,66 @@ mod tests {
|
||||
assert!(!readiness.ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_preflight_warns_when_task_file_is_absent_on_branch() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(tmp.path())
|
||||
.output()
|
||||
.expect("git init should run");
|
||||
fs::create_dir_all(tmp.path().join("src")).expect("src dir");
|
||||
fs::write(tmp.path().join("src/lib.rs"), "pub fn present() {}\n").expect("write file");
|
||||
Command::new("git")
|
||||
.args(["add", "src/lib.rs"])
|
||||
.current_dir(tmp.path())
|
||||
.output()
|
||||
.expect("git add should run");
|
||||
|
||||
let warnings = startup_preflight_warnings(
|
||||
tmp.path(),
|
||||
"Fix src/lib.rs and rust/crates/runtime/src/trident.rs before testing.",
|
||||
);
|
||||
|
||||
assert!(warnings.iter().any(|warning| {
|
||||
warning.kind == WorkerStartupPreflightWarningKind::FileAbsentOnBranch
|
||||
&& warning.path.as_deref() == Some("rust/crates/runtime/src/trident.rs")
|
||||
}));
|
||||
assert!(!warnings.iter().any(|warning| {
|
||||
warning.kind == WorkerStartupPreflightWarningKind::FileAbsentOnBranch
|
||||
&& warning.path.as_deref() == Some("src/lib.rs")
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_preflight_records_structured_warning_event() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(tmp.path())
|
||||
.output()
|
||||
.expect("git init should run");
|
||||
let registry = WorkerRegistry::new();
|
||||
let worker = registry.create(&tmp.path().display().to_string(), &[], true);
|
||||
|
||||
let observed = registry
|
||||
.observe_startup_preflight(&worker.worker_id, "Open missing/file.rs")
|
||||
.expect("preflight should run");
|
||||
|
||||
let event = observed
|
||||
.events
|
||||
.iter()
|
||||
.find(|event| event.kind == WorkerEventKind::StartupPreflightWarning)
|
||||
.expect("preflight warning event");
|
||||
assert!(matches!(
|
||||
event.payload,
|
||||
Some(WorkerEventPayload::StartupPreflightWarning {
|
||||
kind: WorkerStartupPreflightWarningKind::FileAbsentOnBranch,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_timeout_classifies_tool_permission_prompt() {
|
||||
let registry = WorkerRegistry::new();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user