From e75d67dfd34f270b4c6d1b9ba77d04e90e0bd089 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Sun, 12 Apr 2026 11:56:00 +0000 Subject: [PATCH] Make successful lanes explain what artifacts they actually produced The next repo-local sweep target was ROADMAP #64: downstream consumers still had to infer artifact provenance from prose even though the repo already emitted structured lane events. The fix extends `lane.finished` metadata with structured artifact provenance so successful completions can report roadmap ids, files, diff stat, verification state, and commit sha without relying on narration alone. Constraint: Preserve the existing commit-created event and lane-finished metadata paths while adding structured provenance to successful completions Rejected: Introduce a separate artifact event type first | unnecessary for this focused closeout because `lane.finished` already carries structured data and existing consumers can read it there Confidence: high Scope-risk: narrow Reversibility: clean Directive: If artifact provenance extraction rules change later, update `extract_artifact_provenance`, its regression payload, and the ROADMAP closeout together Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE Not-tested: Downstream consumers that ignore `lane.finished.data.artifactProvenance` and still parse only prose output --- ROADMAP.md | 2 +- rust/crates/tools/src/lib.rs | 181 +++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 63925ca..00c9109 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -500,7 +500,7 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes 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. -64. **Artifact provenance is post-hoc narration, not structured events** — dogfooded 2026-04-12. The ultraclaw batch delivered 4 ROADMAP items and 3 commits, but the event stream only contained log-shaped text ("+410 lines detected", "committing...", "pushed"). Downstream consumers (clawhip, lane orchestrators, monitors) must reconstruct provenance from chat messages rather than consuming first-class events. **Fix shape:** emit structured artifact/result events with: `sourceLanes`, `roadmapIds`, `files`, `diffStat`, `verification: tested|committed|pushed|merged`, `commitSha`. Remove dependency on human/bot narration layer to explain what actually landed. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12. +64. **Artifact provenance is post-hoc narration, not structured events** — **done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now attaches structured `artifactProvenance` metadata to `lane.finished`, including `sourceLanes`, `roadmapIds`, `files`, `diffStat`, `verification`, and `commitSha`, while keeping the existing `lane.commit.created` provenance event intact. Regression coverage locks a successful completion payload that carries roadmap ids, file paths, diff stat, verification states, and commit sha without relying on prose re-parsing. **Original filing below.** 65. **Backlog-scanning team lanes emit opaque stops, not structured selection outcomes** — **done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now recognizes backlog-scan selection summaries and records structured `selectionOutcome` metadata on `lane.finished`, including `chosenItems`, `skippedItems`, `action`, and optional `rationale`, while preserving existing non-selection and review-lane behavior. Regression coverage locks the structured backlog-scan payload alongside the earlier quality-floor and review-verdict paths. **Original filing below.** diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index 5561193..abbad7f 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -3844,6 +3844,8 @@ struct LaneFinishedSummaryData { review_rationale: Option, #[serde(rename = "selectionOutcome", skip_serializing_if = "Option::is_none")] selection_outcome: Option, + #[serde(rename = "artifactProvenance", skip_serializing_if = "Option::is_none")] + artifact_provenance: Option, } #[derive(Debug, Clone)] @@ -3877,6 +3879,22 @@ struct SelectionOutcome { rationale: Option, } +#[derive(Debug, Clone, Serialize)] +struct ArtifactProvenance { + #[serde(rename = "sourceLanes", skip_serializing_if = "Vec::is_empty")] + source_lanes: Vec, + #[serde(rename = "roadmapIds", skip_serializing_if = "Vec::is_empty")] + roadmap_ids: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + files: Vec, + #[serde(rename = "diffStat", skip_serializing_if = "Option::is_none")] + diff_stat: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + verification: Vec, + #[serde(rename = "commitSha", skip_serializing_if = "Option::is_none")] + commit_sha: Option, +} + fn build_lane_finished_summary( manifest: &AgentOutput, result: Option<&str>, @@ -3894,6 +3912,7 @@ fn build_lane_finished_summary( .map(|_| manifest.description.trim()) .filter(|value| !value.is_empty()) .map(str::to_string); + let artifact_provenance = extract_artifact_provenance(manifest, raw_summary); LaneFinishedSummary { detail, @@ -3908,6 +3927,7 @@ fn build_lane_finished_summary( review_target, review_rationale: review_outcome.and_then(|outcome| outcome.rationale), selection_outcome: extract_selection_outcome(raw_summary.unwrap_or_default()), + artifact_provenance, }, } } @@ -4084,6 +4104,102 @@ fn extract_roadmap_items(line: &str) -> Vec { items } +fn extract_artifact_provenance( + manifest: &AgentOutput, + raw_summary: Option<&str>, +) -> Option { + let summary = raw_summary?; + let mut roadmap_ids = extract_roadmap_items(summary); + roadmap_ids.extend(extract_roadmap_items(&manifest.description)); + roadmap_ids.sort(); + roadmap_ids.dedup(); + + let mut files = extract_file_paths(summary); + files.sort(); + files.dedup(); + + let mut verification = Vec::new(); + let lowered = summary.to_ascii_lowercase(); + for (needle, label) in [ + ("tested", "tested"), + ("committed", "committed"), + ("pushed", "pushed"), + ("merged", "merged"), + ] { + if lowered.contains(needle) { + verification.push(label.to_string()); + } + } + + let commit_sha = extract_commit_sha(summary); + let diff_stat = extract_diff_stat(summary); + let source_lanes = vec![manifest.name.clone()]; + + if roadmap_ids.is_empty() + && files.is_empty() + && verification.is_empty() + && commit_sha.is_none() + && diff_stat.is_none() + { + return None; + } + + Some(ArtifactProvenance { + source_lanes, + roadmap_ids, + files, + diff_stat, + verification, + commit_sha, + }) +} + +fn extract_file_paths(summary: &str) -> Vec { + summary + .split(|ch: char| ch.is_whitespace() || matches!(ch, ',' | ';' | '(' | ')' | '[' | ']')) + .map(|token| { + token + .trim_matches('`') + .trim_matches('"') + .trim_matches('\'') + .trim_end_matches('.') + }) + .filter(|token| { + token.contains('.') + && !token.starts_with("http") + && !token + .chars() + .all(|ch| ch.is_ascii_digit() || ch == '.' || ch == '+' || ch == '-') + }) + .map(str::to_string) + .collect() +} + +fn extract_diff_stat(summary: &str) -> Option { + summary + .split('\n') + .map(str::trim) + .find_map(|line| { + line.find("Diff stat:") + .map(|index| normalize_diff_stat(&line[(index + "Diff stat:".len())..])) + .or_else(|| { + line.find("Diff:") + .map(|index| normalize_diff_stat(&line[(index + "Diff:".len())..])) + }) + }) + .filter(|value| !value.is_empty()) +} + +fn normalize_diff_stat(value: &str) -> String { + let trimmed = value.trim(); + for marker in [" Tested", " Committed", " committed", " pushed", " merged"] { + if let Some((prefix, _)) = trimmed.split_once(marker) { + return prefix.trim().to_string(); + } + } + trimmed.to_string() +} + fn derive_agent_state( status: &str, result: Option<&str>, @@ -7764,6 +7880,71 @@ mod tests { "#65 is the next repo-local lane-finished metadata task." ); + let artifact = execute_agent_with_spawn( + AgentInput { + description: "Land ROADMAP #64 provenance hardening".to_string(), + prompt: "Ship structured artifact provenance".to_string(), + subagent_type: Some("Explore".to_string()), + name: Some("artifact-lane".to_string()), + model: None, + }, + |job| { + persist_agent_terminal_state( + &job.manifest, + "completed", + Some( + "Completed ROADMAP #64. Files: rust/crates/tools/src/lib.rs ROADMAP.md. Diff stat: 2 files, +12/-1. Tested, committed, pushed as commit deadbee.", + ), + None, + ) + }, + ) + .expect("artifact agent should succeed"); + + let artifact_manifest = std::fs::read_to_string(&artifact.manifest_file) + .expect("artifact manifest should exist"); + let artifact_manifest_json: serde_json::Value = + serde_json::from_str(&artifact_manifest).expect("artifact manifest json"); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["sourceLanes"][0], + "artifact-lane" + ); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["roadmapIds"][0], + "ROADMAP #64" + ); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["files"][0], + "ROADMAP.md" + ); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["files"][1], + "rust/crates/tools/src/lib.rs" + ); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["diffStat"], + "2 files, +12/-1." + ); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["verification"] + [0], + "tested" + ); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["verification"] + [1], + "committed" + ); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["verification"] + [2], + "pushed" + ); + assert_eq!( + artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["commitSha"], + "deadbee" + ); + let spawn_error = execute_agent_with_spawn( AgentInput { description: "Spawn error task".to_string(),