@yemi33/minions 0.1.2241 → 0.1.2243

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 CHANGED
@@ -1358,6 +1358,15 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1358
1358
  // Clear stale beacons AFTER the kill so the old dashboard's last writes
1359
1359
  // can't repopulate the file in the gap between clear and shutdown.
1360
1360
  _clearDashboardBrowserState(MINIONS_HOME);
1361
+ // Respawn-race guard: the watchdog (or a surviving supervisor) can spawn a
1362
+ // fresh engine/dashboard during the kill→spawn window — those become ORPHANS
1363
+ // the new stack never reaps, and orphan engines keep dispatching agents +
1364
+ // spawning copilot (the root of the recurring multi-engine / MCP-auth storms).
1365
+ // stop-intent is STILL set here, so any respawn has already stood down or is
1366
+ // about to; reap once more so the stack we spawn below is the ONLY one. Scoped
1367
+ // by command-line to engine/dashboard/supervisor — never touches agent/copilot
1368
+ // children, preserving the re-attach invariant.
1369
+ killMinionsProcesses(['engine.js', 'dashboard.js', 'supervisor.js']);
1361
1370
  // Clear stop-intent so the freshly-spawned supervisor resumes guarding.
1362
1371
  clearStopIntent();
1363
1372
  spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs });
