@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 +9 -0
- package/engine/live-checkout.js +12 -7
- package/engine/shared.js +25 -0
- package/engine.js +45 -5
- package/package.json +1 -1
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 });
|
package/engine/live-checkout.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
134
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
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.
|
|
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"
|