@yemi33/minions 0.1.2424 → 0.1.2425
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +6 -4
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/internal-install.md +190 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
package/engine/worktree-gc.js
CHANGED
|
@@ -27,6 +27,9 @@ const path = require('path');
|
|
|
27
27
|
const shared = require('./shared');
|
|
28
28
|
const worktreePool = require('./worktree-pool');
|
|
29
29
|
|
|
30
|
+
const DEFAULT_ORPHAN_RETENTION_MS = 2 * 60 * 60 * 1000;
|
|
31
|
+
const DEFAULT_QUARANTINE_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
|
|
30
33
|
let keepProcessSweep = null;
|
|
31
34
|
function _keepProcessSweep() {
|
|
32
35
|
if (!keepProcessSweep) keepProcessSweep = require('./keep-process-sweep');
|
|
@@ -41,20 +44,29 @@ function _managedSpawn() {
|
|
|
41
44
|
|
|
42
45
|
const _noopLog = () => {};
|
|
43
46
|
|
|
44
|
-
function
|
|
47
|
+
function _pathIsLiveFailOpen(checkFn, worktreePath, opts) {
|
|
48
|
+
try { return !!checkFn(worktreePath, opts); }
|
|
49
|
+
catch { return true; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _collectAnchoredCwds(listManagedSpecs, listKeepProcessCwds, opts = {}) {
|
|
45
53
|
const out = [];
|
|
46
54
|
try {
|
|
47
55
|
for (const rec of (listManagedSpecs() || [])) {
|
|
48
56
|
if (!rec || typeof rec.cwd !== 'string' || !rec.cwd) continue;
|
|
49
57
|
try { out.push(shared.realPathForComparison(rec.cwd)); } catch {}
|
|
50
58
|
}
|
|
51
|
-
} catch {
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (opts.throwOnFailure) throw err;
|
|
61
|
+
}
|
|
52
62
|
try {
|
|
53
63
|
for (const cwd of (listKeepProcessCwds() || [])) {
|
|
54
64
|
if (typeof cwd !== 'string' || !cwd) continue;
|
|
55
65
|
try { out.push(shared.realPathForComparison(cwd)); } catch {}
|
|
56
66
|
}
|
|
57
|
-
} catch {
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (opts.throwOnFailure) throw err;
|
|
69
|
+
}
|
|
58
70
|
return out;
|
|
59
71
|
}
|
|
60
72
|
|
|
@@ -67,6 +79,51 @@ function _isWorktreeCwdAnchored(worktreePath, anchoredCwds) {
|
|
|
67
79
|
return false;
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
function _isQuarantineDirName(name) {
|
|
83
|
+
return /-quarantine-(?:protected-)?\d+(?:$|-)/.test(String(name || ''));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _quarantineCreatedAt(name, fallbackMs) {
|
|
87
|
+
const match = String(name || '').match(/-quarantine-(?:protected-)?(\d+)(?:$|-)/);
|
|
88
|
+
if (!match) return fallbackMs;
|
|
89
|
+
const parsed = Number(match[1]);
|
|
90
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackMs;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isLegacyMinionsWorktreeDir(worktreePath, commonGitDir, opts = {}) {
|
|
94
|
+
const _fs = opts.fs || fs;
|
|
95
|
+
if (!worktreePath || !commonGitDir) return false;
|
|
96
|
+
const name = path.basename(path.resolve(worktreePath));
|
|
97
|
+
if (!/^W-[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) return false;
|
|
98
|
+
const dotGit = path.join(worktreePath, '.git');
|
|
99
|
+
let stat;
|
|
100
|
+
try { stat = _fs.lstatSync(dotGit); } catch { return false; }
|
|
101
|
+
if (!stat || !stat.isFile()) return false;
|
|
102
|
+
let raw;
|
|
103
|
+
try { raw = String(_fs.readFileSync(dotGit, 'utf8')); } catch { return false; }
|
|
104
|
+
const match = raw.match(/^gitdir:\s*(.+?)\s*$/im);
|
|
105
|
+
if (!match) return false;
|
|
106
|
+
let gitdir;
|
|
107
|
+
try {
|
|
108
|
+
gitdir = path.resolve(worktreePath, match[1].trim());
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const commonWorktrees = worktreePool._normalizePath(path.join(commonGitDir, 'worktrees'));
|
|
113
|
+
const candidate = worktreePool._normalizePath(gitdir);
|
|
114
|
+
return !!candidate && candidate.startsWith(commonWorktrees + '/');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _resolveGitCommonDir(rootDir, execSilent) {
|
|
118
|
+
const raw = String(execSilent('git rev-parse --git-common-dir', {
|
|
119
|
+
cwd: rootDir,
|
|
120
|
+
timeout: 10000,
|
|
121
|
+
windowsHide: true,
|
|
122
|
+
}) || '').trim();
|
|
123
|
+
if (!raw) return null;
|
|
124
|
+
return path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(rootDir, raw);
|
|
125
|
+
}
|
|
126
|
+
|
|
70
127
|
// ── Stuck-dir escalation (Layer 4 of W-mq5o6bvy000x7191) ─────────────────────
|
|
71
128
|
// In-memory ring of paths that have failed removal N consecutive times. After
|
|
72
129
|
// escalation we (a) write a dedup'd inbox note, (b) suppress repeat per-tick
|
|
@@ -333,7 +390,7 @@ function _resetStuckPathsForTesting() { _stuckPaths.clear(); }
|
|
|
333
390
|
// OS to release the killed processes' file handles, then attempts ONE more
|
|
334
391
|
// removal. On success, writes the recovered-via-holder-reap note via
|
|
335
392
|
// `_markStuckSuccess` with the reaped-pid attribution.
|
|
336
|
-
function _postReapRetry(wtPath, gitRoot, parentDir, resolvedPath, _removeWorktree, opts,
|
|
393
|
+
function _postReapRetry(wtPath, gitRoot, parentDir, resolvedPath, _removeWorktree, opts, stats, log, reapedPids) {
|
|
337
394
|
try { shared.clearWorktreeFailureCache(resolvedPath); } catch { /* optional */ }
|
|
338
395
|
|
|
339
396
|
// The actual removal attempt, run after the settle window.
|
|
@@ -343,8 +400,12 @@ function _postReapRetry(wtPath, gitRoot, parentDir, resolvedPath, _removeWorktre
|
|
|
343
400
|
if (removed) {
|
|
344
401
|
// Down-count the failure we recorded just before; this dispatch
|
|
345
402
|
// ultimately succeeded after the auto-reap.
|
|
346
|
-
|
|
347
|
-
|
|
403
|
+
for (const stat of stats) {
|
|
404
|
+
if (!stat) continue;
|
|
405
|
+
if (typeof stat.failed === 'number') stat.failed = Math.max(0, stat.failed - 1);
|
|
406
|
+
if (typeof stat.evicted === 'number') stat.evicted++;
|
|
407
|
+
if (typeof stat.removed === 'number') stat.removed++;
|
|
408
|
+
}
|
|
348
409
|
_markStuckSuccess(resolvedPath, {
|
|
349
410
|
writeToInbox: opts.writeToInbox,
|
|
350
411
|
recoveryReason: 'holder-reap',
|
|
@@ -378,13 +439,16 @@ function _postReapRetry(wtPath, gitRoot, parentDir, resolvedPath, _removeWorktre
|
|
|
378
439
|
try { settled = sleepFn(2000); } catch { /* sleep init failure — fire retry anyway */ }
|
|
379
440
|
|
|
380
441
|
if (settled && typeof settled.then === 'function') {
|
|
381
|
-
// Async settle window —
|
|
382
|
-
|
|
383
|
-
return
|
|
442
|
+
// Async settle window — callers may observe completion without blocking
|
|
443
|
+
// the tick that scheduled it.
|
|
444
|
+
return Promise.resolve(settled)
|
|
445
|
+
.then(() => _doRetry(), () => false)
|
|
446
|
+
.catch(() => false);
|
|
384
447
|
}
|
|
385
448
|
|
|
386
449
|
// Sync settle window (test injection) — fire retry immediately.
|
|
387
450
|
try { _doRetry(); } catch { /* ignore */ }
|
|
451
|
+
return null;
|
|
388
452
|
}
|
|
389
453
|
|
|
390
454
|
/**
|
|
@@ -424,7 +488,8 @@ function _postReapRetry(wtPath, gitRoot, parentDir, resolvedPath, _removeWorktre
|
|
|
424
488
|
* matching gcDispatchWorktreeIfOrphan's existing contract. When `stats`
|
|
425
489
|
* objects are supplied the helper mutates their evicted/failed/kept counters
|
|
426
490
|
* directly (the sweeps); the dispatch-end caller passes none and maps the
|
|
427
|
-
* return value instead.
|
|
491
|
+
* return value instead. Async post-reap completions are appended to an
|
|
492
|
+
* optional `deferredRetries` collector for aggregate callers.
|
|
428
493
|
*/
|
|
429
494
|
function reapAndRemoveWorktree(opts) {
|
|
430
495
|
const {
|
|
@@ -449,6 +514,7 @@ function reapAndRemoveWorktree(opts) {
|
|
|
449
514
|
sleepSyncFn = null,
|
|
450
515
|
// accounting + logging
|
|
451
516
|
stats = [],
|
|
517
|
+
deferredRetries = null,
|
|
452
518
|
successReason = 'orphan',
|
|
453
519
|
successMetric = null,
|
|
454
520
|
recheckSuppressedAfterEscalate = false,
|
|
@@ -472,6 +538,7 @@ function reapAndRemoveWorktree(opts) {
|
|
|
472
538
|
if (typeof slowRetryMs === 'number') {
|
|
473
539
|
if (!_shouldSlowRetry(resolved, slowRetryMs)) {
|
|
474
540
|
_bumpStats('kept');
|
|
541
|
+
_bumpStats('skipped');
|
|
475
542
|
return { ..._idle(), reason: 'slow-retry' };
|
|
476
543
|
}
|
|
477
544
|
}
|
|
@@ -483,6 +550,7 @@ function reapAndRemoveWorktree(opts) {
|
|
|
483
550
|
catch (_e) { owned = false; }
|
|
484
551
|
if (!owned) {
|
|
485
552
|
_bumpStats('kept');
|
|
553
|
+
_bumpStats('preserved');
|
|
486
554
|
if (typeof onMarkerSkipLog === 'function') log('debug', onMarkerSkipLog());
|
|
487
555
|
return { ..._idle(), reason: 'no-marker' };
|
|
488
556
|
}
|
|
@@ -512,12 +580,21 @@ function reapAndRemoveWorktree(opts) {
|
|
|
512
580
|
return !escResult.alreadyEscalated && !suppressed;
|
|
513
581
|
};
|
|
514
582
|
const _postReapOpts = { writeToInbox, sleepSyncFn };
|
|
583
|
+
const _queuePostReapRetry = (reapedPids) => {
|
|
584
|
+
if (!enablePostReapRetry || reapedPids.length === 0) return;
|
|
585
|
+
const completion = _postReapRetry(
|
|
586
|
+
worktreePath, gitRoot, parentDir, resolved, _removeFn,
|
|
587
|
+
_postReapOpts, stats, log, reapedPids
|
|
588
|
+
);
|
|
589
|
+
if (completion && Array.isArray(deferredRetries)) deferredRetries.push(completion);
|
|
590
|
+
};
|
|
515
591
|
|
|
516
592
|
// (4)+(5) remove + accounting. removeWorktree owns its own backoff loop.
|
|
517
593
|
try {
|
|
518
594
|
const removed = _removeFn(worktreePath, gitRoot, parentDir, rmOpts);
|
|
519
595
|
if (removed) {
|
|
520
596
|
_bumpStats('evicted');
|
|
597
|
+
_bumpStats('removed');
|
|
521
598
|
_markStuckSuccess(resolved, { writeToInbox });
|
|
522
599
|
if (successMetric) { try { shared.bumpWorktreeGcMetric(successMetric); } catch { /* optional */ } }
|
|
523
600
|
if (typeof onRemovedLog === 'function') log('info', onRemovedLog());
|
|
@@ -527,18 +604,14 @@ function reapAndRemoveWorktree(opts) {
|
|
|
527
604
|
const escResult = _maybeEscalateStuck(resolved, 'remove returned false', _escalateOpts);
|
|
528
605
|
if (_shouldWarn(escResult) && typeof onFailLog === 'function') log('warn', onFailLog('false'));
|
|
529
606
|
const reapedPids = escResult.reapedPids || [];
|
|
530
|
-
|
|
531
|
-
_postReapRetry(worktreePath, gitRoot, parentDir, resolved, _removeFn, _postReapOpts, stats[0] || null, stats[1] || null, log, reapedPids);
|
|
532
|
-
}
|
|
607
|
+
_queuePostReapRetry(reapedPids);
|
|
533
608
|
return { outcome: 'gc-failed', removed: false, reason: 'remove-failed', escalated: escResult.escalated, stuckEscalated: escResult.escalated, reapedPids };
|
|
534
609
|
} catch (rmErr) {
|
|
535
610
|
_bumpStats('failed');
|
|
536
611
|
const escResult = _maybeEscalateStuck(resolved, rmErr && rmErr.message, _escalateOpts);
|
|
537
612
|
if (_shouldWarn(escResult) && typeof onFailLog === 'function') log('warn', onFailLog('threw', rmErr && rmErr.message));
|
|
538
613
|
const reapedPids = escResult.reapedPids || [];
|
|
539
|
-
|
|
540
|
-
_postReapRetry(worktreePath, gitRoot, parentDir, resolved, _removeFn, _postReapOpts, stats[0] || null, stats[1] || null, log, reapedPids);
|
|
541
|
-
}
|
|
614
|
+
_queuePostReapRetry(reapedPids);
|
|
542
615
|
return { outcome: 'gc-failed', removed: false, reason: 'remove-threw', escalated: escResult.escalated, stuckEscalated: escResult.escalated, reapedPids };
|
|
543
616
|
}
|
|
544
617
|
}
|
|
@@ -613,7 +686,10 @@ function shouldGcDispatchWorktree(opts) {
|
|
|
613
686
|
return { gc: false, reason: 'managed-spawn-anchored' };
|
|
614
687
|
}
|
|
615
688
|
|
|
616
|
-
// keep_processes anchor check:
|
|
689
|
+
// keep_processes anchor check: only live PIDs whose declared cwd is inside
|
|
690
|
+
// this worktree protect it. An agent can leave a process from an older
|
|
691
|
+
// dispatch alive, so an agent-wide PID match alone is not a path anchor.
|
|
692
|
+
// Legacy records without cwd remain fail-open and retain the worktree.
|
|
617
693
|
if (agentId) {
|
|
618
694
|
try {
|
|
619
695
|
const fn = typeof getActiveAnchorPidsForAgent === 'function'
|
|
@@ -621,7 +697,19 @@ function shouldGcDispatchWorktree(opts) {
|
|
|
621
697
|
: _keepProcessSweep().getActiveAnchorPidsForAgent;
|
|
622
698
|
const res = fn(agentId);
|
|
623
699
|
if (res && res.pids && res.pids.size > 0) {
|
|
624
|
-
|
|
700
|
+
const anchorCwd = res.record && typeof res.record.cwd === 'string'
|
|
701
|
+
? res.record.cwd
|
|
702
|
+
: '';
|
|
703
|
+
if (!anchorCwd) {
|
|
704
|
+
return { gc: false, reason: 'keep-processes-anchored' };
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
if (shared.isPathInsideOrEqual(anchorCwd, worktreePath)) {
|
|
708
|
+
return { gc: false, reason: 'keep-processes-anchored' };
|
|
709
|
+
}
|
|
710
|
+
} catch (_e) {
|
|
711
|
+
return { gc: false, reason: 'keep-processes-anchored' };
|
|
712
|
+
}
|
|
625
713
|
}
|
|
626
714
|
} catch (_e) { /* sidecar reader optional */ }
|
|
627
715
|
}
|
|
@@ -718,6 +806,404 @@ function gcDispatchWorktreeIfOrphan(opts) {
|
|
|
718
806
|
return { outcome: 'gc-failed', reason: r.reason, removed: false, stuckEscalated: r.stuckEscalated };
|
|
719
807
|
}
|
|
720
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Reconcile each configured worktree root against Git's registry and remove
|
|
811
|
+
* stale on-disk residue that registry-derived GC cannot see. This pass is
|
|
812
|
+
* intentionally narrower than pruneOrphanWorktrees:
|
|
813
|
+
*
|
|
814
|
+
* - registered paths are not deleted here; only locked/initializing entries
|
|
815
|
+
* are protected from the downstream dispatch-oriented pruner;
|
|
816
|
+
* - active SQL claims, pool entries, process anchors, and cwd holders are
|
|
817
|
+
* preserved;
|
|
818
|
+
* - ownership requires the marker, or a legacy Windows W-* name whose .git
|
|
819
|
+
* pointer targets this project's common .git/worktrees directory;
|
|
820
|
+
* - ordinary residue waits two hours; quarantine residue waits 24 hours;
|
|
821
|
+
* - any Git registration-list failure skips the entire shared root.
|
|
822
|
+
*/
|
|
823
|
+
function reconcileStaleWorktreeDirectories(opts) {
|
|
824
|
+
opts = opts || {};
|
|
825
|
+
const projects = Array.isArray(opts.projects) ? opts.projects : [];
|
|
826
|
+
const dispatchSnap = opts.dispatchSnap || { active: [], pending: [] };
|
|
827
|
+
const worktreeRootRel = typeof opts.worktreeRootRel === 'string' && opts.worktreeRootRel.length > 0
|
|
828
|
+
? opts.worktreeRootRel
|
|
829
|
+
: (shared.ENGINE_DEFAULTS && shared.ENGINE_DEFAULTS.worktreeRoot) || '../worktrees';
|
|
830
|
+
const log = typeof opts.log === 'function' ? opts.log : _noopLog;
|
|
831
|
+
const _fs = opts.fs || fs;
|
|
832
|
+
const _execSilent = typeof opts.execSilent === 'function' ? opts.execSilent : shared.execSilent;
|
|
833
|
+
const _parseWorktreePorcelain = typeof opts.parseWorktreePorcelain === 'function'
|
|
834
|
+
? opts.parseWorktreePorcelain
|
|
835
|
+
: shared.parseWorktreePorcelain;
|
|
836
|
+
const _removeWorktree = typeof opts.removeWorktree === 'function'
|
|
837
|
+
? opts.removeWorktree
|
|
838
|
+
: shared.removeWorktree;
|
|
839
|
+
const _hasOwnerMarker = typeof opts.hasOwnerMarker === 'function'
|
|
840
|
+
? opts.hasOwnerMarker
|
|
841
|
+
: shared.hasWorktreeOwnerMarker;
|
|
842
|
+
const _writeOwnerMarker = typeof opts.writeOwnerMarker === 'function'
|
|
843
|
+
? opts.writeOwnerMarker
|
|
844
|
+
: shared.writeWorktreeOwnerMarker;
|
|
845
|
+
const _hasProtectedQuarantineMarker = typeof opts.hasProtectedQuarantineMarker === 'function'
|
|
846
|
+
? opts.hasProtectedQuarantineMarker
|
|
847
|
+
: shared.hasProtectedQuarantineMarker;
|
|
848
|
+
const _isWorktreePathLive = typeof opts.isWorktreePathLive === 'function'
|
|
849
|
+
? opts.isWorktreePathLive
|
|
850
|
+
: shared.isWorktreePathLive;
|
|
851
|
+
const _findProcessesWithCwdInside = typeof opts.findProcessesWithCwdInside === 'function'
|
|
852
|
+
? opts.findProcessesWithCwdInside
|
|
853
|
+
: (process.platform === 'win32' && typeof shared.findProcessCwdHolders === 'function'
|
|
854
|
+
? shared.findProcessCwdHolders
|
|
855
|
+
: shared.findProcessesWithCwdInside);
|
|
856
|
+
const _resolveCommonDir = typeof opts.resolveGitCommonDir === 'function'
|
|
857
|
+
? opts.resolveGitCommonDir
|
|
858
|
+
: (rootDir) => _resolveGitCommonDir(rootDir, _execSilent);
|
|
859
|
+
const _parsedTreesByProject = opts.parsedTreesByProject instanceof Map
|
|
860
|
+
? opts.parsedTreesByProject
|
|
861
|
+
: null;
|
|
862
|
+
const _liveOpts = opts.db ? { db: opts.db } : undefined;
|
|
863
|
+
const now = Number.isFinite(Number(opts.now)) ? Number(opts.now) : Date.now();
|
|
864
|
+
const orphanRetentionMs = Math.max(0,
|
|
865
|
+
Number.isFinite(Number(opts.orphanRetentionMs))
|
|
866
|
+
? Number(opts.orphanRetentionMs)
|
|
867
|
+
: DEFAULT_ORPHAN_RETENTION_MS);
|
|
868
|
+
const quarantineRetentionMs = Math.max(0,
|
|
869
|
+
Number.isFinite(Number(opts.quarantineRetentionMs))
|
|
870
|
+
? Number(opts.quarantineRetentionMs)
|
|
871
|
+
: DEFAULT_QUARANTINE_RETENTION_MS);
|
|
872
|
+
|
|
873
|
+
const result = {
|
|
874
|
+
scanned: 0,
|
|
875
|
+
removed: 0,
|
|
876
|
+
kept: 0,
|
|
877
|
+
failed: 0,
|
|
878
|
+
registered: 0,
|
|
879
|
+
skippedLive: 0,
|
|
880
|
+
pooled: 0,
|
|
881
|
+
anchored: 0,
|
|
882
|
+
processHeld: 0,
|
|
883
|
+
recent: 0,
|
|
884
|
+
foreign: 0,
|
|
885
|
+
protected: 0,
|
|
886
|
+
perRoot: {},
|
|
887
|
+
};
|
|
888
|
+
const protectedPaths = new Set();
|
|
889
|
+
const failedRoots = new Set();
|
|
890
|
+
Object.defineProperties(result, {
|
|
891
|
+
protectedPaths: { value: protectedPaths, enumerable: false },
|
|
892
|
+
failedRoots: { value: failedRoots, enumerable: false },
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
const groups = new Map();
|
|
896
|
+
for (const project of projects) {
|
|
897
|
+
if (!project || !project.localPath) continue;
|
|
898
|
+
let rootDir;
|
|
899
|
+
try { rootDir = path.resolve(String(project.localPath)); } catch { continue; }
|
|
900
|
+
let rootExists = false;
|
|
901
|
+
try { rootExists = _fs.existsSync(rootDir); } catch { rootExists = false; }
|
|
902
|
+
const wtParent = path.resolve(rootDir, worktreeRootRel);
|
|
903
|
+
let parentExists = false;
|
|
904
|
+
try { parentExists = _fs.existsSync(wtParent); } catch { parentExists = false; }
|
|
905
|
+
if (!parentExists) continue;
|
|
906
|
+
const key = worktreePool._normalizePath(wtParent);
|
|
907
|
+
if (!groups.has(key)) groups.set(key, { wtParent, projects: [] });
|
|
908
|
+
const group = groups.get(key);
|
|
909
|
+
if (!group.projects.some((entry) => entry.rootDir === rootDir)) {
|
|
910
|
+
group.projects.push({ project, rootDir, rootExists });
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const globalLiveDirNames = new Set();
|
|
915
|
+
for (const dispatch of [...(dispatchSnap.active || []), ...(dispatchSnap.pending || [])]) {
|
|
916
|
+
if (!dispatch) continue;
|
|
917
|
+
if (dispatch.worktreePath) {
|
|
918
|
+
try { globalLiveDirNames.add(path.basename(path.resolve(dispatch.worktreePath))); } catch {}
|
|
919
|
+
}
|
|
920
|
+
if (!dispatch.id || !dispatch.meta?.branch) continue;
|
|
921
|
+
const projectName = typeof dispatch.meta.project === 'string'
|
|
922
|
+
? dispatch.meta.project
|
|
923
|
+
: (dispatch.meta.project?.name || 'default');
|
|
924
|
+
try {
|
|
925
|
+
globalLiveDirNames.add(shared.buildWorktreeDirName({
|
|
926
|
+
dispatchId: dispatch.id,
|
|
927
|
+
projectName,
|
|
928
|
+
branchName: dispatch.meta.branch,
|
|
929
|
+
}));
|
|
930
|
+
} catch {}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const poolPaths = new Set();
|
|
934
|
+
let poolReadError = null;
|
|
935
|
+
try {
|
|
936
|
+
for (const entry of (worktreePool.readPool() || { entries: [] }).entries || []) {
|
|
937
|
+
if (entry?.path) poolPaths.add(worktreePool._normalizePath(entry.path));
|
|
938
|
+
}
|
|
939
|
+
} catch (err) {
|
|
940
|
+
poolReadError = err;
|
|
941
|
+
}
|
|
942
|
+
if (Array.isArray(opts.extraPoolPaths)) {
|
|
943
|
+
for (const candidate of opts.extraPoolPaths) {
|
|
944
|
+
poolPaths.add(worktreePool._normalizePath(candidate));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (poolReadError) {
|
|
948
|
+
for (const group of groups.values()) {
|
|
949
|
+
failedRoots.add(worktreePool._normalizePath(group.wtParent));
|
|
950
|
+
}
|
|
951
|
+
result.failed = groups.size || 1;
|
|
952
|
+
log('warn', `worktree-reconcile: pool state unavailable (${poolReadError.message}) — fail-closed`);
|
|
953
|
+
return result;
|
|
954
|
+
}
|
|
955
|
+
const listManagedSpecs = typeof opts.listManagedSpecs === 'function'
|
|
956
|
+
? opts.listManagedSpecs
|
|
957
|
+
: (() => _managedSpawn().listManagedSpecs());
|
|
958
|
+
const listKeepProcessCwds = typeof opts.listKeepProcessCwds === 'function'
|
|
959
|
+
? opts.listKeepProcessCwds
|
|
960
|
+
: (() => _keepProcessSweep().getActiveKeepProcessCwds());
|
|
961
|
+
let anchoredCwds;
|
|
962
|
+
try {
|
|
963
|
+
anchoredCwds = _collectAnchoredCwds(
|
|
964
|
+
listManagedSpecs,
|
|
965
|
+
listKeepProcessCwds,
|
|
966
|
+
{ throwOnFailure: true },
|
|
967
|
+
);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
for (const group of groups.values()) {
|
|
970
|
+
failedRoots.add(worktreePool._normalizePath(group.wtParent));
|
|
971
|
+
}
|
|
972
|
+
result.failed = groups.size || 1;
|
|
973
|
+
log('warn', `worktree-reconcile: process anchor state unavailable (${err.message}) — fail-closed`);
|
|
974
|
+
return result;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
for (const group of groups.values()) {
|
|
978
|
+
const stats = {
|
|
979
|
+
scanned: 0, removed: 0, kept: 0, failed: 0,
|
|
980
|
+
registered: 0, skippedLive: 0, pooled: 0, anchored: 0,
|
|
981
|
+
processHeld: 0, recent: 0, foreign: 0, protected: 0,
|
|
982
|
+
};
|
|
983
|
+
result.perRoot[group.wtParent] = stats;
|
|
984
|
+
const keep = (kind, candidate, protectDownstream = true) => {
|
|
985
|
+
if (kind) {
|
|
986
|
+
stats[kind]++;
|
|
987
|
+
result[kind]++;
|
|
988
|
+
}
|
|
989
|
+
stats.kept++;
|
|
990
|
+
result.kept++;
|
|
991
|
+
if (candidate && protectDownstream) protectedPaths.add(worktreePool._normalizePath(candidate));
|
|
992
|
+
};
|
|
993
|
+
const registeredPaths = new Set();
|
|
994
|
+
const lockedRegisteredPaths = new Set();
|
|
995
|
+
const commonGitDirs = [];
|
|
996
|
+
let registrationError = null;
|
|
997
|
+
let holderSnapshot = null;
|
|
998
|
+
let holderProbeError = null;
|
|
999
|
+
let holderProbeAttempted = false;
|
|
1000
|
+
const isProcessHeld = (candidate) => {
|
|
1001
|
+
if (!holderProbeAttempted) {
|
|
1002
|
+
holderProbeAttempted = true;
|
|
1003
|
+
try {
|
|
1004
|
+
holderSnapshot = _findProcessesWithCwdInside(group.wtParent, {
|
|
1005
|
+
timeoutMs: process.platform === 'win32'
|
|
1006
|
+
? (opts.config?.engine?.statusProbeKillTimeoutMs
|
|
1007
|
+
?? shared.ENGINE_DEFAULTS.statusProbeKillTimeoutMs)
|
|
1008
|
+
: (opts.config?.engine?.orphanHolderScanTimeoutMs
|
|
1009
|
+
?? shared.ENGINE_DEFAULTS.orphanHolderScanTimeoutMs),
|
|
1010
|
+
throwOnFailure: true,
|
|
1011
|
+
}) || [];
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
holderProbeError = err;
|
|
1014
|
+
stats.failed++;
|
|
1015
|
+
result.failed++;
|
|
1016
|
+
log('warn', `worktree-reconcile: holder probe failed for ${group.wtParent}: ${err.message}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (holderProbeError) return true;
|
|
1020
|
+
return holderSnapshot.some((holder) => {
|
|
1021
|
+
if (!holder?.cwd) return true;
|
|
1022
|
+
try { return shared.isPathInsideOrEqual(holder.cwd, candidate); }
|
|
1023
|
+
catch { return true; }
|
|
1024
|
+
});
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
for (const { project, rootDir, rootExists } of group.projects) {
|
|
1028
|
+
let trees;
|
|
1029
|
+
try {
|
|
1030
|
+
if (!rootExists) {
|
|
1031
|
+
throw new Error(`configured project localPath does not exist: ${rootDir}`);
|
|
1032
|
+
}
|
|
1033
|
+
if (_parsedTreesByProject && _parsedTreesByProject.has(rootDir)) {
|
|
1034
|
+
trees = _parsedTreesByProject.get(rootDir);
|
|
1035
|
+
} else {
|
|
1036
|
+
const raw = String(_execSilent('git --no-optional-locks worktree list --porcelain', {
|
|
1037
|
+
cwd: rootDir,
|
|
1038
|
+
timeout: 15000,
|
|
1039
|
+
windowsHide: true,
|
|
1040
|
+
}) || '');
|
|
1041
|
+
trees = _parseWorktreePorcelain(raw);
|
|
1042
|
+
}
|
|
1043
|
+
if (!Array.isArray(trees)) throw new Error('worktree registry parser did not return an array');
|
|
1044
|
+
for (const tree of trees) {
|
|
1045
|
+
if (!tree?.path) continue;
|
|
1046
|
+
const normalized = worktreePool._normalizePath(tree.path);
|
|
1047
|
+
registeredPaths.add(normalized);
|
|
1048
|
+
if (tree.locked) lockedRegisteredPaths.add(normalized);
|
|
1049
|
+
}
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
registrationError = err;
|
|
1052
|
+
log('warn', `worktree-reconcile: registry list failed for ${project.name || rootDir}: ${err.message}`);
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
const commonDir = _resolveCommonDir(rootDir, project);
|
|
1057
|
+
if (commonDir) commonGitDirs.push(path.resolve(commonDir));
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
log('debug', `worktree-reconcile: common git dir unavailable for ${project.name || rootDir}: ${err.message}`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (registrationError) {
|
|
1064
|
+
stats.failed++;
|
|
1065
|
+
result.failed++;
|
|
1066
|
+
failedRoots.add(worktreePool._normalizePath(group.wtParent));
|
|
1067
|
+
try { shared.bumpWorktreeGcMetric('reconcileFailures'); } catch {}
|
|
1068
|
+
if (typeof opts.writeToInbox === 'function') {
|
|
1069
|
+
try {
|
|
1070
|
+
const rootSlug = path.basename(group.wtParent).replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
1071
|
+
opts.writeToInbox('engine', `worktree-reconcile-failed-${rootSlug}`, [
|
|
1072
|
+
'# Worktree filesystem reconciliation failed closed',
|
|
1073
|
+
'',
|
|
1074
|
+
`**Worktree root:** ${group.wtParent}`,
|
|
1075
|
+
`**Error:** ${registrationError.message}`,
|
|
1076
|
+
'',
|
|
1077
|
+
'No directories were removed because Git registrations could not be enumerated safely.',
|
|
1078
|
+
].join('\n'));
|
|
1079
|
+
} catch {}
|
|
1080
|
+
}
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
let entries;
|
|
1085
|
+
try {
|
|
1086
|
+
entries = _fs.readdirSync(group.wtParent, { withFileTypes: true });
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
stats.failed++;
|
|
1089
|
+
result.failed++;
|
|
1090
|
+
failedRoots.add(worktreePool._normalizePath(group.wtParent));
|
|
1091
|
+
log('warn', `worktree-reconcile: readdir failed for ${group.wtParent}: ${err.message}`);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
for (const entry of entries) {
|
|
1096
|
+
if (!entry || (typeof entry.isDirectory === 'function' && !entry.isDirectory())) continue;
|
|
1097
|
+
const name = entry.name || entry;
|
|
1098
|
+
if (typeof name !== 'string' || !name || shared.isWorktreeRootInfraEntry(name)) continue;
|
|
1099
|
+
const candidate = path.resolve(group.wtParent, name);
|
|
1100
|
+
if (!candidate.startsWith(group.wtParent + path.sep)) continue;
|
|
1101
|
+
stats.scanned++;
|
|
1102
|
+
result.scanned++;
|
|
1103
|
+
|
|
1104
|
+
const normalized = worktreePool._normalizePath(candidate);
|
|
1105
|
+
if (lockedRegisteredPaths.has(normalized)) {
|
|
1106
|
+
keep('registered', candidate);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (globalLiveDirNames.has(name) || _pathIsLiveFailOpen(_isWorktreePathLive, candidate, _liveOpts)) {
|
|
1110
|
+
keep('skippedLive', candidate);
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (poolPaths.has(normalized)) {
|
|
1114
|
+
keep('pooled', candidate);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (_isWorktreeCwdAnchored(candidate, anchoredCwds)) {
|
|
1118
|
+
keep('anchored', candidate);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
let protectedQuarantine = true;
|
|
1122
|
+
try { protectedQuarantine = !!_hasProtectedQuarantineMarker(candidate); } catch {}
|
|
1123
|
+
if (protectedQuarantine) {
|
|
1124
|
+
keep('protected', candidate);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
let owned = false;
|
|
1129
|
+
let legacyOwned = false;
|
|
1130
|
+
try { owned = !!_hasOwnerMarker(candidate); } catch {}
|
|
1131
|
+
if (!owned) {
|
|
1132
|
+
for (const commonGitDir of commonGitDirs) {
|
|
1133
|
+
if (isLegacyMinionsWorktreeDir(candidate, commonGitDir, { fs: _fs })) {
|
|
1134
|
+
owned = true;
|
|
1135
|
+
legacyOwned = true;
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (!owned) {
|
|
1141
|
+
keep('foreign', candidate);
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
let stat;
|
|
1145
|
+
try { stat = _fs.statSync(candidate); }
|
|
1146
|
+
catch {
|
|
1147
|
+
keep(null, candidate);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const quarantine = _isQuarantineDirName(name);
|
|
1151
|
+
const createdAt = quarantine
|
|
1152
|
+
? _quarantineCreatedAt(name, stat.mtimeMs)
|
|
1153
|
+
: stat.mtimeMs;
|
|
1154
|
+
const retentionMs = quarantine ? quarantineRetentionMs : orphanRetentionMs;
|
|
1155
|
+
if ((now - createdAt) < retentionMs) {
|
|
1156
|
+
keep('recent', candidate);
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Adopt legacy residue immediately before deletion. The pre-adoption
|
|
1161
|
+
// stat above keeps the original retention clock; the marker makes a
|
|
1162
|
+
// partially removed husk provably owned on the next retry.
|
|
1163
|
+
if (legacyOwned && !_writeOwnerMarker(candidate, { reconciledLegacy: true })) {
|
|
1164
|
+
stats.failed++; result.failed++;
|
|
1165
|
+
keep('foreign', candidate);
|
|
1166
|
+
log('warn', `worktree-reconcile: could not stamp legacy ownership marker for ${candidate}; preserving`);
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (isProcessHeld(candidate)) {
|
|
1171
|
+
if (holderProbeError) {
|
|
1172
|
+
keep(null, candidate);
|
|
1173
|
+
} else {
|
|
1174
|
+
keep('processHeld', candidate);
|
|
1175
|
+
}
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (registeredPaths.has(normalized)) {
|
|
1179
|
+
// Preserve here without shielding safe, unlocked registrations from downstream pruning.
|
|
1180
|
+
keep('registered', candidate, false);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
try {
|
|
1185
|
+
const rootDir = group.projects[0].rootDir;
|
|
1186
|
+
const removed = _removeWorktree(candidate, rootDir, group.wtParent, {
|
|
1187
|
+
config: opts.config,
|
|
1188
|
+
});
|
|
1189
|
+
if (removed) {
|
|
1190
|
+
stats.removed++; result.removed++;
|
|
1191
|
+
try { shared.bumpWorktreeGcMetric('reconciledStaleDirs'); } catch {}
|
|
1192
|
+
log('info', `worktree-reconcile: removed stale unregistered directory ${candidate}`);
|
|
1193
|
+
} else {
|
|
1194
|
+
stats.failed++; result.failed++;
|
|
1195
|
+
log('warn', `worktree-reconcile: removeWorktree declined ${candidate}`);
|
|
1196
|
+
}
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
stats.failed++; result.failed++;
|
|
1199
|
+
log('warn', `worktree-reconcile: removal failed for ${candidate}: ${err.message}`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return result;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
721
1207
|
/**
|
|
722
1208
|
* Walk each project's worktree root and evict orphan W-* dirs.
|
|
723
1209
|
*
|
|
@@ -740,7 +1226,8 @@ function gcDispatchWorktreeIfOrphan(opts) {
|
|
|
740
1226
|
* completed dispatch with long-running services survives engine
|
|
741
1227
|
* restart. (PR #2627 review — Issue 1)
|
|
742
1228
|
*
|
|
743
|
-
* Returns `{ scanned, kept, evicted, failed
|
|
1229
|
+
* Returns legacy `{ scanned, kept, evicted, failed }` counters plus explicit
|
|
1230
|
+
* `{ removed, skipped, skippedLive, preserved, perProject }` outcomes.
|
|
744
1231
|
*/
|
|
745
1232
|
function pruneOrphanWorktrees(opts) {
|
|
746
1233
|
opts = opts || {};
|
|
@@ -762,6 +1249,12 @@ function pruneOrphanWorktrees(opts) {
|
|
|
762
1249
|
const _hasOwnerMarker = typeof opts.hasOwnerMarker === 'function'
|
|
763
1250
|
? opts.hasOwnerMarker
|
|
764
1251
|
: shared.hasWorktreeOwnerMarker;
|
|
1252
|
+
const _isWorktreePathLive = typeof opts.isWorktreePathLive === 'function'
|
|
1253
|
+
? opts.isWorktreePathLive
|
|
1254
|
+
: shared.isWorktreePathLive;
|
|
1255
|
+
const _liveOpts = opts.db ? { db: opts.db } : undefined;
|
|
1256
|
+
const protectedPaths = opts.protectedPaths instanceof Set ? opts.protectedPaths : new Set();
|
|
1257
|
+
const failedRoots = opts.failedRoots instanceof Set ? opts.failedRoots : new Set();
|
|
765
1258
|
const _listManagedSpecs = typeof opts.listManagedSpecs === 'function'
|
|
766
1259
|
? opts.listManagedSpecs
|
|
767
1260
|
: (() => {
|
|
@@ -802,7 +1295,12 @@ function pruneOrphanWorktrees(opts) {
|
|
|
802
1295
|
if (d.worktreePath) {
|
|
803
1296
|
try { globalLiveDirNames.add(path.basename(path.resolve(d.worktreePath))); } catch {}
|
|
804
1297
|
}
|
|
805
|
-
if (!d.meta
|
|
1298
|
+
if (!d.meta) continue;
|
|
1299
|
+
let dispatchBranch = d.meta.branch;
|
|
1300
|
+
if (!dispatchBranch && d.meta.project) {
|
|
1301
|
+
dispatchBranch = shared.READ_ONLY_ROOT_TASK_TYPES.has(d.type) ? 'read-only' : 'branchless';
|
|
1302
|
+
}
|
|
1303
|
+
if (!dispatchBranch) continue;
|
|
806
1304
|
const dispatchProject = typeof d.meta.project === 'string'
|
|
807
1305
|
? d.meta.project
|
|
808
1306
|
: (d.meta.project?.name || 'default');
|
|
@@ -810,14 +1308,18 @@ function pruneOrphanWorktrees(opts) {
|
|
|
810
1308
|
globalLiveDirNames.add(_buildWorktreeDirName({
|
|
811
1309
|
dispatchId: d.id,
|
|
812
1310
|
projectName: dispatchProject,
|
|
813
|
-
branchName:
|
|
1311
|
+
branchName: dispatchBranch,
|
|
814
1312
|
}));
|
|
815
1313
|
} catch (_e) { /* defensive */ }
|
|
816
1314
|
}
|
|
817
1315
|
|
|
818
1316
|
const anchoredCwds = _collectAnchoredCwds(_listManagedSpecs, _listKeepProcessCwds);
|
|
819
1317
|
|
|
820
|
-
const result = {
|
|
1318
|
+
const result = {
|
|
1319
|
+
scanned: 0, kept: 0, evicted: 0, failed: 0,
|
|
1320
|
+
removed: 0, skipped: 0, skippedLive: 0, preserved: 0,
|
|
1321
|
+
perProject: {},
|
|
1322
|
+
};
|
|
821
1323
|
|
|
822
1324
|
for (const project of projects) {
|
|
823
1325
|
if (!project || !project.localPath) continue;
|
|
@@ -827,6 +1329,10 @@ function pruneOrphanWorktrees(opts) {
|
|
|
827
1329
|
try { rootExists = _fs.existsSync(rootDir); } catch { rootExists = false; }
|
|
828
1330
|
if (!rootExists) continue;
|
|
829
1331
|
const wtParent = path.resolve(rootDir, worktreeRootRel);
|
|
1332
|
+
if (failedRoots.has(worktreePool._normalizePath(wtParent))) {
|
|
1333
|
+
log('warn', `worktree-gc: skipping ${wtParent} because filesystem reconciliation failed closed`);
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
830
1336
|
let parentExists = false;
|
|
831
1337
|
try { parentExists = _fs.existsSync(wtParent); } catch { parentExists = false; }
|
|
832
1338
|
if (!parentExists) continue;
|
|
@@ -838,7 +1344,10 @@ function pruneOrphanWorktrees(opts) {
|
|
|
838
1344
|
continue;
|
|
839
1345
|
}
|
|
840
1346
|
|
|
841
|
-
const projStats = {
|
|
1347
|
+
const projStats = {
|
|
1348
|
+
scanned: 0, kept: 0, evicted: 0, failed: 0,
|
|
1349
|
+
removed: 0, skipped: 0, skippedLive: 0, preserved: 0,
|
|
1350
|
+
};
|
|
842
1351
|
for (const ent of entries) {
|
|
843
1352
|
if (!ent || (typeof ent.isDirectory === 'function' && !ent.isDirectory())) continue;
|
|
844
1353
|
const name = ent.name || ent;
|
|
@@ -848,23 +1357,72 @@ function pruneOrphanWorktrees(opts) {
|
|
|
848
1357
|
projStats.scanned++;
|
|
849
1358
|
result.scanned++;
|
|
850
1359
|
const wtPath = path.join(wtParent, name);
|
|
1360
|
+
if (protectedPaths.has(worktreePool._normalizePath(wtPath))) {
|
|
1361
|
+
projStats.kept++; result.kept++;
|
|
1362
|
+
projStats.preserved++; result.preserved++;
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
851
1365
|
|
|
852
1366
|
// 1. Live-dispatch protection (cross-project safe).
|
|
853
|
-
|
|
1367
|
+
const retryMarker = name.lastIndexOf(shared.WORKTREE_RETRY_PATH_MARKER);
|
|
1368
|
+
const liveBaseName = retryMarker > 0 ? name.slice(0, retryMarker) : name;
|
|
1369
|
+
if (globalLiveDirNames.has(name) || globalLiveDirNames.has(liveBaseName)) {
|
|
854
1370
|
projStats.kept++; result.kept++;
|
|
1371
|
+
projStats.skipped++; result.skipped++;
|
|
1372
|
+
projStats.skippedLive++; result.skippedLive++;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
// The persisted path is authoritative. Derived directory names are only
|
|
1376
|
+
// a pre-persistence fallback and can miss legacy or reused worktrees.
|
|
1377
|
+
if (_pathIsLiveFailOpen(_isWorktreePathLive, wtPath, _liveOpts)) {
|
|
1378
|
+
projStats.kept++; result.kept++;
|
|
1379
|
+
projStats.skipped++; result.skipped++;
|
|
1380
|
+
projStats.skippedLive++; result.skippedLive++;
|
|
855
1381
|
continue;
|
|
856
1382
|
}
|
|
857
1383
|
// 2. Pool membership protection.
|
|
858
1384
|
const normPath = worktreePool._normalizePath(wtPath);
|
|
859
1385
|
if (poolPaths.has(normPath)) {
|
|
860
1386
|
projStats.kept++; result.kept++;
|
|
1387
|
+
projStats.preserved++; result.preserved++;
|
|
861
1388
|
continue;
|
|
862
1389
|
}
|
|
863
1390
|
// 3. managed_spawn / keep_processes cwd anchor protection.
|
|
864
1391
|
if (_isWorktreeCwdAnchored(wtPath, anchoredCwds)) {
|
|
865
1392
|
projStats.kept++; result.kept++;
|
|
1393
|
+
projStats.preserved++; result.preserved++;
|
|
866
1394
|
continue;
|
|
867
1395
|
}
|
|
1396
|
+
// Quarantine directories are recovery artifacts, not ordinary crash
|
|
1397
|
+
// residue. Keep them for a deterministic recovery window, and keep
|
|
1398
|
+
// protected quarantines indefinitely when no durable backup ref exists.
|
|
1399
|
+
if (_isQuarantineDirName(name)) {
|
|
1400
|
+
let protectedQuarantine = true;
|
|
1401
|
+
try { protectedQuarantine = shared.hasProtectedQuarantineMarker(wtPath); } catch {}
|
|
1402
|
+
if (protectedQuarantine) {
|
|
1403
|
+
projStats.kept++; result.kept++;
|
|
1404
|
+
projStats.preserved++; result.preserved++;
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
let quarantineStat;
|
|
1408
|
+
try { quarantineStat = _fs.statSync(wtPath); }
|
|
1409
|
+
catch {
|
|
1410
|
+
projStats.kept++; result.kept++;
|
|
1411
|
+
projStats.preserved++; result.preserved++;
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
const now = Number.isFinite(Number(opts.now)) ? Number(opts.now) : Date.now();
|
|
1415
|
+
const retentionMs = Math.max(0,
|
|
1416
|
+
Number.isFinite(Number(opts.quarantineRetentionMs))
|
|
1417
|
+
? Number(opts.quarantineRetentionMs)
|
|
1418
|
+
: DEFAULT_QUARANTINE_RETENTION_MS);
|
|
1419
|
+
const createdAt = _quarantineCreatedAt(name, quarantineStat.mtimeMs);
|
|
1420
|
+
if ((now - createdAt) < retentionMs) {
|
|
1421
|
+
projStats.kept++; result.kept++;
|
|
1422
|
+
projStats.preserved++; result.preserved++;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
868
1426
|
|
|
869
1427
|
// P-c7e2b405 — route the slow-retry gate, ownership-marker gate (now
|
|
870
1428
|
// requireOwnerMarker:true to match the out-of-root pruner), remove,
|
|
@@ -888,6 +1446,7 @@ function pruneOrphanWorktrees(opts) {
|
|
|
888
1446
|
writeToInbox: opts.writeToInbox,
|
|
889
1447
|
sleepSyncFn: opts.sleepSyncFn,
|
|
890
1448
|
stats: [projStats, result],
|
|
1449
|
+
deferredRetries: opts.deferredRetries,
|
|
891
1450
|
removeWorktree: _removeWorktree,
|
|
892
1451
|
hasOwnerMarker: _hasOwnerMarker,
|
|
893
1452
|
log,
|
|
@@ -932,7 +1491,9 @@ function pruneOrphanWorktrees(opts) {
|
|
|
932
1491
|
* 4. Path is in the worktree-pool → keep.
|
|
933
1492
|
* 5. managed_spawn cwd anchored → keep.
|
|
934
1493
|
*
|
|
935
|
-
* Returns `{ scanned, kept, evicted, failed, prunedRegistry
|
|
1494
|
+
* Returns legacy `{ scanned, kept, evicted, failed, prunedRegistry }` counters
|
|
1495
|
+
* plus explicit `{ removed, skipped, skippedLive, preserved, perProject }`
|
|
1496
|
+
* outcomes.
|
|
936
1497
|
*
|
|
937
1498
|
* W-mq5o6bvy000x7191 Layer 3.
|
|
938
1499
|
*/
|
|
@@ -983,6 +1544,10 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
|
|
|
983
1544
|
const _hasOwnerMarker = typeof opts.hasOwnerMarker === 'function'
|
|
984
1545
|
? opts.hasOwnerMarker
|
|
985
1546
|
: shared.hasWorktreeOwnerMarker;
|
|
1547
|
+
const _isWorktreePathLive = typeof opts.isWorktreePathLive === 'function'
|
|
1548
|
+
? opts.isWorktreePathLive
|
|
1549
|
+
: shared.isWorktreePathLive;
|
|
1550
|
+
const _liveOpts = opts.db ? { db: opts.db } : undefined;
|
|
986
1551
|
|
|
987
1552
|
// Branches of active/pending dispatches (with and without `refs/heads/`
|
|
988
1553
|
// normalization). Matched case-insensitively to mirror git's behavior on
|
|
@@ -1010,7 +1575,11 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
|
|
|
1010
1575
|
|
|
1011
1576
|
const anchoredCwds = _collectAnchoredCwds(_listManagedSpecs, _listKeepProcessCwds);
|
|
1012
1577
|
|
|
1013
|
-
const result = {
|
|
1578
|
+
const result = {
|
|
1579
|
+
scanned: 0, kept: 0, evicted: 0, failed: 0, prunedRegistry: 0,
|
|
1580
|
+
removed: 0, skipped: 0, skippedLive: 0, preserved: 0,
|
|
1581
|
+
perProject: {},
|
|
1582
|
+
};
|
|
1014
1583
|
const _seenAbs = new Set(); // dedup across projects sharing a parent
|
|
1015
1584
|
|
|
1016
1585
|
for (const project of projects) {
|
|
@@ -1026,7 +1595,10 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
|
|
|
1026
1595
|
const wtParentAbs = path.resolve(rootDir, worktreeRootRel);
|
|
1027
1596
|
const wtParentPrefix = wtParentAbs + path.sep;
|
|
1028
1597
|
|
|
1029
|
-
const projStats = {
|
|
1598
|
+
const projStats = {
|
|
1599
|
+
scanned: 0, kept: 0, evicted: 0, failed: 0, prunedRegistry: 0,
|
|
1600
|
+
removed: 0, skipped: 0, skippedLive: 0, preserved: 0,
|
|
1601
|
+
};
|
|
1030
1602
|
let trees;
|
|
1031
1603
|
if (_parsedTreesByProject && _parsedTreesByProject.has(rootDir)) {
|
|
1032
1604
|
trees = _parsedTreesByProject.get(rootDir);
|
|
@@ -1065,16 +1637,29 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
|
|
|
1065
1637
|
|
|
1066
1638
|
// Branch protection
|
|
1067
1639
|
if (wt.branch && protectedBranches.has(String(wt.branch).toLowerCase())) {
|
|
1068
|
-
projStats.kept++; result.kept++;
|
|
1640
|
+
projStats.kept++; result.kept++;
|
|
1641
|
+
projStats.skipped++; result.skipped++;
|
|
1642
|
+
projStats.skippedLive++; result.skippedLive++;
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
if (_pathIsLiveFailOpen(_isWorktreePathLive, wtAbs, _liveOpts)) {
|
|
1646
|
+
projStats.kept++; result.kept++;
|
|
1647
|
+
projStats.skipped++; result.skipped++;
|
|
1648
|
+
projStats.skippedLive++; result.skippedLive++;
|
|
1649
|
+
continue;
|
|
1069
1650
|
}
|
|
1070
1651
|
// Pool protection
|
|
1071
1652
|
const normPath = worktreePool._normalizePath(wtAbs);
|
|
1072
1653
|
if (poolPaths.has(normPath)) {
|
|
1073
|
-
projStats.kept++; result.kept++;
|
|
1654
|
+
projStats.kept++; result.kept++;
|
|
1655
|
+
projStats.preserved++; result.preserved++;
|
|
1656
|
+
continue;
|
|
1074
1657
|
}
|
|
1075
1658
|
// managed_spawn / keep_processes cwd anchor protection.
|
|
1076
1659
|
if (_isWorktreeCwdAnchored(wtAbs, anchoredCwds)) {
|
|
1077
|
-
projStats.kept++; result.kept++;
|
|
1660
|
+
projStats.kept++; result.kept++;
|
|
1661
|
+
projStats.preserved++; result.preserved++;
|
|
1662
|
+
continue;
|
|
1078
1663
|
}
|
|
1079
1664
|
|
|
1080
1665
|
// P-c7e2b405 — ownership-marker gate, slow-retry gate, remove, escalate,
|
|
@@ -1103,6 +1688,7 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
|
|
|
1103
1688
|
writeToInbox: opts.writeToInbox,
|
|
1104
1689
|
sleepSyncFn: opts.sleepSyncFn,
|
|
1105
1690
|
stats: [projStats, result],
|
|
1691
|
+
deferredRetries: opts.deferredRetries,
|
|
1106
1692
|
removeWorktree: _removeWorktree,
|
|
1107
1693
|
hasOwnerMarker: _hasOwnerMarker,
|
|
1108
1694
|
log,
|
|
@@ -1168,7 +1754,8 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
|
|
|
1168
1754
|
* when set for a project, skip the self-shell of `git worktree list
|
|
1169
1755
|
* --porcelain` and reuse the supplied parsed trees).
|
|
1170
1756
|
*
|
|
1171
|
-
* Returns `{ scanned, reclaimed, skippedLive, failed,
|
|
1757
|
+
* Returns `{ scanned, reclaimed, skippedLive, failed, removed, skipped,
|
|
1758
|
+
* preserved, perProject }` where
|
|
1172
1759
|
* `scanned` counts only the missing-dir candidates considered (dirs that still
|
|
1173
1760
|
* exist are skipped before scanning).
|
|
1174
1761
|
*/
|
|
@@ -1196,7 +1783,11 @@ function reclaimMissingDirWorktrees(opts) {
|
|
|
1196
1783
|
: shared.isWorktreePathLive;
|
|
1197
1784
|
const _liveOpts = opts.db ? { db: opts.db } : undefined;
|
|
1198
1785
|
|
|
1199
|
-
const result = {
|
|
1786
|
+
const result = {
|
|
1787
|
+
scanned: 0, reclaimed: 0, skippedLive: 0, failed: 0,
|
|
1788
|
+
removed: 0, skipped: 0, preserved: 0,
|
|
1789
|
+
perProject: {},
|
|
1790
|
+
};
|
|
1200
1791
|
const _seenAbs = new Set(); // dedup across projects sharing a parent repo
|
|
1201
1792
|
|
|
1202
1793
|
for (const project of projects) {
|
|
@@ -1207,7 +1798,10 @@ function reclaimMissingDirWorktrees(opts) {
|
|
|
1207
1798
|
try { rootExists = _fs.existsSync(rootDir); } catch { rootExists = false; }
|
|
1208
1799
|
if (!rootExists) continue;
|
|
1209
1800
|
|
|
1210
|
-
const projStats = {
|
|
1801
|
+
const projStats = {
|
|
1802
|
+
scanned: 0, reclaimed: 0, skippedLive: 0, failed: 0,
|
|
1803
|
+
removed: 0, skipped: 0, preserved: 0,
|
|
1804
|
+
};
|
|
1211
1805
|
let trees;
|
|
1212
1806
|
if (_parsedTreesByProject && _parsedTreesByProject.has(rootDir)) {
|
|
1213
1807
|
trees = _parsedTreesByProject.get(rootDir);
|
|
@@ -1250,11 +1844,9 @@ function reclaimMissingDirWorktrees(opts) {
|
|
|
1250
1844
|
|
|
1251
1845
|
// SAFETY 2 — never reclaim a path a non-terminal dispatch still claims
|
|
1252
1846
|
// (fail-open: isWorktreePathLive returns true when SQL is unreachable).
|
|
1253
|
-
|
|
1254
|
-
try { live = !!_isWorktreePathLive(wt.path, _liveOpts); }
|
|
1255
|
-
catch (_e) { live = true; }
|
|
1256
|
-
if (live) {
|
|
1847
|
+
if (_pathIsLiveFailOpen(_isWorktreePathLive, wt.path, _liveOpts)) {
|
|
1257
1848
|
projStats.skippedLive++; result.skippedLive++;
|
|
1849
|
+
projStats.skipped++; result.skipped++;
|
|
1258
1850
|
log('debug', `worktree-gc: missing-dir reclaim skipping live-claimed ${wtAbs}`);
|
|
1259
1851
|
continue;
|
|
1260
1852
|
}
|
|
@@ -1265,9 +1857,32 @@ function reclaimMissingDirWorktrees(opts) {
|
|
|
1265
1857
|
});
|
|
1266
1858
|
reclaimedHere++;
|
|
1267
1859
|
projStats.reclaimed++; result.reclaimed++;
|
|
1860
|
+
projStats.removed++; result.removed++;
|
|
1268
1861
|
try { shared.bumpWorktreeGcMetric('missingDirReclaimed'); } catch { /* metric optional */ }
|
|
1269
1862
|
log('info', `worktree-gc: reclaimed missing-dir worktree ${wtAbs}${wt.locked ? ' (was locked)' : ''}${wt.branch ? ` [branch ${wt.branch}]` : ''}`);
|
|
1270
1863
|
} catch (e) {
|
|
1864
|
+
// Another cleanup path may have removed the registration after this
|
|
1865
|
+
// sweep captured its shared porcelain snapshot. Re-list before calling
|
|
1866
|
+
// an idempotently completed race a failure.
|
|
1867
|
+
let stillRegistered = true;
|
|
1868
|
+
try {
|
|
1869
|
+
const freshRaw = String(_execSilent('git --no-optional-locks worktree list --porcelain', {
|
|
1870
|
+
cwd: rootDir, timeout: 15000, windowsHide: true,
|
|
1871
|
+
}) || '');
|
|
1872
|
+
const freshTrees = _parseWorktreePorcelain(freshRaw);
|
|
1873
|
+
const targetNorm = worktreePool._normalizePath(wtAbs);
|
|
1874
|
+
stillRegistered = freshTrees.some((fresh) => {
|
|
1875
|
+
return fresh && fresh.path
|
|
1876
|
+
&& worktreePool._normalizePath(fresh.path) === targetNorm;
|
|
1877
|
+
});
|
|
1878
|
+
} catch { /* keep the original failure authoritative */ }
|
|
1879
|
+
if (!stillRegistered) {
|
|
1880
|
+
reclaimedHere++;
|
|
1881
|
+
projStats.reclaimed++; result.reclaimed++;
|
|
1882
|
+
projStats.removed++; result.removed++;
|
|
1883
|
+
log('debug', `worktree-gc: missing-dir registration already absent after concurrent cleanup ${wtAbs}`);
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1271
1886
|
projStats.failed++; result.failed++;
|
|
1272
1887
|
log('warn', `worktree-gc: missing-dir reclaim remove -f -f failed for ${wtAbs}: ${(e.message || '').split('\n')[0]}`);
|
|
1273
1888
|
}
|
|
@@ -1292,10 +1907,14 @@ function reclaimMissingDirWorktrees(opts) {
|
|
|
1292
1907
|
module.exports = {
|
|
1293
1908
|
shouldGcDispatchWorktree,
|
|
1294
1909
|
gcDispatchWorktreeIfOrphan,
|
|
1910
|
+
reconcileStaleWorktreeDirectories,
|
|
1295
1911
|
pruneOrphanWorktrees,
|
|
1296
1912
|
pruneOrphanWorktreesFromGitRegistry,
|
|
1297
1913
|
reclaimMissingDirWorktrees, // W-mqifblkf00149df5 — locked-initializing missing-dir reaper
|
|
1298
1914
|
reapAndRemoveWorktree, // P-c7e2b405 — shared reap-then-remove core (exported for testing)
|
|
1915
|
+
isLegacyMinionsWorktreeDir, // exported for testing
|
|
1916
|
+
DEFAULT_ORPHAN_RETENTION_MS,
|
|
1917
|
+
DEFAULT_QUARANTINE_RETENTION_MS,
|
|
1299
1918
|
// exported for testing (W-mq5o6bvy000x7191)
|
|
1300
1919
|
_resetStuckPathsForTesting,
|
|
1301
1920
|
_stuckPaths,
|