Skip to content

Commit 73c0c64

Browse files
author
Chris Huber
committed
fix(agent-task): preserve compact review candidates [AI: OpenAI gpt-5.6-sol via OpenCode]
1 parent c954717 commit 73c0c64

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 {
@@ -1478,7 +1501,7 @@ fn promotion_candidates(
14781501
.unwrap_or_else(|| candidate.artifact_ids.clone());
14791502
let selection_required = artifact_ids.len() > 1;
14801503
artifact_ids.into_iter().map(move |artifact_id| {
1481-
let mut command = vec![
1504+
let command = vec![
14821505
"homeboy".to_string(),
14831506
"agent-task".to_string(),
14841507
"promote".to_string(),
@@ -1495,37 +1518,40 @@ fn promotion_candidates(
14951518
&& promotion.pointer("/patch_artifact/id").and_then(Value::as_str)
14961519
== Some(artifact_id.as_str())
14971520
});
1498-
if let Some(to_worktree) = continuation
1521+
let destination = continuation
14991522
.and_then(|promotion| promotion.pointer("/target/worktree"))
15001523
.and_then(Value::as_str)
1501-
.or(context.to_worktree)
1502-
{
1524+
.or(context.to_worktree);
1525+
let command = destination.map(|destination| {
1526+
let mut command = command;
15031527
command.push("--to-worktree".to_string());
1504-
command.push(to_worktree.to_string());
1505-
}
1506-
if let Some(contract) = continuation
1507-
.and_then(|promotion| promotion.pointer("/provenance/resume_contract"))
1508-
{
1509-
append_resume_contract(&mut command, contract);
1510-
} else if let Some(base) = context.cook_base {
1511-
command.extend(["--base".to_string(), base.to_string()]);
1512-
}
1513-
if let Some(provider_command) = context.provider_command {
1514-
command.push("--provider-command".to_string());
1515-
command.push(provider_command.to_string());
1516-
}
1517-
command.extend(
1518-
context.provider_argv
1519-
.iter()
1520-
.map(|argument| format!("--provider-argv={argument}")),
1521-
);
1528+
command.push(destination.to_string());
1529+
if let Some(contract) = continuation
1530+
.and_then(|promotion| promotion.pointer("/provenance/resume_contract"))
1531+
{
1532+
append_resume_contract(&mut command, contract);
1533+
} else if let Some(base) = context.cook_base {
1534+
command.extend(["--base".to_string(), base.to_string()]);
1535+
}
1536+
if let Some(provider_command) = context.provider_command {
1537+
command.push("--provider-command".to_string());
1538+
command.push(provider_command.to_string());
1539+
}
1540+
command.extend(
1541+
context.provider_argv
1542+
.iter()
1543+
.map(|argument| format!("--provider-argv={argument}")),
1544+
);
1545+
command
1546+
});
15221547

15231548
serde_json::json!({
15241549
"task_id": candidate.task_id,
15251550
"artifact_id": artifact_id,
15261551
"reason": candidate.reason,
15271552
"command": command,
1528-
"ready": context.to_worktree.is_some(),
1553+
"ready": destination.is_some(),
1554+
"destination_required": destination.is_none(),
15291555
"selection_required": selection_required,
15301556
})
15311557
})
@@ -1635,7 +1661,9 @@ fn review_next_actions(
16351661
if to_worktree.is_some() {
16361662
actions.push("review `promotion_candidates` and run the generated `homeboy agent-task promote` command for the selected patch artifact".to_string());
16371663
} else {
1638-
actions.push("rerun review with `--to-worktree <handle>` to generate complete promotion commands for apply candidates".to_string());
1664+
actions.push(format!(
1665+
"rerun review with `homeboy agent-task review {run_id} --to-worktree <managed-worktree>` to generate executable promotion commands for apply candidates"
1666+
));
16391667
}
16401668
}
16411669
if review.summary.retry_candidates > 0 {
@@ -1832,6 +1860,42 @@ mod tests {
18321860
assert_eq!(diagnostic["response_body"]["omitted_bytes"], 100_000);
18331861
}
18341862

1863+
#[test]
1864+
fn compact_apply_candidate_uses_the_selected_task_and_artifact_id() {
1865+
let value = serde_json::json!({
1866+
"promotion_candidates": [{
1867+
"task_id": "selected-task",
1868+
"artifact_id": "shared-patch"
1869+
}],
1870+
"aggregate_review": {
1871+
"artifact_inventory": [
1872+
{
1873+
"task_id": "other-task",
1874+
"artifact_id": "shared-patch",
1875+
"kind": "patch",
1876+
"path": "/tmp/other.patch",
1877+
"size_bytes": 1,
1878+
"metadata": { "changed_files": ["other.rs"] }
1879+
},
1880+
{
1881+
"task_id": "selected-task",
1882+
"artifact_id": "shared-patch",
1883+
"kind": "patch",
1884+
"path": "/tmp/selected.patch",
1885+
"size_bytes": 17394,
1886+
"metadata": { "changed_files": ["a.rs", "b.rs", "c.rs", "d.rs", "e.rs", "f.rs"] }
1887+
}
1888+
]
1889+
}
1890+
});
1891+
1892+
let selected = compact_apply_candidate(&value).expect("selected candidate");
1893+
1894+
assert_eq!(selected["artifact"]["path"], "/tmp/selected.patch");
1895+
assert_eq!(selected["size_bytes"], 17394);
1896+
assert_eq!(selected["changed_files"].as_array().map(Vec::len), Some(6));
1897+
}
1898+
18351899
#[test]
18361900
fn compact_review_preserves_one_promoted_candidate_fingerprint() {
18371901
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)