@yemi33/minions 0.1.1923 → 0.1.1925

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.
@@ -97,7 +97,7 @@ async function openSettings() {
97
97
  settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
98
98
  settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard)') +
99
99
  settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in)') +
100
- settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', !!e.ccUseWorkerPool, 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn (opt-in)') +
100
+ 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
101
  '</div>' +
102
102
 
103
103
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">PR Polling &amp; Dispatch Gates</h3>' +
package/dashboard.js CHANGED
@@ -1190,21 +1190,24 @@ function getDiskVersion() {
1190
1190
  }
1191
1191
 
1192
1192
  // ── MCP server discovery ─────────────────────────────────────────────────────
1193
- // Mirrors what each CLI actually loads at runtime:
1194
- // • `claude mcp list` user + plugin (+ workspace if cwd in project)
1195
- // • `copilot mcp list --json` → user + workspace + plugin + builtin (merged)
1193
+ // Reads MCP server registrations directly from each CLI's config file:
1194
+ // • `~/.claude.json` `mcpServers` (Claude user scope)
1195
+ // • `~/.copilot/mcp-config.json` → `mcpServers` (Copilot user scope)
1196
1196
  // • Each registered project's `<localPath>/.mcp.json` → workspace scope
1197
1197
  //
1198
- // CLI calls are slow (claude health-checks every server on stdio), so we cache
1199
- // for ~5 minutes and refresh in the background. getMcpServers() always returns
1200
- // synchronously from cache; first call before background fill returns [].
1198
+ // We used to shell out to `claude mcp list` + `copilot mcp list --json`, but
1199
+ // `claude mcp list` blocks on a TTY check when the dashboard daemon has no
1200
+ // parent console (bin/minions.js spawns it with windowsHide:true), so every
1201
+ // refresh timed out and the cache stayed empty. File reads are zero-spawn and
1202
+ // the config files are the authoritative source for registration.
1203
+ //
1204
+ // Cost: we lose live connection status (✓ Connected / ! Needs auth) and the
1205
+ // Claude plugin: <plugin>:<server> rows. The UI shows status only in a hover
1206
+ // tooltip, and plugin servers are rare; reliability matters more.
1201
1207
 
1202
1208
  const _MCP_CACHE_TTL = 5 * 60 * 1000;
1203
- const _MCP_CLAUDE_TIMEOUT = 30000;
1204
- const _MCP_COPILOT_TIMEOUT = 15000;
1205
1209
  let _mcpServersCache = null;
1206
1210
  let _mcpServersCacheTs = 0;
1207
- let _mcpRefreshInflight = null;
1208
1211
 
1209
1212
  function _parseClaudeMcpListLine(line) {
1210
1213
  const trimmed = (line || '').trim();
@@ -1248,19 +1251,43 @@ function _parseCopilotMcpListJson(raw) {
1248
1251
  } catch { return []; }
1249
1252
  }
1250
1253
 
