@yemi33/minions 0.1.1984 → 0.1.1986
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 +3 -1
- package/dashboard/js/qa.js +53 -0
- package/dashboard/js/refresh.js +4 -2
- package/dashboard/js/render-managed.js +43 -9
- package/dashboard/js/render-other.js +41 -11
- package/dashboard/layout.html +1 -0
- package/dashboard/pages/qa.html +23 -0
- package/dashboard-build.js +2 -2
- package/dashboard.js +135 -24
- package/docs/.nojekyll +0 -0
- package/docs/README.md +2 -0
- package/docs/constellation-bridge.md +94 -0
- package/docs/security.md +177 -0
- package/engine/ado-git-auth.js +206 -0
- package/engine/bridge.js +124 -0
- package/engine/cc-worker-pool.js +48 -1
- package/engine/cleanup.js +72 -23
- package/engine/cli.js +169 -12
- package/engine/dispatch.js +26 -11
- package/engine/github.js +79 -26
- package/engine/issues.js +14 -3
- package/engine/lifecycle.js +55 -14
- package/engine/llm.js +16 -9
- package/engine/meeting.js +16 -5
- package/engine/queries.js +123 -52
- package/engine/recovery.js +6 -0
- package/engine/shared.js +281 -9
- package/engine/spawn-agent.js +13 -5
- package/engine/timeout.js +4 -2
- package/engine.js +242 -52
- package/package.json +1 -1
package/engine.js
CHANGED
|
@@ -30,6 +30,7 @@ const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
|
|
|
30
30
|
FAILURE_CLASS } = shared;
|
|
31
31
|
const { resolveRuntime } = require('./engine/runtimes');
|
|
32
32
|
const { assertStaleHeadOk } = require('./engine/spawn-agent');
|
|
33
|
+
const adoGitAuth = require('./engine/ado-git-auth');
|
|
33
34
|
const queries = require('./engine/queries');
|
|
34
35
|
|
|
35
36
|
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
@@ -194,6 +195,64 @@ function parseConflictFiles(mergeOutput) {
|
|
|
194
195
|
return [...new Set(files)]; // dedupe
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
// Build the work item used by the dep-merge-failure auto-queue path
|
|
199
|
+
// (W-mpcwojgr000a0244). Routes the conflict-fix through the shared-branch
|
|
200
|
+
// dispatch path (`branchStrategy: 'shared-branch'` + `featureBranch:
|
|
201
|
+
// depConflictBranch`) so spawnAgent's `git worktree add <wt> origin/<branch>`
|
|
202
|
+
// (engine.js:1113) checks out the dep's existing branch directly. Commits land
|
|
203
|
+
// back on that branch via `git push`, the existing PR picks them up, and no
|
|
204
|
+
// redundant fresh-branch PR is opened.
|
|
205
|
+
//
|
|
206
|
+
// Cross-project caveat: the project field is stamped from the blocked item's
|
|
207
|
+
// project. If the dep PR lives in a DIFFERENT project, the agent would still
|
|
208
|
+
// be spawned in the blocked item's worktree root. That is a PRE-EXISTING
|
|
209
|
+
// limitation of the dep-conflict-fix path — single-project plans (the common
|
|
210
|
+
// case, and the trigger scenario P-wi1-bridge-readonly-{a,b,c}) are unaffected.
|
|
211
|
+
function buildDepConflictFixItem({
|
|
212
|
+
depConflictBranch,
|
|
213
|
+
depConflictFiles = [],
|
|
214
|
+
isInterDepConflict = false,
|
|
215
|
+
preflightConflictPrev = null,
|
|
216
|
+
mainBranch,
|
|
217
|
+
blockedItem = null,
|
|
218
|
+
projectName = null,
|
|
219
|
+
}) {
|
|
220
|
+
if (!depConflictBranch) throw new Error('buildDepConflictFixItem: depConflictBranch is required');
|
|
221
|
+
if (!mainBranch) throw new Error('buildDepConflictFixItem: mainBranch is required');
|
|
222
|
+
const conflictFixId = `conflict-fix-${depConflictBranch.replace(/[^a-zA-Z0-9-]/g, '-')}`;
|
|
223
|
+
const filesDesc = depConflictFiles.length > 0
|
|
224
|
+
? `\n\nConflicting files:\n${depConflictFiles.map(f => '- ' + f).join('\n')}`
|
|
225
|
+
: '';
|
|
226
|
+
// Wording is explicit that the agent must push to the SAME branch — the
|
|
227
|
+
// shared-branch dispatch path puts them directly on `depConflictBranch`, so
|
|
228
|
+
// a fresh branch / new PR would defeat the whole point of this auto-queue.
|
|
229
|
+
const conflictFixDesc = isInterDepConflict && preflightConflictPrev
|
|
230
|
+
? `Branch \`${depConflictBranch}\` conflicts with dependency branch \`${preflightConflictPrev}\`. Your worktree is already checked out on \`${depConflictBranch}\`. Rebase \`${depConflictBranch}\` onto \`${preflightConflictPrev}\` (or merge \`${preflightConflictPrev}\` into \`${depConflictBranch}\`), resolve conflicts, then \`git push\` to the SAME branch (\`--force-with-lease\` ok after rebase). Do NOT create a new branch and do NOT open a new PR. The existing PR for \`${depConflictBranch}\` will pick up your commits automatically.`
|
|
231
|
+
: `Branch \`${depConflictBranch}\` conflicts with \`${mainBranch}\`. Your worktree is already checked out on this branch. Merge \`${mainBranch}\` into it, resolve conflicts, then \`git push\` to the SAME branch — do NOT create a new branch and do NOT open a new PR. The existing PR for \`${depConflictBranch}\` will pick up your commits automatically.`;
|
|
232
|
+
const blockedSuffix = blockedItem
|
|
233
|
+
? `\n\nBlocked downstream item: \`${blockedItem.id}\` — ${blockedItem.title || ''}`
|
|
234
|
+
: '';
|
|
235
|
+
return {
|
|
236
|
+
id: conflictFixId,
|
|
237
|
+
title: `Fix merge conflict: ${depConflictBranch} conflicts with ${isInterDepConflict ? preflightConflictPrev : mainBranch}`,
|
|
238
|
+
type: WORK_TYPE.FIX,
|
|
239
|
+
priority: 'high',
|
|
240
|
+
status: WI_STATUS.PENDING,
|
|
241
|
+
description: `${conflictFixDesc}${filesDesc}${blockedSuffix}`,
|
|
242
|
+
created: ts(),
|
|
243
|
+
createdBy: 'engine:dep-conflict-fix',
|
|
244
|
+
// Route through the shared-branch dispatch path (engine.js:4629, :4681,
|
|
245
|
+
// :1113) so the agent operates on the dep's existing branch — pushing
|
|
246
|
+
// straight into the existing PR instead of opening a new one.
|
|
247
|
+
branchStrategy: 'shared-branch',
|
|
248
|
+
featureBranch: depConflictBranch,
|
|
249
|
+
_branch: depConflictBranch,
|
|
250
|
+
_blockedItem: blockedItem ? blockedItem.id : null,
|
|
251
|
+
_isInterDepConflict: !!isInterDepConflict,
|
|
252
|
+
project: projectName || null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
197
256
|
// Prune dep branches that are ancestors of other dep branches (#958)
|
|
198
257
|
// When B already contains A's commits, merging both A and B causes conflicts.
|
|
199
258
|
async function pruneAncestorDeps(deps, gitOpts, cwd) {
|
|
@@ -527,7 +586,16 @@ async function syncReusedWorktree(rootDir, worktreePath, branchName, gitOpts = {
|
|
|
527
586
|
async function findExistingWorktree(repoDir, branchName) {
|
|
528
587
|
try {
|
|
529
588
|
const out = await shared.shellSafeGit(['worktree', 'list', '--porcelain'], { cwd: repoDir, timeout: 10000 });
|
|
530
|
-
|
|
589
|
+
// Skip worktrees at or inside `repoDir` itself: the main checkout (always
|
|
590
|
+
// first in the porcelain output) sits AT repoDir, and nested worktrees
|
|
591
|
+
// under it would trip assertWorktreeOutsideProject downstream anyway.
|
|
592
|
+
// Returning either as "reusable" creates an unresolvable preflight
|
|
593
|
+
// rejection loop when the branch happens to be checked out at the
|
|
594
|
+
// project root (common when an operator works on the minions repo
|
|
595
|
+
// locally while the engine tries to review that same branch).
|
|
596
|
+
const found = shared.parseWorktreePorcelain(out).find(w =>
|
|
597
|
+
w.branch === branchName && !shared.isPathInsideOrEqual(w.path, repoDir)
|
|
598
|
+
);
|
|
531
599
|
if (found && fs.existsSync(found.path)) return found.path;
|
|
532
600
|
} catch (e) { log('warn', 'git: ' + e.message); }
|
|
533
601
|
return null;
|
|
@@ -816,7 +884,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
816
884
|
let branchName = _preBranchName;
|
|
817
885
|
const worktreeCreateTimeout = Math.max(60000, Number(engineConfig.worktreeCreateTimeout) || ENGINE_DEFAULTS.worktreeCreateTimeout);
|
|
818
886
|
const worktreeCreateRetries = Math.max(0, Math.min(3, Number(engineConfig.worktreeCreateRetries) || ENGINE_DEFAULTS.worktreeCreateRetries));
|
|
819
|
-
|
|
887
|
+
// W-mpcuc8i80003a7b3 — for ADO projects, inject a per-invocation
|
|
888
|
+
// `-c http.<host>.extraHeader=Authorization: Bearer <token>` so headless
|
|
889
|
+
// git ops survive missing/expired Git Credential Manager state. Returns
|
|
890
|
+
// [] for GitHub/local projects so this is a no-op there.
|
|
891
|
+
const _adoGitExtraArgs = adoGitAuth.getAdoGitExtraArgs(project);
|
|
892
|
+
const _gitOpts = { stdio: 'pipe', timeout: 30000, windowsHide: true, env: shared.gitEnv(), gitExtraArgs: _adoGitExtraArgs };
|
|
820
893
|
const _worktreeGitOpts = { ..._gitOpts, timeout: worktreeCreateTimeout };
|
|
821
894
|
|
|
822
895
|
// Build the initial prompt before worktree setup, then refresh shared-branch
|
|
@@ -862,14 +935,34 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
862
935
|
: taskPromptWithReport;
|
|
863
936
|
};
|
|
864
937
|
let fullTaskPrompt = buildFullTaskPrompt(taskPrompt);
|
|
865
|
-
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
866
|
-
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
867
938
|
const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
|
|
868
|
-
|
|
939
|
+
// P-f6-tmp-toctou: per-dispatch unique tmp dir closes the symlink-plant
|
|
940
|
+
// window on engine/tmp/prompt-<id>.md (mkdtempSync + chmod 0o700). Filename
|
|
941
|
+
// pattern inside the dir is unchanged so spawn-agent.js's prompt→pid regex
|
|
942
|
+
// derivation still works.
|
|
943
|
+
const dispatchTmpDir = shared.createDispatchTmpDir(id);
|
|
944
|
+
const promptPath = path.join(dispatchTmpDir, `prompt-${safeId}.md`);
|
|
869
945
|
safeWrite(promptPath, fullTaskPrompt);
|
|
870
|
-
const sysPromptPath = path.join(
|
|
946
|
+
const sysPromptPath = path.join(dispatchTmpDir, `sysprompt-${safeId}.md`);
|
|
871
947
|
safeWrite(sysPromptPath, systemPrompt);
|
|
872
|
-
|
|
948
|
+
// Stamp the tmpDir on the dispatch entry so cleanup/orphan-reap/kill paths
|
|
949
|
+
// (dispatch.js, meeting.js, cli.js, cleanup.js, timeout.js) can find the
|
|
950
|
+
// dir via shared.dispatchPidCandidates / findDispatchPidFile. Best-effort —
|
|
951
|
+
// backward-compat fallbacks still scan dispatch-<safeId>-* dirs by id.
|
|
952
|
+
try {
|
|
953
|
+
dispatchItem.tmpDir = dispatchTmpDir;
|
|
954
|
+
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
955
|
+
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
956
|
+
for (const queue of ['pending', 'active', 'completed']) {
|
|
957
|
+
const arr = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
|
|
958
|
+
if (!arr) continue;
|
|
959
|
+
const found = arr.find(d => d && d.id === id);
|
|
960
|
+
if (found) found.tmpDir = dispatchTmpDir;
|
|
961
|
+
}
|
|
962
|
+
return dispatch;
|
|
963
|
+
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
964
|
+
} catch (e) { log('warn', `spawnAgent: failed to persist tmpDir for ${id}: ${e.message}`); }
|
|
965
|
+
const _cleanupPromptFiles = () => { shared.removeDispatchTmpDir(dispatchTmpDir); };
|
|
873
966
|
// Convert a WORKTREE_NESTED_IN_PROJECT throw into a fail-fast non-retryable
|
|
874
967
|
// dispatch failure (W-mp62taw2000ubcc3). The error's `.code` is set by
|
|
875
968
|
// shared.assertWorktreeOutsideProject so we don't have to parse the message.
|
|
@@ -1173,6 +1266,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1173
1266
|
let depMergeFailed = false;
|
|
1174
1267
|
let depConflictBranch = null; // track which dep branch caused the conflict
|
|
1175
1268
|
let depConflictFiles = []; // conflicting file names parsed from git output
|
|
1269
|
+
// W-mpcuc8i80003a7b3 — track whether ANY git op in the dep phase
|
|
1270
|
+
// failed with an ADO auth signature. If so, we escalate as
|
|
1271
|
+
// FAILURE_CLASS.AUTH (non-retryable + dedup'd inbox alert) instead
|
|
1272
|
+
// of FAILURE_CLASS.MERGE_CONFLICT (which retries 3x against the
|
|
1273
|
+
// same broken auth path).
|
|
1274
|
+
let _depAuthFailed = false;
|
|
1176
1275
|
// Fetch all dependency branches in parallel (git fetches are independent)
|
|
1177
1276
|
const fetchable = depBranches.filter(d => !_failedRefCache.has(d.branch));
|
|
1178
1277
|
const unfetchable = depBranches.filter(d => _failedRefCache.has(d.branch));
|
|
@@ -1188,7 +1287,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1188
1287
|
}
|
|
1189
1288
|
const fetchResults = await Promise.allSettled(
|
|
1190
1289
|
fetchable.map(({ branch: depBranch }) =>
|
|
1191
|
-
|
|
1290
|
+
// runAdoGit for ADO projects auto-retries once on auth failure
|
|
1291
|
+
// with a refreshed bearer token (handles mid-dispatch expiry).
|
|
1292
|
+
// For non-ADO projects it's a thin pass-through to shellSafeGit.
|
|
1293
|
+
adoGitAuth.runAdoGit(project, ['fetch', 'origin', depBranch], { ..._gitOpts, cwd: rootDir }).then(() => depBranch)
|
|
1192
1294
|
)
|
|
1193
1295
|
);
|
|
1194
1296
|
const hasFetchFailures = fetchResults.some(r => r.status === 'rejected');
|
|
@@ -1221,6 +1323,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1221
1323
|
}
|
|
1222
1324
|
_failedRefCache.add(failedBranch);
|
|
1223
1325
|
log('warn', `Failed to fetch dependency ${failedBranch}: ${errMsg}`);
|
|
1326
|
+
// W-mpcuc8i80003a7b3 — detect ADO bearer-token / GCM credential
|
|
1327
|
+
// failures so we escalate as FAILURE_CLASS.AUTH below instead of
|
|
1328
|
+
// mis-classifying as a merge conflict and burning 3 retries.
|
|
1329
|
+
if (adoGitAuth.isAdoAuthFailure(fetchResults[i].reason)) {
|
|
1330
|
+
_depAuthFailed = true;
|
|
1331
|
+
}
|
|
1224
1332
|
depMergeFailed = true;
|
|
1225
1333
|
}
|
|
1226
1334
|
}
|
|
@@ -1302,6 +1410,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1302
1410
|
// Merge failed — possibly due to diverged history from a force-pushed (rebased) dep branch.
|
|
1303
1411
|
// Abort partial merge, reset worktree to clean main base, and re-merge all deps from scratch.
|
|
1304
1412
|
log('warn', `Merge of ${depBranch} into ${branchName} failed: ${mergeErr.message} — attempting reset and re-merge of all deps`);
|
|
1413
|
+
// W-mpcuc8i80003a7b3 — defense in depth. `git merge origin/<dep>`
|
|
1414
|
+
// is local so an auth failure here is unexpected, but mark the
|
|
1415
|
+
// flag so the final dispatch failure routes through the AUTH
|
|
1416
|
+
// path instead of MERGE_CONFLICT.
|
|
1417
|
+
if (adoGitAuth.isAdoAuthFailure(mergeErr)) _depAuthFailed = true;
|
|
1305
1418
|
try { await shared.shellSafeGit(['merge', '--abort'], { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
|
|
1306
1419
|
const mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
1307
1420
|
try {
|
|
@@ -1316,6 +1429,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1316
1429
|
} catch (resetErr) {
|
|
1317
1430
|
const errOutput = (resetErr.message || '') + '\n' + (resetErr.stdout?.toString?.() || '') + '\n' + (resetErr.stderr?.toString?.() || '');
|
|
1318
1431
|
log('warn', `Failed to reset and re-merge deps for ${branchName}: ${resetErr.message}`);
|
|
1432
|
+
if (adoGitAuth.isAdoAuthFailure(resetErr)) _depAuthFailed = true;
|
|
1319
1433
|
try { await shared.shellSafeGit(['merge', '--abort'], { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
|
|
1320
1434
|
// Post-mortem: incremental simulation to identify which dep caused the conflict (#958)
|
|
1321
1435
|
// Uses same chained merge-tree approach as pre-flight to catch inter-dep conflicts
|
|
@@ -1366,6 +1480,58 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1366
1480
|
_phaseT.depMergeEnd = Date.now();
|
|
1367
1481
|
if (depMergeFailed) {
|
|
1368
1482
|
_cleanupPromptFiles();
|
|
1483
|
+
// W-mpcuc8i80003a7b3 — short-circuit to AUTH classification when any
|
|
1484
|
+
// git op in the dep phase looked like an ADO credential failure.
|
|
1485
|
+
// This BYPASSES the merge-conflict path entirely: no retries (auth
|
|
1486
|
+
// is structural — re-running won't fix missing az / GCM creds),
|
|
1487
|
+
// no conflict-fix WI (would just re-trigger the same auth wall),
|
|
1488
|
+
// and a single dedup'd inbox alert pointing at the recovery steps.
|
|
1489
|
+
if (_depAuthFailed) {
|
|
1490
|
+
const projName = project.name || 'unknown';
|
|
1491
|
+
const authFailReason = `ADO git authentication failed for project ${projName}: dependency fetch could not authenticate to origin (likely missing/expired az CLI token, no cached GCM credentials, or token broker unavailable in headless context)`;
|
|
1492
|
+
try {
|
|
1493
|
+
writeInboxAlert(`ado-auth-${projName}`, [
|
|
1494
|
+
`# ADO git authentication failed — ${projName}`,
|
|
1495
|
+
'',
|
|
1496
|
+
`Work item: \`${meta?.item?.id || '(unknown)'}\``,
|
|
1497
|
+
`Branch: \`${branchName || '(unknown)'}\``,
|
|
1498
|
+
'',
|
|
1499
|
+
'## Symptom',
|
|
1500
|
+
'',
|
|
1501
|
+
'Engine could not fetch dependency branches from the ADO origin. ',
|
|
1502
|
+
'Git Credential Manager has no cached credentials and falls back to ',
|
|
1503
|
+
'a TTY prompt, which the headless engine cannot satisfy:',
|
|
1504
|
+
'',
|
|
1505
|
+
'```',
|
|
1506
|
+
"fatal: could not read Username for 'https://<adoOrg>.visualstudio.com'",
|
|
1507
|
+
'```',
|
|
1508
|
+
'',
|
|
1509
|
+
'## Recovery',
|
|
1510
|
+
'',
|
|
1511
|
+
'From an interactive shell on the engine host, refresh ADO credentials:',
|
|
1512
|
+
'',
|
|
1513
|
+
'```bash',
|
|
1514
|
+
'# Option A — Azure CLI (preferred)',
|
|
1515
|
+
'az login',
|
|
1516
|
+
'az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv',
|
|
1517
|
+
'',
|
|
1518
|
+
'# Option B — azureauth (corp environments)',
|
|
1519
|
+
'azureauth ado token --mode iwa --mode broker --output token --timeout 1',
|
|
1520
|
+
'```',
|
|
1521
|
+
'',
|
|
1522
|
+
'Once a valid token can be acquired, the engine will pick it up automatically on the next tick (30-min cache + 10-min acquire backoff).',
|
|
1523
|
+
'',
|
|
1524
|
+
'## Why no auto-retry',
|
|
1525
|
+
'',
|
|
1526
|
+
'This dispatch is failed as `FAILURE_CLASS.AUTH` (non-retryable). Mechanical retry would burn slots against the same broken credential path. After you fix the credentials, manually retry the work item via the dashboard or `/api/work-items/retry`.',
|
|
1527
|
+
].join('\n'));
|
|
1528
|
+
} catch (alertErr) {
|
|
1529
|
+
log('warn', `Failed to write ADO auth inbox alert: ${alertErr.message}`);
|
|
1530
|
+
}
|
|
1531
|
+
completeDispatch(id, DISPATCH_RESULT.ERROR, authFailReason, '', { failureClass: FAILURE_CLASS.AUTH, agentRetryable: false });
|
|
1532
|
+
cleanupTempAgent(agentId);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1369
1535
|
// Build actionable failReason identifying the conflicting branch and files (#958)
|
|
1370
1536
|
const mainBranch = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
1371
1537
|
let failReason = 'Dependency merge failed';
|
|
@@ -1380,39 +1546,29 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1380
1546
|
}
|
|
1381
1547
|
completeDispatch(id, DISPATCH_RESULT.ERROR, failReason, '', { failureClass: FAILURE_CLASS.MERGE_CONFLICT });
|
|
1382
1548
|
|
|
1383
|
-
// Auto-queue conflict-fix work item when a specific dep branch is identified
|
|
1549
|
+
// Auto-queue conflict-fix work item when a specific dep branch is identified.
|
|
1550
|
+
// Routes through the shared-branch dispatch path (see buildDepConflictFixItem)
|
|
1551
|
+
// so commits land on the dep's existing PR branch (W-mpcwojgr000a0244).
|
|
1384
1552
|
if (depConflictBranch && meta?.item?.id && project) {
|
|
1385
1553
|
try {
|
|
1386
1554
|
const wiPath = project.name
|
|
1387
1555
|
? projectWorkItemsPath(project)
|
|
1388
1556
|
: path.join(MINIONS_DIR, 'work-items.json');
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
:
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
:
|
|
1557
|
+
const newItem = buildDepConflictFixItem({
|
|
1558
|
+
depConflictBranch,
|
|
1559
|
+
depConflictFiles,
|
|
1560
|
+
isInterDepConflict: _isInterDepConflict,
|
|
1561
|
+
preflightConflictPrev: _preflightConflictPrev,
|
|
1562
|
+
mainBranch,
|
|
1563
|
+
blockedItem: meta.item,
|
|
1564
|
+
projectName: project.name || null,
|
|
1565
|
+
});
|
|
1397
1566
|
mutateWorkItems(wiPath, items => {
|
|
1398
1567
|
// Don't create duplicate conflict-fix items
|
|
1399
|
-
const existing = items.find(i => i.id ===
|
|
1568
|
+
const existing = items.find(i => i.id === newItem.id && i.status !== WI_STATUS.DONE && i.status !== WI_STATUS.FAILED && i.status !== WI_STATUS.CANCELLED);
|
|
1400
1569
|
if (existing) return;
|
|
1401
|
-
items.push(
|
|
1402
|
-
|
|
1403
|
-
title: `Fix merge conflict: ${depConflictBranch} conflicts with ${_isInterDepConflict ? _preflightConflictPrev : mainBranch}`,
|
|
1404
|
-
type: WORK_TYPE.FIX,
|
|
1405
|
-
priority: 'high',
|
|
1406
|
-
status: WI_STATUS.PENDING,
|
|
1407
|
-
description: `${conflictFixDesc}${filesDesc}\n\nBlocked downstream item: \`${meta.item.id}\` — ${meta.item.title || ''}`,
|
|
1408
|
-
created: ts(),
|
|
1409
|
-
createdBy: 'engine:dep-conflict-fix',
|
|
1410
|
-
_branch: depConflictBranch,
|
|
1411
|
-
_blockedItem: meta.item.id,
|
|
1412
|
-
_isInterDepConflict: _isInterDepConflict || false,
|
|
1413
|
-
project: project.name || null,
|
|
1414
|
-
});
|
|
1415
|
-
log('info', `Auto-queued conflict-fix work item ${conflictFixId} for ${depConflictBranch} (blocked: ${meta.item.id})`);
|
|
1570
|
+
items.push(newItem);
|
|
1571
|
+
log('info', `Auto-queued conflict-fix work item ${newItem.id} for ${depConflictBranch} (blocked: ${meta.item.id})`);
|
|
1416
1572
|
});
|
|
1417
1573
|
} catch (e) { log('warn', `Failed to auto-queue conflict-fix: ${e.message}`); }
|
|
1418
1574
|
}
|
|
@@ -1707,6 +1863,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1707
1863
|
if (pidFilePath) {
|
|
1708
1864
|
try { safeUnlink(pidFilePath); } catch { /* may not exist yet */ }
|
|
1709
1865
|
}
|
|
1866
|
+
// P-f6-tmp-toctou: rm the per-dispatch tmp dir too so prompt/sysprompt
|
|
1867
|
+
// sidecars don't leak when runFile throws before onAgentClose can run.
|
|
1868
|
+
try { _cleanupPromptFiles(); } catch { /* cleanup is best-effort */ }
|
|
1710
1869
|
cleanupTempAgent(agentId);
|
|
1711
1870
|
throw spawnErr;
|
|
1712
1871
|
}
|
|
@@ -1847,7 +2006,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1847
2006
|
const pendingForResume = steering.buildPendingSteeringPrompt(agentId);
|
|
1848
2007
|
const steerPromptBody = pendingForResume.prompt || steerMsg;
|
|
1849
2008
|
const steerPrompt = `Message from your human teammate:\n\n${steerPromptBody}\n\nRespond to this, then continue working on your current task.`;
|
|
1850
|
-
const steerPromptPath = path.join(
|
|
2009
|
+
const steerPromptPath = path.join(dispatchTmpDir, `prompt-steer-${safeId}.md`);
|
|
1851
2010
|
try { safeWrite(steerPromptPath, steerPrompt); } catch (e) {
|
|
1852
2011
|
log('warn', `Steering: failed to write prompt for ${agentId}: ${e.message}`);
|
|
1853
2012
|
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Could not write prompt. Message was: ${steerMsg}\n`); } catch {}
|
|
@@ -2705,10 +2864,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2705
2864
|
} catch (e) { log('warn', `keep-processes acceptance: failed to set _pendingReason: ${e.message}`); }
|
|
2706
2865
|
}
|
|
2707
2866
|
|
|
2708
|
-
// Cleanup temp files (
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2867
|
+
// Cleanup temp files (whole per-dispatch dir, including PID/sysprompt
|
|
2868
|
+
// tmp/prompt — P-f6-tmp-toctou). removeDispatchTmpDir validates the path
|
|
2869
|
+
// resolves under engine/tmp/dispatch-* before rmSync, so a corrupted
|
|
2870
|
+
// dispatch.json field can't redirect this to an arbitrary path.
|
|
2871
|
+
try { shared.removeDispatchTmpDir(dispatchTmpDir); } catch { /* cleanup */ }
|
|
2712
2872
|
|
|
2713
2873
|
log('info', `Agent ${agentId} completed. Output saved to ${archivePath}`);
|
|
2714
2874
|
|
|
@@ -3981,16 +4141,24 @@ async function discoverFromPrs(config, project) {
|
|
|
3981
4141
|
const currentHeadSha = String(pr.headSha || pr._adoSourceCommit || pr._adoHeadCommit || '').trim();
|
|
3982
4142
|
const lastHumanDispatch = pr._lastDispatchByCause?.[shared.PR_FIX_CAUSE.HUMAN_FEEDBACK];
|
|
3983
4143
|
const currentCommentId = String(pr.humanFeedback?.lastProcessedCommentId || '');
|
|
3984
|
-
|
|
4144
|
+
// Issue #2632: this same-head/same-comment guard MUST be cause-local. A
|
|
4145
|
+
// previous `continue` here aborted the whole PR iteration and starved
|
|
4146
|
+
// the build-failure / re-review / conflict-fix evaluation blocks below
|
|
4147
|
+
// (live repro on ADO PR `office-bohemia#5215610`: had `buildStatus=failing`
|
|
4148
|
+
// + `buildFailureSignature` but no `build-fix-*` dispatch was ever
|
|
4149
|
+
// queued). Skip only the human-feedback dispatch path; leave
|
|
4150
|
+
// `fixDispatched=false` so downstream causes are still evaluated.
|
|
4151
|
+
const skipHumanFeedback = !!(lastHumanDispatch?.outcome === 'noop'
|
|
3985
4152
|
&& lastHumanDispatch.headSha
|
|
3986
4153
|
&& currentHeadSha
|
|
3987
4154
|
&& lastHumanDispatch.headSha === currentHeadSha
|
|
3988
4155
|
&& lastHumanDispatch.lastProcessedCommentId
|
|
3989
4156
|
&& currentCommentId
|
|
3990
|
-
&& lastHumanDispatch.lastProcessedCommentId === currentCommentId)
|
|
4157
|
+
&& lastHumanDispatch.lastProcessedCommentId === currentCommentId);
|
|
4158
|
+
if (skipHumanFeedback) {
|
|
3991
4159
|
log('info', `Skipping human-feedback fix for ${pr.id}: last human-feedback dispatch was noop on the same head ${currentHeadSha.slice(0, 8)} and same comment ${currentCommentId.slice(0, 32)} (${(lastHumanDispatch.reason || '').slice(0, 120)})`);
|
|
3992
|
-
continue;
|
|
3993
4160
|
}
|
|
4161
|
+
if (!skipHumanFeedback) {
|
|
3994
4162
|
const key = humanFixKey;
|
|
3995
4163
|
if (isPrAutomationCauseHandledOrPending(project, pr, humanCauseKey)) continue;
|
|
3996
4164
|
let staleCoalesced = [];
|
|
@@ -4035,6 +4203,7 @@ async function discoverFromPrs(config, project) {
|
|
|
4035
4203
|
review_note: reviewNote,
|
|
4036
4204
|
}, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, automationCauseKey: humanCauseKey, source: 'pr-human-feedback', pr, branch: prBranch, project: projMeta });
|
|
4037
4205
|
if (item) { newWork.push(item); fixDispatched = true; }
|
|
4206
|
+
} // end if (!skipHumanFeedback) — cause-local guard for #2632
|
|
4038
4207
|
}
|
|
4039
4208
|
|
|
4040
4209
|
// Re-review after fix: trigger when a fix was pushed after the last minions review,
|
|
@@ -4150,13 +4319,18 @@ async function discoverFromPrs(config, project) {
|
|
|
4150
4319
|
// a new commit landed (live repro on PR #2433).
|
|
4151
4320
|
const currentHeadSha = String(pr.headSha || pr._adoSourceCommit || pr._adoHeadCommit || '').trim();
|
|
4152
4321
|
const lastBuildDispatch = pr._lastDispatchByCause?.[shared.PR_FIX_CAUSE.BUILD_FAILURE];
|
|
4153
|
-
|
|
4322
|
+
// Issue #2632 audit: cause-local guard. A previous `continue` here
|
|
4323
|
+
// aborted the whole PR iteration and starved the conflict-fix block
|
|
4324
|
+
// below — symmetric to the human-feedback bug. Skip only the build-fix
|
|
4325
|
+
// dispatch path; downstream merge-conflict resolution must still run.
|
|
4326
|
+
const skipBuildFix = !!(lastBuildDispatch?.outcome === 'noop'
|
|
4154
4327
|
&& lastBuildDispatch.headSha
|
|
4155
4328
|
&& currentHeadSha
|
|
4156
|
-
&& lastBuildDispatch.headSha === currentHeadSha)
|
|
4329
|
+
&& lastBuildDispatch.headSha === currentHeadSha);
|
|
4330
|
+
if (skipBuildFix) {
|
|
4157
4331
|
log('info', `Skipping build-fix for ${pr.id}: last build-failure dispatch was noop on the same head ${currentHeadSha.slice(0, 8)} (${(lastBuildDispatch.reason || '').slice(0, 120)})`);
|
|
4158
|
-
continue;
|
|
4159
4332
|
}
|
|
4333
|
+
if (!skipBuildFix) {
|
|
4160
4334
|
const buildCauseKey = getPrAutomationCauseKey('build', pr);
|
|
4161
4335
|
const key = getPrAutomationDispatchKey(`build-fix-${project?.name || 'default'}-${prDisplayId}`, buildCauseKey);
|
|
4162
4336
|
if (isPrAutomationCauseHandledOrPending(project, pr, buildCauseKey)) continue;
|
|
@@ -4245,6 +4419,7 @@ async function discoverFromPrs(config, project) {
|
|
|
4245
4419
|
});
|
|
4246
4420
|
} catch (e) { log('warn', 'mark build fail notified: ' + e.message); }
|
|
4247
4421
|
}
|
|
4422
|
+
} // end if (!skipBuildFix) — cause-local guard for #2632 audit
|
|
4248
4423
|
}
|
|
4249
4424
|
|
|
4250
4425
|
// PRs with merge conflicts — dispatch fix to resolve (gated by provider polling + autoFixConflicts)
|
|
@@ -4886,10 +5061,19 @@ function materializeSpecsAsWorkItems(config, project) {
|
|
|
4886
5061
|
let recentSpecs = [];
|
|
4887
5062
|
for (const pattern of filePatterns) {
|
|
4888
5063
|
try {
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
5064
|
+
// P-f7-git-log: argv form via execFileSync (shell:false) so file patterns
|
|
5065
|
+
// from operator config.json reach git as literal pathspec args, never as
|
|
5066
|
+
// shell tokens. Spread the pattern after the '--' pathspec separator.
|
|
5067
|
+
const args = [
|
|
5068
|
+
'log',
|
|
5069
|
+
'--diff-filter=AM',
|
|
5070
|
+
'--name-only',
|
|
5071
|
+
'--pretty=format:COMMIT:%H|%s',
|
|
5072
|
+
`--since=${sinceDate}`,
|
|
5073
|
+
'--',
|
|
5074
|
+
pattern,
|
|
5075
|
+
];
|
|
5076
|
+
const result = shared.shellSafeGitSync(args, { cwd: root, timeout: 10000 }).trim();
|
|
4893
5077
|
if (!result) continue;
|
|
4894
5078
|
|
|
4895
5079
|
let currentCommit = null;
|
|
@@ -5724,12 +5908,15 @@ async function tickInner() {
|
|
|
5724
5908
|
process.exit(0);
|
|
5725
5909
|
}
|
|
5726
5910
|
|
|
5727
|
-
//
|
|
5728
|
-
|
|
5911
|
+
// W-mpcyvff6000pf828 (#2653) — control.heartbeat is written by a dedicated
|
|
5912
|
+
// 15s interval in engine/cli.js (createHeartbeatWriter), decoupled from
|
|
5913
|
+
// tickInner so a slow tick (cold runtime spawn, sequential PR polls, slow
|
|
5914
|
+
// worktree create) cannot starve heartbeats and flip the dashboard to STALE
|
|
5915
|
+
// on an otherwise healthy engine.
|
|
5729
5916
|
|
|
5730
5917
|
// P-c2e5a1d9-a — Initial wiring guard: bail immediately if a force-release
|
|
5731
|
-
// reclaimed our lock while the
|
|
5732
|
-
// guards inside the rest of tickInner are sub-task -b's scope.
|
|
5918
|
+
// reclaimed our lock while the startup control-state read was in flight.
|
|
5919
|
+
// Per-phase guards inside the rest of tickInner are sub-task -b's scope.
|
|
5733
5920
|
if (_isTickStale(myGeneration)) return;
|
|
5734
5921
|
|
|
5735
5922
|
const config = getConfig();
|
|
@@ -6394,14 +6581,17 @@ module.exports = {
|
|
|
6394
6581
|
// Discovery
|
|
6395
6582
|
discoverWork, discoverFromPrs, discoverFromWorkItems, discoverCentralWorkItems,
|
|
6396
6583
|
materializePlansAsWorkItems,
|
|
6584
|
+
materializeSpecsAsWorkItems, // exported for testing (P-f7-git-log)
|
|
6397
6585
|
reservePrdFilename, // exported for testing (P-9b7e5d3c)
|
|
6398
6586
|
sweepStaleArchivedPrdBackups, // exported for testing
|
|
6399
6587
|
|
|
6400
6588
|
// Shared helpers (used by lifecycle.js and tests)
|
|
6401
6589
|
reconcileItemsWithPrs, detectDependencyCycles,
|
|
6402
6590
|
parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
|
|
6591
|
+
buildDepConflictFixItem, // exported for testing (W-mpcwojgr000a0244)
|
|
6403
6592
|
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, // exported for testing
|
|
6404
6593
|
pruneStaleWorktreeForBranch, // exported for testing
|
|
6594
|
+
findExistingWorktree, // exported for testing
|
|
6405
6595
|
_maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
|
|
6406
6596
|
promoteCheckpointSteeringForClose, // exported for testing
|
|
6407
6597
|
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.
|
|
3
|
+
"version": "0.1.1986",
|
|
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"
|