@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/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 DO treat ahead-of-origin as unsafe because the underlying bug
1434
- // (#2996) was a 157-commit-ahead local branch.)
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
- // - Block when local commits exist that aren't on origin/<branch>
1440
- // (would lose unpushed work). When quarantineOnUnsafe=true,
1441
- // QUARANTINE instead: rename worktree to <path>-quarantine-<ts>,
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 failureClassValue = isQuarantineEnvBlocked
3819
- ? FAILURE_CLASS.WORKTREE_QUARANTINE_ENV_BLOCKED
3820
- : (isDivergent ? FAILURE_CLASS.WORKTREE_DIVERGENT : FAILURE_CLASS.WORKTREE_DIRTY);
3821
- const failureClassName = isQuarantineEnvBlocked
3822
- ? 'WORKTREE_QUARANTINE_ENV_BLOCKED'
3823
- : (isDivergent ? 'WORKTREE_DIVERGENT' : 'WORKTREE_DIRTY');
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 = cleanResult.quarantined
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
- { agentRetryable: isStatusProbeFailed && cleanResult.quarantined, failureClass: failureClassValue },
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.2381",
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"