Skip to content

Commit 8ce5ebc

Browse files
authored
Merge pull request #685 from Extra-Chill/fix/issue-675-stale-worktree-cleanup
Add stale worktree cleanup profile
2 parents 4eba1e1 + 6171c47 commit 8ce5ebc

10 files changed

Lines changed: 294 additions & 85 deletions

inc/Abilities/WorkspaceAbilities.php

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,29 +1562,33 @@ private function registerAbilities(): void {
15621562
'input_schema' => array(
15631563
'type' => 'object',
15641564
'properties' => array(
1565-
'mode' => array(
1565+
'mode' => array(
15661566
'type' => 'string',
1567-
'description' => 'Cleanup mode: inventory, artifacts, retention, or emergency.',
1567+
'description' => 'Cleanup mode: inventory, artifacts, retention, stale-worktrees, or emergency.',
15681568
),
1569-
'force' => array(
1569+
'force' => array(
15701570
'type' => 'boolean',
15711571
'description' => 'Forward force=true to cleanup tasks that support it.',
15721572
),
1573-
'dry_run' => array(
1573+
'dry_run' => array(
15741574
'type' => 'boolean',
15751575
'description' => 'Rejected for background cleanup scheduling; use review abilities for dry-runs.',
15761576
),
1577-
'older_than' => array(
1577+
'older_than' => array(
15781578
'type' => 'string',
15791579
'description' => 'Optional worktree retention age gate such as 14d.',
15801580
),
1581-
'source' => array(
1581+
'worktree_stale_only' => array(
1582+
'type' => 'boolean',
1583+
'description' => 'Only plan stale/inactive worktrees for destructive removal.',
1584+
),
1585+
'source' => array(
15821586
'type' => 'string',
15831587
'description' => 'Caller source marker.',
15841588
),
1585-
'user_id' => array( 'type' => 'integer' ),
1586-
'agent_id' => array( 'type' => 'integer' ),
1587-
'agent_slug' => array( 'type' => 'string' ),
1589+
'user_id' => array( 'type' => 'integer' ),
1590+
'agent_id' => array( 'type' => 'integer' ),
1591+
'agent_slug' => array( 'type' => 'string' ),
15881592
),
15891593
),
15901594
'output_schema' => array(
@@ -2327,18 +2331,20 @@ private function registerAbilities(): void {
23272331
'force_artifact_cleanup' => array( 'type' => 'boolean' ),
23282332
'worktree_older_than' => array( 'type' => 'string' ),
23292333
'worktree_sort' => array( 'type' => 'string' ),
2334+
'worktree_stale_only' => array( 'type' => 'boolean' ),
23302335
'plan' => array( 'type' => 'object' ),
23312336
),
23322337
),
23332338
'output_schema' => array(
23342339
'type' => 'object',
23352340
'properties' => array(
2336-
'success' => array( 'type' => 'boolean' ),
2337-
'mode' => array( 'type' => 'string' ),
2338-
'plan_id' => array( 'type' => 'string' ),
2339-
'rows' => array( 'type' => 'object' ),
2340-
'chunks' => array( 'type' => 'array' ),
2341-
'summary' => array( 'type' => 'object' ),
2341+
'success' => array( 'type' => 'boolean' ),
2342+
'mode' => array( 'type' => 'string' ),
2343+
'plan_id' => array( 'type' => 'string' ),
2344+
'rows' => array( 'type' => 'object' ),
2345+
'action_rows' => array( 'type' => 'object' ),
2346+
'chunks' => array( 'type' => 'array' ),
2347+
'summary' => array( 'type' => 'object' ),
23422348
),
23432349
),
23442350
'execute_callback' => array( self::class, 'workspaceCleanupPlan' ),
@@ -3609,7 +3615,7 @@ public static function workspaceCleanupRun( array $input ): array|\WP_Error {
36093615

36103616
$mode = strtolower(preg_replace('/[^a-z0-9_\-]/', '', (string) ( $input['mode'] ?? 'retention' )));
36113617
$map = array(
3612-
'inventory' => array(
3618+
'inventory' => array(
36133619
'task_type' => 'workspace_hygiene_report',
36143620
'params' => array(
36153621
'include_cleanup' => true,
@@ -3618,7 +3624,7 @@ public static function workspaceCleanupRun( array $input ): array|\WP_Error {
36183624
'size_limit' => 200,
36193625
),
36203626
),
3621-
'artifacts' => array(
3627+
'artifacts' => array(
36223628
'task_type' => 'workspace_retention_cleanup',
36233629
'params' => array(
36243630
'dry_run' => false,
@@ -3627,7 +3633,18 @@ public static function workspaceCleanupRun( array $input ): array|\WP_Error {
36273633
'skip_github' => true,
36283634
),
36293635
),
3630-
'retention' => array(
3636+
'stale-worktrees' => array(
3637+
'task_type' => 'workspace_retention_cleanup',
3638+
'params' => array(
3639+
'dry_run' => false,
3640+
'artifact_cleanup' => false,
3641+
'worktree_cleanup' => true,
3642+
'skip_github' => true,
3643+
'worktree_older_than' => '14d',
3644+
'worktree_stale_only' => true,
3645+
),
3646+
),
3647+
'retention' => array(
36313648
'task_type' => 'workspace_retention_cleanup',
36323649
'params' => array(
36333650
'dry_run' => false,
@@ -3637,7 +3654,7 @@ public static function workspaceCleanupRun( array $input ): array|\WP_Error {
36373654
'worktree_older_than' => '14d',
36383655
),
36393656
),
3640-
'emergency' => array(
3657+
'emergency' => array(
36413658
'task_type' => 'workspace_disk_emergency_cleanup',
36423659
'params' => array(
36433660
'artifact_chunk_size' => 10,
@@ -3663,6 +3680,21 @@ public static function workspaceCleanupRun( array $input ): array|\WP_Error {
36633680
if ( isset($input['older_than']) && '' !== trim( (string) $input['older_than']) ) {
36643681
$params['worktree_older_than'] = trim( (string) $input['older_than']);
36653682
}
3683+
if ( isset($input['worktree_stale_only']) ) {
3684+
$params['worktree_stale_only'] = (bool) $input['worktree_stale_only'];
3685+
}
3686+
if ( 'artifacts' === $mode ) {
3687+
if ( isset($input['limit']) ) {
3688+
$params['limit'] = (int) $input['limit'];
3689+
}
3690+
if ( isset($input['offset']) ) {
3691+
$params['offset'] = (int) $input['offset'];
3692+
}
3693+
if ( ! empty($input['exhaustive']) ) {
3694+
$params['exhaustive'] = true;
3695+
}
3696+
}
3697+
36663698
$context = array();
36673699
if ( isset($input['user_id']) ) {
36683700
$context['user_id'] = (int) $input['user_id'];
@@ -4046,6 +4078,7 @@ public static function workspaceCleanupPlan( array $input ): array|\WP_Error {
40464078
'force_artifact_cleanup' => ! empty($input['force_artifact_cleanup']),
40474079
'include_resolvers' => ! empty($input['include_resolvers']),
40484080
'mode' => (string) ( $input['mode'] ?? 'cleanup_plan' ),
4081+
'worktree_stale_only' => ! empty($input['worktree_stale_only']),
40494082
);
40504083
foreach ( array( 'include_artifacts', 'include_worktrees' ) as $key ) {
40514084
if ( array_key_exists($key, $input) ) {

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class WorkspaceCommand extends BaseCommand {
3131

3232
private const CLEANUP_CLI_SOURCE = 'workspace_cleanup_cli';
3333

34-
private const CLEANUP_MODES = array( 'inventory', 'artifacts', 'retention', 'emergency' );
34+
private const CLEANUP_MODES = array( 'inventory', 'artifacts', 'retention', 'stale-worktrees', 'emergency' );
3535

3636
private const METADATA_RECONCILE_DEFAULT_LIMIT = 25;
3737

@@ -583,6 +583,7 @@ public function adopt_repo( array $args, array $assoc_args ): void {
583583
* - metadata
584584
* - artifacts
585585
* - retention
586+
* - stale-worktrees
586587
* - emergency
587588
* ---
588589
*
@@ -596,6 +597,7 @@ public function adopt_repo( array $args, array $assoc_args ): void {
596597
* : For `plan --mode=retention`, also include the exhaustive artifact cleanup
597598
* scan. Omitted by default so safe retention planning stays bounded on large
598599
* workspaces; use `--mode=artifacts` for an artifact-only plan.
600+
* `--mode=stale-worktrees` never includes artifacts unless this flag is passed.
599601
*
600602
* [--older-than=<duration>]
601603
* : Pass an age gate such as 7d or 24h into cleanup task params.
@@ -651,6 +653,10 @@ public function adopt_repo( array $args, array $assoc_args ): void {
651653
* # Start task-backed retention cleanup and capture the returned run_id
652654
* wp datamachine-code workspace cleanup run --mode=retention
653655
*
656+
* # Review and then apply destructive stale worktree removal only
657+
* wp datamachine-code workspace cleanup plan --mode=stale-worktrees --older-than=14d --format=json
658+
* wp datamachine-code workspace cleanup run --mode=stale-worktrees --older-than=14d
659+
*
654660
* # Review artifact cleanup synchronously (bounded; default limit=100)
655661
* wp datamachine-code workspace cleanup run --mode=artifacts --dry-run
656662
*
@@ -899,6 +905,23 @@ private function cleanup_run_input( string $mode, array $assoc_args ): array {
899905
if ( isset($assoc_args['older-than']) && '' !== trim( (string) $assoc_args['older-than']) ) {
900906
$input['older_than'] = trim( (string) $assoc_args['older-than']);
901907
}
908+
if ( 'stale-worktrees' === $mode ) {
909+
$input['worktree_stale_only'] = true;
910+
if ( ! isset($input['older_than']) ) {
911+
$input['older_than'] = '14d';
912+
}
913+
}
914+
if ( 'artifacts' === $mode ) {
915+
if ( isset($assoc_args['limit']) ) {
916+
$input['limit'] = (int) $assoc_args['limit'];
917+
}
918+
if ( isset($assoc_args['offset']) ) {
919+
$input['offset'] = (int) $assoc_args['offset'];
920+
}
921+
if ( ! empty($assoc_args['exhaustive']) ) {
922+
$input['exhaustive'] = true;
923+
}
924+
}
902925

903926
return $input;
904927
}
@@ -953,6 +976,12 @@ private function cleanup_plan_input( string $mode, array $assoc_args ): array {
953976
if ( isset($assoc_args['force']) ) {
954977
$input['force_artifact_cleanup'] = (bool) $assoc_args['force'];
955978
}
979+
if ( 'stale-worktrees' === $mode ) {
980+
$input['worktree_stale_only'] = true;
981+
if ( empty($input['worktree_older_than']) ) {
982+
$input['worktree_older_than'] = '14d';
983+
}
984+
}
956985

957986
return $input;
958987
}
@@ -1026,6 +1055,19 @@ private function run_cleanup_review( array $assoc_args ): void {
10261055
$this->render_worktree_emergency_cleanup_result_from_ability($result, $assoc_args);
10271056
return;
10281057

1058+
case 'stale-worktrees':
1059+
$ability = wp_get_ability('datamachine-code/workspace-worktree-cleanup');
1060+
$input = array(
1061+
'dry_run' => true,
1062+
'force' => ! empty($assoc_args['force']),
1063+
'skip_github' => true,
1064+
'stale_liveness_only' => true,
1065+
'older_than' => isset($assoc_args['older-than']) && '' !== trim( (string) $assoc_args['older-than']) ? trim( (string) $assoc_args['older-than']) : '14d',
1066+
);
1067+
$result = $ability ? $ability->execute($input) : new \WP_Error('worktree_cleanup_ability_missing', 'Worktree cleanup ability not registered.');
1068+
$this->render_worktree_cleanup_result_from_ability($result, $assoc_args);
1069+
return;
1070+
10291071
case 'retention':
10301072
default:
10311073
$ability = wp_get_ability('datamachine-code/workspace-worktree-cleanup');

inc/Tasks/WorkspaceRetentionCleanupTask.php

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,12 @@ public function executeTask( int $jobId, array $params ): void {
8787
}
8888

8989
$opts = array(
90-
'dry_run' => ! empty($params['dry_run']),
91-
'force' => ! empty($params['force']),
92-
'skip_github' => array_key_exists('skip_github', $params) ? (bool) $params['skip_github'] : true,
93-
'worktree_cleanup' => array_key_exists('worktree_cleanup', $params) ? (bool) $params['worktree_cleanup'] : true,
94-
'artifact_cleanup' => array_key_exists('artifact_cleanup', $params) ? (bool) $params['artifact_cleanup'] : true,
90+
'dry_run' => ! empty($params['dry_run']),
91+
'force' => ! empty($params['force']),
92+
'skip_github' => array_key_exists('skip_github', $params) ? (bool) $params['skip_github'] : true,
93+
'worktree_cleanup' => array_key_exists('worktree_cleanup', $params) ? (bool) $params['worktree_cleanup'] : true,
94+
'artifact_cleanup' => array_key_exists('artifact_cleanup', $params) ? (bool) $params['artifact_cleanup'] : true,
95+
'worktree_stale_only' => ! empty($params['worktree_stale_only']),
9596
);
9697
if ( isset($params['worktree_older_than']) && '' !== trim( (string) $params['worktree_older_than']) ) {
9798
$opts['worktree_older_than'] = trim( (string) $params['worktree_older_than']);
@@ -217,6 +218,7 @@ private function schedule_job_backed_cleanup( int $jobId, Workspace $workspace,
217218
'worktree_cleanup' => (bool) $opts['worktree_cleanup'],
218219
'artifact_cleanup' => (bool) $opts['artifact_cleanup'],
219220
'worktree_older_than' => (string) ( $opts['worktree_older_than'] ?? '14d' ),
221+
'worktree_stale_only' => (bool) $opts['worktree_stale_only'],
220222
'skip_github' => (bool) $opts['skip_github'],
221223
'force' => (bool) $opts['force'],
222224
),
@@ -272,11 +274,12 @@ private function build_cleanup_chunk_rows( Workspace $workspace, array $opts, ar
272274
if ( ! empty($opts['worktree_cleanup']) ) {
273275
$worktree_plan = $workspace->worktree_cleanup_merged(
274276
array(
275-
'dry_run' => true,
276-
'force' => ! empty($opts['force']),
277-
'skip_github' => ! empty($opts['skip_github']),
278-
'older_than' => (string) ( $opts['worktree_older_than'] ?? '14d' ),
279-
'sort' => 'age',
277+
'dry_run' => true,
278+
'force' => ! empty($opts['force']),
279+
'skip_github' => ! empty($opts['skip_github']),
280+
'older_than' => (string) ( $opts['worktree_older_than'] ?? '14d' ),
281+
'sort' => 'age',
282+
'stale_liveness_only' => ! empty($opts['worktree_stale_only']),
280283
)
281284
);
282285
if ( $worktree_plan instanceof \WP_Error ) {
@@ -320,11 +323,12 @@ private function build_cleanup_chunk_params( array $chunk_rows, array $opts, arr
320323
foreach ( array( 'artifacts', 'metadata', 'worktrees' ) as $type ) {
321324
foreach ( array_chunk( (array) ( $chunk_rows[ $type ] ?? array() ), $sizes[ $type ]) as $index => $rows ) {
322325
$item_params[] = array(
323-
'chunk_type' => $type,
324-
'chunk_index' => $index,
325-
'rows' => $rows,
326-
'force' => ! empty($opts['force']),
327-
'skip_github' => ! empty($opts['skip_github']),
326+
'chunk_type' => $type,
327+
'chunk_index' => $index,
328+
'rows' => $rows,
329+
'force' => ! empty($opts['force']),
330+
'skip_github' => ! empty($opts['skip_github']),
331+
'stale_liveness_only' => ! empty($opts['worktree_stale_only']),
328332
);
329333
}
330334
}

inc/Tasks/WorktreeCleanupChunkTask.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,14 @@ public function executeTask( int $jobId, array $params ): void {
110110
'apply_plan' => array( 'candidates' => $rows ),
111111
'skip_github' => array_key_exists('skip_github', $params) ? (bool) $params['skip_github'] : true,
112112
'include_repaired_metadata' => ! empty($params['include_repaired_metadata']),
113+
'stale_liveness_only' => ! empty($params['stale_liveness_only']),
113114
)
114115
),
115116
default => new \WP_Error('invalid_cleanup_chunk_type', sprintf('Unknown cleanup chunk type: %s', $chunk_type), array( 'status' => 400 )),
116117
};
117118

118119
if ( $result instanceof \WP_Error ) {
119-
$failed = $this->rows_to_failed($rows, $result->get_error_code(), $result->get_error_message());
120+
$failed = $this->rows_to_failed($rows, (string) $result->get_error_code(), $result->get_error_message());
120121
$this->completeJob(
121122
$jobId,
122123
$this->build_chunk_result(
@@ -246,7 +247,7 @@ private function execute_artifact_discovery_chunk( int $jobId, array $params, fl
246247
$planned,
247248
array(),
248249
$skipped,
249-
$this->rows_to_failed($planned, $result->get_error_code(), $result->get_error_message()),
250+
$this->rows_to_failed($planned, (string) $result->get_error_code(), $result->get_error_message()),
250251
0,
251252
$started_at,
252253
array(

0 commit comments

Comments
 (0)