mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-13 12:15:44 +08:00
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
This commit is contained in:
parent
2e34949507
commit
e75d67dfd3
@ -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.**
|
||||
|
||||
|
||||
@ -3844,6 +3844,8 @@ struct LaneFinishedSummaryData {
|
||||
review_rationale: Option<String>,
|
||||
#[serde(rename = "selectionOutcome", skip_serializing_if = "Option::is_none")]
|
||||
selection_outcome: Option<SelectionOutcome>,
|
||||
#[serde(rename = "artifactProvenance", skip_serializing_if = "Option::is_none")]
|
||||
artifact_provenance: Option<ArtifactProvenance>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -3877,6 +3879,22 @@ struct SelectionOutcome {
|
||||
rationale: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct ArtifactProvenance {
|
||||
#[serde(rename = "sourceLanes", skip_serializing_if = "Vec::is_empty")]
|
||||
source_lanes: Vec<String>,
|
||||
#[serde(rename = "roadmapIds", skip_serializing_if = "Vec::is_empty")]
|
||||
roadmap_ids: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
files: Vec<String>,
|
||||
#[serde(rename = "diffStat", skip_serializing_if = "Option::is_none")]
|
||||
diff_stat: Option<String>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
verification: Vec<String>,
|
||||
#[serde(rename = "commitSha", skip_serializing_if = "Option::is_none")]
|
||||
commit_sha: Option<String>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
items
|
||||
}
|
||||
|
||||
fn extract_artifact_provenance(
|
||||
manifest: &AgentOutput,
|
||||
raw_summary: Option<&str>,
|
||||
) -> Option<ArtifactProvenance> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user