Skip to content

Commit b5e30e2

Browse files
committed
Make completed review lanes emit machine-readable verdicts
The next repo-local sweep target was ROADMAP ultraworkers#67: scoped review lanes could stop with prose-only output, leaving downstream consumers to infer approval or rejection from later chatter. The fix teaches completed lane persistence to recognize review-style `APPROVE`/`REJECT`/`BLOCKED` results, attach structured verdict metadata to `lane.finished`, and keep ordinary non-review lanes on the existing quality-floor path. Constraint: Preserve the existing non-review lane summary path while enriching only review-style completions Rejected: Add a brand-new lane event type just for review results | unnecessary when `lane.finished` already carries structured metadata and downstream consumers can read it there Confidence: high Scope-risk: narrow Reversibility: clean Directive: If review verdict parsing changes later, update `extract_review_outcome`, the finished-event payload fields, and the review-lane regression together Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE Not-tested: External consumers that may still ignore `lane.finished.data.reviewVerdict`
1 parent dbc2824 commit b5e30e2

2 files changed

Lines changed: 94 additions & 3 deletions

File tree

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
496496

497497
62. **Worker state file surface not implemented****done (verified 2026-04-12):** current `main` already wires `emit_state_file(worker)` into the worker transition path in `rust/crates/runtime/src/worker_boot.rs`, atomically writes `.claw/worker-state.json`, and exposes the documented reader surface through `claw state` / `claw state --output-format json` in `rust/crates/rusty-claude-cli/src/main.rs`. Fresh proof exists in `runtime` regression `emit_state_file_writes_worker_status_on_transition`, the end-to-end `tools` regression `recovery_loop_state_file_reflects_transitions`, and direct CLI parsing coverage for `state` / `state --output-format json`. Source: Jobdori dogfood.
498498

499-
**Scope note (verified 2026-04-12):** ROADMAP #31, #43, and #63-#68 currently appear to describe acpx/droid or upstream OMX/server orchestration behavior, not claw-code source already present in this repository. Repo-local searches for `acpx`, `use-droid`, `run-acpx`, `commit-wrapper`, `ultraclaw`, `roadmap-nudge-10min`, `OMX_TMUX_INJECT`, `/hooks/health`, and `/hooks/status` found no implementation hits outside `ROADMAP.md`, and the earlier state-surface note already records that the HTTP server is not owned by claw-code. With #45 and #69 now fixed, the remaining unresolved items in this section look like external tracking notes rather than confirmed repo-local backlog; re-check if new repo-local evidence appears.
499+
**Scope note (verified 2026-04-12):** ROADMAP #31, #43, and #63-#68 currently appear to describe acpx/droid or upstream OMX/server orchestration behavior, not claw-code source already present in this repository. Repo-local searches for `acpx`, `use-droid`, `run-acpx`, `commit-wrapper`, `ultraclaw`, `roadmap-nudge-10min`, `OMX_TMUX_INJECT`, `/hooks/health`, and `/hooks/status` found no implementation hits outside `ROADMAP.md`, and the earlier state-surface note already records that the HTTP server is not owned by claw-code. With #45, #67, and #69 now fixed, the remaining unresolved items in this section look like external tracking notes rather than confirmed repo-local backlog; re-check if new repo-local evidence appears.
500500

501501
63. **Droid session completion semantics broken: code arrives after "status: completed"** — dogfooded 2026-04-12. Ultraclaw droid sessions (use-droid via acpx) report `session.status: completed` before file writes are fully flushed/synced to the working tree. Discovered +410 lines of "late-arriving" droid output that appeared after I had already assessed 8 sessions as "no code produced." This creates false-negative assessments and duplicate work. **Fix shape:** (a) droid agent should only report completion after explicit file-write confirmation (fsync or existence check); (b) or, claw-code should expose a `pending_writes` status that indicates "agent responded, disk flush pending"; (c) lane orchestrators should poll for file changes for N seconds after completion before final assessment. **Blocker:** none. Source: Jobdori ultraclaw dogfood 2026-04-12.
502502

@@ -506,7 +506,7 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
506506

507507
66. **Completion-aware reminder shutdown missing** — dogfooded 2026-04-12. Ultraclaw batch completed and was reported as done, but 10-minute cron reminder (`roadmap-nudge-10min`) kept firing into channel as if work still pending. Reminder/cron state not coupled to terminal task state. **Fix shape:** (a) cron jobs should check task completion state before firing; (b) or, provide explicit `cron.remove` on task completion; (c) or, reminders should include "work complete" detection and auto-expire. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
508508

