Skip to content

Commit 69c730f

Browse files
Add cleanup-eligible drain command
1 parent 478b13a commit 69c730f

4 files changed

Lines changed: 548 additions & 5 deletions

File tree

inc/Abilities/WorkspaceAbilities.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use DataMachineCode\Workspace\RunnerWorkspacePublisher;
2222
use DataMachineCode\Workspace\Workspace;
2323
use DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator;
24+
use DataMachineCode\Workspace\WorkspaceCleanupEligibleDrainOrchestrator;
2425
use DataMachineCode\Workspace\WorkspaceReader;
2526
use DataMachineCode\Workspace\WorkspaceWriter;
2627
use DataMachineCode\Support\GitRunner;
@@ -2423,6 +2424,35 @@ private function registerAbilities(): void {
24232424
)
24242425
);
24252426

2427+
AbilityRegistry::register(
2428+
'datamachine-code/workspace-worktree-cleanup-eligible-drain',
2429+
array(
2430+
'label' => 'Drain Cleanup-Eligible Worktrees',
2431+
'description' => 'Repeat the bounded cleanup-eligible apply primitive until no safe candidates remain, the pass limit is reached, or the time budget expires. Defaults to preview mode and never discards unpushed commits.',
2432+
'category' => 'datamachine-code-workspace',
2433+
'input_schema' => array(
2434+
'type' => 'object',
2435+
'properties' => array(
2436+
'apply' => array( 'type' => 'boolean' ),
2437+
'force' => array( 'type' => 'boolean' ),
2438+
'limit' => array( 'type' => 'integer' ),
2439+
'passes' => array( 'type' => 'integer' ),
2440+
'until_budget' => array( 'type' => 'string' ),
2441+
'older_than' => array( 'type' => 'string' ),
2442+
'sort' => array( 'type' => 'string' ),
2443+
'remove_timeout' => array( 'type' => 'integer' ),
2444+
'include_repaired_metadata' => array( 'type' => 'boolean' ),
2445+
'discard_unpushed' => array( 'type' => 'boolean' ),
2446+
'source' => array( 'type' => 'string' ),
2447+
),
2448+
),
2449+
'output_schema' => array( 'type' => 'object' ),
2450+
'execute_callback' => array( self::class, 'worktreeCleanupEligibleDrain' ),
2451+
'permission_callback' => fn() => PermissionHelper::can_manage(),
2452+
'meta' => array( 'show_in_rest' => false ),
2453+
)
2454+
);
2455+
24262456
AbilityRegistry::register(
24272457
'datamachine-code/workspace-cleanup-plan',
24282458
array(
@@ -4283,6 +4313,18 @@ public static function worktreeBoundedCleanupEligibleApply( array $input ): arra
42834313
return $workspace->worktree_bounded_cleanup_eligible_apply($opts);
42844314
}
42854315

4316+
/**
4317+
* Drain cleanup-eligible worktrees with repeated bounded apply passes.
4318+
*
4319+
* @param array $input Input parameters.
4320+
* @return array<string,mixed>|\WP_Error
4321+
*/
4322+
public static function worktreeCleanupEligibleDrain( array $input ): array|\WP_Error {
4323+
$orchestrator = new WorkspaceCleanupEligibleDrainOrchestrator();
4324+
4325+
return $orchestrator->run($input);
4326+
}
4327+
42864328
/**
42874329
* Build or apply a disk-pressure emergency cleanup plan.
42884330
*

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class WorkspaceCommand extends BaseCommand {
4747
'cleanup' => array( 'ability' => 'datamachine-code/workspace-worktree-cleanup' ),
4848
'cleanup-artifacts' => array( 'ability' => 'datamachine-code/workspace-worktree-cleanup-artifacts' ),
4949
'bounded-cleanup-eligible-apply' => array( 'ability' => 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' ),
50+
'cleanup-eligible-drain' => array( 'ability' => 'datamachine-code/workspace-worktree-cleanup-eligible-drain' ),
5051
'emergency-cleanup' => array( 'ability' => 'datamachine-code/workspace-worktree-emergency-cleanup' ),
5152
'reconcile-metadata' => array( 'ability' => 'datamachine-code/workspace-worktree-reconcile-metadata' ),
5253
'active-no-signal-report' => array(
@@ -3190,7 +3191,7 @@ private function renderGitOperationResult( string $operation, array $result, arr
31903191
*
31913192
* <operation>
31923193
* : Worktree operation: add, list, remove, prune, locks, cleanup, cleanup-artifacts,
3193-
* bounded-cleanup-eligible-apply, emergency-cleanup, reconcile-metadata,
3194+
* bounded-cleanup-eligible-apply, cleanup-eligible-drain, emergency-cleanup, reconcile-metadata,
31943195
* active-no-signal-report, active-no-signal-finalized-apply,
31953196
* active-no-signal-equivalent-clean-apply,
31963197
* active-no-signal-merged-apply, active-no-signal-remote-clean-apply,
@@ -3337,7 +3338,8 @@ private function renderGitOperationResult( string $operation, array $result, arr
33373338
* [--discard-unpushed]
33383339
* : With bounded-cleanup-eligible-apply only, explicitly discard unpushed
33393340
* commits after reviewed cleanup eligibility and fresh safety probes. This
3340-
* is a data-loss mode and is not implied by --force.
3341+
* is a data-loss mode and is not implied by --force. Cleanup-eligible-drain
3342+
* refuses this option.
33413343
*
33423344
* [--older-than=<duration>]
33433345
* : Limit cleanup candidates to worktrees with lifecycle `created_at`
@@ -3393,7 +3395,8 @@ private function renderGitOperationResult( string $operation, array $result, arr
33933395
*
33943396
* [--passes=<count>]
33953397
* : For `abandoned`, maximum apply passes to run after marking eligible rows.
3396-
* Preview mode always runs a single non-destructive classification pass.
3398+
* For `cleanup-eligible-drain`, maximum bounded cleanup-eligible apply
3399+
* passes to run. Preview mode always runs one non-destructive pass.
33973400
*
33983401
* [--stage=<stage>]
33993402
* : For `abandoned`, resume from a specific orchestration stage. Supported
@@ -3406,7 +3409,7 @@ private function renderGitOperationResult( string $operation, array $result, arr
34063409
* passing the previous response's `pagination.next_offset`.
34073410
*
34083411
* [--until-budget=<duration>]
3409-
* : For `cleanup --dry-run` and `reconcile-metadata`, enforce a compact
3412+
* : For `cleanup --dry-run`, `cleanup-eligible-drain`, and `reconcile-metadata`, enforce a compact
34103413
* wall-clock budget for dry-run pages or direct-apply drains (e.g. 60s,
34113414
* 10m). Also supported by `active-no-signal-report` and the active/no-signal
34123415
* apply flows. Returns continuation
@@ -3503,6 +3506,8 @@ private function renderGitOperationResult( string $operation, array $result, arr
35033506
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --via-jobs --limit=10 --older-than=7d
35043507
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --dry-run --include-repaired-metadata --older-than=7d --limit=25
35053508
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --include-repaired-metadata --older-than=7d --limit=25
3509+
* wp datamachine-code workspace worktree cleanup-eligible-drain --limit=25 --format=json
3510+
* wp datamachine-code workspace worktree cleanup-eligible-drain --apply --limit=25 --passes=10 --until-budget=120s --format=json
35063511
*
35073512
* # Local-only detection (no GitHub API call)
35083513
* wp datamachine-code workspace worktree cleanup --skip-github
@@ -3560,7 +3565,7 @@ public function worktree( array $args, array $assoc_args ): void {
35603565
$operation = $args[0] ?? '';
35613566

35623567
if ( '' === $operation ) {
3563-
WP_CLI::error('Usage: wp datamachine-code workspace worktree <add|list|remove|prune|locks|cleanup|cleanup-artifacts|abandoned|bounded-cleanup-eligible-apply|emergency-cleanup|reconcile-metadata|backfill-origin-session|active-no-signal-report|active-no-signal-finalized-apply|active-no-signal-equivalent-clean-apply|active-no-signal-merged-apply|active-no-signal-remote-clean-apply|refresh-context|finalize|mark-cleanup-eligible> [<repo>] [<branch>] [--flags]');
3568+
WP_CLI::error('Usage: wp datamachine-code workspace worktree <add|list|remove|prune|locks|cleanup|cleanup-artifacts|abandoned|bounded-cleanup-eligible-apply|cleanup-eligible-drain|emergency-cleanup|reconcile-metadata|backfill-origin-session|active-no-signal-report|active-no-signal-finalized-apply|active-no-signal-equivalent-clean-apply|active-no-signal-merged-apply|active-no-signal-remote-clean-apply|refresh-context|finalize|mark-cleanup-eligible> [<repo>] [<branch>] [--flags]');
35643569
return;
35653570
}
35663571

@@ -3877,6 +3882,24 @@ public function worktree( array $args, array $assoc_args ): void {
38773882
$input['remove_timeout'] = (int) $assoc_args['remove-timeout'];
38783883
}
38793884
break;
3885+
3886+
case 'cleanup-eligible-drain':
3887+
$input['apply'] = ! empty($assoc_args['apply']);
3888+
$input['force'] = ! empty($assoc_args['force']);
3889+
$input['discard_unpushed'] = ! empty($assoc_args['discard-unpushed']);
3890+
$input['include_repaired_metadata'] = ! empty($assoc_args['include-repaired-metadata']);
3891+
$input['source'] = self::CLEANUP_CLI_SOURCE;
3892+
foreach ( array( 'limit', 'passes', 'remove-timeout' ) as $key ) {
3893+
if ( isset($assoc_args[ $key ]) ) {
3894+
$input[ str_replace('-', '_', $key) ] = (int) $assoc_args[ $key ];
3895+
}
3896+
}
3897+
foreach ( array( 'older-than', 'sort', 'until-budget' ) as $key ) {
3898+
if ( isset($assoc_args[ $key ]) && '' !== trim( (string) $assoc_args[ $key ]) ) {
3899+
$input[ str_replace('-', '_', $key) ] = trim( (string) $assoc_args[ $key ]);
3900+
}
3901+
}
3902+
break;
38803903
}
38813904

38823905
$result = $ability->execute($input);
@@ -4262,6 +4285,10 @@ function ( $wt ) {
42624285
$this->render_worktree_bounded_cleanup_eligible_apply_result($result, $assoc_args);
42634286
return;
42644287

4288+
case 'cleanup-eligible-drain':
4289+
$this->render_worktree_cleanup_eligible_drain_result($result, $assoc_args);
4290+
return;
4291+
42654292
case 'emergency-cleanup':
42664293
$this->render_worktree_emergency_cleanup_result($result, $assoc_args);
42674294
return;
@@ -6019,6 +6046,67 @@ private function render_worktree_bounded_cleanup_eligible_apply_result( array $r
60196046
}
60206047
}
60216048

6049+
/**
6050+
* Render cleanup-eligible drain output.
6051+
*
6052+
* @param array $result Drain result.
6053+
* @param array $assoc_args CLI args.
6054+
* @return void
6055+
*/
6056+
private function render_worktree_cleanup_eligible_drain_result( array $result, array $assoc_args ): void {
6057+
if ( 'json' === (string) ( $assoc_args['format'] ?? '' ) ) {
6058+
$this->renderer()->json($result);
6059+
return;
6060+
}
6061+
6062+
$summary = (array) ( $result['summary'] ?? array() );
6063+
WP_CLI::log('Cleanup-eligible drain summary:');
6064+
$this->format_items(
6065+
array(
6066+
array( 'metric' => 'mode', 'value' => ! empty($result['applied']) ? 'apply' : 'preview' ),
6067+
array( 'metric' => 'passes', 'value' => (int) ( $summary['passes'] ?? 0 ) ),
6068+
array( 'metric' => 'processed', 'value' => (int) ( $summary['processed'] ?? 0 ) ),
6069+
array( 'metric' => 'would_remove', 'value' => (int) ( $summary['would_remove'] ?? 0 ) ),
6070+
array( 'metric' => 'removed', 'value' => (int) ( $summary['removed'] ?? 0 ) ),
6071+
array( 'metric' => 'skipped', 'value' => (int) ( $summary['skipped'] ?? 0 ) ),
6072+
array( 'metric' => 'bytes_reclaimed', 'value' => $this->format_bytes($summary['bytes_reclaimed'] ?? 0) ),
6073+
array( 'metric' => 'stop_reason', 'value' => (string) ( $summary['stop_reason'] ?? '' ) ),
6074+
array( 'metric' => 'final_free_space', 'value' => (string) ( (array) ( $summary['final_free_space'] ?? array() )['free_human'] ?? 'unknown' ) ),
6075+
),
6076+
array( 'metric', 'value' ),
6077+
array( 'format' => 'table' ),
6078+
'metric'
6079+
);
6080+
6081+
$passes = (array) ( $result['pass_results'] ?? array() );
6082+
if ( ! empty($passes) ) {
6083+
WP_CLI::log('');
6084+
WP_CLI::log('Pass evidence:');
6085+
$this->format_items(
6086+
array_map(
6087+
fn( $row ) => array(
6088+
'pass' => (int) ( $row['pass'] ?? 0 ),
6089+
'processed' => (int) ( $row['processed'] ?? 0 ),
6090+
'would_remove' => (int) ( $row['would_remove'] ?? 0 ),
6091+
'removed' => (int) ( $row['removed'] ?? 0 ),
6092+
'skipped' => (int) ( $row['skipped'] ?? 0 ),
6093+
'remaining_total' => (int) ( $row['remaining_total'] ?? 0 ),
6094+
'bytes' => $this->format_bytes($row['bytes_reclaimed'] ?? 0),
6095+
),
6096+
$passes
6097+
),
6098+
array( 'pass', 'processed', 'would_remove', 'removed', 'skipped', 'remaining_total', 'bytes' ),
6099+
array( 'format' => 'table' ),
6100+
'pass'
6101+
);
6102+
}
6103+
6104+
if ( ! empty($result['next_commands']) ) {
6105+
WP_CLI::log('');
6106+
WP_CLI::log('Next command: ' . (string) ( (array) $result['next_commands'] )[0]);
6107+
}
6108+
}
6109+
60226110
/**
60236111
* Compact bounded cleanup JSON for chat/operator output.
60246112
*

0 commit comments

Comments
 (0)