@yemi33/minions 0.1.2232 → 0.1.2234
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/minions.js +29 -2
- package/dashboard/js/render-other.js +37 -4
- package/dashboard/js/render-work-items.js +8 -22
- package/dashboard/js/settings.js +2 -0
- package/dashboard.js +6 -0
- package/docs/completion-reports.md +17 -0
- package/docs/worktree-lifecycle.md +9 -5
- package/engine/cleanup.js +30 -1
- package/engine/restart-health.js +54 -2
- package/engine/shared.js +51 -13
- package/engine/worktree-gc.js +313 -179
- package/engine.js +55 -37
- package/package.json +1 -1
package/engine.js
CHANGED
|
@@ -1047,37 +1047,6 @@ function _statusPorcelainCmd() {
|
|
|
1047
1047
|
: 'git status --porcelain';
|
|
1048
1048
|
}
|
|
1049
1049
|
|
|
1050
|
-
// W-mq5n1zx5 — Layer 1a: rename worktree dir with jittered backoff. The
|
|
1051
|
-
// raw `fs.renameSync` used to throw Windows EBUSY/EPERM/EACCES if a
|
|
1052
|
-
// lingering `git.exe` descendant (typically leaked by a status-probe
|
|
1053
|
-
// timeout) still held a packfile handle. We retry with capped exponential
|
|
1054
|
-
// backoff + random jitter so the descendant has time to exit on its own.
|
|
1055
|
-
// Worst-case wall time ≈ baseMs * (2^attempts) ≈ 16s when attempts=6,
|
|
1056
|
-
// baseMs=250 — small enough not to wedge a tick, large enough to clear
|
|
1057
|
-
// the typical race. Throws the LAST error on exhaustion so the caller
|
|
1058
|
-
// can decide whether to fall back to `git worktree remove --force`.
|
|
1059
|
-
async function _renameWithRetry(src, dst, opts = {}) {
|
|
1060
|
-
const attempts = Number(opts.attempts) > 0
|
|
1061
|
-
? Number(opts.attempts)
|
|
1062
|
-
: ENGINE_DEFAULTS.quarantineRenameRetryAttempts;
|
|
1063
|
-
const baseMs = Number(opts.baseMs) > 0
|
|
1064
|
-
? Number(opts.baseMs)
|
|
1065
|
-
: ENGINE_DEFAULTS.quarantineRenameRetryBaseMs;
|
|
1066
|
-
let lastErr;
|
|
1067
|
-
for (let i = 0; i < attempts; i++) {
|
|
1068
|
-
try { fs.renameSync(src, dst); return { attempts: i + 1 }; }
|
|
1069
|
-
catch (e) {
|
|
1070
|
-
if (!['EBUSY', 'EPERM', 'EACCES', 'ENOTEMPTY'].includes(e.code)) throw e;
|
|
1071
|
-
lastErr = e;
|
|
1072
|
-
if (i < attempts - 1) {
|
|
1073
|
-
const delay = baseMs * (2 ** i) + Math.random() * 200;
|
|
1074
|
-
await new Promise(r => setTimeout(r, delay));
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
throw lastErr;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
1050
|
// W-mqila0t5 — resolve the worktree-holder reap probe timeout, honoring a
|
|
1082
1051
|
// `config.engine.statusProbeKillTimeoutMs` override (the dashboard Settings
|
|
1083
1052
|
// control persists there) and falling back to the ENGINE_DEFAULTS default.
|
|
@@ -1223,7 +1192,17 @@ function _reapWorktreeHolders(worktreePath, opts = {}) {
|
|
|
1223
1192
|
}
|
|
1224
1193
|
if (live) { result.skipped = true; result.reason = 'live'; return result; }
|
|
1225
1194
|
|
|
1226
|
-
// Layer 0 — legacy git.exe-by-cmdline sweep.
|
|
1195
|
+
// Layer 0 — legacy git.exe-by-cmdline sweep. NOT redundant with Layer 2
|
|
1196
|
+
// (P-e2a6c9d4, evaluated): Layer 2 (findProcessCwdHolders) matches purely on a
|
|
1197
|
+
// process's CWD resolving at/under THIS worktree (engine/shared.js
|
|
1198
|
+
// _windowsCwdProbeScript + _parseCwdHolderLines filter on cwd only). Layer 0
|
|
1199
|
+
// matches `git.exe` by `CommandLine -like '*<worktreePath>*'` regardless of CWD.
|
|
1200
|
+
// The case Layer 2 cannot see: a git.exe the engine itself spawns FROM the repo
|
|
1201
|
+
// root referencing the worktree by argument — e.g. `git -C <gitRoot> worktree
|
|
1202
|
+
// remove <wt>` / `git worktree prune` / `git worktree add <wt>` — its CWD is the
|
|
1203
|
+
// repo root, not under <wt>, so Layer 2 never emits it, but it can still hold a
|
|
1204
|
+
// packfile/dir handle on <wt>. Layer 0's command-line match is the only layer
|
|
1205
|
+
// that reaps it. Keep both.
|
|
1227
1206
|
try { result.layer0 = (layer0Fn(worktreePath).killed) || 0; }
|
|
1228
1207
|
catch { /* best-effort */ }
|
|
1229
1208
|
|
|
@@ -1653,6 +1632,11 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
1653
1632
|
// field is populated and `result.quarantined` stays false (env-blocked
|
|
1654
1633
|
// path); a normal return means quarantine succeeded.
|
|
1655
1634
|
async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOpts, diag = {}) {
|
|
1635
|
+
// P-c7e2b405 — INTENTIONAL divergence from worktree-gc.reapAndRemoveWorktree:
|
|
1636
|
+
// this is the bespoke dirty-worktree quarantine flow (rename → branch backup-ref
|
|
1637
|
+
// → reset → fetch), NOT a plain reap-then-remove. It is deliberately NOT folded
|
|
1638
|
+
// into the shared helper. It does share the same live-guard + holder-reap
|
|
1639
|
+
// primitives (isWorktreePathLive + _reapWorktreeHolders below).
|
|
1656
1640
|
const ts = Date.now();
|
|
1657
1641
|
const quarantinedPath = `${worktreePath}-quarantine-${ts}`;
|
|
1658
1642
|
_bumpQuarantineOutcome('attempts', 1);
|
|
@@ -1693,11 +1677,24 @@ async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOp
|
|
|
1693
1677
|
// W-mq5n1zx5 Layer 1a: rename with jittered backoff. Replaces the bare
|
|
1694
1678
|
// `fs.renameSync(worktreePath, quarantinedPath)` that used to throw
|
|
1695
1679
|
// Windows EBUSY uncatchably and burn the WI's auto-recovery budget.
|
|
1680
|
+
// Consolidated onto shared._retryFsOp (P-a1c4f7e2): same base*2^i+jitter
|
|
1681
|
+
// backoff math and identical retryable-code set (shared._WORKTREE_RETRYABLE_CODES
|
|
1682
|
+
// === {EBUSY,EPERM,EACCES,ENOTEMPTY}, the exact set the old _renameWithRetry
|
|
1683
|
+
// used). Pass the quarantine-specific attempts/baseMs so the rename keeps its
|
|
1684
|
+
// own retry budget rather than adopting the worktree-remove default. _retryFsOp
|
|
1685
|
+
// is synchronous (blocking sleepMs backoff), so no await here.
|
|
1696
1686
|
let renameAttempts = 0;
|
|
1697
1687
|
let renameError = null;
|
|
1698
1688
|
try {
|
|
1699
|
-
const r =
|
|
1700
|
-
|
|
1689
|
+
const r = shared._retryFsOp(
|
|
1690
|
+
() => fs.renameSync(worktreePath, quarantinedPath),
|
|
1691
|
+
`rename ${worktreePath} -> ${quarantinedPath}`,
|
|
1692
|
+
{
|
|
1693
|
+
attempts: ENGINE_DEFAULTS.quarantineRenameRetryAttempts,
|
|
1694
|
+
baseMs: ENGINE_DEFAULTS.quarantineRenameRetryBaseMs,
|
|
1695
|
+
},
|
|
1696
|
+
);
|
|
1697
|
+
renameAttempts = r.attempt || 1;
|
|
1701
1698
|
} catch (e) {
|
|
1702
1699
|
renameError = e;
|
|
1703
1700
|
}
|
|
@@ -2634,8 +2631,29 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2634
2631
|
log('info', `Retrying -b create after prune for ${branchName}`);
|
|
2635
2632
|
try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 15000 }); } catch { /* optional */ }
|
|
2636
2633
|
removeStaleIndexLock(rootDir);
|
|
2637
|
-
// Clean up partial worktree directory from failed
|
|
2638
|
-
|
|
2634
|
+
// Clean up partial worktree directory from the failed -b
|
|
2635
|
+
// attempt. Keep this a RAW fs.rmSync: this husk pre-dates
|
|
2636
|
+
// `git worktree add`, so git does not yet own it and
|
|
2637
|
+
// shared.removeWorktree's `git worktree remove --force` would
|
|
2638
|
+
// no-op on it (P-b3d9a162). Two guards before the delete:
|
|
2639
|
+
// 1. isWorktreePathLive (fail-open): skip if another
|
|
2640
|
+
// dispatch raced onto this path — it returns true on a
|
|
2641
|
+
// SQL outage, so we leak the husk rather than nuke a
|
|
2642
|
+
// sibling agent's live worktree. Exclude our OWN id.
|
|
2643
|
+
// 2. belt-and-braces: never recurse-delete a path that
|
|
2644
|
+
// carries its own `.git` DIRECTORY (a mis-pointed
|
|
2645
|
+
// worktreePath onto a real repo) — a linked worktree's
|
|
2646
|
+
// `.git` is a FILE, so this never blocks a real husk.
|
|
2647
|
+
try {
|
|
2648
|
+
const _huskGit = path.join(worktreePath, '.git');
|
|
2649
|
+
let _huskIsRealRepo = false;
|
|
2650
|
+
try { _huskIsRealRepo = fs.existsSync(_huskGit) && fs.lstatSync(_huskGit).isDirectory(); } catch { /* unreadable — treat as husk */ }
|
|
2651
|
+
if (fs.existsSync(worktreePath)
|
|
2652
|
+
&& !_huskIsRealRepo
|
|
2653
|
+
&& !shared.isWorktreePathLive(worktreePath, { excludeDispatchId: id })) {
|
|
2654
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
2655
|
+
}
|
|
2656
|
+
} catch { /* optional */ }
|
|
2639
2657
|
try {
|
|
2640
2658
|
await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, 0);
|
|
2641
2659
|
} catch (e1b) {
|
|
@@ -10016,7 +10034,7 @@ module.exports = {
|
|
|
10016
10034
|
gitOutputToString, gitErrorOutput, classifyDepMergeFailureOutput, listUnmergedFiles, // exported for testing
|
|
10017
10035
|
buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
|
|
10018
10036
|
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
|
|
10019
|
-
|
|
10037
|
+
_statusPorcelainCmd, _killGitDescendantsForWorktree, _bumpQuarantineOutcome, // exported for testing (W-mq5n1zx5)
|
|
10020
10038
|
_reapWorktreeHolders, _findTerminalWorktreeOwners, // exported for testing (W-mqila0t5 — CWD-pinned holder reap)
|
|
10021
10039
|
pruneStaleWorktreeForBranch, // exported for testing
|
|
10022
10040
|
findExistingWorktree, // exported for testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2234",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|