Skip to content

Commit 06872bf

Browse files
authored
fix(agent-task): preserve compact review candidates [AI: OpenAI gpt-5.6-sol via OpenCode] (#11993)
1 parent 66a6691 commit 06872bf

3 files changed

Lines changed: 148 additions & 24 deletions

File tree

crates/homeboy-cli/src/commands/agent_task/review.rs

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ pub(crate) fn review(args: ReviewArgs) -> CmdResult<Value> {
268268
"record": record,
269269
"logs": log,
270270
"artifacts": artifacts,
271+
"aggregate": aggregate,
271272
"aggregate_review": aggregate_review,
272273
"diagnostic_summary": diagnostic_summary,
273274
"failure_reasons": failure_reasons,
@@ -327,6 +328,7 @@ fn compact_review(value: Value, full: bool) -> Value {
327328
let promotion = value.pointer("/record/metadata/latest_promotion");
328329
let selected_candidate = promotion
329330
.map(compact_selected_candidate)
331+
.or_else(|| compact_apply_candidate(&value))
330332
.unwrap_or(Value::Null);
331333
let gates = promotion
332334
.and_then(|promotion| promotion.get("deterministic_gates"))
@@ -382,6 +384,27 @@ fn compact_selected_candidate(promotion: &Value) -> Value {
382384
})
383385
}
384386

387+
fn compact_apply_candidate(value: &Value) -> Option<Value> {
388+
let candidate = value.pointer("/promotion_candidates/0")?;
389+
let task_id = candidate.get("task_id").and_then(Value::as_str)?;
390+
let artifact_id = candidate.get("artifact_id").and_then(Value::as_str)?;
391+
let artifact = value
392+
.pointer("/aggregate_review/artifact_inventory")?
393+
.as_array()?
394+
.iter()
395+
.find(|artifact| {
396+
artifact.get("task_id").and_then(Value::as_str) == Some(task_id)
397+
&& artifact.get("artifact_id").and_then(Value::as_str) == Some(artifact_id)
398+
})?;
399+
Some(serde_json::json!({
400+
"status": "available",
401+
"task_id": candidate.get("task_id"),
402+
"artifact": compact_fields(artifact, &["artifact_id", "kind", "path", "sha256", "metadata"]),
403+
"size_bytes": artifact.get("size_bytes"),
404+
"changed_files": artifact.pointer("/metadata/changed_files"),
405+
}))
406+
}
407+
385408
fn compact_fields(value: &Value, fields: &[&str]) -> Value {
386409
let mut object = serde_json::Map::new();
387410
for field in fields {
@@ -1500,7 +1523,7 @@ fn promotion_candidates(
15001523
.unwrap_or_else(|| candidate.artifact_ids.clone());
15011524
let selection_required = artifact_ids.len() > 1;
15021525
artifact_ids.into_iter().map(move |artifact_id| {
1503-
let mut command = vec![
1526+
let command = vec![
15041527
"homeboy".to_string(),
15051528
"agent-task".to_string(),
15061529
"promote".to_string(),
@@ -1517,37 +1540,40 @@ fn promotion_candidates(
15171540
&& promotion.pointer("/patch_artifact/id").and_then(Value::as_str)
15181541
== Some(artifact_id.as_str())
15191542
});
1520-
if let Some(to_worktree) = continuation
1543+
let destination = continuation
15211544
.and_then(|promotion| promotion.pointer("/target/worktree"))
15221545
.and_then(Value::as_str)
1523-
.or(context.to_worktree)
1524-
{
1546+
.or(context.to_worktree);
1547+
let command = destination.map(|destination| {
1548+
let mut command = command;
15251549
command.push("--to-worktree".to_string());
1526-
command.push(to_worktree.to_string());
1527-
}
1528-
if let Some(contract) = continuation
1529-
.and_then(|promotion| promotion.pointer("/provenance/resume_contract"))
1530-
{
1531-
append_resume_contract(&mut command, contract);
1532-
} else if let Some(base) = context.cook_base {
1533-
command.extend(["--base".to_string(), base.to_string()]);
1534-
}
1535-
if let Some(provider_command) = context.provider_command {
1536-
command.push("--provider-command".to_string());
1537-
command.push(provider_command.to_string());
1538-
}
1539-
command.extend(
1540-
context.provider_argv
1541-
.iter()
1542-
.map(|argument| format!("--provider-argv={argument}")),
1543-
);
1550+
command.push(destination.to_string());
1551+
if let Some(contract) = continuation
1552+
.and_then(|promotion| promotion.pointer("/provenance/resume_contract"))
1553+
{
1554+
append_resume_contract(&mut command, contract);
1555+
} else if let Some(base) = context.cook_base {
1556+
command.extend(["--base".to_string(), base.to_string()]);
1557+
}
1558+
if let Some(provider_command) = context.provider_command {
1559+
command.push("--provider-command".to_string());
1560+
command.push(provider_command.to_string());
1561+
}
1562+
command.extend(
1563+
context.provider_argv
1564+
.iter()
1565+
.map(|argument| format!("--provider-argv={argument}")),
1566+
);
1567+
command
1568+
});
15441569

15451570
serde_json::json!({
15461571
"task_id": candidate.task_id,
15471572
"artifact_id": artifact_id,
15481573
"reason": candidate.reason,
15491574
"command": command,
1550-
"ready": context.to_worktree.is_some(),
1575+
"ready": destination.is_some(),
1576+
"destination_required": destination.is_none(),
15511577
"selection_required": selection_required,
15521578
})
15531579
})
@@ -1657,7 +1683,9 @@ fn review_next_actions(
16571683
if to_worktree.is_some() {
16581684
actions.push("review `promotion_candidates` and run the generated `homeboy agent-task promote` command for the selected patch artifact".to_string());
16591685
} else {
1660-
actions.push("rerun review with `--to-worktree <handle>` to generate complete promotion commands for apply candidates".to_string());
1686+
actions.push(format!(
1687+
"rerun review with `homeboy agent-task review {run_id} --to-worktree <managed-worktree>` to generate executable promotion commands for apply candidates"
1688+
));
16611689
}
16621690
}
16631691
if review.summary.retry_candidates > 0 {
@@ -1875,6 +1903,42 @@ mod tests {
18751903
assert_eq!(diagnostic["response_body"]["omitted_bytes"], 100_000);
18761904
}
18771905

1906+
#[test]
1907+
fn compact_apply_candidate_uses_the_selected_task_and_artifact_id() {
1908+
let value = serde_json::json!({
1909+
"promotion_candidates": [{
1910+
"task_id": "selected-task",
1911+
"artifact_id": "shared-patch"
1912+
}],
1913+
"aggregate_review": {
1914+
"artifact_inventory": [
1915+
{
1916+
"task_id": "other-task",
1917+
"artifact_id": "shared-patch",
1918+
"kind": "patch",
1919+
"path": "/tmp/other.patch",
1920+
"size_bytes": 1,
1921+
"metadata": { "changed_files": ["other.rs"] }
1922+
},
1923+
{
1924+
"task_id": "selected-task",
1925+
"artifact_id": "shared-patch",
1926+
"kind": "patch",
1927+
"path": "/tmp/selected.patch",
1928+
"size_bytes": 17394,
1929+
"metadata": { "changed_files": ["a.rs", "b.rs", "c.rs", "d.rs", "e.rs", "f.rs"] }
1930+
}
1931+
]
1932+
}
1933+
});
1934+
1935+
let selected = compact_apply_candidate(&value).expect("selected candidate");
1936+
1937+
assert_eq!(selected["artifact"]["path"], "/tmp/selected.patch");
1938+
assert_eq!(selected["size_bytes"], 17394);
1939+
assert_eq!(selected["changed_files"].as_array().map(Vec::len), Some(6));
1940+
}
1941+
18781942
#[test]
18791943
fn compact_review_preserves_one_promoted_candidate_fingerprint() {
18801944
let patch = tempfile::NamedTempFile::new().expect("patch");

crates/homeboy-cli/src/commands/agent_task/tests/promotion_review.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,20 @@ fn default_review_is_bounded_and_points_to_full_evidence() {
180180
value["full_command"],
181181
"homeboy agent-task review run-review-default-bounded --full"
182182
);
183+
assert_eq!(value["canonical_candidate"]["state"], "patch_available");
184+
assert_eq!(value["selected_candidate"]["size_bytes"], 42);
185+
assert!(value["promotion_candidates"][0]["command"].is_null());
186+
assert_eq!(
187+
value["promotion_candidates"][0]["destination_required"],
188+
true
189+
);
190+
let destination_guidance = value["next_actions"][0]
191+
.as_str()
192+
.expect("destination guidance");
193+
assert!(destination_guidance.contains(
194+
"homeboy agent-task review run-review-default-bounded --to-worktree <managed-worktree>"
195+
));
196+
assert!(!destination_guidance.contains("agent-task promote"));
183197
});
184198
}
185199

crates/homeboy-cli/src/commands/agent_task_summary/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,52 @@ mod tests {
11851185
assert!(!summary.contains("promotion_candidates"));
11861186
}
11871187

1188+
#[test]
1189+
fn review_summary_keeps_the_default_compact_apply_candidate() {
1190+
// #11982: default review must preserve the same substantive candidate
1191+
// selected for promotion even though full artifact inventories are omitted.
1192+
let payload = json!({
1193+
"run_id": "agent-task-11982",
1194+
"state": "succeeded",
1195+
"aggregate_review": { "summary": { "apply_candidates": 1, "failed": 0 } },
1196+
"canonical_candidate": {
1197+
"schema": "homeboy/agent-task-candidate/v1",
1198+
"state": "patch_available",
1199+
"diff_bytes": 17394,
1200+
"counts": { "patch_available": 1 },
1201+
"scan": { "degraded": false }
1202+
},
1203+
"selected_candidate": {
1204+
"status": "available",
1205+
"task_id": "task-a",
1206+
"artifact": {
1207+
"artifact_id": "patch-a",
1208+
"kind": "patch",
1209+
"path": "/tmp/patch-a.diff",
1210+
"metadata": { "changed_files": ["a.rs", "b.rs", "c.rs", "d.rs", "e.rs", "f.rs"] }
1211+
},
1212+
"size_bytes": 17394,
1213+
"changed_files": ["a.rs", "b.rs", "c.rs", "d.rs", "e.rs", "f.rs"]
1214+
},
1215+
"promotion_candidates": [{
1216+
"artifact_id": "patch-a",
1217+
"command": null,
1218+
"destination_required": true
1219+
}],
1220+
"next_actions": ["rerun review with `homeboy agent-task review agent-task-11982 --to-worktree <managed-worktree>` to generate executable promotion commands for apply candidates"]
1221+
});
1222+
1223+
let summary = render_agent_task_summary(AgentTaskSummaryKind::Review, &payload).unwrap();
1224+
1225+
assert!(summary.contains("Outcome: patch produced, not promoted\n"));
1226+
assert!(summary.contains("Patch candidates: 1 non-empty / 0 empty\n"));
1227+
assert!(summary.contains("Candidate state: patch_available\n"));
1228+
assert!(summary.contains("Changed files: 6\n"));
1229+
assert!(summary.contains("Diff bytes: 17394\n"));
1230+
assert!(summary.contains("Next: rerun review with `homeboy agent-task review agent-task-11982 --to-worktree <managed-worktree>` to generate executable promotion commands for apply candidates\n"));
1231+
assert!(!summary.contains("Next: homeboy agent-task promote"));
1232+
}
1233+
11881234
#[test]
11891235
fn review_summary_does_not_treat_stale_promotion_candidates_as_patches() {
11901236
let payload = json!({

0 commit comments

Comments
 (0)