@@ -34,9 +34,13 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error
3434 'include_worktrees ' => array_key_exists ('include_worktrees ' , $ opts ) ? (bool ) $ opts ['include_worktrees ' ] : true ,
3535 'include_resolvers ' => ! empty ($ opts ['include_resolvers ' ]),
3636 'top_n ' => isset ($ opts ['top_n ' ]) ? max (1 , min (50 , (int ) $ opts ['top_n ' ])) : 10 ,
37+ 'limit ' => isset ($ opts ['limit ' ]) ? max (1 , (int ) $ opts ['limit ' ]) : self ::CLEANUP_PLAN_DEFAULT_LIMIT ,
38+ 'offset ' => isset ($ opts ['offset ' ]) ? max (0 , (int ) $ opts ['offset ' ]) : 0 ,
39+ 'until_budget ' => isset ($ opts ['until_budget ' ]) && '' !== trim ( (string ) $ opts ['until_budget ' ]) ? trim ( (string ) $ opts ['until_budget ' ]) : self ::CLEANUP_PLAN_DEFAULT_BUDGET ,
40+ 'full_workspace ' => ! empty ($ opts ['full_workspace ' ]),
3741 'worktree_older_than ' => isset ($ opts ['worktree_older_than ' ]) ? trim ( (string ) $ opts ['worktree_older_than ' ]) : '' ,
38- 'worktree_sort ' => isset ($ opts ['worktree_sort ' ]) && '' !== trim ( (string ) $ opts ['worktree_sort ' ]) ? trim ( (string ) $ opts ['worktree_sort ' ]) : 'size ' ,
39- 'artifact_sort ' => isset ($ opts ['artifact_sort ' ]) && '' !== trim ( (string ) $ opts ['artifact_sort ' ]) ? trim ( (string ) $ opts ['artifact_sort ' ]) : 'size ' ,
42+ 'worktree_sort ' => isset ($ opts ['worktree_sort ' ]) && '' !== trim ( (string ) $ opts ['worktree_sort ' ]) ? trim ( (string ) $ opts ['worktree_sort ' ]) : '' ,
43+ 'artifact_sort ' => isset ($ opts ['artifact_sort ' ]) && '' !== trim ( (string ) $ opts ['artifact_sort ' ]) ? trim ( (string ) $ opts ['artifact_sort ' ]) : '' ,
4044 'worktree_stale_only ' => ! empty ($ opts ['worktree_stale_only ' ]),
4145 );
4246
@@ -46,14 +50,13 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error
4650 'summary ' => array (),
4751 );
4852 if ( $ inputs ['include_artifacts ' ] ) {
49- // Workspace cleanup plan is the source-of-truth orchestrator that later
50- // chunks/jobs consume. Use whole-workspace inventory planning so hundreds
51- // of worktrees are normal; apply still revalidates every row before delete.
5253 $ artifact_plan = $ this ->worktree_cleanup_artifacts (
5354 array (
5455 'dry_run ' => true ,
5556 'force ' => $ inputs ['force_artifact_cleanup ' ],
56- 'full_workspace ' => true ,
57+ 'full_workspace ' => $ inputs ['full_workspace ' ],
58+ 'limit ' => $ inputs ['limit ' ],
59+ 'offset ' => $ inputs ['offset ' ],
5760 'sort ' => $ inputs ['artifact_sort ' ],
5861 )
5962 );
@@ -68,15 +71,20 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error
6871 'summary ' => array (),
6972 );
7073 if ( $ inputs ['include_worktrees ' ] ) {
71- $ worktree_plan = $ this ->worktree_cleanup_merged (
72- array (
73- 'dry_run ' => true ,
74- 'skip_github ' => true ,
75- 'older_than ' => $ inputs ['worktree_older_than ' ],
76- 'sort ' => $ inputs ['worktree_sort ' ],
77- 'stale_liveness_only ' => $ inputs ['worktree_stale_only ' ],
78- )
74+ $ worktree_args = array (
75+ 'dry_run ' => true ,
76+ 'skip_github ' => true ,
77+ 'inventory_only ' => ! $ inputs ['full_workspace ' ],
78+ 'older_than ' => $ inputs ['worktree_older_than ' ],
79+ 'sort ' => $ inputs ['worktree_sort ' ],
80+ 'stale_liveness_only ' => $ inputs ['worktree_stale_only ' ],
7981 );
82+ if ( ! $ inputs ['full_workspace ' ] ) {
83+ $ worktree_args ['limit ' ] = $ inputs ['limit ' ];
84+ $ worktree_args ['offset ' ] = $ inputs ['offset ' ];
85+ $ worktree_args ['until_budget ' ] = $ inputs ['until_budget ' ];
86+ }
87+ $ worktree_plan = $ this ->worktree_cleanup_merged ($ worktree_args );
8088 if ( $ worktree_plan instanceof \WP_Error ) {
8189 return $ worktree_plan ;
8290 }
@@ -98,12 +106,16 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error
98106 'resolve_signal ' => $ rows ['resolver ' ],
99107 );
100108
109+ $ continuation = $ this ->build_cleanup_plan_continuation ($ artifact_plan , $ worktree_plan , $ inputs );
101110 $ summary = $ this ->build_cleanup_plan_summary ($ rows , $ blocked , $ artifact_plan , $ worktree_plan , $ inputs );
102111 $ summary ['rows_by_action ' ] = array (
103112 'remove_artifacts ' => count ($ action_rows ['remove_artifacts ' ]),
104113 'remove_worktree ' => count ($ action_rows ['remove_worktree ' ]),
105114 'resolve_signal ' => count ($ action_rows ['resolve_signal ' ]),
106115 );
116+ if ( array () !== $ continuation ) {
117+ $ summary ['continuation ' ] = $ continuation ;
118+ }
107119
108120 $ plan = array (
109121 'success ' => true ,
@@ -127,6 +139,9 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error
127139 'action_rows ' => $ action_rows ,
128140 'summary ' => $ summary ,
129141 );
142+ if ( array () !== $ continuation ) {
143+ $ plan ['continuation ' ] = $ continuation ;
144+ }
130145
131146 $ plan ['plan_id ' ] = $ this ->stable_cleanup_hash (
132147 array (
@@ -369,6 +384,66 @@ private function build_cleanup_plan_summary( array $rows, array $blocked = array
369384 );
370385 }
371386
387+ /**
388+ * Build operator continuation evidence from bounded child cleanup plans.
389+ *
390+ * @param array<string,mixed> $artifact_plan Artifact cleanup child plan.
391+ * @param array<string,mixed> $worktree_plan Worktree cleanup child plan.
392+ * @param array<string,mixed> $inputs Normalized plan inputs.
393+ * @return array<string,mixed>
394+ */
395+ private function build_cleanup_plan_continuation ( array $ artifact_plan , array $ worktree_plan , array $ inputs ): array {
396+ $ limit = max (1 , (int ) ( $ inputs ['limit ' ] ?? self ::CLEANUP_PLAN_DEFAULT_LIMIT ));
397+ $ offset = max (0 , (int ) ( $ inputs ['offset ' ] ?? 0 ));
398+ $ next_offset = null ;
399+ $ lanes = array ();
400+
401+ $ plans = array (
402+ 'artifact_cleanup ' => $ artifact_plan ,
403+ 'worktree_removal ' => $ worktree_plan ,
404+ );
405+
406+ foreach ( $ plans as $ lane => $ plan ) {
407+ $ pagination = is_array ($ plan ['pagination ' ] ?? null ) ? $ plan ['pagination ' ] : ( is_array ($ plan ['summary ' ]['pagination ' ] ?? null ) ? $ plan ['summary ' ]['pagination ' ] : null );
408+ if ( null === $ pagination ) {
409+ continue ;
410+ }
411+
412+ $ lane_next = $ pagination ['next_offset ' ] ?? null ;
413+ $ lanes [ $ lane ] = array (
414+ 'complete ' => ! empty ($ pagination ['complete ' ]),
415+ 'partial ' => ! empty ($ pagination ['partial ' ]),
416+ 'offset ' => (int ) ( $ pagination ['offset ' ] ?? $ offset ),
417+ 'limit ' => isset ($ pagination ['limit ' ]) ? (int ) $ pagination ['limit ' ] : $ limit ,
418+ 'scanned ' => (int ) ( $ pagination ['scanned ' ] ?? 0 ),
419+ 'total ' => (int ) ( $ pagination ['total ' ] ?? 0 ),
420+ 'next_offset ' => null === $ lane_next ? null : (int ) $ lane_next ,
421+ 'budget_stopped ' => ! empty ($ pagination ['budget_stopped ' ]),
422+ );
423+ if ( null !== $ lane_next ) {
424+ $ next_offset = null === $ next_offset ? (int ) $ lane_next : min ($ next_offset , (int ) $ lane_next );
425+ }
426+ }
427+
428+ if ( array () === $ lanes ) {
429+ return array ();
430+ }
431+
432+ $ complete = null === $ next_offset ;
433+ return array (
434+ 'bounded ' => empty ($ inputs ['full_workspace ' ]),
435+ 'complete ' => $ complete ,
436+ 'partial ' => ! $ complete ,
437+ 'limit ' => $ limit ,
438+ 'offset ' => $ offset ,
439+ 'next_offset ' => $ next_offset ,
440+ 'lanes ' => $ lanes ,
441+ 'next_command ' => null === $ next_offset ? null : sprintf ('studio wp datamachine-code workspace cleanup plan --mode=retention --limit=%d --offset=%d --format=json ' , $ limit , $ next_offset ),
442+ 'full_audit_command ' => 'studio wp datamachine-code workspace cleanup plan --mode=retention --exhaustive --format=json ' ,
443+ 'operator_note ' => empty ($ inputs ['full_workspace ' ]) ? 'Default cleanup planning is bounded for large workspaces; review/apply this page or continue with next_command for the next page. ' : 'Full-workspace cleanup audit requested explicitly. ' ,
444+ );
445+ }
446+
372447 /**
373448 * Return the bytes a cleanup row is expected to reclaim.
374449 *
@@ -584,8 +659,8 @@ private function cleanup_plan_recommended_commands( array $inputs ): array {
584659 array (
585660 'label ' => 'inspect_full_plan_json ' ,
586661 'risk ' => 'none ' ,
587- 'command ' => 'studio wp datamachine-code workspace cleanup plan --mode=retention --format=json ' ,
588- 'when ' => 'export the full plan for review or archival ' ,
662+ 'command ' => 'studio wp datamachine-code workspace cleanup plan --mode=retention --exhaustive -- format=json ' ,
663+ 'when ' => 'operator explicitly wants a full unbounded audit for review or archival ' ,
589664 ),
590665 array (
591666 'label ' => 'resolve_metadata_blockers ' ,
@@ -596,8 +671,8 @@ private function cleanup_plan_recommended_commands( array $inputs ): array {
596671 array (
597672 'label ' => 'refresh_merge_signals ' ,
598673 'risk ' => 'none ' ,
599- 'command ' => 'studio wp datamachine-code workspace worktree cleanup --dry-run --format=json ' ,
600- 'when ' => 'active or lifecycle rows need full merge/PR signal review ' ,
674+ 'command ' => 'studio wp datamachine-code workspace worktree cleanup --dry-run --limit=100 --offset=0 --until-budget=30s -- format=json ' ,
675+ 'when ' => 'active or lifecycle rows need deeper merge/PR signal review after the cheap inventory pass ' ,
601676 ),
602677 );
603678
0 commit comments