@@ -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 {
@@ -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" ) ;
0 commit comments