Skip to content

feat: bounded cleanup-eligible apply for explicit lifecycle metadata#223

Merged
chubes4 merged 1 commit intomainfrom
issue-216-bounded-apply
May 4, 2026
Merged

feat: bounded cleanup-eligible apply for explicit lifecycle metadata#223
chubes4 merged 1 commit intomainfrom
issue-216-bounded-apply

Conversation

@chubes4
Copy link
Copy Markdown
Member

@chubes4 chubes4 commented May 4, 2026

Summary

Adds an operator-safe bounded cleanup-eligible apply path for worktrees that have explicit lifecycle cleanup_eligible metadata. Closes the gap from #216: hundreds of worktrees pile up, full retention dry-run/apply paths time out or take too long to be useful, and there is no end-to-end way to safely reclaim the explicit cleanup_eligible rows in bounded batches.

This PR adds that path without touching the existing retention/artifact/emergency cleanup behaviors and without introducing any new internal pipelines or flows — purely WP-CLI wrapping a workspace ability, the same pattern as the rest of workspace worktree *.

What changed

Workspace::worktree_bounded_cleanup_eligible_apply() (new public method on inc/Workspace/Workspace.php):

  • Reuses the existing inventory-only path for candidate discovery, so it never triggers full worktree_list / git fetch / GitHub API work just to plan. Only worktrees whose persisted lifecycle metadata satisfies WorktreeContextInjector::has_cleanup_signal() are considered; rows without an explicit cleanup_eligible signal stay skipped via the existing inventory gate (no_inventory_cleanup_signal, requires_full_scan).
  • Caps each call with --limit (default 25, hard ceiling 200).
  • Optional --older-than gate against lifecycle created_at metadata.
  • Revalidates each candidate immediately before mutation via cheap gates:
    • missing repo/branch/path metadata
    • external / containment violation (path outside workspace root)
    • missing or non-directory path
    • primary .git directory (refuses to remove primaries)
    • dirty working tree (git status --porcelain, bounded by CLEANUP_GIT_PROBE_TIMEOUT)
    • unpushed commits (hard stop — never overridden by force)
    • missing primary checkout
  • --via-jobs schedules per-candidate worktree_cleanup_chunk jobs (single-row chunks) for resumable async apply, reusing the existing chunk task + lock flow.
  • Returns evidence with summary.processed / removed / skipped / bytes_reclaimed, plus a continuation envelope (remaining_total, remaining_handles, next_call_hint) so operators can drain the workspace in successive bounded calls.

Surfaces

  • New ability: datamachine/workspace-worktree-bounded-cleanup-eligible-apply (inc/Abilities/WorkspaceAbilities.php).
  • New CLI subcommand:
    wp datamachine-code workspace worktree bounded-cleanup-eligible-apply \
        [--dry-run] [--limit=N] [--older-than=DUR] \
        [--sort=size|age] [--force] [--via-jobs]
    
    Routed through the new ability with table/JSON renderer (inc/Cli/Commands/WorkspaceCommand.php).

Acceptance criteria

Criterion How it's met
Operator can apply explicit cleanup-eligible candidates without full scan Inventory-only candidate discovery — no worktree_list, no GitHub. Only rows with explicit lifecycle cleanup_eligible metadata become candidates.
Each deletion revalidated immediately before mutation revalidate_bounded_cleanup_eligible_candidate() runs the cheap safety gates per candidate at apply time.
Dirty/unpushed/ambiguous/missing-metadata stay protected Each gate returns a stable reason_code and the worktree survives on disk; verified by smoke.
Job-backed/resumable where appropriate --via-jobs schedules per-candidate worktree_cleanup_chunk jobs via TaskScheduler::scheduleBatch.
Evidence: processed / removed / skipped / bytes / continuation Returned in summary + continuation.

Tests