509-
67. **Scoped review lanes do not emit structured verdicts**dogfooded 2026-04-12. OMX review lanes now have improved scope (specific ROADMAP items, specific files, explicit APPROVE/REJECT contract), but the stop event only contains the review request — not the actual verdict. Operators must infer approval/rejection/blockage from later git commits or surrounding chatter. **Fix shape:** emit structured review result on stop with: `verdict: approve|reject|blocked`, `target: commit/diff reviewed`, `rationale: short summary`. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
509+
67. **Scoped review lanes do not emit structured verdicts****done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now recognizes review-style `APPROVE`/`REJECT`/`BLOCKED` results and records structured `reviewVerdict`, `reviewTarget`, and `reviewRationale` metadata on the `lane.finished` event while preserving existing non-review lane behavior. Regression coverage locks both the normal completion path and a scoped review-lane completion payload. **Original filing below.**
510510

511511
68. **Internal reinjection/resume paths leak opaque control prose** — dogfooded 2026-04-12. OMX lanes stopping with `Continue from current mode state. [OMX_TMUX_INJECT]` expose internal implementation details instead of operator-meaningful state. The event tells us *that* tmux reinjection happened, but not *why* (retry after failure? resume after idle? manual recovery?), *what state was preserved*, or *what the lane was trying to do*. **Fix shape:** recovery/reinject events should emit structured cause like: `resume_after_stop`, `retry_after_tool_failure`, `tmux_reinject_after_idle`, `manual_recovery` plus preserved state / target lane info. Never leak bare internal markers like `[OMX_TMUX_INJECT]` as the primary summary. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
512512

rust/crates/tools/src/lib.rs

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3783,6 +3783,11 @@ fn persist_agent_terminal_state(
37833783
}
37843784

37853785
const MIN_LANE_SUMMARY_WORDS: usize = 7;
3786+
const REVIEW_VERDICTS: &[(&str, &str)] = &[
3787+
("APPROVE", "approve"),
3788+
("REJECT", "reject"),
3789+
("BLOCKED", "blocked"),
3790+
];
37863791
const CONTROL_ONLY_SUMMARY_WORDS: &[&str] = &[
37873792
"ack",
37883793
"commit",
@@ -3831,6 +3836,12 @@ struct LaneFinishedSummaryData {
38313836
raw_summary: Option<String>,
38323837
#[serde(rename = "wordCount")]
38333838
word_count: usize,
3839+
#[serde(rename = "reviewVerdict", skip_serializing_if = "Option::is_none")]
3840+
review_verdict: Option<String>,
3841+
#[serde(rename = "reviewTarget", skip_serializing_if = "Option::is_none")]
3842+
review_target: Option<String>,
3843+
#[serde(rename = "reviewRationale", skip_serializing_if = "Option::is_none")]
3844+
review_rationale: Option<String>,
38343845
}
38353846

38363847
#[derive(Debug, Clone)]
@@ -3844,6 +3855,13 @@ struct LaneSummaryAssessment {
38443855
apply_quality_floor: bool,
38453856
reasons: Vec<String>,
38463857
word_count: usize,
3858+
review_outcome: Option<ReviewLaneOutcome>,
3859+
}
3860+
3861+
#[derive(Debug, Clone)]
3862+
struct ReviewLaneOutcome {
3863+
verdict: String,
3864+
rationale: Option<String>,
38473865
}
38483866

38493867
fn build_lane_finished_summary(
@@ -3857,6 +3875,12 @@ fn build_lane_finished_summary(
38573875
Some(summary) => Some(compose_lane_summary_fallback(manifest, Some(summary))),
38583876
None => Some(compose_lane_summary_fallback(manifest, None)),
38593877
};
3878+
let review_outcome = assessment.review_outcome.clone();
3879+
let review_target = review_outcome
3880+
.as_ref()
3881+
.map(|_| manifest.description.trim())
3882+
.filter(|value| !value.is_empty())
3883+
.map(str::to_string);
38603884

38613885
LaneFinishedSummary {
38623886
detail,
@@ -3865,6 +3889,11 @@ fn build_lane_finished_summary(
38653889
reasons: assessment.reasons,
38663890
raw_summary: raw_summary.map(str::to_string),
38673891
word_count: assessment.word_count,
3892+
review_verdict: review_outcome
3893+
.as_ref()
3894+
.map(|outcome| outcome.verdict.clone()),
3895+
review_target,
3896+
review_rationale: review_outcome.and_then(|outcome| outcome.rationale),
38683897
},
38693898
}
38703899
}
@@ -3882,18 +3911,21 @@ fn assess_lane_summary_quality(summary: &str) -> LaneSummaryAssessment {
38823911
reasons.push(String::from("empty"));
38833912
}
38843913

3914+
let review_outcome = extract_review_outcome(summary);
3915+
38853916
let control_only = !words.is_empty()
38863917
&& words
38873918
.iter()
38883919
.all(|word| CONTROL_ONLY_SUMMARY_WORDS.contains(&word.as_str()));
3889-
if control_only {
3920+
if control_only && review_outcome.is_none() {
38903921
reasons.push(String::from("control_only"));
38913922
}
38923923

38933924
let has_context_signal = summary.contains('`')
38943925
|| summary.contains('/')
38953926
|| summary.contains(':')
38963927
|| summary.contains('#')
3928+
|| review_outcome.is_some()
38973929
|| words
38983930
.iter()
38993931
.any(|word| CONTEXTUAL_SUMMARY_WORDS.contains(&word.as_str()));
@@ -3905,6 +3937,7 @@ fn assess_lane_summary_quality(summary: &str) -> LaneSummaryAssessment {
39053937
apply_quality_floor: !reasons.is_empty(),
39063938
reasons,
39073939
word_count,
3940+
review_outcome,
39083941
}
39093942
}
39103943

@@ -3928,6 +3961,24 @@ fn compose_lane_summary_fallback(manifest: &AgentOutput, raw_summary: Option<&st
39283961
}
39293962
}
39303963

