Problem
workspace cleanup safe --dry-run ignores its wall-clock budget. An explicit
10-second budget produces a ~104-second run:
$ time studio wp datamachine-code workspace cleanup safe --dry-run --until-budget=10s
safe dry-run with 10s budget: 103866 ms
WP-CLI/Studio bootstrap is not the cause — a trivial wp option get siteurl
on the same install costs 4.4s.
WorkspaceSafeCleanupOrchestrator::DEFAULT_BUDGET is 45s, so even the
unspecified default is overrun by ~2.7x.
Root cause
The budget is computed and plumbed, then dropped on the only stage the safe
dry-run actually runs.
WorkspaceCleanupPlan::workspace_cleanup_plan() resolves until_budget
(WorkspaceCleanupPlan.php:48) and forwards it to the worktree branch
only:
// WorkspaceCleanupPlan.php:96 — worktree branch receives the budget
$worktree_args['until_budget'] = $inputs['until_budget'];
The artifact branch never receives it (WorkspaceCleanupPlan.php:61-73):
$artifact_plan = $this->worktree_cleanup_artifacts( array(
'dry_run' => true,
'force' => $inputs['force_artifact_cleanup'],
'full_workspace' => $inputs['full_workspace'],
'limit' => $inputs['limit'],
'offset' => $inputs['offset'],
'sort' => $inputs['artifact_sort'],
'older_than' => $inputs['worktree_older_than'],
// no 'until_budget'
) );
WorkspaceArtifactCleanup::worktree_cleanup_artifacts() has no wall-clock
budget parameter at all — its documented $opts are dry_run, force, allow_active_artifact_cleanup, allow_unavailable_process_probe, apply_plan, limit, offset, exhaustive, safety_probes, older_than, only_handles. It is
bounded only by limit.
The safe dry-run then disables the one branch that honors the budget.
WorkspaceSafeCleanupOrchestrator.php:167-173:
$artifact_input = $dry_run ? array(
'mode' => 'artifacts',
'include_artifacts' => true,
'include_worktrees' => false, // the budgeted branch is off
'limit' => $limit,
) : ...
So on the safe dry-run path, 100% of the resolved budget is discarded. The
orchestrator's own pre-stage checks (budget_partial_result()) still work, but
they only gate between stages — they cannot bound a stage that runs long.
Impact
--until-budget is inert on the documented safe-cleanup preview path.
- The operation cannot be used as a bounded, resumable preview as designed.
- Callers that budget around it are terminated mid-inventory. Homeboy's
worktree-provider cleanup delegates here and reports
inventory_completeness: partial with 0 reclaimable bytes on every run,
while a direct DMC cleanup on the same workspace reclaimed ~147 GiB across
93 worktrees.
Measured stage costs
Timed individually against this workspace (~300 worktrees):
| stage |
budget |
wall clock |
worktree locks --prune-stale --dry-run |
n/a |
2.9s |
bounded-cleanup-eligible-apply --dry-run |
5s |
3.2s (honored) |
cleanup-artifacts --dry-run --limit=25 |
n/a |
3.5s |
cleanup safe --dry-run |
10s |
103.9s |
The individually-timed stages are all cheap, so the ~104s is not yet
attributed to a single stage. Note the low-level cleanup plan --mode=artifacts
CLI defaults (limit=100, include_worktrees=true) differ from the
orchestrator's inputs (limit=25, include_worktrees=false), so that command
is not a faithful proxy — per-stage timing evidence from the orchestrator
itself is still needed to attribute the remaining time.
Acceptance criteria
worktree_cleanup_artifacts() accepts and enforces a wall-clock budget.
workspace_cleanup_plan() forwards until_budget to the artifact branch.
cleanup safe --dry-run --until-budget=<d> returns within <d> plus a
bounded reporting margin, emitting a resumable continuation instead of
overrunning.
- Regression coverage asserts a short budget produces a short run and a
continuation, rather than only asserting the option is accepted.
- Per-stage elapsed timing is reported in the run evidence so a future overrun
is attributable without external timing.
AI assistance disclosure: investigated and written with Claude Sonnet 4.5 (Anthropic) driving OpenCode via the Kimaki Discord bridge. The model ran the timings above and traced the budget plumbing through the source; a human directed the work. An earlier hypothesis that sort=size forced a full-workspace scan was tested and disproven (3.5s), and is recorded here so it is not retried.
Problem
workspace cleanup safe --dry-runignores its wall-clock budget. An explicit10-second budget produces a ~104-second run:
WP-CLI/Studio bootstrap is not the cause — a trivial
wp option get siteurlon the same install costs 4.4s.
WorkspaceSafeCleanupOrchestrator::DEFAULT_BUDGETis45s, so even theunspecified default is overrun by ~2.7x.
Root cause
The budget is computed and plumbed, then dropped on the only stage the safe
dry-run actually runs.
WorkspaceCleanupPlan::workspace_cleanup_plan()resolvesuntil_budget(
WorkspaceCleanupPlan.php:48) and forwards it to the worktree branchonly:
The artifact branch never receives it (
WorkspaceCleanupPlan.php:61-73):WorkspaceArtifactCleanup::worktree_cleanup_artifacts()has no wall-clockbudget parameter at all — its documented
$optsaredry_run, force, allow_active_artifact_cleanup, allow_unavailable_process_probe, apply_plan, limit, offset, exhaustive, safety_probes, older_than, only_handles. It isbounded only by
limit.The safe dry-run then disables the one branch that honors the budget.
WorkspaceSafeCleanupOrchestrator.php:167-173:So on the safe dry-run path, 100% of the resolved budget is discarded. The
orchestrator's own pre-stage checks (
budget_partial_result()) still work, butthey only gate between stages — they cannot bound a stage that runs long.
Impact
--until-budgetis inert on the documented safe-cleanup preview path.worktree-provider cleanup delegates here and reports
inventory_completeness: partialwith 0 reclaimable bytes on every run,while a direct DMC cleanup on the same workspace reclaimed ~147 GiB across
93 worktrees.
Measured stage costs
Timed individually against this workspace (~300 worktrees):
worktree locks --prune-stale --dry-runbounded-cleanup-eligible-apply --dry-runcleanup-artifacts --dry-run --limit=25cleanup safe --dry-runThe individually-timed stages are all cheap, so the ~104s is not yet
attributed to a single stage. Note the low-level
cleanup plan --mode=artifactsCLI defaults (
limit=100,include_worktrees=true) differ from theorchestrator's inputs (
limit=25,include_worktrees=false), so that commandis not a faithful proxy — per-stage timing evidence from the orchestrator
itself is still needed to attribute the remaining time.
Acceptance criteria
worktree_cleanup_artifacts()accepts and enforces a wall-clock budget.workspace_cleanup_plan()forwardsuntil_budgetto the artifact branch.cleanup safe --dry-run --until-budget=<d>returns within<d>plus abounded reporting margin, emitting a resumable continuation instead of
overrunning.
continuation, rather than only asserting the option is accepted.
is attributable without external timing.
AI assistance disclosure: investigated and written with Claude Sonnet 4.5 (Anthropic) driving OpenCode via the Kimaki Discord bridge. The model ran the timings above and traced the budget plumbing through the source; a human directed the work. An earlier hypothesis that
sort=sizeforced a full-workspace scan was tested and disproven (3.5s), and is recorded here so it is not retried.