New tests/smoke-worktree-bounded-cleanup-eligible-apply.php (30 assertions, all green) — builds a real workspace with a mix of cleanup-eligible / dirty / unpushed / active / missing-metadata worktrees and asserts:

  • Inventory-only path is used (no full worktree_list calls).
  • --limit actually bounds the batch and continuation.remaining_total reports the rest.
  • --dry-run returns success without mutating anything; --via-jobs + --dry-run is rejected with a stable error.
  • Synchronous apply removes only the safe rows; dirty/unpushed/missing-metadata/active rows survive on disk with the correct reason_code.
  • force=true permits dirty cleanup but the unpushed-commit gate is never overridden.
tests/smoke-worktree-bounded-cleanup-eligible-apply.php  30/30 passed
tests/smoke-worktree-cleanup.php                        129/129 passed
tests/smoke-worktree-cleanup-chunks.php                  14/14 passed
tests/smoke-worktree-cleanup-artifacts.php               48/48 passed
tests/smoke-worktree-metadata-reconcile.php              32/32 passed

Out of scope

  • No new pipelines, flows, or recurring schedules — this stays a CLI/ability surface per the issue's "system-task/ability patterns" constraint.
  • No changes to worktree_cleanup_merged / worktree_cleanup_artifacts / worktree_emergency_cleanup semantics.

AI assistance

  • AI assistance: Yes
  • Tool(s): OpenCode (GPT-5.5)
  • Used for: Drafted the bounded apply method, ability registration, CLI subcommand, and smoke test based on the existing inventory-only and chunked retention cleanup patterns. Author reviewed against the issue acceptance criteria and ran the smoke + neighboring cleanup tests locally.

Closes #216

Adds an operator-safe bounded apply path so worktrees with explicit
lifecycle `cleanup_eligible` metadata can be reclaimed in bounded
batches without running the slow retention scan, full git worktree
discovery, or GitHub API lookups first.

Workspace::worktree_bounded_cleanup_eligible_apply():
  - Uses the existing inventory_only path for candidate discovery
    (no full worktree_list / fetch / GitHub work). Only worktrees
    whose persisted lifecycle metadata satisfies
    WorktreeContextInjector::has_cleanup_signal() are considered;
    rows without explicit cleanup_eligible signal are skipped with a
    stable reason code via the inventory gate.
  - Caps each call with --limit (default 25, hard ceiling 200).
  - Optional --older-than gate against lifecycle created_at metadata.
  - Revalidates each candidate immediately before mutation through
    cheap gates: missing-metadata, external/containment, missing path,
    primary .git directory, dirty (porcelain), unpushed (hard stop —
    never overridden by force), and missing primary checkout.
  - --via-jobs schedules per-candidate worktree_cleanup_chunk jobs for
    resumable async apply, reusing the existing chunk task and lock
    flow.
  - Evidence: summary.processed/removed/skipped/bytes_reclaimed plus a
    continuation envelope (remaining_total/remaining_handles/
    next_call_hint) so operators can drain the workspace in successive
    bounded calls.

Surfaces:
  - New ability: datamachine/workspace-worktree-bounded-cleanup-eligible-apply.
  - New CLI subcommand:
      wp datamachine-code workspace worktree bounded-cleanup-eligible-apply
        [--dry-run] [--limit=N] [--older-than=DUR]
        [--sort=size|age] [--force] [--via-jobs]
    Routed through the new ability with table/JSON renderer.

Smoke test (tests/smoke-worktree-bounded-cleanup-eligible-apply.php)
covers the bounded limit, dry-run/apply paths, every revalidation
gate, and force semantics (dirty allowed, unpushed never).

Refs #216
@chubes4 chubes4 force-pushed the issue-216-bounded-apply branch from 4cfc619 to 209efb5 Compare May 4, 2026 11:40
@chubes4 chubes4 changed the title feat: bounded cleanup apply for obvious worktrees feat: bounded cleanup-eligible apply for explicit lifecycle metadata May 4, 2026
@chubes4 chubes4 merged commit c8655fb into main May 4, 2026
@chubes4 chubes4 deleted the issue-216-bounded-apply branch May 4, 2026 11:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add operator-safe bounded cleanup apply for obvious candidates

1 participant