@yemi33/minions 0.1.2039 → 0.1.2041

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.
@@ -98,7 +98,7 @@ async function openSettings() {
98
98
  settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
99
99
  settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard)') +
100
100
  settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in)') +
101
- settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', (e.ccUseWorkerPool === undefined ? ((e.ccCli || e.defaultCli) === 'copilot') : !!e.ccUseWorkerPool), 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn. Default ON for copilot runtime (cold-spawn is ~20s on Windows); default OFF for claude.') +
101
+ settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', (e.ccUseWorkerPool === undefined ? ((e.ccCli || e.defaultCli) === 'copilot') : !!e.ccUseWorkerPool), 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn. Copilot-only (Agent Client Protocol transport); Claude does not implement ACP, so this toggle has no effect when CC runtime is Claude. Default ON for copilot (cold-spawn ~20s on Windows); forced OFF for non-copilot CC runtimes regardless of this toggle.') +
102
102
  '</div>' +
103
103
 
104
104
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">PR Polling &amp; Dispatch Gates</h3>' +
@@ -35,14 +35,23 @@ const FEATURES = {
35
35
  // ccUseWorkerPool — sub-tasks B/C/D of W-mp2w003600196c51 (CC perf).
36
36
  // Routes Command Center / doc-chat through engine/cc-worker-pool.js
37
37
  // (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI
38
- // per turn. Saves ~14 s of cold-start cost. Copilot-only — the pool drives
39
- // ACP which Claude does not implement.
38
+ // per turn. Saves ~14 s of cold-start cost.
39
+ //
40
+ // **Copilot-only — structural, not just a UX preference.** The pool's
41
+ // transport is Agent Client Protocol (JSON-RPC over stdin/stdout, framed
42
+ // initialize → session/new → session/prompt → session/update). Claude Code
43
+ // has no equivalent `--acp` mode; its only session-reuse path is
44
+ // per-invocation `--resume <id>` against `~/.claude/projects/<dir>/<id>.jsonl`.
45
+ // Honoring this flag on a Claude CC runtime would silently switch the
46
+ // operator's CC turns to copilot — see `shared.resolveCcUseWorkerPool`
47
+ // which now refuses the override when CC runtime is not copilot
48
+ // (W-mphlriic00095f69).
40
49
  //
41
50
  // Resolution: `shared.resolveCcUseWorkerPool(engine)` (engine/shared.js)
42
51
  // is the canonical predicate every dashboard.js call site uses. Explicit
43
- // `engine.ccUseWorkerPool` true/false in config wins; otherwise the helper
44
- // defaults ON for `copilot` and OFF for `claude`. PR #2492 flipped the
45
- // default ON for copilot; see that PR for the cold-spawn measurements.
52
+ // `engine.ccUseWorkerPool` true/false in config wins ONLY when CC runtime
53
+ // is copilot; otherwise the helper forces `false`. PR #2492 flipped the
54
+ // copilot default ON; see that PR for the cold-spawn measurements.
46
55
  //
47
56
  // `requiredCcRuntime: 'copilot'` here is a UX hint — the Settings panel
48
57
  // greys out the toggle when the resolved CC runtime mismatches so users
package/engine/shared.js CHANGED
@@ -1865,7 +1865,7 @@ const ENGINE_DEFAULTS = {
1865
1865
  copilotSuppressAgentsMd: true, // Copilot --no-custom-instructions: stop AGENTS.md auto-load from fighting Minions playbook prompts
1866
1866
  copilotStreamMode: 'on', // Copilot --stream <on|off>: 'on' streams assistant.message_delta events live; 'off' batches them
1867
1867
  copilotReasoningSummaries: false, // Copilot --enable-reasoning-summaries (Anthropic-family models only)
1868
- ccUseWorkerPool: false, // Sub-task C of W-mp2w003600196c51 (CC perf): when true, _invokeCcStream routes through engine/cc-worker-pool.js (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI per turn. Off by default — opt-in feature flag. Engine/agent dispatch path stays per-process regardless.
1868
+ ccUseWorkerPool: false, // Sub-task C of W-mp2w003600196c51 (CC perf): when true AND CC runtime is copilot, _invokeCcStream routes through engine/cc-worker-pool.js (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI per turn. Off by default — opt-in feature flag. **Structurally copilot-only**: the pool spawns `copilot --acp` (Agent Client Protocol); Claude Code does not implement ACP, so resolveCcUseWorkerPool returns false on non-copilot CC runtimes even with explicit-true (W-mphlriic00095f69 — prevents silent runtime switch). Engine/agent dispatch path stays per-process regardless.
1869
1869
  maxBudgetUsd: undefined, // fleet USD ceiling for --max-budget-usd (per-agent override: agents.<id>.maxBudgetUsd). Honors 0 via ?? so a literal cap of $0 works
1870
1870
  disableModelDiscovery: false, // skip runtime.listModels() REST calls fleet-wide (settings UI falls back to free-text)
1871
1871
  maxPendingContexts: 20, // cap pendingContexts arrays in cooldowns.json to prevent unbounded growth
@@ -2097,22 +2097,31 @@ function resolveCcCli(engine) {
2097
2097
  /**
2098
2098
  * Resolve whether the Command Center / doc-chat path should route through the
2099
2099
  * persistent ACP worker pool. Priority:
2100
- * 1. `engine.ccUseWorkerPool` explicit true/falseoperator override always wins
2101
- * 2. Runtime-aware default: ON when CC runtime resolves to `copilot`
2100
+ * 1. CC runtime guardpool spawns `copilot --acp` (Agent Client Protocol).
2101
+ * Claude Code does not implement ACP, and `_invokeCcStream` would silently
2102
+ * switch a Claude operator's CC turns to copilot if the pool ran for them.
2103
+ * Refuse the pool when CC runtime is not copilot, even if explicit-true is
2104
+ * set. Future runtimes that don't implement ACP fall into the same bucket.
2105
+ * (W-mphlriic00095f69)
2106
+ * 2. `engine.ccUseWorkerPool` explicit true/false — operator override wins
2107
+ * *within* the copilot runtime
2108
+ * 3. Runtime-aware default: ON when CC runtime resolves to `copilot`
2102
2109
  * (Copilot's cold-spawn is 18-21s on Windows — pool turns subsequent turns
2103
- * from 47-140s back into a few seconds). Claude's cold-spawn is much
2104
- * cheaper and the ACP pool wasn't designed for it, so it stays OFF there.
2105
- * 3. ENGINE_DEFAULTS.ccUseWorkerPool — final fallback
2110
+ * from 47-140s back into a few seconds).
2111
+ * 4. ENGINE_DEFAULTS.ccUseWorkerPool final fallback
2106
2112
  *
2107
2113
  * Strict boolean check on the override so a literal `false` opts out even on
2108
2114
  * Copilot, matching `_isMeaningful` semantics for boolean flags.
2109
2115
  */
2110
2116
  function resolveCcUseWorkerPool(engine) {
2117
+ // Guard 1 (W-mphlriic00095f69): pool transport is ACP-only. If CC runtime
2118
+ // isn't copilot, the pool cannot run regardless of operator opt-in — don't
2119
+ // silently switch their runtime to copilot.
2120
+ if (resolveCcCli(engine) !== 'copilot') return false;
2111
2121
  if (engine && (engine.ccUseWorkerPool === true || engine.ccUseWorkerPool === false)) {
2112
2122
  return engine.ccUseWorkerPool;
2113
2123
  }
2114
- if (resolveCcCli(engine) === 'copilot') return true;
2115
- return !!ENGINE_DEFAULTS.ccUseWorkerPool;
2124
+ return true; // CC runtime is copilot default ON (cold-start savings)
2116
2125
  }
2117
2126
 
2118
2127
  /**
package/engine.js CHANGED
@@ -698,6 +698,35 @@ async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeC
698
698
  if (lastErr) throw lastErr;
699
699
  }
700
700
 
701
+ // W-mphnm6a1000281b8: probe whether origin already advertises a head for
702
+ // `branchName`. Used by the fresh-create worktree path to choose between
703
+ // `git worktree add <path> <branch>` (checkout, local-track origin) and
704
+ // `git worktree add -b <branch> origin/<mainRef>` (fresh branch off main).
705
+ // Without this probe, PR-targeted fix/review/test/verify dispatches whose
706
+ // source branch is N commits ahead of main upstream (mirror branches like
707
+ // `sync/yemi33-master`, force-pushed PR branches) start their worktree on
708
+ // origin/<mainRef>; the stale-HEAD guard at the top of spawn-agent then
709
+ // trips on every dispatch and the cooldown machinery starves the PR.
710
+ //
711
+ // Returns true only on a confirmed advertised head; ls-remote exit code 2
712
+ // (no matching ref) and any other failure (network/auth) return false so
713
+ // the caller falls back to the existing `-b origin/<mainRef>` path.
714
+ async function probeBranchOnRemote(rootDir, branchName, gitOpts) {
715
+ if (!branchName || !rootDir) return false;
716
+ try {
717
+ await shared.shellSafeGit(
718
+ ['ls-remote', '--exit-code', '--heads', 'origin', branchName],
719
+ { ...gitOpts, cwd: rootDir, timeout: 10000 },
720
+ );
721
+ return true;
722
+ } catch (e) {
723
+ if (e && e.code !== 2) {
724
+ log('warn', `probeBranchOnRemote: ls-remote --heads origin ${branchName} failed: ${(e.message || '').split('\n')[0].slice(0, 200)} — treating as not-on-remote`);
725
+ }
726
+ return false;
727
+ }
728
+ }
729
+
701
730
  // Detect and remove worktree registrations whose backing directory is missing
702
731
  // on disk. `git worktree add` fails with "branch is already used by worktree
703
732
  // at <path>" when a prior crash left such an entry behind, sometimes still in
@@ -1188,84 +1217,143 @@ async function spawnAgent(dispatchItem, config) {
1188
1217
  } else {
1189
1218
  log('info', `Creating worktree: ${worktreePath} on branch ${branchName}`);
1190
1219
  const mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
1191
- // W-mph6n4p00006ce38: mirror the pool-borrow path (~line 1110-1114)
1192
- // fetch fresh origin/<mainRef> and start the new branch off it,
1193
- // not the local ref. Without this, fresh-create dispatches inherit
1194
- // whatever stale local master the engine clone happens to be on
1195
- // (most painful: long-lived engine processes between restarts).
1196
- // Non-fatal: if the fetch fails (network blip, transient auth),
1197
- // fall back to local mainRef so the dispatch still progresses;
1198
- // the dep-merge phase's own fetch + the on-failure
1199
- // `git reset --hard origin/<mainRef>` recovery remain as safety nets.
1200
- let _freshCreateBase = mainRef;
1201
- try {
1202
- await shared.shellSafeGit(['fetch', 'origin', mainRef], { ..._gitOpts, cwd: rootDir, timeout: 30000 });
1203
- _freshCreateBase = `origin/${mainRef}`;
1204
- } catch (mainFetchErr) {
1205
- log('warn', `Failed to fetch origin/${mainRef} before fresh-create worktree for ${branchName}: ${mainFetchErr.message} — falling back to local ${mainRef}`);
1206
- }
1207
- try {
1208
- await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, worktreeCreateRetries);
1209
- } catch (e1) {
1210
- const branchExists = e1.message?.includes('already exists');
1211
- log('warn', `Worktree -b failed for ${branchName}: ${e1.message?.split('\n')[0]}`);
1212
- if (!branchExists) {
1213
- // Transient error (lock, timeout) prune, clean, and retry -b once more
1214
- log('info', `Retrying -b create after prune for ${branchName}`);
1215
- try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 15000 }); } catch { /* optional */ }
1216
- removeStaleIndexLock(rootDir);
1217
- // Clean up partial worktree directory from failed attempt
1218
- try { if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true }); } catch { /* optional */ }
1219
- try {
1220
- await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, 0);
1221
- } catch (e1b) {
1222
- log('error', `Worktree -b retry also failed for ${branchName}: ${e1b.message?.split('\n')[0]}`);
1223
- throw e1b;
1224
- }
1225
- } else {
1226
- // Branch already exists — try checkout without -b
1227
- try { await shared.shellSafeGit(['fetch', 'origin', branchName], { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
1228
- try {
1229
- await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
1230
- log('info', `Reusing existing branch: ${branchName}`);
1231
- } catch (e2) {
1232
- // "already checked out" or "already used by worktree" — find and reuse or recover
1233
- const alreadyUsed = e2.message?.includes('already checked out') || e2.message?.includes('already used by worktree')
1234
- || e1.message?.includes('already checked out') || e1.message?.includes('already used by worktree');
1235
- if (alreadyUsed) {
1236
- const existingWtPath = await findExistingWorktree(rootDir, branchName);
1237
- if (existingWtPath && fs.existsSync(existingWtPath)) {
1238
- // Bug fix: read dispatch under file lock so check-and-act is atomic
1239
- let activelyUsed = false;
1240
- mutateDispatch((dp) => {
1241
- activelyUsed = (dp.active || []).some(d => {
1242
- const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
1243
- return dBranch === branchName && d.id !== id;
1244
- });
1245
- return dp;
1220
+
1221
+ // W-mphnm6a1000281b8: probe whether origin already has this branch.
1222
+ // If yes, prefer `git worktree add <path> <branch>` (checkout +
1223
+ // local-track origin/<branch>) over `-b <branch> origin/<mainRef>`.
1224
+ // The shared-branch path (~line 1158) already does this. PR-targeted
1225
+ // fix/review/test/verify dispatches hit this path with branchName =
1226
+ // the PR's source branch; branching off main when the PR branch is
1227
+ // N commits ahead upstream guarantees the stale-HEAD guard
1228
+ // (~line 1777) trips and the dispatch errors. Live repro:
1229
+ // opg-microsoft/minions PR #57 (sync/yemi33-master, 109 commits
1230
+ // ahead of main) — two consecutive fix dispatches errored on
1231
+ // STALE_HEAD over 10 min and the engine then starved the PR for
1232
+ // the cooldown window (60 min effective).
1233
+ const _branchOnRemote = await probeBranchOnRemote(rootDir, branchName, _gitOpts);
1234
+
1235
+ if (_branchOnRemote) {
1236
+ // Mirror shared-branch fetch+add (~line 1157-1159).
1237
+ log('info', `origin/${branchName} exists checking out remote branch instead of -b from ${mainRef}`);
1238
+ try { await shared.shellSafeGit(['fetch', 'origin', branchName], { ..._gitOpts, cwd: rootDir, timeout: 30000 }); } catch (e) { log('warn', 'git: ' + e.message); }
1239
+ try {
1240
+ await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
1241
+ } catch (eRemote) {
1242
+ const alreadyUsed = eRemote.message?.includes('already used by worktree') || eRemote.message?.includes('already checked out');
1243
+ if (alreadyUsed) {
1244
+ const existingWtPath = await findExistingWorktree(rootDir, branchName);
1245
+ if (existingWtPath && fs.existsSync(existingWtPath)) {
1246
+ let activelyUsed = false;
1247
+ mutateDispatch((dp) => {
1248
+ activelyUsed = (dp.active || []).some(d => {
1249
+ const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
1250
+ return dBranch === branchName && d.id !== id;
1246
1251
  });
1247
- if (activelyUsed) {
1248
- log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
1249
- throw e2;
1250
- }
1251
- try { shared.assertWorktreeOutsideProject(existingWtPath, rootDir); }
1252
- catch (assertErr) {
1253
- if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
1254
- throw assertErr;
1252
+ return dp;
1253
+ });
1254
+ if (activelyUsed) {
1255
+ log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
1256
+ throw eRemote;
1257
+ }
1258
+ try { shared.assertWorktreeOutsideProject(existingWtPath, rootDir); }
1259
+ catch (assertErr) {
1260
+ if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
1261
+ throw assertErr;
1262
+ }
1263
+ log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
1264
+ worktreePath = existingWtPath;
1265
+ } else {
1266
+ const pruned = await pruneStaleWorktreeForBranch(rootDir, branchName, _gitOpts);
1267
+ if (pruned > 0) {
1268
+ log('info', `Pruned ${pruned} stale worktree entry(ies) for ${branchName}; retrying worktree add`);
1269
+ await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, 0);
1270
+ } else { throw eRemote; }
1271
+ }
1272
+ } else { throw eRemote; }
1273
+ }
1274
+ } else {
1275
+ // Branch isn't on origin (or probe failed) — start a fresh local
1276
+ // branch from origin/<mainRef>. This is the dominant path for new
1277
+ // implement/decompose dispatches.
1278
+ // W-mph6n4p00006ce38: mirror the pool-borrow path (~line 1110-1114)
1279
+ // — fetch fresh origin/<mainRef> and start the new branch off it,
1280
+ // not the local ref. Without this, fresh-create dispatches inherit
1281
+ // whatever stale local master the engine clone happens to be on
1282
+ // (most painful: long-lived engine processes between restarts).
1283
+ // Non-fatal: if the fetch fails (network blip, transient auth),
1284
+ // fall back to local mainRef so the dispatch still progresses;
1285
+ // the dep-merge phase's own fetch + the on-failure
1286
+ // `git reset --hard origin/<mainRef>` recovery remain as safety nets.
1287
+ let _freshCreateBase = mainRef;
1288
+ try {
1289
+ await shared.shellSafeGit(['fetch', 'origin', mainRef], { ..._gitOpts, cwd: rootDir, timeout: 30000 });
1290
+ _freshCreateBase = `origin/${mainRef}`;
1291
+ } catch (mainFetchErr) {
1292
+ log('warn', `Failed to fetch origin/${mainRef} before fresh-create worktree for ${branchName}: ${mainFetchErr.message} — falling back to local ${mainRef}`);
1293
+ }
1294
+ try {
1295
+ await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, worktreeCreateRetries);
1296
+ } catch (e1) {
1297
+ const branchExists = e1.message?.includes('already exists');
1298
+ log('warn', `Worktree -b failed for ${branchName}: ${e1.message?.split('\n')[0]}`);
1299
+ if (!branchExists) {
1300
+ // Transient error (lock, timeout) — prune, clean, and retry -b once more
1301
+ log('info', `Retrying -b create after prune for ${branchName}`);
1302
+ try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 15000 }); } catch { /* optional */ }
1303
+ removeStaleIndexLock(rootDir);
1304
+ // Clean up partial worktree directory from failed attempt
1305
+ try { if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true }); } catch { /* optional */ }
1306
+ try {
1307
+ await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, 0);
1308
+ } catch (e1b) {
1309
+ log('error', `Worktree -b retry also failed for ${branchName}: ${e1b.message?.split('\n')[0]}`);
1310
+ throw e1b;
1311
+ }
1312
+ } else {
1313
+ // Branch already exists — try checkout without -b
1314
+ try { await shared.shellSafeGit(['fetch', 'origin', branchName], { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
1315
+ try {
1316
+ await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
1317
+ log('info', `Reusing existing branch: ${branchName}`);
1318
+ } catch (e2) {
1319
+ // "already checked out" or "already used by worktree" — find and reuse or recover
1320
+ const alreadyUsed = e2.message?.includes('already checked out') || e2.message?.includes('already used by worktree')
1321
+ || e1.message?.includes('already checked out') || e1.message?.includes('already used by worktree');
1322
+ if (alreadyUsed) {
1323
+ const existingWtPath = await findExistingWorktree(rootDir, branchName);
1324
+ if (existingWtPath && fs.existsSync(existingWtPath)) {
1325
+ // Bug fix: read dispatch under file lock so check-and-act is atomic
1326
+ let activelyUsed = false;
1327
+ mutateDispatch((dp) => {
1328
+ activelyUsed = (dp.active || []).some(d => {
1329
+ const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
1330
+ return dBranch === branchName && d.id !== id;
1331
+ });
1332
+ return dp;
1333
+ });
1334
+ if (activelyUsed) {
1335
+ log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
1336
+ throw e2;
1337
+ }
1338
+ try { shared.assertWorktreeOutsideProject(existingWtPath, rootDir); }
1339
+ catch (assertErr) {
1340
+ if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
1341
+ throw assertErr;
1342
+ }
1343
+ log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
1344
+ worktreePath = existingWtPath;
1345
+ } else if (existingWtPath && !fs.existsSync(existingWtPath)) {
1346
+ log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
1347
+ try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
1348
+ await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
1349
+ log('info', `Recovered worktree for ${branchName} after stale entry prune`);
1350
+ } else {
1351
+ try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
1352
+ await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
1255
1353
  }
1256
- log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
1257
- worktreePath = existingWtPath;
1258
- } else if (existingWtPath && !fs.existsSync(existingWtPath)) {
1259
- log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
1260
- try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
1261
- await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
1262
- log('info', `Recovered worktree for ${branchName} after stale entry prune`);
1263
1354
  } else {
1264
- try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
1265
- await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
1355
+ throw e2;
1266
1356
  }
1267
- } else {
1268
- throw e2;
1269
1357
  }
1270
1358
  }
1271
1359
  }
@@ -4502,29 +4590,48 @@ async function discoverFromPrs(config, project) {
4502
4590
  && !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.REVIEW_FEEDBACK)) {
4503
4591
  const reviewCauseKey = getPrAutomationCauseKey('review-feedback', pr);
4504
4592
  const key = getPrAutomationDispatchKey(`fix-${project?.name || 'default'}-${prDisplayId}`, reviewCauseKey);
4505
- if (isPrAutomationCauseHandledOrPending(project, pr, reviewCauseKey)) continue;
4506
- if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
4507
- const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
4508
- if (!agentId) continue;
4509
- const prBranch = ensurePrBranchForDispatch(project, pr, 'fix');
4510
- if (!prBranch) continue;
4511
-
4512
- const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
4513
- pr_id: pr.id, pr_branch: prBranch,
4514
- review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
4515
- }, `Fix ${pr.id}: ${pr.title || ''} review feedback`, {
4516
- dispatchKey: key, cooldownKey: key, automationCauseKey: reviewCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta,
4517
- // W-mpg58wv3 — closure-loop binding. Carries the originating minion review
4518
- // WI id (and any ADO thread ids it cited) onto the fix WI so the
4519
- // post-completion path in lifecycle.js can auto-dispatch a re-review
4520
- // against the same PR. Both fields fall through to null/[] when the
4521
- // upstream review didn't expose them.
4522
- addresses_review_wi: pr.minionsReview?.sourceItem || null,
4523
- addresses_threads: Array.isArray(pr.minionsReview?.threads) ? pr.minionsReview.threads.slice() : [],
4524
- });
4525
- if (item) {
4526
- newWork.push(item); fixDispatched = true;
4527
- }
4593
+ // W-mphnm6a1000281b8: cause-local skip mirroring the human-feedback
4594
+ // (#2632) and build-failure (#2632 audit) guards above. A previous bare
4595
+ // `continue` here aborted the entire PR iteration when the
4596
+ // review-feedback dispatch was throttled, on cooldown, or already
4597
+ // dispatched starving the build-fix and conflict-fix blocks below
4598
+ // even though their own dedupe keys had not been hit. Live repro on
4599
+ // opg-microsoft/minions PR #57: two STALE_HEAD-errored review-feedback
4600
+ // dispatches stamped a 60-min effective cooldown, and the PR received
4601
+ // zero build-fix dispatches for the rest of the cooldown window even
4602
+ // though buildStatus stayed `failing` and the build-fix key was clean.
4603
+ // Skip ONLY this cause; let iteration fall through to downstream
4604
+ // blocks (re-review already ran above; build-fix + conflict-fix run
4605
+ // below).
4606
+ const skipReviewFeedback =
4607
+ isPrAutomationCauseHandledOrPending(project, pr, reviewCauseKey)
4608
+ || fixThrottled
4609
+ || isAlreadyDispatched(key)
4610
+ || isOnCooldown(key, cooldownMs);
4611
+ if (!skipReviewFeedback) {
4612
+ const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
4613
+ if (agentId) {
4614
+ const prBranch = ensurePrBranchForDispatch(project, pr, 'fix');
4615
+ if (prBranch) {
4616
+ const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
4617
+ pr_id: pr.id, pr_branch: prBranch,
4618
+ review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
4619
+ }, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, {
4620
+ dispatchKey: key, cooldownKey: key, automationCauseKey: reviewCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta,
4621
+ // W-mpg58wv3 — closure-loop binding. Carries the originating minion review
4622
+ // WI id (and any ADO thread ids it cited) onto the fix WI so the
4623
+ // post-completion path in lifecycle.js can auto-dispatch a re-review
4624
+ // against the same PR. Both fields fall through to null/[] when the
4625
+ // upstream review didn't expose them.
4626
+ addresses_review_wi: pr.minionsReview?.sourceItem || null,
4627
+ addresses_threads: Array.isArray(pr.minionsReview?.threads) ? pr.minionsReview.threads.slice() : [],
4628
+ });
4629
+ if (item) {
4630
+ newWork.push(item); fixDispatched = true;
4631
+ }
4632
+ }
4633
+ }
4634
+ } // end if (!skipReviewFeedback) — cause-local guard for W-mphnm6a1000281b8
4528
4635
  }
4529
4636
 
4530
4637
  // PRs with build failures — route to author (has session context from implementing)
@@ -6872,6 +6979,7 @@ module.exports = {
6872
6979
  isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, // exported for testing
6873
6980
  pruneStaleWorktreeForBranch, // exported for testing
6874
6981
  findExistingWorktree, // exported for testing
6982
+ probeBranchOnRemote, // exported for testing (W-mphnm6a1000281b8)
6875
6983
  _maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
6876
6984
  promoteCheckpointSteeringForClose, // exported for testing
6877
6985
  normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2039",
3
+ "version": "0.1.2041",
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"