@@ -125,20 +125,25 @@ async function prepareLiveCheckout(opts = {}) {
125
125
  const exists = (typeof _exists === 'function') ? _exists : fs.existsSync;
126
126
  const baseOpts = { cwd: localPath, ...(gitOpts || {}) };
127
127
 
128
- // ── Step 1: git status --porcelain. Bail early on dirty tree. ─────────
129
- // Porcelain format begins each line with a two-char XY status code (e.g.
128
+ // ── Step 1: git status --porcelain=v1 -b. Bail early on dirty tree. ─────
129
+ // Porcelain v1 -b adds a `## <branch>` header line as the first output line
130
+ // so callers get branch diagnostics for free alongside the file-status lines.
131
+ // The file-status lines still use the two-char XY status code (e.g.
130
132
  // " M file.js", "?? new.js", "MM stage+unstage.js"). The leading space on
131
133
  // unstaged-only lines is SIGNIFICANT, so we split first and strip ONLY
132
- // trailing whitespace/CR per line — outer-trimming the whole blob would
133
- // eat the leading XY space on the first line.
134
- const statusRaw = await git(['status', '--porcelain'], baseOpts);
134
+ // trailing whitespace/CR per line — outer-trimming the whole blob would eat
135
+ // the leading XY space on the first line. The `## ` header is separated out
136
+ // before the dirtyFiles check so callers receive it as `branchInfo`.
137
+ const statusRaw = await git(['status', '--porcelain=v1', '-b'], baseOpts);
135
138
  const statusStr = typeof statusRaw === 'string' ? statusRaw : '';
136
- const dirtyFiles = statusStr
139
+ const statusLines = statusStr
137
140
  .split(/\r?\n/)
138
141
  .map((line) => line.replace(/\s+$/, ''))
139
142
  .filter((line) => line.length > 0);
143
+ const branchInfo = statusLines.find((line) => line.startsWith('## ')) || '';
144
+ const dirtyFiles = statusLines.filter((line) => !line.startsWith('## '));
140
145
  if (dirtyFiles.length > 0) {
141
- return { ok: false, reason: 'dirty', dirtyFiles };
146
+ return { ok: false, reason: 'dirty', dirtyFiles, branchInfo };
142
147
  }
143
148
 
144
149
  // ── Step 2: mid-operation / detached-HEAD preflight (P-b2e8d4a6). ──────
package/engine/shared.js CHANGED
@@ -5666,6 +5666,30 @@ function resolveAgentCopilotHome(minionsDir) {
5666
5666
  return path.join(base, '.minions-agent-copilot-home');
5667
5667
  }
5668
5668
 
5669
+ /**
5670
+ * Derive a scratch/temp base dir on the same volume agent work already lives on
5671
+ * — the configured worktree location (engine.worktreeRoot, default
5672
+ * `../worktrees`). Agents (copilot/node/git + JVM-based MCP servers) write heavy
5673
+ * temp under %TEMP% / $TMPDIR; when that resolves to a full system drive (e.g.
5674
+ * C:), operations thrash or fail and routinely overrun the CLI's per-command
5675
+ * timeout, surfacing to the operator as "Operation cancelled". Anchoring agent
5676
+ * temp next to the worktrees keeps scratch on the operator's work volume with NO
5677
+ * config knob and NO hardcoded path — it simply follows wherever worktrees
5678
+ * already go (a location the operator already placed off the system drive).
5679
+ *
5680
+ * Returns an absolute dir path, or null when no anchor can be resolved — callers
5681
+ * MUST treat null as "leave the inherited TEMP untouched" (fail-open).
5682
+ */
5683
+ function resolveAgentTempBaseDir(project, engine, minionsDir) {
5684
+ let projectRoot;
5685
+ try { projectRoot = resolveProjectRootDir(project && project.localPath, minionsDir); }
5686
+ catch { return null; }
5687
+ const wtRel = (engine && engine.worktreeRoot) || ENGINE_DEFAULTS.worktreeRoot;
5688
+ let anchor;
5689
+ try { anchor = path.resolve(projectRoot, wtRel); } catch { return null; }
5690
+ return path.join(anchor, '.agent-temp');
5691
+ }
5692
+
5669
5693
  // Seed the agent COPILOT_HOME's mcp-config (copy of `~/.copilot/mcp-config.json`
5670
5694
  // MINUS `engine.copilotAgentDisabledMcpServers`) and return the home path. This
5671
5695
  // is the single source of truth for "what MCP servers may a minions-spawned
@@ -8995,6 +9019,7 @@ module.exports = {
8995
9019
  assertWorktreeOutsideProject,
8996
9020
  resolveProjectRootDir,
8997
9021
  resolveAgentCopilotHome,
9022
+ resolveAgentTempBaseDir,
8998
9023
  ensureAgentCopilotHome,
8999
9024
  resolveSpawnPaths,
9000
9025
  validateWorkItemWorkdir,
package/engine.js CHANGED
@@ -1915,6 +1915,25 @@ function _applyAgentCopilotHome(childEnv) {
1915
1915
  catch { /* leave inherited COPILOT_HOME untouched */ }
1916
1916
  }
1917
1917
 
1918
+ // Point a spawned agent's TEMP/TMP/TMPDIR at a scratch dir on the worktree
1919
+ // volume instead of the (often full) system drive — see
1920
+ // shared.resolveAgentTempBaseDir. copilot/node/git + JVM-based MCP servers write
1921
+ // heavy temp there; on a full C: that thrashes and overruns the CLI's per-command
1922
+ // timeout ("Operation cancelled"). Best-effort + fail-open: any resolution/mkdir
1923
+ // failure leaves the inherited TEMP in place. Sets all three vars to cover
1924
+ // Windows (TEMP/TMP) and POSIX (TMPDIR).
1925
+ function _applyAgentTempEnv(childEnv, project) {
1926
+ try {
1927
+ const agentTmp = shared.resolveAgentTempBaseDir(project, getConfig()?.engine, MINIONS_DIR);
1928
+ if (agentTmp) {
1929
+ fs.mkdirSync(agentTmp, { recursive: true });
1930
+ childEnv.TEMP = agentTmp;
1931
+ childEnv.TMP = agentTmp;
1932
+ childEnv.TMPDIR = agentTmp;
1933
+ }
1934
+ } catch { /* leave inherited TEMP untouched */ }
1935
+ }
1936
+
1918
1937
  async function spawnAgent(dispatchItem, config) {
1919
1938
  const { id, agent: agentId, type, meta } = dispatchItem;
1920
1939
  // Resolve prompt — prefers sidecar file when dispatchItem._promptFile is set
@@ -2340,6 +2359,14 @@ async function spawnAgent(dispatchItem, config) {
2340
2359
  }
2341
2360
  if (_liveResult && _liveResult.ok === false && _liveResult.reason === 'dirty') {
2342
2361
  const _dirtyFiles = Array.isArray(_liveResult.dirtyFiles) ? _liveResult.dirtyFiles : [];
2362
+ const _branchInfo = typeof _liveResult.branchInfo === 'string' ? _liveResult.branchInfo : '';
2363
+ // #329: Check if this is a retry after a prior dirty failure. If the WI
2364
+ // already carries `_pendingReason:'live_checkout_dirty'` from a previous
2365
+ // dispatch, the dirty state is persistent (user-owned) and we mark it
2366
+ // non-retryable. First-attempt dirty failures are retried once so a
2367
+ // transient or engine-owned dirty state can clear before permanently
2368
+ // failing the plan item.
2369
+ const _alreadyDirtyFailed = dispatchItem.meta?.item?._pendingReason === 'live_checkout_dirty';
2343
2370
  const _alertBody = [
2344
2371
  '# Live-checkout refused: dirty worktree',
2345
2372
  '',
@@ -2348,16 +2375,19 @@ async function spawnAgent(dispatchItem, config) {
2348
2375
  `**Branch:** ${branchName}`,
2349
2376
  `**Work item:** ${_wiIdForAlert}`,
2350
2377
  `**Dispatch:** ${id}`,
2378
+ ...(_branchInfo ? ['', `**Current checkout:** \`${_branchInfo}\``] : []),
2351
2379
  '',
2352
2380
  `The engine refused to spawn the agent because \`${cwd}\` has uncommitted changes. Live-checkout mode runs in your project directly — the engine never \`git reset\` or \`git clean\` your tree.`,
2353
2381
  '',
2354
- '## Dirty files (`git status --porcelain`)',
2382
+ '## Dirty files (`git status --porcelain=v1 -b`)',
2355
2383
  '',
2356
2384
  '```',
2357
2385
  ...(_dirtyFiles.length > 0 ? _dirtyFiles : ['(none reported)']),
2358
2386
  '```',
2359
2387
  '',
2360
- 'Recovery: commit, stash, or discard the changes in the project checkout, then re-dispatch the work item.',
2388
+ _alreadyDirtyFailed
2389
+ ? 'Recovery: dirty state persisted across retries — commit, stash, or discard the changes in the project checkout, then re-dispatch the work item.'
2390
+ : 'This dispatch will be retried once automatically in case the dirty state is transient. If dirty again on retry, commit, stash, or discard the changes, then re-dispatch.',
2361
2391
  ].join('\n');
2362
2392
  try { writeInboxAlert(`live-checkout-dirty-${_wiIdForAlert}`, _alertBody); }
2363
2393
  catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
@@ -2372,14 +2402,19 @@ async function spawnAgent(dispatchItem, config) {
2372
2402
  });
2373
2403
  }
2374
2404
  } catch (e) { log('warn', `live-checkout: failed to stamp _pendingReason: ${e.message}`); }
2405
+ const _dirtyMsg = _branchInfo
2406
+ ? `live-checkout refused: ${_dirtyFiles.length} dirty file(s) in ${cwd} (${_branchInfo.replace(/^## /, '')})`
2407
+ : `live-checkout refused: ${_dirtyFiles.length} dirty file(s) in ${cwd}`;
2375
2408
  log('error', `spawnAgent: live-checkout refused for ${id}: ${_dirtyFiles.length} dirty file(s) in ${cwd}`);
2376
2409
  _cleanupPromptFiles();
2377
2410
  completeDispatch(
2378
2411
  id,
2379
2412
  DISPATCH_RESULT.ERROR,
2380
- `live-checkout refused: ${_dirtyFiles.length} dirty file(s) in ${cwd}`.slice(0, 800),
2381
- 'Live-checkout mode requires a clean tree; engine refuses to overwrite operator changes. Commit/stash/discard and re-dispatch.',
2382
- { failureClass: FAILURE_CLASS.LIVE_CHECKOUT_DIRTY, agentRetryable: false },
2413
+ _dirtyMsg.slice(0, 800),
2414
+ _alreadyDirtyFailed
2415
+ ? 'Live-checkout dirty state persisted across retries — operator must commit/stash/discard before re-dispatch.'
2416
+ : 'Live-checkout dirty tree retried once automatically; if dirty persists, operator must commit/stash/discard.',
2417
+ { failureClass: FAILURE_CLASS.LIVE_CHECKOUT_DIRTY, agentRetryable: !_alreadyDirtyFailed },
2383
2418
  );
2384
2419
  cleanupTempAgent(agentId);
2385
2420
  return null;
@@ -3750,6 +3785,9 @@ async function spawnAgent(dispatchItem, config) {
3750
3785
  // MCP-free copilot config so agents never pop a per-spawn Microsoft auth
3751
3786
  // window — see _applyAgentCopilotHome / shared.resolveAgentCopilotHome.
3752
3787
  _applyAgentCopilotHome(childEnv);
3788
+ // Keep agent scratch off a (possibly full) system drive — anchors TEMP/TMP to
3789
+ // the worktree volume. See _applyAgentTempEnv / shared.resolveAgentTempBaseDir.
3790
+ _applyAgentTempEnv(childEnv, project);
3753
3791
 
3754
3792
  if (getRepoHost(project) === 'ado') {
3755
3793
  // Inject cached ADO token so ADO agents skip re-authentication (#998).
@@ -4262,6 +4300,8 @@ async function spawnAgent(dispatchItem, config) {
4262
4300
  childEnv.MINIONS_NO_AUTO_OPEN = '1';
4263
4301
  // Same MCP-free copilot home on steering resume (see the initial spawn site).
4264
4302
  _applyAgentCopilotHome(childEnv);
4303
+ // Same agent-scratch redirect on steering resume.
4304
+ _applyAgentTempEnv(childEnv, project);
4265
4305
  if (getRepoHost(project) === 'ado') {
4266
4306
  // Inject cached ADO token for steering session too (#998)
4267
4307
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2241",
3
+ "version": "0.1.2243",
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"