Skip to content

Commit e75d67d

Browse files
committed
Make successful lanes explain what artifacts they actually produced
The next repo-local sweep target was ROADMAP ultraworkers#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
1 parent 2e34949 commit e75d67d

2 files changed

Lines changed: 182 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
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

503-
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.
503+
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.**
504504

505505
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.**
506506

rust/crates/tools/src/lib.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3844,6 +3844,8 @@ struct LaneFinishedSummaryData {
38443844
review_rationale: Option<String>,
38453845
#[serde(rename = "selectionOutcome", skip_serializing_if = "Option::is_none")]
38463846
selection_outcome: Option<SelectionOutcome>,
3847+
#[serde(rename = "artifactProvenance", skip_serializing_if = "Option::is_none")]
3848+
artifact_provenance: Option<ArtifactProvenance>,
38473849
}
38483850

38493851
#[derive(Debug, Clone)]
@@ -3877,6 +3879,22 @@ struct SelectionOutcome {
38773879
rationale: Option<String>,
38783880
}
38793881

3882+
#[derive(Debug, Clone, Serialize)]
3883+
struct ArtifactProvenance {
3884+
#[serde(rename = "sourceLanes", skip_serializing_if = "Vec::is_empty")]
3885+
source_lanes: Vec<String>,
3886+
#[serde(rename = "roadmapIds", skip_serializing_if = "Vec::is_empty")]
3887+
roadmap_ids: Vec<String>,
3888+
#[serde(skip_serializing_if = "Vec::is_empty")]
3889+
files: Vec<String>,
3890+
#[serde(rename = "diffStat", skip_serializing_if = "Option::is_none")]
3891+
diff_stat: Option<String>,
3892+
#[serde(skip_serializing_if = "Vec::is_empty")]
3893+
verification: Vec<String>,
3894+
#[serde(rename = "commitSha", skip_serializing_if = "Option::is_none")]
3895+
commit_sha: Option<String>,
3896+
}
3897+
38803898
fn build_lane_finished_summary(
38813899
manifest: &AgentOutput,
38823900
result: Option<&str>,
@@ -3894,6 +3912,7 @@ fn build_lane_finished_summary(
38943912
.map(|_| manifest.description.trim())
38953913
.filter(|value| !value.is_empty())
38963914
.map(str::to_string);
3915+
let artifact_provenance = extract_artifact_provenance(manifest, raw_summary);
38973916

38983917
LaneFinishedSummary {
38993918
detail,
@@ -3908,6 +3927,7 @@ fn build_lane_finished_summary(
39083927
review_target,
39093928
review_rationale: review_outcome.and_then(|outcome| outcome.rationale),
39103929
selection_outcome: extract_selection_outcome(raw_summary.unwrap_or_default()),
3930+
artifact_provenance,
39113931
},
39123932
}
39133933
}
@@ -4084,6 +4104,102 @@ fn extract_roadmap_items(line: &str) -> Vec<String> {
40844104
items
40854105
}
40864106

4107+
fn extract_artifact_provenance(
4108+
manifest: &AgentOutput,
4109+
raw_summary: Option<&str>,
4110+
) -> Option<ArtifactProvenance> {
4111+
let summary = raw_summary?;
4112+
let mut roadmap_ids = extract_roadmap_items(summary);
4113+
roadmap_ids.extend(extract_roadmap_items(&manifest.description));
4114+
roadmap_ids.sort();
4115+
roadmap_ids.dedup();
4116+
4117+
let mut files = extract_file_paths(summary);
4118+
files.sort();
4119+
files.dedup();
4120+
4121+
let mut verification = Vec::new();
4122+
let lowered = summary.to_ascii_lowercase();
4123+
for (needle, label) in [
4124+
("tested", "tested"),
4125+
("committed", "committed"),
4126+
("pushed", "pushed"),
4127+
("merged", "merged"),
4128+
] {
4129+
if lowered.contains(needle) {
4130+
verification.push(label.to_string());
4131+
}
4132+
}
4133+
4134+
let commit_sha = extract_commit_sha(summary);
4135+
let diff_stat = extract_diff_stat(summary);
4136+
let source_lanes = vec![manifest.name.clone()];
4137+
4138+
if roadmap_ids.is_empty()
4139+
&& files.is_empty()
4140+
&& verification.is_empty()
4141+
&& commit_sha.is_none()
4142+
&& diff_stat.is_none()
4143+
{
4144+
return None;
4145+
}
4146+
4147+
Some(ArtifactProvenance {
4148+
source_lanes,
4149+
roadmap_ids,
4150+
files,
4151+
diff_stat,
4152+
verification,
4153+
commit_sha,
4154+
})
4155+
}
4156+
4157+
fn extract_file_paths(summary: &str) -> Vec<String> {
4158+
summary
4159+
.split(|ch: char| ch.is_whitespace() || matches!(ch, ',' | ';' | '(' | ')' | '[' | ']'))
4160+
.map(|token| {
4161+
token
4162+
.trim_matches('`')
4163+
.trim_matches('"')
4164+
.trim_matches('\'')
4165+
.trim_end_matches('.')
4166+
})
4167+
.filter(|token| {
4168+
token.contains('.')
4169+
&& !token.starts_with("http")
4170+
&& !token
4171+
.chars()
4172+
.all(|ch| ch.is_ascii_digit() || ch == '.' || ch == '+' || ch == '-')
4173+
})
4174+
.map(str::to_string)
4175+
.collect()
4176+
}
4177+
4178+
fn extract_diff_stat(summary: &str) -> Option<String> {
4179+
summary
4180+
.split('\n')
4181+
.map(str::trim)
4182+
.find_map(|line| {
4183+
line.find("Diff stat:")
4184+
.map(|index| normalize_diff_stat(&line[(index + "Diff stat:".len())..]))
4185+
.or_else(|| {
4186+
line.find("Diff:")
4187+
.map(|index| normalize_diff_stat(&line[(index + "Diff:".len())..]))
4188+
})
4189+
})
4190+
.filter(|value| !value.is_empty())
4191+
}
4192+
4193+
fn normalize_diff_stat(value: &str) -> String {
4194+
let trimmed = value.trim();
4195+
for marker in [" Tested", " Committed", " committed", " pushed", " merged"] {
4196+
if let Some((prefix, _)) = trimmed.split_once(marker) {
4197+
return prefix.trim().to_string();
4198+
}
4199+
}
4200+
trimmed.to_string()
4201+
}
4202+
40874203
fn derive_agent_state(
40884204
status: &str,
40894205
result: Option<&str>,
@@ -7764,6 +7880,71 @@ mod tests {
77647880
"#65 is the next repo-local lane-finished metadata task."
77657881
);
77667882

7883+
let artifact = execute_agent_with_spawn(
7884+
AgentInput {
7885+
description: "Land ROADMAP #64 provenance hardening".to_string(),
7886+
prompt: "Ship structured artifact provenance".to_string(),
7887+
subagent_type: Some("Explore".to_string()),
7888+
name: Some("artifact-lane".to_string()),
7889+
model: None,
7890+
},
7891+
|job| {
7892+
persist_agent_terminal_state(
7893+
&job.manifest,
7894+
"completed",
7895+
Some(
7896+
"Completed ROADMAP #64. Files: rust/crates/tools/src/lib.rs ROADMAP.md. Diff stat: 2 files, +12/-1. Tested, committed, pushed as commit deadbee.",
7897+
),
7898+
None,
7899+
)
7900+
},
7901+
)
7902+
.expect("artifact agent should succeed");
7903+
7904+
let artifact_manifest = std::fs::read_to_string(&artifact.manifest_file)
7905+
.expect("artifact manifest should exist");
7906+
let artifact_manifest_json: serde_json::Value =
7907+
serde_json::from_str(&artifact_manifest).expect("artifact manifest json");
7908+
assert_eq!(
7909+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["sourceLanes"][0],
7910+
"artifact-lane"
7911+
);
7912+
assert_eq!(
7913+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["roadmapIds"][0],
7914+
"ROADMAP #64"
7915+
);
7916+
assert_eq!(
7917+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["files"][0],
7918+
"ROADMAP.md"
7919+
);
7920+
assert_eq!(
7921+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["files"][1],
7922+
"rust/crates/tools/src/lib.rs"
7923+
);
7924+
assert_eq!(
7925+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["diffStat"],
7926+
"2 files, +12/-1."
7927+
);
7928+
assert_eq!(
7929+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["verification"]
7930+
[0],
7931+
"tested"
7932+
);
7933+
assert_eq!(
7934+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["verification"]
7935+
[1],
7936+
"committed"
7937+
);
7938+
assert_eq!(
7939+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["verification"]
7940+
[2],
7941+
"pushed"
7942+
);
7943+
assert_eq!(
7944+
artifact_manifest_json["laneEvents"][1]["data"]["artifactProvenance"]["commitSha"],
7945+
"deadbee"
7946+
);
7947+
77677948
let spawn_error = execute_agent_with_spawn(
77687949
AgentInput {
77697950
description: "Spawn error task".to_string(),

0 commit comments

Comments
 (0)