@@ -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+
385408fn 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" ) ;
0 commit comments