@yemi33/minions 0.1.2381 → 0.1.2382
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 +41 -2
- package/dashboard/js/refresh.js +8 -2
- package/dashboard/js/render-other.js +38 -0
- package/dashboard/js/render-work-items.js +61 -20
- package/dashboard/js/settings.js +7 -8
- package/dashboard/pages/engine.html +6 -0
- package/dashboard.js +84 -15
- package/docs/README.md +1 -0
- package/docs/claude-md-propagation.md +118 -0
- package/docs/engine-restart.md +10 -0
- package/docs/worktree-lifecycle.md +9 -0
- package/engine/claude-md-context.js +195 -0
- package/engine/consolidation.js +47 -3
- package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
- package/engine/dispatch.js +73 -21
- package/engine/lifecycle.js +70 -12
- package/engine/pipeline.js +147 -20
- package/engine/playbook.js +35 -0
- package/engine/prd-store.js +14 -6
- package/engine/quarantine-refs.js +33 -0
- package/engine/queries.js +8 -6
- package/engine/restart-health.js +69 -4
- package/engine/runtimes/claude.js +6 -0
- package/engine/runtimes/codex.js +18 -0
- package/engine/runtimes/copilot.js +7 -0
- package/engine/shared.js +63 -44
- package/engine/supervisor.js +72 -16
- package/engine/timeout.js +87 -16
- package/engine/untrusted-fence.js +2 -1
- package/engine.js +251 -16
- package/package.json +1 -1
package/engine.js
CHANGED
|
@@ -1430,15 +1430,15 @@ const _quarantinePathLocks = new Map();
|
|
|
1430
1430
|
// (Pure shared-branch callers pass quarantineOnUnsafe=false and DO NOT
|
|
1431
1431
|
// treat ahead-of-origin as dirty — that path's existing semantics are
|
|
1432
1432
|
// preserved. PR-targeted reused worktrees pass quarantineOnUnsafe=true
|
|
1433
|
-
// and
|
|
1434
|
-
// (#
|
|
1433
|
+
// and preserve clean strictly-ahead commits by pushing them before any
|
|
1434
|
+
// quarantine fallback (#853).)
|
|
1435
1435
|
// 3. If unsafe, decide:
|
|
1436
1436
|
// - Block (preserve dirty tree for inspection) when ANOTHER active
|
|
1437
1437
|
// dispatch claims the same branch (ownership ambiguity — NEVER
|
|
1438
1438
|
// quarantine, even when quarantineOnUnsafe=true).
|
|
1439
|
-
// -
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1439
|
+
// - When clean local commits are strictly ahead, push them to origin.
|
|
1440
|
+
// If that push fails or the branch is genuinely diverged, quarantine:
|
|
1441
|
+
// rename worktree to <path>-quarantine-<ts>,
|
|
1442
1442
|
// prune git's stale metadata, back up the local branch HEAD to
|
|
1443
1443
|
// refs/minions/quarantine/<sanitized>/<ts>, reset
|
|
1444
1444
|
// refs/heads/<branch> → origin/<branch>, and write a notes/inbox/
|
|
@@ -1458,6 +1458,124 @@ const _quarantinePathLocks = new Map();
|
|
|
1458
1458
|
// quarantined, quarantinedPath, backupRef } so the caller can fail
|
|
1459
1459
|
// fast with a first-class DIRTY_WORKTREE / WORKTREE_DIRTY /
|
|
1460
1460
|
// WORKTREE_DIVERGENT reason and retry semantics.
|
|
1461
|
+
async function pushCleanAheadBranch({ rootDir, worktreePath, branchName, mainBranch, project, gitOpts = {} } = {}) {
|
|
1462
|
+
const result = { pushed: false, reason: null, ahead: 0, behind: 0 };
|
|
1463
|
+
if (!rootDir || !worktreePath || !branchName || !fs.existsSync(worktreePath)) {
|
|
1464
|
+
return { ...result, reason: 'invalid-worktree' };
|
|
1465
|
+
}
|
|
1466
|
+
if (mainBranch && sanitizeBranch(branchName) === sanitizeBranch(mainBranch)) {
|
|
1467
|
+
return { ...result, reason: 'main-branch' };
|
|
1468
|
+
}
|
|
1469
|
+
const { gitExtraArgs: _staleGitExtraArgs, ...networkGitOpts } = gitOpts;
|
|
1470
|
+
const runNetworkGit = (args, opts) => project
|
|
1471
|
+
? adoGitAuth.runAdoGit(project, args, { ...networkGitOpts, ...opts })
|
|
1472
|
+
: shared.shellSafeGit(args, { ...gitOpts, ...opts });
|
|
1473
|
+
try {
|
|
1474
|
+
const current = String(await shared.shellSafeGit(
|
|
1475
|
+
['symbolic-ref', '--quiet', '--short', 'HEAD'],
|
|
1476
|
+
{ ...gitOpts, cwd: worktreePath, timeout: 10000 },
|
|
1477
|
+
) || '').trim();
|
|
1478
|
+
if (current !== branchName) return { ...result, reason: 'branch-mismatch' };
|
|
1479
|
+
|
|
1480
|
+
const status = String(await shared.shellSafeGit(
|
|
1481
|
+
['--no-optional-locks', 'status', '--porcelain'],
|
|
1482
|
+
{ ...gitOpts, cwd: worktreePath, timeout: 15000 },
|
|
1483
|
+
) || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean)
|
|
1484
|
+
.filter(line => !shared.isWorktreeOwnerMarkerStatusLine(line));
|
|
1485
|
+
if (status.length > 0) return { ...result, reason: 'dirty' };
|
|
1486
|
+
|
|
1487
|
+
let remoteExists = false;
|
|
1488
|
+
let trackingRefExists = false;
|
|
1489
|
+
try {
|
|
1490
|
+
await shared.shellSafeGit(
|
|
1491
|
+
['show-ref', '--verify', '--quiet', `refs/remotes/origin/${branchName}`],
|
|
1492
|
+
{ ...gitOpts, cwd: rootDir, timeout: 10000 },
|
|
1493
|
+
);
|
|
1494
|
+
remoteExists = true;
|
|
1495
|
+
trackingRefExists = true;
|
|
1496
|
+
} catch {
|
|
1497
|
+
// A first-attempt branch may not have a local remote-tracking ref. Only
|
|
1498
|
+
// pay for ls-remote in that uncommon case; aligned dispatch closes stay
|
|
1499
|
+
// local-only and do not add a network round trip.
|
|
1500
|
+
try {
|
|
1501
|
+
const remote = String(await runNetworkGit(
|
|
1502
|
+
['ls-remote', '--exit-code', '--heads', 'origin', branchName],
|
|
1503
|
+
{ cwd: rootDir, timeout: 15000 },
|
|
1504
|
+
) || '').trim();
|
|
1505
|
+
remoteExists = !!remote;
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
if (err?.code !== 2 && err?.status !== 2) {
|
|
1508
|
+
return { ...result, reason: 'remote-probe-failed', error: err.message };
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (remoteExists) {
|
|
1514
|
+
if (trackingRefExists) {
|
|
1515
|
+
const localCounts = String(await shared.shellSafeGit(
|
|
1516
|
+
['rev-list', '--left-right', '--count', `refs/remotes/origin/${branchName}...HEAD`],
|
|
1517
|
+
{ ...gitOpts, cwd: worktreePath, timeout: 15000 },
|
|
1518
|
+
) || '').trim().split(/\s+/).map(Number);
|
|
1519
|
+
const locallyAhead = Number.isFinite(localCounts[1]) ? localCounts[1] : 0;
|
|
1520
|
+
if (locallyAhead === 0) {
|
|
1521
|
+
return {
|
|
1522
|
+
...result,
|
|
1523
|
+
behind: Number.isFinite(localCounts[0]) ? localCounts[0] : 0,
|
|
1524
|
+
reason: 'aligned',
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
await runNetworkGit(
|
|
1530
|
+
['fetch', 'origin', branchName],
|
|
1531
|
+
{ cwd: rootDir, timeout: 30000 },
|
|
1532
|
+
);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
return { ...result, reason: 'fetch-failed', error: err.message };
|
|
1535
|
+
}
|
|
1536
|
+
const counts = String(await shared.shellSafeGit(
|
|
1537
|
+
['rev-list', '--left-right', '--count', `refs/remotes/origin/${branchName}...HEAD`],
|
|
1538
|
+
{ ...gitOpts, cwd: worktreePath, timeout: 15000 },
|
|
1539
|
+
) || '').trim().split(/\s+/).map(Number);
|
|
1540
|
+
result.behind = Number.isFinite(counts[0]) ? counts[0] : 0;
|
|
1541
|
+
result.ahead = Number.isFinite(counts[1]) ? counts[1] : 0;
|
|
1542
|
+
if (result.behind > 0) return { ...result, reason: 'diverged' };
|
|
1543
|
+
if (result.ahead === 0) return { ...result, reason: 'aligned' };
|
|
1544
|
+
} else {
|
|
1545
|
+
if (!mainBranch) return { ...result, reason: 'no-upstream-no-main' };
|
|
1546
|
+
try {
|
|
1547
|
+
result.ahead = Number(String(await shared.shellSafeGit(
|
|
1548
|
+
['rev-list', '--count', `refs/remotes/origin/${mainBranch}..HEAD`],
|
|
1549
|
+
{ ...gitOpts, cwd: worktreePath, timeout: 15000 },
|
|
1550
|
+
) || '').trim());
|
|
1551
|
+
} catch (err) {
|
|
1552
|
+
return { ...result, reason: 'main-compare-failed', error: err.message };
|
|
1553
|
+
}
|
|
1554
|
+
if (!Number.isFinite(result.ahead) || result.ahead <= 0) {
|
|
1555
|
+
return { ...result, ahead: 0, reason: 'no-local-commits' };
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
try {
|
|
1560
|
+
await runNetworkGit(
|
|
1561
|
+
['push', 'origin', `HEAD:refs/heads/${branchName}`],
|
|
1562
|
+
{ cwd: worktreePath, timeout: 30000 },
|
|
1563
|
+
);
|
|
1564
|
+
log('info', `Preserved ${result.ahead} clean local commit(s) by pushing ${branchName} to origin`);
|
|
1565
|
+
return {
|
|
1566
|
+
...result,
|
|
1567
|
+
pushed: true,
|
|
1568
|
+
reason: remoteExists ? 'pushed-ahead' : 'pushed-new-branch',
|
|
1569
|
+
};
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
log('warn', `Failed to preserve clean ahead branch ${branchName}: ${err.message}`);
|
|
1572
|
+
return { ...result, reason: 'push-failed', error: err.message };
|
|
1573
|
+
}
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
return { ...result, reason: 'probe-failed', error: err.message };
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1461
1579
|
async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, dispatchId, gitOpts = {}, opts = {}) {
|
|
1462
1580
|
const quarantineOnUnsafe = !!opts.quarantineOnUnsafe;
|
|
1463
1581
|
// W-mq1habhf: status probe timeout is now configurable so large/GVFS repos
|
|
@@ -1638,6 +1756,41 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
1638
1756
|
}
|
|
1639
1757
|
}
|
|
1640
1758
|
|
|
1759
|
+
// #853 — a clean branch that is strictly ahead contains completed agent
|
|
1760
|
+
// commits, not contamination. Preserve them on the PR/work branch before
|
|
1761
|
+
// quarantine can move the work to a local-only backup ref. A rejected or
|
|
1762
|
+
// non-fast-forward push falls through to the existing quarantine path.
|
|
1763
|
+
if (!filesystemDirty && quarantineOnUnsafe && (result.ahead > 0 || !upstreamKnown)) {
|
|
1764
|
+
const preservation = await pushCleanAheadBranch({
|
|
1765
|
+
rootDir,
|
|
1766
|
+
worktreePath,
|
|
1767
|
+
branchName,
|
|
1768
|
+
mainBranch: opts.mainBranch,
|
|
1769
|
+
project: opts.project,
|
|
1770
|
+
gitOpts,
|
|
1771
|
+
});
|
|
1772
|
+
const preservationProvedClean = preservation.pushed ||
|
|
1773
|
+
['aligned', 'no-local-commits'].includes(preservation.reason);
|
|
1774
|
+
if (preservationProvedClean) {
|
|
1775
|
+
result.clean = true;
|
|
1776
|
+
result.pushed = preservation.pushed;
|
|
1777
|
+
result.ahead = preservation.ahead;
|
|
1778
|
+
result.behind = preservation.behind;
|
|
1779
|
+
result.reason = preservation.reason;
|
|
1780
|
+
return result;
|
|
1781
|
+
}
|
|
1782
|
+
result.pushFailure = preservation.reason;
|
|
1783
|
+
result.pushError = preservation.error;
|
|
1784
|
+
if (preservation.reason !== 'diverged') {
|
|
1785
|
+
// A network/auth failure does not prove the branch is unsafe. Leave the
|
|
1786
|
+
// clean ahead worktree intact so a later retry can push the same commits.
|
|
1787
|
+
result.reason = preservation.reason;
|
|
1788
|
+
return result;
|
|
1789
|
+
}
|
|
1790
|
+
result.ahead = preservation.ahead;
|
|
1791
|
+
result.behind = preservation.behind;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1641
1794
|
// 5. Unpushed-commit check — refuse to reset when local work would be lost.
|
|
1642
1795
|
// Use the ahead count we already computed (cheaper + correct vs.
|
|
1643
1796
|
// `git log @{u}..HEAD` which needs an upstream config and silently
|
|
@@ -3787,12 +3940,14 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3787
3940
|
{
|
|
3788
3941
|
quarantineOnUnsafe: true,
|
|
3789
3942
|
mainBranch: shared.resolveMainBranch(rootDir, project.mainBranch),
|
|
3943
|
+
project,
|
|
3790
3944
|
statusTimeoutMs: engineConfig.assertCleanStatusTimeoutMs ?? ENGINE_DEFAULTS.assertCleanStatusTimeoutMs,
|
|
3791
3945
|
},
|
|
3792
3946
|
);
|
|
3793
3947
|
_phaseT.dirtyReusedCheckEnd = Date.now();
|
|
3794
3948
|
if (!cleanResult.clean) {
|
|
3795
3949
|
const previewFiles = (cleanResult.dirtyFiles || []).slice(0, 5).join(', ');
|
|
3950
|
+
const isPushBlocked = ['remote-probe-failed', 'fetch-failed', 'push-failed'].includes(cleanResult.reason);
|
|
3796
3951
|
const isDivergent = (cleanResult.ahead || 0) > 0 || cleanResult.reason === 'no-upstream';
|
|
3797
3952
|
// W-mq1habhf: status-failed is a different beast than
|
|
3798
3953
|
// has-unpushed-commits / no-upstream. We couldn't even read the
|
|
@@ -3815,12 +3970,21 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3815
3970
|
// the existing _quarantineRecoveryCount cap. This stops env failures
|
|
3816
3971
|
// from burning the per-agent retry budget the way they did before.
|
|
3817
3972
|
const isQuarantineEnvBlocked = !!(cleanResult.quarantineError && !cleanResult.quarantined);
|
|
3818
|
-
const
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3973
|
+
const isPushAuthBlocked = isPushBlocked && adoGitAuth.isAdoAuthFailure(
|
|
3974
|
+
new Error(cleanResult.pushError || ''),
|
|
3975
|
+
);
|
|
3976
|
+
let failureClassValue = FAILURE_CLASS.WORKTREE_DIRTY;
|
|
3977
|
+
let failureClassName = 'WORKTREE_DIRTY';
|
|
3978
|
+
if (isPushBlocked) {
|
|
3979
|
+
failureClassValue = isPushAuthBlocked ? FAILURE_CLASS.AUTH : FAILURE_CLASS.NETWORK_ERROR;
|
|
3980
|
+
failureClassName = isPushAuthBlocked ? 'AUTH' : 'NETWORK_ERROR';
|
|
3981
|
+
} else if (isQuarantineEnvBlocked) {
|
|
3982
|
+
failureClassValue = FAILURE_CLASS.WORKTREE_QUARANTINE_ENV_BLOCKED;
|
|
3983
|
+
failureClassName = 'WORKTREE_QUARANTINE_ENV_BLOCKED';
|
|
3984
|
+
} else if (isDivergent) {
|
|
3985
|
+
failureClassValue = FAILURE_CLASS.WORKTREE_DIVERGENT;
|
|
3986
|
+
failureClassName = 'WORKTREE_DIVERGENT';
|
|
3987
|
+
}
|
|
3824
3988
|
// #284: distinguish the ORIGINAL worktree-preflight failure from a
|
|
3825
3989
|
// failure on the automatic fresh-worktree retry. The quarantine
|
|
3826
3990
|
// auto-recovery loop (discoverFromWorkItems) flips a failed
|
|
@@ -3833,7 +3997,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3833
3997
|
const retryLabel = priorQuarantineRetry > 0
|
|
3834
3998
|
? ` [retry-in-fresh-worktree #${priorQuarantineRetry} also failed]`
|
|
3835
3999
|
: ' [original worktree-preflight issue]';
|
|
3836
|
-
const reasonMsg =
|
|
4000
|
+
const reasonMsg = isPushBlocked
|
|
4001
|
+
? `${failureClassName}: clean ahead worktree at ${worktreePath} could not be pushed (${cleanResult.reason}: ${cleanResult.pushError || 'unknown error'}). The worktree and local commits were preserved for retry.`
|
|
4002
|
+
: cleanResult.quarantined
|
|
3837
4003
|
? `${failureClassName}: reused worktree at ${worktreePath} was dirty/divergent (${cleanResult.reason}; ${cleanResult.ahead || 0} ahead, ${cleanResult.behind || 0} behind, ${cleanResult.dirtyFiles?.length || 0} dirty file(s)${previewFiles ? ': ' + previewFiles : ''}) — quarantined to ${cleanResult.quarantinedPath}. Next dispatch will start fresh.`
|
|
3838
4004
|
: `${failureClassName}: reused worktree at ${worktreePath} is dirty/divergent (${cleanResult.reason}; ${cleanResult.ahead || 0} ahead, ${cleanResult.behind || 0} behind, ${cleanResult.dirtyFiles?.length || 0} file(s)${previewFiles ? ': ' + previewFiles : ''}). Quarantine ${cleanResult.quarantineError ? 'errored: ' + cleanResult.quarantineError : (cleanResult.quarantineSkipped ? 'was skipped — another live dispatch claims the worktree (see notes/inbox/ engine-worktree-skip-live note).' : 'was not attempted (' + cleanResult.reason + ').')}`;
|
|
3839
4005
|
log('error', reasonMsg);
|
|
@@ -3843,7 +4009,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3843
4009
|
DISPATCH_RESULT.ERROR,
|
|
3844
4010
|
reasonMsg.slice(0, 500),
|
|
3845
4011
|
`Engine preflight refused to dispatch into a dirty/divergent reused worktree (#2996).${retryLabel} Reason: ${cleanResult.reason}.${cleanResult.quarantined ? ` Worktree quarantined to ${cleanResult.quarantinedPath}; backup ref ${cleanResult.backupRef || '(skipped)'}. See notes/inbox/ for recovery instructions.` : (cleanResult.quarantineSkipped ? ' Quarantine was skipped because another live dispatch claims this worktree path; this dispatch will not auto-retry until the live claimant clears.' : '')}${isQuarantineEnvBlocked ? ' Environmental quarantine failure (Windows EBUSY); WI auto-recovery loop will re-queue without bumping per-agent retry counter.' : ''}`,
|
|
3846
|
-
{
|
|
4012
|
+
{
|
|
4013
|
+
agentRetryable: (isStatusProbeFailed && cleanResult.quarantined) ||
|
|
4014
|
+
(isPushBlocked && !isPushAuthBlocked),
|
|
4015
|
+
failureClass: failureClassValue,
|
|
4016
|
+
},
|
|
3847
4017
|
);
|
|
3848
4018
|
cleanupTempAgent(agentId);
|
|
3849
4019
|
return null;
|
|
@@ -4943,6 +5113,25 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4943
5113
|
ackPendingSteeringFiles(agentId, procInfo, steeringAckStdout);
|
|
4944
5114
|
promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutputPath);
|
|
4945
5115
|
|
|
5116
|
+
// #853 — the child is gone, so its clean committed state is stable. Push
|
|
5117
|
+
// any strictly-ahead dispatch branch before steering resume, pool return,
|
|
5118
|
+
// quarantine, or dispatch-end GC can detach/remove the worktree.
|
|
5119
|
+
const branchPath = worktreePath || (liveMode ? project?.localPath : null);
|
|
5120
|
+
const branchRootDir = liveMode ? project?.localPath : rootDir;
|
|
5121
|
+
if (branchPath && branchRootDir && branchName && fs.existsSync(branchPath)) {
|
|
5122
|
+
const preservation = await pushCleanAheadBranch({
|
|
5123
|
+
rootDir: branchRootDir,
|
|
5124
|
+
worktreePath: branchPath,
|
|
5125
|
+
branchName,
|
|
5126
|
+
mainBranch: shared.resolveMainBranch(branchRootDir, project?.mainBranch),
|
|
5127
|
+
project,
|
|
5128
|
+
gitOpts: _gitOpts,
|
|
5129
|
+
});
|
|
5130
|
+
if (!preservation.pushed && !['aligned', 'dirty', 'no-local-commits'].includes(preservation.reason)) {
|
|
5131
|
+
log('warn', `Dispatch-close branch preservation skipped for ${id}: ${preservation.reason}`);
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
|
|
4946
5135
|
// Check if this was a steering kill — re-spawn with resume
|
|
4947
5136
|
if (procInfo?._steeringMessage) {
|
|
4948
5137
|
const steerMsg = procInfo._steeringMessage;
|
|
@@ -5272,7 +5461,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5272
5461
|
// delete clears the entry.
|
|
5273
5462
|
const expectedNonce = activeProcesses.get(id)?._completionNonce || null;
|
|
5274
5463
|
const completionNonceRequired = engineConfig.completionNonceRequired ?? ENGINE_DEFAULTS.completionNonceRequired;
|
|
5275
|
-
const { resultSummary, autoRecovered, completionContractFailure, structuredCompletion, agentReportedFailure, agentRetryable, nonceMismatch } = await runPostCompletionHooks(dispatchItem, agentId, code, stdout, config, { expectedNonce, completionNonceRequired });
|
|
5464
|
+
const { resultSummary, autoRecovered, completionContractFailure, structuredCompletion, agentReportedFailure, agentRetryable, nonceMismatch, benignNoop } = await runPostCompletionHooks(dispatchItem, agentId, code, stdout, config, { expectedNonce, completionNonceRequired });
|
|
5276
5465
|
const retryableDecision = typeof agentRetryable === 'boolean' ? agentRetryable : failureInfo.retryable;
|
|
5277
5466
|
|
|
5278
5467
|
// W-mp6k7ywi000fa33c — keep_processes acceptance gate. When the work
|
|
@@ -5678,7 +5867,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5678
5867
|
const managedSpawnHealthcheckFail = !!managedSpawnHealthcheckFailure;
|
|
5679
5868
|
const effectiveResult = (hardContractFail || nonceFail || keepProcessesAcceptanceFail || managedSpawnAcceptanceFail || managedSpawnHealthcheckFail)
|
|
5680
5869
|
? DISPATCH_RESULT.ERROR
|
|
5681
|
-
: (((code === 0 && !agentReportedFailure) || autoRecovered) ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
5870
|
+
: (((code === 0 && !agentReportedFailure) || autoRecovered || benignNoop) ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
5682
5871
|
const finalCompletionReportPath = structuredCompletion?._path || dispatchItem.meta?.completionReportPath || shared.dispatchCompletionReportPath(id);
|
|
5683
5872
|
const completionOpts = {
|
|
5684
5873
|
...(finalCompletionReportPath ? { completionReportPath: finalCompletionReportPath } : {}),
|
|
@@ -8174,6 +8363,35 @@ function _buildRunnerBriefVars(item, project) {
|
|
|
8174
8363
|
/**
|
|
8175
8364
|
* Scan a project work-item scope for manually queued tasks.
|
|
8176
8365
|
*/
|
|
8366
|
+
/**
|
|
8367
|
+
* W-mrdon0pe000l045a — collect directory/file path hints for CLAUDE.md
|
|
8368
|
+
* propagation. These are the paths most relevant to a dispatch; the
|
|
8369
|
+
* CLAUDE.md-context module walks UP from each toward the project root collecting
|
|
8370
|
+
* the nearest applicable CLAUDE.md files. Sources (best-effort, cheap):
|
|
8371
|
+
* - item.meta.workdir (explicit sub-project directory)
|
|
8372
|
+
* - item.references[*].path (changed/relevant files)
|
|
8373
|
+
* Returns a de-duplicated array of repo-relative path strings. Empty when no
|
|
8374
|
+
* hints exist — the module still falls back to the project-root CLAUDE.md.
|
|
8375
|
+
*/
|
|
8376
|
+
function _deriveClaudeMdPathHints(item) {
|
|
8377
|
+
const hints = [];
|
|
8378
|
+
const seen = new Set();
|
|
8379
|
+
const add = (p) => {
|
|
8380
|
+
if (typeof p !== 'string') return;
|
|
8381
|
+
const v = p.trim();
|
|
8382
|
+
if (!v || seen.has(v)) return;
|
|
8383
|
+
seen.add(v);
|
|
8384
|
+
hints.push(v);
|
|
8385
|
+
};
|
|
8386
|
+
if (item && item.meta && typeof item.meta.workdir === 'string') add(item.meta.workdir);
|
|
8387
|
+
if (item && Array.isArray(item.references)) {
|
|
8388
|
+
for (const r of item.references) {
|
|
8389
|
+
if (r && typeof r.path === 'string') add(r.path);
|
|
8390
|
+
}
|
|
8391
|
+
}
|
|
8392
|
+
return hints;
|
|
8393
|
+
}
|
|
8394
|
+
|
|
8177
8395
|
function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, project, root, branchName, options = {}) {
|
|
8178
8396
|
const worktreePath = options.worktreePath || path.resolve(root, config.engine?.worktreeRoot || '../worktrees', `${branchName}`);
|
|
8179
8397
|
const vars = {
|
|
@@ -8327,6 +8545,14 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
8327
8545
|
// failure via the qa-session-draft-failed / qa-session-execute-failed
|
|
8328
8546
|
// path. (See playbooks/qa-session-draft.md → "Failure path" section.)
|
|
8329
8547
|
..._buildRunnerBriefVars(item, project),
|
|
8548
|
+
// W-mrdon0pe000l045a — CLAUDE.md propagation context for non-Claude runtimes.
|
|
8549
|
+
// renderPlaybook resolves the runtime capability + config flag and, when the
|
|
8550
|
+
// runtime doesn't auto-load CLAUDE.md (Copilot/Codex), bounded-discovers the
|
|
8551
|
+
// nearest applicable CLAUDE.md files walking up from these path hints toward
|
|
8552
|
+
// the project root. runtime_cli lets renderPlaybook check the adapter's
|
|
8553
|
+
// claudeMdNativeDiscovery capability without re-resolving the agent config.
|
|
8554
|
+
runtime_cli: shared.resolveAgentCli(config.agents?.[agentId] || null, config.engine),
|
|
8555
|
+
claude_md_path_hints: _deriveClaudeMdPathHints(item),
|
|
8330
8556
|
// M006 — typed handoff envelope. Set on follow-up dispatch metas by
|
|
8331
8557
|
// lifecycle.js when queuing an item after a completing agent. The
|
|
8332
8558
|
// renderPlaybook path injects a "Handoff Context" section when truthy
|
|
@@ -8571,6 +8797,15 @@ function discoverFromWorkItems(config, project) {
|
|
|
8571
8797
|
}
|
|
8572
8798
|
|
|
8573
8799
|
if (item.status !== WI_STATUS.QUEUED && item.status !== WI_STATUS.PENDING) continue;
|
|
8800
|
+
if (item._preDispatchEval?.exhausted === true
|
|
8801
|
+
&& item._preDispatchEval.description === String(item.description || '')) {
|
|
8802
|
+
if (item._pendingReason !== 'pre_dispatch_eval_rejected') {
|
|
8803
|
+
item._pendingReason = 'pre_dispatch_eval_rejected';
|
|
8804
|
+
needsWrite = true;
|
|
8805
|
+
}
|
|
8806
|
+
skipped.gated++;
|
|
8807
|
+
continue;
|
|
8808
|
+
}
|
|
8574
8809
|
|
|
8575
8810
|
// Dependency gate: skip items whose depends_on are not yet met; propagate failure
|
|
8576
8811
|
if (item.depends_on && item.depends_on.length > 0) {
|
|
@@ -11396,7 +11631,7 @@ module.exports = {
|
|
|
11396
11631
|
gitOutputToString, gitErrorOutput, classifyDepMergeFailureOutput, listUnmergedFiles, // exported for testing
|
|
11397
11632
|
buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
|
|
11398
11633
|
runWorktreeAdd, // exported for testing (W-mqvaxv65000m76f2 — GVFS partial-checkout behavioral test)
|
|
11399
|
-
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
|
|
11634
|
+
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, pushCleanAheadBranch, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
|
|
11400
11635
|
_statusPorcelainCmd, _killGitDescendantsForWorktree, _bumpQuarantineOutcome, // exported for testing (W-mq5n1zx5)
|
|
11401
11636
|
_reapWorktreeHolders, _findTerminalWorktreeOwners, // exported for testing (W-mqila0t5 — CWD-pinned holder reap)
|
|
11402
11637
|
pruneStaleWorktreeForBranch, // 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.2382",
|
|
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"
|