1251
- function _runCliAsync(cmd, args, timeoutMs) {
1252
- return new Promise(resolve => {
1253
- try {
1254
- const { exec } = require('child_process');
1255
- // exec() runs through the shell so Windows resolves .cmd/.exe shims off
1256
- // PATH. Args are static so there's no injection surface.
1257
- const line = [cmd, ...args].join(' ');
1258
- exec(line, {
1259
- timeout: timeoutMs, windowsHide: true, encoding: 'utf8',
1260
- maxBuffer: 1024 * 1024,
1261
- }, (err, stdout) => resolve(err ? null : stdout));
1262
- } catch { resolve(null); }
1263
- });
1254
+ // Read Claude's user-scope MCP servers from `~/.claude.json` directly.
1255
+ // Shelling out to `claude mcp list` is unreliable from the dashboard daemon —
1256
+ // the CLI's MCP health probes block on a TTY check when the parent has no
1257
+ // console (bin/minions.js launches the dashboard with windowsHide:true), and
1258
+ // neither exec() nor spawn(..., {detached:true}) reliably sidesteps it.
1259
+ // Config-file reads are zero-spawn, instant, and the file is the authoritative
1260
+ // source for which servers are registered.
1261
+ function _readClaudeUserMcpServers() {
1262
+ try {
1263
+ const data = safeJsonObj(path.join(os.homedir(), '.claude.json'));
1264
+ const servers = data?.mcpServers || {};
1265
+ return Object.entries(servers).map(([name, cfg]) => ({
1266
+ name,
1267
+ source: 'Claude user',
1268
+ command: cfg.command || cfg.url || '',
1269
+ args: Array.isArray(cfg.args) ? cfg.args.join(' ') : (cfg.args || ''),
1270
+ }));
1271
+ } catch { return []; }
1272
+ }
1273
+
1274
+ // Copilot stores its MCP config at `~/.copilot/mcp-config.json` (may have a
1275
+ // UTF-8 BOM — Windows tooling tends to write one).
1276
+ function _readCopilotUserMcpServers() {
1277
+ try {
1278
+ const configPath = path.join(os.homedir(), '.copilot', 'mcp-config.json');
1279
+ if (!fs.existsSync(configPath)) return [];
1280
+ let raw = fs.readFileSync(configPath, 'utf8');
1281
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
1282
+ const data = JSON.parse(raw);
1283
+ const servers = data?.mcpServers || {};
1284
+ return Object.entries(servers).map(([name, cfg]) => ({
1285
+ name,
1286
+ source: 'Copilot',
1287
+ command: cfg.command || cfg.url || '',
1288
+ args: Array.isArray(cfg.args) ? cfg.args.join(' ') : (cfg.args || ''),
1289
+ }));
1290
+ } catch { return []; }
1264
1291
  }
1265
1292
 
1266
1293
  function _readWorkspaceMcpServers(projects) {
@@ -1293,42 +1320,26 @@ function _dedupeMcpServers(entries) {
1293
1320
  return out;
1294
1321
  }
1295
1322
 
1296
- async function _refreshMcpServersCache() {
1297
- if (_mcpRefreshInflight) return _mcpRefreshInflight;
1298
- _mcpRefreshInflight = (async () => {
1299
- try {
1300
- const [claudeOut, copilotOut] = await Promise.all([
1301
- _runCliAsync('claude', ['mcp', 'list'], _MCP_CLAUDE_TIMEOUT),
1302
- _runCliAsync('copilot', ['mcp', 'list', '--json'], _MCP_COPILOT_TIMEOUT),
1303
- ]);
1304
- const claudeEntries = (claudeOut || '').split(/\r?\n/).map(_parseClaudeMcpListLine).filter(Boolean);
1305
- const copilotEntries = _parseCopilotMcpListJson(copilotOut || '{}');
1306
- const workspaceEntries = _readWorkspaceMcpServers(PROJECTS);
1307
- _mcpServersCache = _dedupeMcpServers([...claudeEntries, ...copilotEntries, ...workspaceEntries]);
1308
- _mcpServersCacheTs = Date.now();
1309
- } catch (e) {
1310
- console.error('[mcp-discovery] refresh failed:', e.message?.split('\n')?.[0]);
1311
- if (!_mcpServersCache) _mcpServersCache = [];
1312
- _mcpServersCacheTs = Date.now();
1313
- } finally {
1314
- _mcpRefreshInflight = null;
1315
- }
1316
- })();
1317
- return _mcpRefreshInflight;
1318
- }
1319
-
1320
1323
  function getMcpServers() {
1321
1324
  const now = Date.now();
1322
- if (!_mcpServersCache || (now - _mcpServersCacheTs) >= _MCP_CACHE_TTL) {
1323
- _refreshMcpServersCache().catch(() => {});
1325
+ if (_mcpServersCache && (now - _mcpServersCacheTs) < _MCP_CACHE_TTL) {
1326
+ return _mcpServersCache;
1327
+ }
1328
+ try {
1329
+ const entries = [
1330
+ ..._readClaudeUserMcpServers(),
1331
+ ..._readCopilotUserMcpServers(),
1332
+ ..._readWorkspaceMcpServers(PROJECTS),
1333
+ ];
1334
+ _mcpServersCache = _dedupeMcpServers(entries);
1335
+ } catch (e) {
1336
+ console.error('[mcp-discovery] refresh failed:', e.message?.split('\n')?.[0]);
1337
+ if (!_mcpServersCache) _mcpServersCache = [];
1324
1338
  }
1325
- return _mcpServersCache || [];
1339
+ _mcpServersCacheTs = now;
1340
+ return _mcpServersCache;
1326
1341
  }
1327
1342
 
1328
- // Kick off first discovery on startup so the dashboard has data before the
1329
- // first slow-state rebuild.
1330
- _refreshMcpServersCache().catch(() => {});
1331
-
1332
1343
  function parsePinnedEntries(content) {
1333
1344
  if (!content) return [];
1334
1345
  const entries = [];
@@ -2622,7 +2633,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
2622
2633
  // alongside docSessions._docHash would risk the next call hitting the
2623
2634
  // "doc unchanged → skip context" branch after the idle reaper killed the
2624
2635
  // worker, silently starving the new ACP session of the doc body.
2625
- if (store === 'doc' && CONFIG.engine && CONFIG.engine.ccUseWorkerPool && sessionKey) {
2636
+ if (store === 'doc' && shared.resolveCcUseWorkerPool(CONFIG.engine) && sessionKey) {
2626
2637
  const poolPrompt = (function buildPoolPrompt() {
2627
2638
  const parts = !skipStatePreamble ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
2628
2639
  if (extraContext) parts.push(extraContext);
@@ -2763,7 +2774,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
2763
2774
  // keeps receiving accumulated text per the callLLMStreaming contract.
2764
2775
  // We also do NOT call updateSession() — see ccCall for the
2765
2776
  // docSessions-eviction-vs-_docHash staleness rationale.
2766
- if (store === 'doc' && CONFIG.engine && CONFIG.engine.ccUseWorkerPool && sessionKey) {
2777
+ if (store === 'doc' && shared.resolveCcUseWorkerPool(CONFIG.engine) && sessionKey) {
2767
2778
  const poolPrompt = (function buildPoolPrompt() {
2768
2779
  const parts = !skipStatePreamble ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
2769
2780
  if (extraContext) parts.push(extraContext);
@@ -3227,7 +3238,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
3227
3238
  // that has never seen the doc. See _buildDocChatPass for the read-side
3228
3239
  // rationale; see the post-call write-back below for the symmetric write
3229
3240
  // guard.
3230
- const usePool = !!(CONFIG.engine && CONFIG.engine.ccUseWorkerPool && sessionKey);
3241
+ const usePool = !!(shared.resolveCcUseWorkerPool(CONFIG.engine) && sessionKey);
3231
3242
 
3232
3243
  // Build the pass once for the first call; the retry helper invokes runOnce()
3233
3244
  // with no args after invalidating the session, so a fresh build there reflects
@@ -3308,7 +3319,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
3308
3319
  // Sub-task D of W-mp2w003600196c51: see ccDocCall above for the
3309
3320
  // read-side / write-side rationale; the streaming variant mirrors the
3310
3321
  // same usePool guard.
3311
- const usePool = !!(CONFIG.engine && CONFIG.engine.ccUseWorkerPool && sessionKey);
3322
+ const usePool = !!(shared.resolveCcUseWorkerPool(CONFIG.engine) && sessionKey);
3312
3323
 
3313
3324
  // Build the pass once; see ccDocCall for the dedup rationale.
3314
3325
  const initialPass = _buildDocChatPass({
@@ -6089,7 +6100,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6089
6100
  // per-tab handles in dashboard state, and matches "tab close" semantics
6090
6101
  // — if the user explicitly aborted, we don't owe them a warm process).
6091
6102
  // Off when the flag is off so legacy SIGTERM-only behavior is preserved.
6092
- if (CONFIG.engine && CONFIG.engine.ccUseWorkerPool) {
6103
+ if (shared.resolveCcUseWorkerPool(CONFIG.engine)) {
6093
6104
  try { ccWorkerPool.closeTab(tabId); } catch { /* swallow */ }
6094
6105
  }
6095
6106
  _clearCcLiveStream(tabId);
@@ -6234,7 +6245,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6234
6245
  * test enforces engine.js does not import cc-worker-pool).
6235
6246
  */
6236
6247
  function _invokeCcStream({ prompt, sessionId, liveState, toolUses, model, effort, maxTurns, engineConfig, systemPrompt = CC_STATIC_SYSTEM_PROMPT, tabId }) {
6237
- if (engineConfig && engineConfig.ccUseWorkerPool) {
6248
+ if (shared.resolveCcUseWorkerPool(engineConfig)) {
6238
6249
  return _invokeCcStreamViaPool({ prompt, liveState, model, effort, engineConfig, systemPrompt, tabId });
6239
6250
  }
6240
6251
  const { callLLMStreaming } = require('./engine/llm');
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-13T23:13:36.239Z"
4
+ "cachedAt": "2026-05-14T02:03:25.197Z"
5
5
  }
package/engine/shared.js CHANGED
@@ -1213,6 +1213,27 @@ function resolveCcCli(engine) {
1213
1213
  return ENGINE_DEFAULTS.defaultCli;
1214
1214
  }
1215
1215
 
1216
+ /**
1217
+ * Resolve whether the Command Center / doc-chat path should route through the
1218
+ * persistent ACP worker pool. Priority:
1219
+ * 1. `engine.ccUseWorkerPool` explicit true/false — operator override always wins
1220
+ * 2. Runtime-aware default: ON when CC runtime resolves to `copilot`
1221
+ * (Copilot's cold-spawn is 18-21s on Windows — pool turns subsequent turns
1222
+ * from 47-140s back into a few seconds). Claude's cold-spawn is much
1223
+ * cheaper and the ACP pool wasn't designed for it, so it stays OFF there.
1224
+ * 3. ENGINE_DEFAULTS.ccUseWorkerPool — final fallback
1225
+ *
1226
+ * Strict boolean check on the override so a literal `false` opts out even on
1227
+ * Copilot, matching `_isMeaningful` semantics for boolean flags.
1228
+ */
1229
+ function resolveCcUseWorkerPool(engine) {
1230
+ if (engine && (engine.ccUseWorkerPool === true || engine.ccUseWorkerPool === false)) {
1231
+ return engine.ccUseWorkerPool;
1232
+ }
1233
+ if (resolveCcCli(engine) === 'copilot') return true;
1234
+ return !!ENGINE_DEFAULTS.ccUseWorkerPool;
1235
+ }
1236
+
1216
1237
  /**
1217
1238
  * Resolve the model for a per-agent spawn. Priority:
1218
1239
  * 1. `agent.model` — per-agent override
@@ -3698,7 +3719,7 @@ module.exports = {
3698
3719
  KB_CATEGORIES,
3699
3720
  classifyInboxItem,
3700
3721
  ENGINE_DEFAULTS,
3701
- resolveAgentCli, resolveCcCli, resolveAgentModel, resolveCcModel,
3722
+ resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
3702
3723
  resolveAgentMaxBudget, resolveAgentBareMode,
3703
3724
  applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
3704
3725
  runtimeConfigWarnings,
package/engine.js CHANGED
@@ -606,6 +606,111 @@ async function pruneStaleWorktreeForBranch(rootDir, branchName, gitOpts) {
606
606
  return removed;
607
607
  }
608
608
 
609
+ // ─── assertCleanSharedWorktree (#2439) ──────────────────────────────────────
610
+ // Engine-side preflight that prevents shared-branch dispatches from spawning
611
+ // into a dirty worktree. Without this, the shared-branch playbook's "git status
612
+ // + bail out" guard fires AFTER dispatch — converting an orchestration hygiene
613
+ // problem into a fake implementation failure that also cascades to dependent
614
+ // work items.
615
+ //
616
+ // Behavior:
617
+ // 1. Run `git status --porcelain` in the target worktree.
618
+ // 2. If clean → { clean: true, healed: false }.
619
+ // 3. If dirty, decide whether self-heal is safe:
620
+ // - Block (preserve dirty tree for inspection) when ANOTHER active
621
+ // dispatch claims the same branch (ownership ambiguity).
622
+ // - Block when local commits exist that aren't on origin/<branch>
623
+ // (would lose unpushed work).
624
+ // - Otherwise reset --hard + clean -fd, then re-verify.
625
+ // 4. Return { clean, healed, reason, dirtyFiles } so the caller can fail
626
+ // fast with a first-class DIRTY_WORKTREE reason and retry semantics.
627
+ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, dispatchId, gitOpts = {}) {
628
+ const result = { clean: false, healed: false, reason: null, dirtyFiles: [] };
629
+ // 1. Status probe
630
+ let statusOut = '';
631
+ try {
632
+ const r = await execAsync('git status --porcelain', { ...gitOpts, cwd: worktreePath, timeout: 10000 });
633
+ statusOut = (r || '').toString().trim();
634
+ } catch (e) {
635
+ result.reason = 'status-failed';
636
+ result.error = e.message;
637
+ return result;
638
+ }
639
+ if (!statusOut) {
640
+ result.clean = true;
641
+ return result;
642
+ }
643
+ result.dirtyFiles = statusOut.split('\n').map(l => l.trim()).filter(Boolean);
644
+
645
+ // 2. Ownership check — another live dispatch on the same branch?
646
+ const sanitized = sanitizeBranch(branchName);
647
+ let otherActive = false;
648
+ try {
649
+ mutateDispatch((dp) => {
650
+ otherActive = (dp.active || []).some(d => {
651
+ if (d.id === dispatchId) return false;
652
+ const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
653
+ return dBranch === sanitized;
654
+ });
655
+ return dp;
656
+ });
657
+ } catch (e) {
658
+ log('warn', `assertCleanSharedWorktree: dispatch read failed (${e.message}) — assuming contended`);
659
+ otherActive = true;
660
+ }
661
+ if (otherActive) {
662
+ result.reason = 'other-dispatch-active';
663
+ return result;
664
+ }
665
+
666
+ // 3. Unpushed-commit check — refuse to clean when local work would be lost.
667
+ // `git log @{u}..HEAD --oneline` returns non-empty when there are commits
668
+ // ahead of the upstream. If there is no upstream at all, treat as unsafe
669
+ // to clean (we can't prove the local commits exist on origin).
670
+ let hasUnpushed = false;
671
+ try {
672
+ const r = await execAsync('git log @{u}..HEAD --oneline', { ...gitOpts, cwd: worktreePath, timeout: 10000 });
673
+ hasUnpushed = (r || '').toString().trim().length > 0;
674
+ } catch (e) {
675
+ // No upstream configured (e.g., branch never pushed) — be conservative.
676
+ hasUnpushed = true;
677
+ }
678
+ if (hasUnpushed) {
679
+ result.reason = 'has-unpushed-commits';
680
+ return result;
681
+ }
682
+
683
+ // 4. Safe to self-heal: reset + clean
684
+ try {
685
+ await execAsync('git reset --hard HEAD', { ...gitOpts, cwd: worktreePath, timeout: 30000 });
686
+ await execAsync('git clean -fd', { ...gitOpts, cwd: worktreePath, timeout: 30000 });
687
+ } catch (e) {
688
+ result.reason = 'clean-failed';
689
+ result.error = e.message;
690
+ return result;
691
+ }
692
+
693
+ // 5. Re-verify
694
+ try {
695
+ const r2 = await execAsync('git status --porcelain', { ...gitOpts, cwd: worktreePath, timeout: 10000 });
696
+ const after = (r2 || '').toString().trim();
697
+ if (after) {
698
+ result.reason = 'dirty-after-clean';
699
+ result.dirtyFiles = after.split('\n').map(l => l.trim()).filter(Boolean);
700
+ return result;
701
+ }
702
+ } catch (e) {
703
+ result.reason = 'recheck-failed';
704
+ result.error = e.message;
705
+ return result;
706
+ }
707
+
708
+ log('info', `assertCleanSharedWorktree: auto-healed dirty shared-branch worktree ${worktreePath} on ${branchName} (${result.dirtyFiles.length} dirty files reset)`);
709
+ result.clean = true;
710
+ result.healed = true;
711
+ return result;
712
+ }
713
+
609
714
  async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts) {
610
715
  if (!branchName) return false;
611
716
  const existingWt = await findExistingWorktree(rootDir, branchName);
@@ -856,6 +961,31 @@ async function spawnAgent(dispatchItem, config) {
856
961
  }
857
962
  }
858
963
 
964
+ // Shared-branch preflight (#2439): refuse to dispatch into a dirty shared
965
+ // worktree. Auto-heals when safe (no other dispatch claims the branch and
966
+ // no unpushed commits); blocks dispatch with a retryable DIRTY_WORKTREE
967
+ // failure otherwise. Without this, the playbook's `git status + bail` guard
968
+ // fires AFTER spawn — converting orchestration hygiene into fake
969
+ // implementation failures and cascading dependent items.
970
+ if (worktreePath && fs.existsSync(worktreePath) && meta?.branchStrategy === 'shared-branch' && branchName) {
971
+ const cleanResult = await assertCleanSharedWorktree(rootDir, worktreePath, branchName, id, _gitOpts);
972
+ if (!cleanResult.clean) {
973
+ const previewFiles = (cleanResult.dirtyFiles || []).slice(0, 5).join(', ');
974
+ const reasonMsg = `DIRTY_WORKTREE: shared branch ${branchName} worktree at ${worktreePath} is dirty (${cleanResult.reason}); ${cleanResult.dirtyFiles?.length || 0} file(s)${previewFiles ? ': ' + previewFiles : ''}`;
975
+ log('error', reasonMsg);
976
+ _cleanupPromptFiles();
977
+ completeDispatch(
978
+ id,
979
+ DISPATCH_RESULT.ERROR,
980
+ reasonMsg.slice(0, 500),
981
+ `Engine preflight refused to spawn into a dirty shared-branch worktree (#2439). Reason: ${cleanResult.reason}.`,
982
+ { agentRetryable: true },
983
+ );
984
+ cleanupTempAgent(agentId);
985
+ return null;
986
+ }
987
+ }
988
+
859
989
  // Merge dependency PR branches into worktree (applies to both reused and new worktrees)
860
990
  if (worktreePath && fs.existsSync(worktreePath)) {
861
991
  cwd = worktreePath;
@@ -5121,7 +5251,7 @@ module.exports = {
5121
5251
  // Shared helpers (used by lifecycle.js and tests)
5122
5252
  reconcileItemsWithPrs, detectDependencyCycles,
5123
5253
  parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
5124
- isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, // exported for testing
5254
+ isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, // exported for testing
5125
5255
  pruneStaleWorktreeForBranch, // exported for testing
5126
5256
  _maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
5127
5257
  promoteCheckpointSteeringForClose, // exported for testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1923",
3
+ "version": "0.1.1925",
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"