@@ -2264,9 +2264,13 @@ private function renderGitOperationResult( string $operation, array $result, arr
22642264 * : For `abandoned`, maximum apply passes to run after marking eligible rows.
22652265 * Preview mode always runs a single non-destructive classification pass.
22662266 *
2267+ * [--stage=<stage>]
2268+ * : For `abandoned`, resume from a specific orchestration stage. Supported
2269+ * values: reconcile, finalized, equivalent-clean, merged, bounded.
2270+ *
22672271 * [--offset=<count>]
22682272 * : For `cleanup --dry-run`, `cleanup-artifacts --dry-run`,
2269- * `reconcile-metadata`, and `active-no-signal-report`,
2273+ * `abandoned`, ` reconcile-metadata`, and `active-no-signal-report`,
22702274 * pagination offset (0-indexed) into the inventory ordering. Walk pages by
22712275 * passing the previous response's `pagination.next_offset`.
22722276 *
@@ -2763,8 +2767,21 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
27632767 $ force = ! empty ($ assoc_args ['force ' ]);
27642768 $ limit = isset ($ assoc_args ['limit ' ]) ? max (1 , min (100 , (int ) $ assoc_args ['limit ' ])) : 100 ;
27652769 $ passes = isset ($ assoc_args ['passes ' ]) ? max (1 , min (25 , (int ) $ assoc_args ['passes ' ])) : 5 ;
2770+ $ offset = isset ($ assoc_args ['offset ' ]) ? max (0 , (int ) $ assoc_args ['offset ' ]) : 0 ;
2771+ $ stage = isset ($ assoc_args ['stage ' ]) ? strtolower ( (string ) preg_replace ('/[^a-zA-Z0-9_-]/ ' , '' , (string ) $ assoc_args ['stage ' ]) ) : 'reconcile ' ;
2772+ $ stage = str_replace ('_ ' , '- ' , $ stage );
27662773 $ until_budget = isset ($ assoc_args ['until-budget ' ]) && '' !== trim ( (string ) $ assoc_args ['until-budget ' ]) ? trim ( (string ) $ assoc_args ['until-budget ' ]) : '' ;
27672774 $ deadline = null ;
2775+ $ stage_order = array (
2776+ 'reconcile ' => 0 ,
2777+ 'finalized ' => 1 ,
2778+ 'equivalent-clean ' => 2 ,
2779+ 'merged ' => 3 ,
2780+ 'bounded ' => 4 ,
2781+ );
2782+ if ( ! isset ($ stage_order [ $ stage ]) ) {
2783+ return new \WP_Error ('invalid_worktree_abandoned_stage ' , 'Invalid --stage value. Use reconcile, finalized, equivalent-clean, merged, or bounded. ' , array ( 'status ' => 400 ));
2784+ }
27682785 if ( '' !== $ until_budget ) {
27692786 $ budget_seconds = $ this ->parse_worktree_abandoned_budget ($ until_budget );
27702787 if ( is_wp_error ($ budget_seconds ) ) {
@@ -2799,6 +2816,8 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
27992816 'destructive ' => $ apply ,
28002817 'force ' => $ force ,
28012818 'limit ' => $ limit ,
2819+ 'stage ' => $ stage ,
2820+ 'offset ' => $ offset ,
28022821 'passes ' => $ passes ,
28032822 'executed_passes ' => 0 ,
28042823 'generated_at ' => gmdate ('c ' ),
@@ -2827,32 +2846,56 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
28272846 'source ' => self ::CLEANUP_CLI_SOURCE ,
28282847 );
28292848
2830- $ reconcile_input = array_merge (
2831- $ common_page ,
2832- array (
2833- 'dry_run ' => ! $ apply ,
2834- 'apply ' => $ apply ,
2835- )
2836- );
2837- $ reconcile = $ this ->drain_worktree_abandoned_pages ($ abilities ['reconcile_metadata ' ], $ reconcile_input , $ apply , $ deadline );
2838- if ( is_wp_error ($ reconcile ) ) {
2839- return $ reconcile ;
2849+ if ( $ stage_order [ $ stage ] <= $ stage_order ['reconcile ' ] ) {
2850+ $ reconcile_input = array_merge (
2851+ $ common_page ,
2852+ array (
2853+ 'dry_run ' => ! $ apply ,
2854+ 'apply ' => $ apply ,
2855+ 'offset ' => 'reconcile ' === $ stage ? $ offset : 0 ,
2856+ )
2857+ );
2858+ $ reconcile = $ this ->drain_worktree_abandoned_pages ($ abilities ['reconcile_metadata ' ], $ reconcile_input , $ apply , $ deadline );
2859+ if ( is_wp_error ($ reconcile ) ) {
2860+ return $ reconcile ;
2861+ }
2862+ $ result ['steps ' ]['reconcile_metadata ' ] = $ this ->summarize_worktree_abandoned_step ($ reconcile );
2863+ $ result ['summary ' ]['reconciled ' ] = (int ) ( $ reconcile ['summary ' ]['written ' ] ?? 0 );
2864+ $ result ['summary ' ]['would_reconcile ' ] = (int ) ( $ reconcile ['summary ' ]['proposed ' ] ?? 0 );
2865+
2866+ if ( $ this ->worktree_abandoned_stage_incomplete ($ reconcile ) ) {
2867+ $ result ['evidence ' ]['budget_exhausted ' ] = $ this ->worktree_abandoned_budget_expired ($ deadline );
2868+ $ result ['continuation ' ] = $ this ->build_worktree_abandoned_continuation ('reconcile ' , $ reconcile , $ limit , $ passes , $ force , $ until_budget );
2869+ $ result ['next_commands ' ][] = (string ) $ result ['continuation ' ]['next_command ' ];
2870+ return $ this ->finalize_worktree_abandoned_result ($ result , $ apply , $ force , $ limit , $ passes , $ until_budget , $ started_at );
2871+ }
28402872 }
2841- $ result ['steps ' ]['reconcile_metadata ' ] = $ this ->summarize_worktree_abandoned_step ($ reconcile );
2842- $ result ['summary ' ]['reconciled ' ] = (int ) ( $ reconcile ['summary ' ]['written ' ] ?? 0 );
2843- $ result ['summary ' ]['would_reconcile ' ] = (int ) ( $ reconcile ['summary ' ]['proposed ' ] ?? 0 );
28442873
28452874 $ mark_steps = array (
2846- 'finalized ' => $ abilities ['finalized ' ],
2847- 'equivalent_clean ' => $ abilities ['equivalent_clean ' ],
2848- 'merged ' => $ abilities ['merged ' ],
2875+ 'finalized ' => array (
2876+ 'stage ' => 'finalized ' ,
2877+ 'ability ' => $ abilities ['finalized ' ],
2878+ ),
2879+ 'equivalent_clean ' => array (
2880+ 'stage ' => 'equivalent-clean ' ,
2881+ 'ability ' => $ abilities ['equivalent_clean ' ],
2882+ ),
2883+ 'merged ' => array (
2884+ 'stage ' => 'merged ' ,
2885+ 'ability ' => $ abilities ['merged ' ],
2886+ ),
28492887 );
28502888
28512889 $ effective_passes = $ apply ? $ passes : 1 ;
28522890 for ( $ pass = 1 ; $ pass <= $ effective_passes ; ++$ pass ) {
28532891 $ result ['executed_passes ' ] = $ pass ;
28542892 $ pass_marked = 0 ;
2855- foreach ( $ mark_steps as $ key => $ ability ) {
2893+ foreach ( $ mark_steps as $ key => $ step_config ) {
2894+ $ step_stage = (string ) $ step_config ['stage ' ];
2895+ if ( $ stage_order [ $ step_stage ] < $ stage_order [ $ stage ] ) {
2896+ continue ;
2897+ }
2898+
28562899 if ( $ this ->worktree_abandoned_budget_expired ($ deadline ) ) {
28572900 $ result ['evidence ' ]['budget_exhausted ' ] = true ;
28582901 break 2 ;
@@ -2862,10 +2905,10 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
28622905 $ common_page ,
28632906 array (
28642907 'dry_run ' => ! $ apply ,
2865- 'offset ' => 0 ,
2908+ 'offset ' => $ step_stage === $ stage ? $ offset : 0 ,
28662909 )
28672910 );
2868- $ step = $ this ->drain_worktree_abandoned_pages ($ ability , $ step_input , $ apply , $ deadline );
2911+ $ step = $ this ->drain_worktree_abandoned_pages ($ step_config [ ' ability ' ] , $ step_input , $ apply , $ deadline );
28692912 if ( is_wp_error ($ step ) ) {
28702913 return $ step ;
28712914 }
@@ -2877,6 +2920,13 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
28772920 $ pass_marked += $ apply ? $ written : $ planned ;
28782921 $ result ['summary ' ]['marked_cleanup_eligible ' ] += $ written ;
28792922 $ result ['summary ' ]['would_mark_cleanup_eligible ' ] += $ planned ;
2923+
2924+ if ( $ this ->worktree_abandoned_stage_incomplete ($ step ) ) {
2925+ $ result ['evidence ' ]['budget_exhausted ' ] = $ this ->worktree_abandoned_budget_expired ($ deadline );
2926+ $ result ['continuation ' ] = $ this ->build_worktree_abandoned_continuation ($ step_stage , $ step , $ limit , $ passes , $ force , $ until_budget );
2927+ $ result ['next_commands ' ][] = (string ) $ result ['continuation ' ]['next_command ' ];
2928+ break 2 ;
2929+ }
28802930 }
28812931
28822932 $ bounded_input = array (
@@ -2918,18 +2968,37 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
29182968 );
29192969 }
29202970
2921- $ result ['blocked ' ] = array_values ($ result ['blocked ' ]);
2971+ return $ this ->finalize_worktree_abandoned_result ($ result , $ apply , $ force , $ limit , $ passes , $ until_budget , $ started_at );
2972+ }
2973+
2974+ /**
2975+ * Finalize abandoned cleanup output.
2976+ *
2977+ * @param array<string,mixed> $result Partial result.
2978+ * @param bool $apply Whether apply mode is active.
2979+ * @param bool $force Whether force mode is active.
2980+ * @param int $limit Page size.
2981+ * @param int $passes Apply passes.
2982+ * @param string $until_budget Original budget argument.
2983+ * @param float $started_at Start time.
2984+ * @return array<string,mixed>
2985+ */
2986+ private function finalize_worktree_abandoned_result ( array $ result , bool $ apply , bool $ force , int $ limit , int $ passes , string $ until_budget , float $ started_at ): array {
2987+ $ result ['blocked ' ] = array_values ( (array ) ( $ result ['blocked ' ] ?? array () ) );
29222988 $ result ['summary ' ]['blocked ' ] = count ($ result ['blocked ' ]);
29232989 foreach ( $ result ['blocked ' ] as $ row ) {
2990+ if ( ! is_array ($ row ) ) {
2991+ continue ;
2992+ }
29242993 $ reason = (string ) ( $ row ['reason_code ' ] ?? 'unknown ' );
29252994
29262995 $ result ['summary ' ]['blocked_by_reason ' ][ $ reason ] = (int ) ( $ result ['summary ' ]['blocked_by_reason ' ][ $ reason ] ?? 0 ) + 1 ;
29272996 }
29282997
2929- if ( ! $ apply ) {
2998+ if ( empty ( $ result [ ' continuation ' ]) && ! $ apply ) {
29302999 $ result ['next_commands ' ][] = sprintf ('studio wp datamachine-code workspace worktree abandoned --apply%s --limit=%d --passes=%d%s --format=json ' , $ force ? ' --force ' : '' , $ limit , $ passes , '' !== $ until_budget ? ' --until-budget= ' . $ until_budget : '' );
29313000 }
2932- if ( ! $ force ) {
3001+ if ( empty ( $ result [ ' continuation ' ]) && ! $ force ) {
29333002 $ result ['next_commands ' ][] = sprintf ('studio wp datamachine-code workspace worktree abandoned --apply --force --limit=%d --passes=%d%s --format=json ' , $ limit , $ passes , '' !== $ until_budget ? ' --until-budget= ' . $ until_budget : '' );
29343003 }
29353004
@@ -2938,6 +3007,52 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
29383007 return $ result ;
29393008 }
29403009
3010+ /**
3011+ * Determine whether a paged abandoned-cleanup stage still has remaining rows.
3012+ *
3013+ * @param array<string,mixed> $step Stage result.
3014+ * @return bool
3015+ */
3016+ private function worktree_abandoned_stage_incomplete ( array $ step ): bool {
3017+ $ pagination = (array ) ( $ step ['pagination ' ] ?? $ step ['continuation ' ] ?? array () );
3018+ if ( empty ($ pagination ) || ! empty ($ pagination ['complete ' ]) || ! isset ($ pagination ['next_offset ' ]) ) {
3019+ return false ;
3020+ }
3021+
3022+ $ next_offset = (int ) $ pagination ['next_offset ' ];
3023+ $ current = (int ) ( $ pagination ['offset ' ] ?? 0 );
3024+ $ total = isset ($ pagination ['total ' ]) ? (int ) $ pagination ['total ' ] : null ;
3025+ if ( null !== $ total && $ next_offset >= $ total ) {
3026+ return false ;
3027+ }
3028+
3029+ return $ next_offset > $ current ;
3030+ }
3031+
3032+ /**
3033+ * Build continuation evidence for a partially drained abandoned-cleanup stage.
3034+ *
3035+ * @param string $stage Stage name.
3036+ * @param array<string,mixed> $step Stage result.
3037+ * @param int $limit Page size.
3038+ * @param int $passes Apply passes.
3039+ * @param bool $force Whether force mode is active.
3040+ * @param string $until_budget Original budget argument.
3041+ * @return array<string,mixed>
3042+ */
3043+ private function build_worktree_abandoned_continuation ( string $ stage , array $ step , int $ limit , int $ passes , bool $ force , string $ until_budget ): array {
3044+ $ pagination = (array ) ( $ step ['pagination ' ] ?? $ step ['continuation ' ] ?? array () );
3045+ $ next_offset = isset ($ pagination ['next_offset ' ]) ? max (0 , (int ) $ pagination ['next_offset ' ]) : 0 ;
3046+ $ command = sprintf ('studio wp datamachine-code workspace worktree abandoned --apply%s --stage=%s --offset=%d --limit=%d --passes=%d%s --format=json ' , $ force ? ' --force ' : '' , $ stage , $ next_offset , $ limit , $ passes , '' !== $ until_budget ? ' --until-budget= ' . $ until_budget : '' );
3047+
3048+ return array (
3049+ 'stage ' => $ stage ,
3050+ 'offset ' => $ next_offset ,
3051+ 'next_command ' => $ command ,
3052+ 'pagination ' => $ pagination ,
3053+ );
3054+ }
3055+
29413056 /**
29423057 * Drain paginated abandoned-cleanup classifier pages.
29433058 *
0 commit comments