@yemi33/minions 0.1.2238 → 0.1.2240

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
@@ -1886,6 +1886,50 @@ async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts
1886
1886
  }
1887
1887
  }
1888
1888
 
1889
+ // Seed a spawned agent's COPILOT_HOME mcp-config as a copy of the operator's
1890
+ // `~/.copilot/mcp-config.json` MINUS the servers in
1891
+ // `engine.copilotAgentDisabledMcpServers`, then point COPILOT_HOME at it.
1892
+ //
1893
+ // Why config-filtering and not `--disable-mcp-server`: copilot SPAWNS every
1894
+ // server in its config on startup and authenticates it; `--disable-mcp-server
1895
+ // <name>` only hides that server's TOOLS from the model, it does NOT stop the
1896
+ // server process. So any auth-requiring server (azure-kusto/azmcp, DevBox,
1897
+ // workiq, loop, teams…) pops a Microsoft sign-in window on EVERY agent spawn —
1898
+ // an unbounded popup storm at minions' spawn cadence. A server simply ABSENT
1899
+ // from the config can't be spawned, so filtering the config is the only real
1900
+ // disable lever.
1901
+ //
1902
+ // Default disabled list `[]` => agents inherit ALL of the operator's MCP servers
1903
+ // (non-breaking; matches interactive copilot). Add a name to disable it for
1904
+ // agents. COPILOT_HOME is copilot-specific; claude/codex ignore it, so this is
1905
+ // set unconditionally (no runtime.name branching, per the adapter contract).
1906
+ // Copilot auth is unaffected (GH_TOKEN / OS credential store); the operator's
1907
+ // real ~/.copilot is untouched. Best-effort/fail-open: any failure leaves the
1908
+ // inherited COPILOT_HOME so a hiccup never blocks a dispatch.
1909
+ function _applyAgentCopilotHome(childEnv) {
1910
+ try {
1911
+ const home = shared.resolveAgentCopilotHome(MINIONS_DIR);
1912
+ fs.mkdirSync(home, { recursive: true });
1913
+ const disabled = new Set(shared.resolveCopilotAgentDisabledMcpServers(null, getConfig()?.engine));
1914
+ let servers = {};
1915
+ try {
1916
+ const userCfgPath = path.join(os.homedir(), '.copilot', 'mcp-config.json');
1917
+ const userCfg = JSON.parse(fs.readFileSync(userCfgPath, 'utf8').replace(/^/, ''));
1918
+ for (const [name, def] of Object.entries(userCfg.mcpServers || {})) {
1919
+ if (!disabled.has(name)) servers[name] = def;
1920
+ }
1921
+ } catch { servers = {}; /* no/unreadable user config → agents get no MCPs */ }
1922
+ // Write only when the content changes — keeps concurrent spawns from racing
1923
+ // on the shared file (the engine-level list is identical across agents).
1924
+ const desired = JSON.stringify({ mcpServers: servers }, null, 2);
1925
+ const cfgFile = path.join(home, 'mcp-config.json');
1926
+ let current = null;
1927
+ try { current = fs.readFileSync(cfgFile, 'utf8'); } catch {}
1928
+ if (current !== desired) fs.writeFileSync(cfgFile, desired);
1929
+ childEnv.COPILOT_HOME = home;
1930
+ } catch { /* leave inherited COPILOT_HOME untouched */ }
1931
+ }
1932
+
1889
1933
  async function spawnAgent(dispatchItem, config) {
1890
1934
  const { id, agent: agentId, type, meta } = dispatchItem;
1891
1935
  // Resolve prompt — prefers sidecar file when dispatchItem._promptFile is set
@@ -2257,10 +2301,15 @@ async function spawnAgent(dispatchItem, config) {
2257
2301
  // cwd = project.localPath, worktreeRootDir = null, liveMode = true.
2258
2302
  // Instead of creating an engine-managed worktree we run prepareLiveCheckout
2259
2303
  // in-place: it validates a clean tree and then checks out (or creates) the
2260
- // target branch. On dirty refusal we write an inbox alert, stamp
2304
+ // target branch. On a CONFIRMED dirty refusal (the helper returns
2305
+ // { ok:false, reason:'dirty', dirtyFiles:[…] }) we write an inbox alert, stamp
2261
2306
  // `_pendingReason: 'live_checkout_dirty'` on the WI, complete the dispatch
2262
2307
  // non-retryably with FAILURE_CLASS.LIVE_CHECKOUT_DIRTY, and return — the
2263
- // engine never runs `git reset`/`git clean` against the operator tree.
2308
+ // engine never runs `git reset`/`git clean` against the operator tree. If the
2309
+ // helper instead THROWS (guard/ref-validation/transient git error), that is
2310
+ // NOT proof of a dirty tree (#305): we complete with the separate
2311
+ // FAILURE_CLASS.LIVE_CHECKOUT_FAILED, which is retryable with bounded backoff
2312
+ // so racy branch-lock handoff or a transient git failure auto-recovers.
2264
2313
  // On success we fall through; the worktree-creation block below is gated
2265
2314
  // on `!liveMode`, so it never runs, and downstream cleanup paths
2266
2315
  // (worktreePool.returnToPool, worktree-gc.gcDispatchWorktreeIfOrphan) are
@@ -2284,14 +2333,22 @@ async function spawnAgent(dispatchItem, config) {
2284
2333
  log: (msg, lvl) => log(lvl || 'info', msg),
2285
2334
  });
2286
2335
  } catch (liveErr) {
2287
- log('error', `spawnAgent: live-checkout helper failed for ${id}: ${liveErr.message}`);
2336
+ // #305: A THROWN helper/git error is NOT proof the worktree is dirty.
2337
+ // Classify it as LIVE_CHECKOUT_FAILED (transient/pre-spawn) and let the
2338
+ // dispatch retry path decide retryability via isRetryableFailureReason —
2339
+ // racy branch-lock handoff, a just-finished sibling dispatch, or a
2340
+ // transient `git status`/`rev-parse`/`checkout` failure usually clears on
2341
+ // the next attempt (bounded by ENGINE_DEFAULTS.maxRetries). Do NOT stamp
2342
+ // _pendingReason: 'live_checkout_dirty' or write a dirty-files alert here —
2343
+ // those belong to the confirmed-dirty result path below.
2344
+ log('error', `spawnAgent: live-checkout helper threw for ${id}: ${liveErr.message}`);
2288
2345
  _cleanupPromptFiles();
2289
2346
  completeDispatch(
2290
2347
  id,
2291
2348
  DISPATCH_RESULT.ERROR,
2292
2349
  `live-checkout failed: ${liveErr.message}`.slice(0, 800),
2293
- 'Live-checkout helper threw before agent spawn; non-retryable until the underlying git state or invocation is fixed.',
2294
- { failureClass: FAILURE_CLASS.LIVE_CHECKOUT_DIRTY, agentRetryable: false },
2350
+ 'Live-checkout helper threw before agent spawn (not proof of a dirty tree). Auto-retried with bounded backoff; transient git/branch-lock state usually clears on the next attempt.',
2351
+ { failureClass: FAILURE_CLASS.LIVE_CHECKOUT_FAILED },
2295
2352
  );
2296
2353
  cleanupTempAgent(agentId);
2297
2354
  return null;
@@ -2342,7 +2399,96 @@ async function spawnAgent(dispatchItem, config) {
2342
2399
  cleanupTempAgent(agentId);
2343
2400
  return null;
2344
2401
  }
2402
+ // P-c5a1f3b8: mid-operation / detached-HEAD refusal. The tree is clean
2403
+ // (the dirty branch above already bailed) but the branch op cannot proceed
2404
+ // because the operator is mid-merge/rebase/cherry-pick/revert or sitting on
2405
+ // a detached HEAD. Distinct operator condition from `dirty` → distinct
2406
+ // FAILURE_CLASS (LIVE_CHECKOUT_MID_OPERATION) and distinct recovery
2407
+ // guidance. The engine NEVER runs git reset/clean/stash or aborts the
2408
+ // operator's in-progress operation — it refuses and tells the operator how
2409
+ // to recover with their own commands.
2410
+ if (_liveResult && _liveResult.ok === false
2411
+ && (_liveResult.reason === 'mid-operation' || _liveResult.reason === 'detached-head')) {
2412
+ const _isMidOp = _liveResult.reason === 'mid-operation';
2413
+ const _op = _isMidOp ? (_liveResult.op || 'operation') : null;
2414
+ const _sha = _isMidOp ? null : (_liveResult.sha || '(unknown)');
2415
+ const _alertBody = [
2416
+ _isMidOp
2417
+ ? '# Live-checkout blocked: in-progress git operation'
2418
+ : '# Live-checkout blocked: detached HEAD',
2419
+ '',
2420
+ `**Project:** ${project.name || '(unknown)'}`,
2421
+ `**Local path:** ${cwd}`,
2422
+ `**Branch:** ${branchName}`,
2423
+ `**Work item:** ${_wiIdForAlert}`,
2424
+ `**Dispatch:** ${id}`,
2425
+ '',
2426
+ _isMidOp
2427
+ ? `The engine refused to spawn the agent because \`${cwd}\` has an in-progress **${_op}**. Live-checkout mode runs in your project directly — the engine never \`git reset\`, \`git clean\`, or aborts an operation in your tree.`
2428
+ : `The engine refused to spawn the agent because \`${cwd}\` is in a **detached-HEAD** state (HEAD at \`${_sha}\`). Branching off a detached HEAD would strand your anonymous commits, so the engine refuses rather than risk losing work.`,
2429
+ '',
2430
+ '## Recovery',
2431
+ '',
2432
+ _isMidOp
2433
+ ? `Finish or abort the in-progress ${_op} with your own git commands (e.g. \`git ${_op} --continue\` or \`git ${_op} --abort\`), or checkout a branch, then re-dispatch the work item.`
2434
+ : 'Checkout a branch (e.g. `git checkout <branch>`) so HEAD points at a named ref, then re-dispatch the work item. If you have commits on the detached HEAD you want to keep, create a branch first (`git branch <name>`).',
2435
+ ].join('\n');
2436
+ try { writeInboxAlert(`live-checkout-blocked-${_wiIdForAlert}`, _alertBody); }
2437
+ catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
2438
+ const _pendingReason = _isMidOp ? 'live_checkout_mid_operation' : 'live_checkout_detached_head';
2439
+ try {
2440
+ const _wiPath = resolveWorkItemPath(dispatchItem.meta);
2441
+ if (_wiPath && dispatchItem.meta?.item?.id) {
2442
+ mutateJsonFileLocked(_wiPath, (data) => {
2443
+ if (!Array.isArray(data)) return data;
2444
+ const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
2445
+ if (wi) wi._pendingReason = _pendingReason;
2446
+ return data;
2447
+ });
2448
+ }
2449
+ } catch (e) { log('warn', `live-checkout: failed to stamp _pendingReason: ${e.message}`); }
2450
+ const _shortMsg = _isMidOp
2451
+ ? `live-checkout blocked: in-progress ${_op} in ${cwd}`
2452
+ : `live-checkout blocked: detached HEAD (${_sha}) in ${cwd}`;
2453
+ const _guidance = _isMidOp
2454
+ ? `Live-checkout mode cannot switch/create a branch while a ${_op} is in progress; the engine never aborts operator operations. Finish/abort the ${_op} or checkout a branch, then re-dispatch.`
2455
+ : 'Live-checkout mode cannot branch off a detached HEAD without risking operator commits; the engine never moves HEAD for you. Checkout a branch, then re-dispatch.';
2456
+ log('error', `spawnAgent: ${_shortMsg}`);
2457
+ _cleanupPromptFiles();
2458
+ completeDispatch(
2459
+ id,
2460
+ DISPATCH_RESULT.ERROR,
2461
+ _shortMsg.slice(0, 800),
2462
+ _guidance,
2463
+ { failureClass: FAILURE_CLASS.LIVE_CHECKOUT_MID_OPERATION, agentRetryable: false },
2464
+ );
2465
+ cleanupTempAgent(agentId);
2466
+ return null;
2467
+ }
2345
2468
  log('info', `live-checkout: ${_liveResult.created ? 'created' : 'switched to'} branch ${branchName} in ${cwd} (in-place; no worktree)`);
2469
+ // P-c5a1f3b8: persist the operator's original ref on the dispatch record so
2470
+ // the dispatch-end auto-restore (P-d9e6b2c4) can return the tree to where the
2471
+ // operator started — even after an engine restart + re-attach, where the
2472
+ // spawnAgent closure is gone and only the persisted dispatch record survives
2473
+ // (same pattern as resolveHarnessPropagated at engine/lifecycle.js:3576).
2474
+ if (_liveResult.originalRef) {
2475
+ dispatchItem.originalRef = _liveResult.originalRef;
2476
+ dispatchItem.originalRefType = _liveResult.originalRefType || 'branch';
2477
+ try {
2478
+ mutateDispatch((dispatch) => {
2479
+ for (const queue of ['pending', 'active', 'completed']) {
2480
+ const arr = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
2481
+ if (!arr) continue;
2482
+ const found = arr.find(d => d && d.id === id);
2483
+ if (found) {
2484
+ found.originalRef = _liveResult.originalRef;
2485
+ found.originalRefType = _liveResult.originalRefType || 'branch';
2486
+ }
2487
+ }
2488
+ return dispatch;
2489
+ });
2490
+ } catch (e) { log('warn', `live-checkout: failed to persist originalRef for ${id}: ${e.message}`); }
2491
+ }
2346
2492
  }
