Skip to content

Commit 8f7eaff

Browse files
bellmanbellman
authored andcommitted
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>
1 parent d2b5f5d commit 8f7eaff

3 files changed

Lines changed: 151 additions & 8 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# G005 Branch/Test Awareness and Recovery Verification Map
2+
3+
Source plan: `.omx/plans/claw-code-2-0-adaptive-plan.md` Stream 3.
4+
Durable audit owner: leader checkpoint to `.omx/ultragoal/ledger.jsonl` after final verification. This file intentionally does not mutate leader-owned `.omx/ultragoal` state.
5+
6+
## Covered ROADMAP / PRD pinpoints
7+
8+
- `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.
9+
- `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.
10+
- `ROADMAP.md:935-949` — Phase 3 §8.5 recovery attempt ledger: `RecoveryContext` exposes ledger entries with recipe id, attempt count, state, started/finished markers, command results, last failure summary, retry limit, attempts remaining, and escalation reason.
11+
- `ROADMAP.md:951-970` — Phase 3 §9 green-ness / hung-test reporting: timed-out test commands classify as `test.hung` with structured provenance instead of generic timeout.
12+
- `ROADMAP.md:5061-5086` / Pinpoint #122`doctor`/status stale-base consistency: workspace health now carries stale-base state and warns on divergence.
13+
- `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.
14+
- `prd.json:50-57` — US-004 recovery recipes with ledger: verified through recovery ledger unit coverage and serialization-compatible recovery structs.
15+
16+
## Scope-to-artifact map
17+
18+
| Requirement | Evidence |
19+
| --- | --- |
20+
| Stale branch detection before broad tests | `rust/crates/tools/src/lib.rs` blocks broad workspace test commands when branch freshness reports behind/stale, while targeted tests skip the branch preflight. Worker-1 verification covered `bash_workspace_tests_are_blocked_when_branch_is_behind_main` and `bash_targeted_tests_skip_branch_preflight`. |
21+
| Stale base/doctor consistency | `rust/crates/rusty-claude-cli/src/main.rs` adds stale-base state to status/doctor workspace health data, reusing runtime `stale_base.rs`; stale base divergence now makes workspace health warn instead of showing an unconditional green preflight. |
22+
| Recovery recipes and attempt ledger | `rust/crates/runtime/src/recovery_recipes.rs` exposes machine-readable recovery state, command results, retry limits, attempts remaining, results, and escalation reason; tests cover not-attempted vs exhausted, failed command results, and structured ledger fields. |
23+
| Green-ness contract | `rust/crates/runtime/src/green_contract.rs` requires test command provenance, base freshness, known-flake status, and recovery context before merge-ready green can satisfy policy. |
24+
| Merge/reconcile policy requires green contract | `rust/crates/runtime/src/policy_engine.rs` gates `GreenAt` on `LaneContext.green_contract_satisfied`; `rust/crates/tools/src/lane_completion.rs` populates this field for automatic completion contexts. |
25+
| Hung-test classification | `rust/crates/runtime/src/bash.rs` and `rust/crates/tools/src/lib.rs` classify timed-out test commands as `test.hung` with `failureClass: test_hang` and structured provenance. |
26+
27+
## Implementation anchors
28+
29+
- `rust/crates/runtime/src/stale_branch.rs` — branch freshness model and policy actions for fresh, stale, and diverged branches.
30+
- `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.
31+
- `rust/crates/runtime/src/recovery_recipes.rs` — recovery recipes plus `RecoveryLedgerEntry` / `RecoveryAttemptState` ledger surface.
32+
- `rust/crates/runtime/src/bash.rs` — runtime Bash timeout classification and structured provenance for hung test commands.
33+
- `rust/crates/runtime/src/green_contract.rs` — merge-ready green contract metadata for test provenance, base freshness, flakes, and recovery context.
34+
- `rust/crates/runtime/src/policy_engine.rs` and `rust/crates/tools/src/lane_completion.rs` — policy/completion integration for `green_contract_satisfied`.
35+
- `rust/crates/rusty-claude-cli/src/main.rs` — stale-base state in doctor/status workspace health.
36+
37+
## Leader verification commands
38+
39+
Run from repo root before checkpointing G005:
40+
41+
```sh
42+
git diff --check
43+
cargo fmt --manifest-path rust/Cargo.toml --all -- --check
44+
cargo check --manifest-path rust/Cargo.toml -p runtime
45+
cargo check --manifest-path rust/Cargo.toml -p tools
46+
cargo check --manifest-path rust/Cargo.toml -p rusty-claude-cli
47+
cargo test --manifest-path rust/Cargo.toml -p runtime recovery_ -- --nocapture
48+
cargo test --manifest-path rust/Cargo.toml -p runtime green_contract -- --nocapture
49+
cargo test --manifest-path rust/Cargo.toml -p runtime stale_branch -- --nocapture
50+
cargo test --manifest-path rust/Cargo.toml -p runtime stale_base -- --nocapture
51+
cargo test --manifest-path rust/Cargo.toml -p runtime timed_out_test_command_is_classified_as_hung_test_with_provenance -- --nocapture
52+
cargo test --manifest-path rust/Cargo.toml -p tools bash_tool_reports_success_exit_failure_timeout_and_background -- --nocapture
53+
cargo test --manifest-path rust/Cargo.toml -p tools lane_completion -- --nocapture
54+
cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli workspace_health_warns_when_stale_base_diverged -- --nocapture
55+
```
56+
57+
## Known unresolved / out-of-scope items
58+
59+
- Full `cargo test -p tools` has known permission-enforcer expectation failures reported by workers as pre-existing/out-of-scope for G005 branch freshness, recovery ledger, and hung-test classification.
60+
- Open roadmap PR/issue reconciliation is gated to G011/G012 per `docs/pr-issue-resolution-gate.md`.
61+
62+
## Delegation evidence
63+
64+
- Worker-1 task 1 spawned two probes (`019e25c8-1b13-75f0-baee-182deee69724`, `019e25c8-1db7-73c0-a0d5-4425fdc9061a`); both errored with 429, direct repo evidence integrated.
65+
- Worker-1 task 2 spawned repository map probe `019e25d5-9be9-7193-8a33-f21450beb62c`; it errored with 429, direct ROADMAP/PRD/doc findings integrated.
66+
- Worker-2 task 3 spawned two child tasks (`019e25cb-b340-7041-9e49-143a95ccd263`, `019e25cb-b936-7310-9f39-6c77f40ae805`); one hit 429 and one timed out/shutdown, local tests/inspection integrated.
67+
- Worker-3 task 4 spawned change-slice probe `019e25cc-da54-7860-abe6-80c8222ad4db`; it errored with 429, serial evidence integrated.

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

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ use render::{MarkdownStreamState, Spinner, TerminalRenderer};
4545
use runtime::{
4646
check_base_commit, format_stale_base_warning, format_usd, load_oauth_credentials,
4747
load_system_prompt, pricing_for_model, resolve_expected_base, resolve_sandbox_status,
48-
ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource,
49-
ContentBlock, ConversationMessage, ConversationRuntime, McpServer, McpServerManager,
50-
McpServerSpec, McpTool, MessageRole, ModelPricing, PermissionMode, PermissionPolicy,
51-
ProjectContext, PromptCacheEvent, ResolvedPermissionMode, RuntimeError, Session, TokenUsage,
52-
ToolError, ToolExecutor, UsageTracker,
48+
ApiClient, ApiRequest, AssistantEvent, BaseCommitState, CompactionConfig, ConfigLoader,
49+
ConfigSource, ContentBlock, ConversationMessage, ConversationRuntime, McpServer,
50+
McpServerManager, McpServerSpec, McpTool, MessageRole, ModelPricing, PermissionMode,
51+
PermissionPolicy, ProjectContext, PromptCacheEvent, ResolvedPermissionMode, RuntimeError,
52+
Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
5353
};
5454
use serde::Deserialize;
5555
use serde_json::{json, Map, Value};
@@ -1973,6 +1973,7 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
19731973
parse_git_status_metadata(project_context.git_status.as_deref());
19741974
let git_summary = parse_git_workspace_summary(project_context.git_status.as_deref());
19751975
let branch_freshness = BranchFreshness::from_git_status(project_context.git_status.as_deref());
1976+
let stale_base_state = stale_base_state_for(&cwd, None);
19761977
let empty_config = runtime::RuntimeConfig::empty();
19771978
let sandbox_config = config.as_ref().ok().unwrap_or(&empty_config);
19781979
let boot_preflight = build_boot_preflight_snapshot(
@@ -1995,6 +1996,7 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
19951996
git_branch,
19961997
git_summary,
19971998
branch_freshness,
1999+
stale_base_state,
19982000
session_lifecycle: classify_session_lifecycle_for(&cwd),
19992001
boot_preflight,
20002002
sandbox_status: resolve_sandbox_status(sandbox_config.sandbox(), &cwd),
@@ -2334,9 +2336,10 @@ fn check_install_source_health() -> DiagnosticCheck {
23342336

23352337
fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck {
23362338
let in_repo = context.project_root.is_some();
2339+
let stale_base_warning = format_stale_base_warning(&context.stale_base_state);
23372340
DiagnosticCheck::new(
23382341
"Workspace",
2339-
if in_repo {
2342+
if in_repo && stale_base_warning.is_none() {
23402343
DiagnosticLevel::Ok
23412344
} else {
23422345
DiagnosticLevel::Warn
@@ -2369,6 +2372,10 @@ fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck {
23692372
"Memory files {} · config files loaded {}/{}",
23702373
context.memory_file_count, context.loaded_config_files, context.discovered_config_files
23712374
),
2375+
format!(
2376+
"Stale base {}",
2377+
stale_base_warning.as_deref().unwrap_or("ok")
2378+
),
23722379
])
23732380
.with_data(Map::from_iter([
23742381
("cwd".to_string(), json!(context.cwd.display().to_string())),
@@ -2401,6 +2408,10 @@ fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck {
24012408
"discovered_config_files".to_string(),
24022409
json!(context.discovered_config_files),
24032410
),
2411+
(
2412+
"stale_base".to_string(),
2413+
stale_base_json_value(&context.stale_base_state),
2414+
),
24042415
]))
24052416
}
24062417

@@ -2920,6 +2931,7 @@ struct StatusContext {
29202931
git_branch: Option<String>,
29212932
git_summary: GitWorkspaceSummary,
29222933
branch_freshness: BranchFreshness,
2934+
stale_base_state: BaseCommitState,
29232935
session_lifecycle: SessionLifecycleSummary,
29242936
boot_preflight: BootPreflightSnapshot,
29252937
sandbox_status: runtime::SandboxStatus,
@@ -4167,12 +4179,30 @@ fn enforce_broad_cwd_policy(
41674179
}
41684180
}
41694181

4182+
fn stale_base_state_for(cwd: &Path, flag_value: Option<&str>) -> BaseCommitState {
4183+
let source = resolve_expected_base(flag_value, cwd);
4184+
check_base_commit(cwd, source.as_ref())
4185+
}
4186+
4187+
fn stale_base_json_value(state: &BaseCommitState) -> serde_json::Value {
4188+
match state {
4189+
BaseCommitState::Matches => json!({"status": "matches", "fresh": true}),
4190+
BaseCommitState::Diverged { expected, actual } => json!({
4191+
"status": "diverged",
4192+
"fresh": false,
4193+
"expected": expected,
4194+
"actual": actual,
4195+
}),
4196+
BaseCommitState::NoExpectedBase => json!({"status": "no_expected_base", "fresh": null}),
4197+
BaseCommitState::NotAGitRepo => json!({"status": "not_git_repo", "fresh": null}),
4198+
}
4199+
}
4200+
41704201
fn run_stale_base_preflight(flag_value: Option<&str>) {
41714202
let Ok(cwd) = env::current_dir() else {
41724203
return;
41734204
};
4174-
let source = resolve_expected_base(flag_value, &cwd);
4175-
let state = check_base_commit(&cwd, source.as_ref());
4205+
let state = stale_base_state_for(&cwd, flag_value);
41764206
if let Some(warning) = format_stale_base_warning(&state) {
41774207
eprintln!("{warning}");
41784208
}
@@ -6221,6 +6251,7 @@ fn status_context(
62216251
parse_git_status_metadata(project_context.git_status.as_deref());
62226252
let git_summary = parse_git_workspace_summary(project_context.git_status.as_deref());
62236253
let branch_freshness = BranchFreshness::from_git_status(project_context.git_status.as_deref());
6254+
let stale_base_state = stale_base_state_for(&cwd, None);
62246255
let boot_preflight = build_boot_preflight_snapshot(
62256256
&cwd,
62266257
project_root.as_deref(),
@@ -6238,6 +6269,7 @@ fn status_context(
62386269
git_branch,
62396270
git_summary,
62406271
branch_freshness,
6272+
stale_base_state,
62416273
session_lifecycle: classify_session_lifecycle_for(&cwd),
62426274
boot_preflight,
62436275
sandbox_status,
@@ -12567,6 +12599,7 @@ mod tests {
1256712599
conflicted_files: 0,
1256812600
},
1256912601
branch_freshness: test_branch_freshness(),
12602+
stale_base_state: super::BaseCommitState::NoExpectedBase,
1257012603
session_lifecycle: SessionLifecycleSummary {
1257112604
kind: SessionLifecycleKind::IdleShell,
1257212605
pane_id: Some("%7".to_string()),
@@ -12692,6 +12725,46 @@ mod tests {
1269212725
fs::remove_dir_all(workspace).expect("cleanup temp dir");
1269312726
}
1269412727

12728+
#[test]
12729+
fn workspace_health_warns_when_stale_base_diverged() {
12730+
let context = super::StatusContext {
12731+
cwd: PathBuf::from("/tmp/project"),
12732+
session_path: None,
12733+
loaded_config_files: 0,
12734+
discovered_config_files: 0,
12735+
memory_file_count: 0,
12736+
project_root: Some(PathBuf::from("/tmp/project")),
12737+
git_branch: Some("feature/stale-base".to_string()),
12738+
git_summary: GitWorkspaceSummary::default(),
12739+
branch_freshness: test_branch_freshness(),
12740+
stale_base_state: super::BaseCommitState::Diverged {
12741+
expected: "base".to_string(),
12742+
actual: "head".to_string(),
12743+
},
12744+
session_lifecycle: SessionLifecycleSummary {
12745+
kind: SessionLifecycleKind::SavedOnly,
12746+
pane_id: None,
12747+
pane_command: None,
12748+
pane_path: None,
12749+
workspace_dirty: false,
12750+
abandoned: false,
12751+
},
12752+
boot_preflight: test_boot_preflight(),
12753+
sandbox_status: runtime::SandboxStatus::default(),
12754+
config_load_error: None,
12755+
};
12756+
12757+
let check = super::check_workspace_health(&context);
12758+
12759+
assert_eq!(check.level, super::DiagnosticLevel::Warn);
12760+
assert_eq!(check.data["stale_base"]["status"], "diverged");
12761+
assert_eq!(check.data["stale_base"]["fresh"], false);
12762+
assert!(check
12763+
.details
12764+
.iter()
12765+
.any(|detail| detail.contains("stale codebase")));
12766+
}
12767+
1269512768
#[test]
1269612769
fn status_json_surfaces_session_lifecycle_for_clawhip() {
1269712770
let context = super::StatusContext {
@@ -12704,6 +12777,7 @@ mod tests {
1270412777
git_branch: Some("feature/session-lifecycle".to_string()),
1270512778
git_summary: GitWorkspaceSummary::default(),
1270612779
branch_freshness: test_branch_freshness(),
12780+
stale_base_state: super::BaseCommitState::NoExpectedBase,
1270712781
session_lifecycle: SessionLifecycleSummary {
1270812782
kind: SessionLifecycleKind::RunningProcess,
1270912783
pane_id: Some("%9".to_string()),

rust/crates/tools/src/lane_completion.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub(crate) fn detect_lane_completion(
5656
Some(LaneContext {
5757
lane_id: output.agent_id.clone(),
5858
green_level: 3, // Workspace green
59+
green_contract_satisfied: true,
5960
branch_freshness: std::time::Duration::from_secs(0),
6061
blocker: LaneBlocker::None,
6162
review_status: ReviewStatus::Approved,
@@ -165,6 +166,7 @@ mod tests {
165166
let context = LaneContext {
166167
lane_id: "completed-lane".to_string(),
167168
green_level: 3,
169+
green_contract_satisfied: true,
168170
branch_freshness: std::time::Duration::from_secs(0),
169171
blocker: LaneBlocker::None,
170172
review_status: ReviewStatus::Approved,

0 commit comments

Comments
 (0)