3964+
fn extract_review_outcome(summary: &str) -> Option<ReviewLaneOutcome> {
3965+
let mut lines = summary
3966+
.lines()
3967+
.map(str::trim)
3968+
.filter(|line| !line.is_empty());
3969+
let first = lines.next()?;
3970+
let verdict = REVIEW_VERDICTS.iter().find_map(|(prefix, verdict)| {
3971+
first
3972+
.eq_ignore_ascii_case(prefix)
3973+
.then(|| (*verdict).to_string())
3974+
})?;
3975+
let rationale = lines.collect::<Vec<_>>().join(" ").trim().to_string();
3976+
Some(ReviewLaneOutcome {
3977+
verdict,
3978+
rationale: (!rationale.is_empty()).then_some(compress_summary_text(&rationale)),
3979+
})
3980+
}
3981+
39313982
fn derive_agent_state(
39323983
status: &str,
39333984
result: Option<&str>,
@@ -7522,6 +7573,46 @@ mod tests {
75227573
"control_only"
75237574
);
75247575

7576+
let review = execute_agent_with_spawn(
7577+
AgentInput {
7578+
description: "Review commit 1234abcd for ROADMAP #67".to_string(),
7579+
prompt: "Review the scoped diff".to_string(),
7580+
subagent_type: Some("Verification".to_string()),
7581+
name: Some("review-lane".to_string()),
7582+
model: None,
7583+
},
7584+
|job| {
7585+
persist_agent_terminal_state(
7586+
&job.manifest,
7587+
"completed",
7588+
Some("APPROVE\n\nTarget: commit 1234abcd\nRationale: scoped diff is safe."),
7589+
None,
7590+
)
7591+
},
7592+
)
7593+
.expect("review agent should succeed");
7594+
7595+
let review_manifest =
7596+
std::fs::read_to_string(&review.manifest_file).expect("review manifest should exist");
7597+
let review_manifest_json: serde_json::Value =
7598+
serde_json::from_str(&review_manifest).expect("review manifest json");
7599+
assert_eq!(
7600+
review_manifest_json["laneEvents"][1]["data"]["reviewVerdict"],
7601+
"approve"
7602+
);
7603+
assert_eq!(
7604+
review_manifest_json["laneEvents"][1]["data"]["reviewTarget"],
7605+
"Review commit 1234abcd for ROADMAP #67"
7606+
);
7607+
assert_eq!(
7608+
review_manifest_json["laneEvents"][1]["data"]["reviewRationale"],
7609+
"Target: commit 1234abcd Rationale: scoped diff is safe."
7610+
);
7611+
assert_eq!(
7612+
review_manifest_json["laneEvents"][1]["data"]["qualityFloorApplied"],
7613+
false
7614+
);
7615+
75257616
let spawn_error = execute_agent_with_spawn(
75267617
AgentInput {
75277618
description: "Spawn error task".to_string(),

0 commit comments

Comments
 (0)