2347
2493
 
2348
2494
  if (branchName && !liveMode) {
@@ -3477,12 +3623,6 @@ async function spawnAgent(dispatchItem, config) {
3477
3623
  const resolvedModel = runtime.resolveModel(shared.resolveAgentModel(agentConfig, engineConfig));
3478
3624
  const resolvedMaxBudget = shared.resolveAgentMaxBudget(agentConfig, engineConfig);
3479
3625
  const resolvedBare = shared.resolveAgentBareMode(agentConfig, engineConfig);
3480
- // P-mcp-storm (opg#134 backport) — user-scope MCP servers to keep out of
3481
- // this agent dispatch. Copilot loads ~/.copilot/mcp-config.json
3482
- // unconditionally; the adapter re-emits these as --disable-mcp-server <name>.
3483
- // Other runtimes ignore the opt (their buildSpawnFlags don't read it), so no
3484
- // runtime.name branch here.
3485
- const resolvedDisabledMcpServers = shared.resolveCopilotAgentDisabledMcpServers(agentConfig, engineConfig);
3486
3626
  // P-49e1c8b7 — hermetic harness opt-out (per-agent override allowed).
3487
3627
  // When true, this dispatch:
3488
3628
  // - skips Claude workspace .mcp.json pre-approval (preApproveWorkspaceMcps),
@@ -3546,7 +3686,6 @@ async function spawnAgent(dispatchItem, config) {
3546
3686
  disableBuiltinMcps: engineConfig.copilotDisableBuiltinMcps,
3547
3687
  suppressAgentsMd: engineConfig.copilotSuppressAgentsMd,
3548
3688
  reasoningSummaries: engineConfig.copilotReasoningSummaries,
3549
- disabledMcpServers: resolvedDisabledMcpServers,
3550
3689
  });
3551
3690
 
3552
3691
  // MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
@@ -3623,6 +3762,9 @@ async function spawnAgent(dispatchItem, config) {
3623
3762
  // random localhost port (blank window). Belt-and-suspenders with dashboard.js's
3624
3763
  // stdout.isTTY gate.
3625
3764
  childEnv.MINIONS_NO_AUTO_OPEN = '1';
3765
+ // MCP-free copilot config so agents never pop a per-spawn Microsoft auth
3766
+ // window — see _applyAgentCopilotHome / shared.resolveAgentCopilotHome.
3767
+ _applyAgentCopilotHome(childEnv);
3626
3768
 
3627
3769
  if (getRepoHost(project) === 'ado') {
3628
3770
  // Inject cached ADO token so ADO agents skip re-authentication (#998).
@@ -3814,6 +3956,15 @@ async function spawnAgent(dispatchItem, config) {
3814
3956
  }
3815
3957
 
3816
3958
  _phaseT.spawnCallStart = Date.now();
3959
+ // P-c5a1f3b8 (Item D — live-mode push/PR-create cwd guard): in live mode
3960
+ // worktreePath stays null, so `cwd` here is project.localPath (it is never
3961
+ // reassigned to a worktree by the `if (worktreePath ...)` block above). The
3962
+ // agent's `git push -u origin <branch>` and PR-create therefore run in-place
3963
+ // against the operator's checkout. This cwd MUST NEVER be null for a mutating
3964
+ // live dispatch — shared.resolveSpawnPaths returns cwd === project.localPath
3965
+ // for live mode (regression-guarded in
3966
+ // test/unit/spawn-agent-live-mode-wiring.test.js). A null cwd would push from
3967
+ // the engine's own process dir and open the PR off the wrong tree.
3817
3968
  // `detached: true` puts the agent in its own process group (POSIX) / job
3818
3969
  // object (Windows), so when the engine dies — gracefully via stop, abruptly
3819
3970
  // via taskkill, or because of a crash — the agent keeps running and can be
@@ -4094,7 +4245,6 @@ async function spawnAgent(dispatchItem, config) {
4094
4245
  disableBuiltinMcps: engineConfig?.copilotDisableBuiltinMcps,
4095
4246
  suppressAgentsMd: engineConfig?.copilotSuppressAgentsMd,
4096
4247
  reasoningSummaries: engineConfig?.copilotReasoningSummaries,
4097
- disabledMcpServers: resolvedDisabledMcpServers,
4098
4248
  });
4099
4249
  if (!resumeArgs.includes('--resume')) {
4100
4250
  log('warn', `Steering: runtime ${runtime.name} did not accept session resume — skipping for ${agentId}`);
@@ -4125,6 +4275,8 @@ async function spawnAgent(dispatchItem, config) {
4125
4275
  // W-mqef-dashboard-tty — same browser-popup suppression on steering resume
4126
4276
  // (see the initial spawn site). Agents must never auto-open a browser.
4127
4277
  childEnv.MINIONS_NO_AUTO_OPEN = '1';
4278
+ // Same MCP-free copilot home on steering resume (see the initial spawn site).
4279
+ _applyAgentCopilotHome(childEnv);
4128
4280
  if (getRepoHost(project) === 'ado') {
4129
4281
  // Inject cached ADO token for steering session too (#998)
4130
4282
  try {
@@ -4946,6 +5098,43 @@ async function spawnAgent(dispatchItem, config) {
4946
5098
  }
4947
5099
  }
4948
5100
 
5101
+ // ── P-d9e6b2c4 — live-mode dispatch-end auto-restore + terminal-failure
5102
+ // notify. Runs on EVERY terminal result for a live-mode dispatch: the
5103
+ // agent ran in-place in the operator's checkout, so switch the tree back
5104
+ // to the ref it was on before (see engine/live-checkout.js#
5105
+ // restoreLiveCheckoutAtDispatchEnd). The worktree-GC block above no-ops
5106
+ // in live mode (worktreePath===null); this is its live-mode counterpart.
5107
+ // originalRef was captured by prepareLiveCheckout and persisted on the
5108
+ // dispatch record at branch-resolution time (P-c5a1f3b8); the in-memory
5109
+ // closure copy is authoritative here. The engine-restart reattach path
5110
+ // fires the same helper from the persisted record in engine/cli.js. The
5111
+ // restore is a plain `git checkout <originalRef>` — never --force/reset/
5112
+ // clean/stash — and is best-effort: a thrown restore never alters the
5113
+ // dispatch result.
5114
+ if (liveMode && branchName) {
5115
+ try {
5116
+ const _liveRestore = require('./engine/live-checkout');
5117
+ await _liveRestore.restoreLiveCheckoutAtDispatchEnd({
5118
+ localPath: cwd,
5119
+ branchName,
5120
+ originalRef: dispatchItem.originalRef || '',
5121
+ originalRefType: dispatchItem.originalRefType || 'branch',
5122
+ dispatchId: id,
5123
+ projectName: project?.name,
5124
+ isTerminalFailure: effectiveResult !== DISPATCH_RESULT.SUCCESS,
5125
+ resultLabel: errorReason || effectiveResult,
5126
+ gitOpts: _gitOpts,
5127
+ log,
5128
+ writeInboxAlert,
5129
+ });
5130
+ } catch (restoreErr) {
5131
+ // restoreLiveCheckoutAtDispatchEnd swallows its own errors; this guard
5132
+ // is belt-and-suspenders so a require() hiccup can never break the
5133
+ // dispatch-completion path that follows.
5134
+ log('warn', `live-checkout: dispatch-end restore wiring threw for ${id}: ${restoreErr.message}`);
5135
+ }
5136
+ }
5137
+
4949
5138
  completeDispatch(id, effectiveResult, errorReason, resultSummary, completeOpts);
4950
5139
 
4951
5140
  // W-mpbpexrg00110661 — surface managed-spawn partial-healthcheck failures
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2238",
3
+ "version": "0.1.2240",
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"