@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/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 = await _renameWithRetry(worktreePath, quarantinedPath);
1700
- renameAttempts = r.attempts || 1;
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 attempt
2638
- try { if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true }); } catch { /* optional */ }
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
- _renameWithRetry, _statusPorcelainCmd, _killGitDescendantsForWorktree, _bumpQuarantineOutcome, // exported for testing (W-mq5n1zx5)
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.2232",
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"