@yemi33/minions 0.1.1984 → 0.1.1985

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/docs/.nojekyll ADDED
File without changes
@@ -0,0 +1,206 @@
1
+ /**
2
+ * engine/ado-git-auth.js — W-mpcuc8i80003a7b3
3
+ *
4
+ * Inject a per-invocation Authorization: Bearer <token> http.extraHeader into
5
+ * git ops that touch an ADO origin. Solves the headless-dispatch failure mode
6
+ * where Git Credential Manager falls back to a TTY prompt (because the engine
7
+ * has no terminal) and emits:
8
+ *
9
+ * fatal: could not read Username for 'https://office.visualstudio.com'
10
+ *
11
+ * The token is sourced via the existing `engine/ado-token.js#acquireAdoTokenSync`
12
+ * helper (az CLI first, azureauth fallback). We cache it for 30 minutes and
13
+ * back off acquisition retries for 10 minutes on failure so we don't hammer
14
+ * az / azureauth on every git op during an outage.
15
+ *
16
+ * Public API:
17
+ * - getAdoGitExtraArgs(project, opts?) → string[]
18
+ * Sync. Returns `['-c', 'http.<scope>.extraHeader=Authorization: Bearer <token>']`
19
+ * for ADO projects, `[]` for everything else (GitHub/local/null). Plumbed
20
+ * into shared.shellSafeGit via `opts.gitExtraArgs`.
21
+ * - invalidateAdoTokenCache()
22
+ * Drops the cached token so the next getAdoGitExtraArgs() re-acquires.
23
+ * Used by runAdoGit on auth failure to handle mid-dispatch token expiry.
24
+ * - isAdoAuthFailure(err) → boolean
25
+ * Pure. Matches credential/auth-specific phrases against err.message,
26
+ * err.stdout, err.stderr. Deliberately NARROW — does not match bare
27
+ * "fatal: unable to access" since that also fires for DNS/TLS/proxy.
28
+ * - runAdoGit(project, gitArgs, opts) → Promise<string>
29
+ * Async wrapper around shellSafeGit. On ADO auth failure, invalidates
30
+ * cache, re-acquires, retries ONCE in the same dispatch. Redacts the
31
+ * bearer token from rethrown error messages so the token never lands in
32
+ * logs / inbox alerts / completion reports.
33
+ *
34
+ * Argv-vs-env tradeoff: putting `-c http.extraHeader=Authorization: Bearer X`
35
+ * in argv is visible via process listing. The simpler alternative
36
+ * `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=... GIT_CONFIG_VALUE_0=...` hides the
37
+ * token from `ps`. We chose argv to match the documented manual-verification
38
+ * recipe (matches the task spec) and explicitly redact on error rethrow.
39
+ * Migrate to env-form if argv exposure becomes a real concern.
40
+ */
41
+
42
+ const shared = require('./shared');
43
+ const adoToken = require('./ado-token');
44
+ const { log } = shared;
45
+
46
+ const TOKEN_TTL_MS = 30 * 60 * 1000; // 30 min — matches gh-token cadence
47
+ const ACQUIRE_BACKOFF_MS = 10 * 60 * 1000; // 10 min — same backoff as gh-token
48
+ const ACQUIRE_TIMEOUT_MS = 30000; // 30s — generous for slow IWA/broker auth
49
+
50
+ let _cached = null; // { token, expiresAt }
51
+ let _backoffUntil = 0; // epoch ms — no acquire attempts before this
52
+
53
+ // Credential/auth patterns. Narrow on purpose: do NOT include bare
54
+ // "fatal: unable to access" — that also fires for DNS, TLS, and proxy errors
55
+ // which are correctly classified elsewhere (NETWORK_ERROR, retryable).
56
+ const ADO_AUTH_PATTERNS = [
57
+ /could not read Username/i,
58
+ /could not read Password/i,
59
+ /Authentication failed/i,
60
+ /terminal prompts disabled/i,
61
+ /TF40081\d/i, // Azure DevOps auth error family
62
+ /HTTP\s*4(?:01|03)\b/i, // explicit 401/403 in error text
63
+ ];
64
+
65
+ function isAdoProject(project) {
66
+ return !!(project && project.repoHost === 'ado');
67
+ }
68
+
69
+ // Scope the header to the ADO host so a misconfigured remote pointing at
70
+ // another host (e.g. github.com on a misclassified project) doesn't leak the
71
+ // bearer token. Falls back to the unscoped `http.extraHeader` key when we
72
+ // can't compute a host (still safe — git only sends extraHeader on HTTP/S
73
+ // transfers, and the engine only invokes ado-git-auth for ADO projects).
74
+ function _resolveScopeUrl(project) {
75
+ try {
76
+ if (!project || !project.adoOrg) return null;
77
+ const base = shared.getAdoOrgBase(project);
78
+ if (typeof base !== 'string' || !base.startsWith('http')) return null;
79
+ const m = base.match(/^(https?:\/\/[^/]+)/);
80
+ return m ? `${m[1]}/` : null;
81
+ } catch (_e) {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function _buildHeaderArgs(token, scopeUrl) {
87
+ const key = scopeUrl ? `http.${scopeUrl}.extraHeader` : 'http.extraHeader';
88
+ return ['-c', `${key}=Authorization: Bearer ${token}`];
89
+ }
90
+
91
+ function _acquireToken(opts = {}) {
92
+ const exec = opts.acquireAdoTokenSync || adoToken.acquireAdoTokenSync;
93
+ const result = exec({ timeout: opts.timeout || ACQUIRE_TIMEOUT_MS });
94
+ return result && result.token ? String(result.token) : null;
95
+ }
96
+
97
+ function getAdoGitExtraArgs(project, opts = {}) {
98
+ if (!isAdoProject(project)) return [];
99
+ const now = Date.now();
100
+ if (_cached && _cached.expiresAt > now) {
101
+ return _buildHeaderArgs(_cached.token, _resolveScopeUrl(project));
102
+ }
103
+ if (now < _backoffUntil) return [];
104
+ try {
105
+ const token = _acquireToken(opts);
106
+ if (!token) {
107
+ _backoffUntil = now + ACQUIRE_BACKOFF_MS;
108
+ log('warn', `ado-git-auth: acquireAdoTokenSync returned empty token; backing off ${ACQUIRE_BACKOFF_MS / 1000}s`);
109
+ return [];
110
+ }
111
+ _cached = { token, expiresAt: now + TOKEN_TTL_MS };
112
+ return _buildHeaderArgs(token, _resolveScopeUrl(project));
113
+ } catch (e) {
114
+ _backoffUntil = now + ACQUIRE_BACKOFF_MS;
115
+ const firstLine = String(e && e.message || e).split('\n')[0];
116
+ log('warn', `ado-git-auth: token acquire failed (${firstLine}); backing off ${ACQUIRE_BACKOFF_MS / 1000}s`);
117
+ return [];
118
+ }
119
+ }
120
+
121
+ function invalidateAdoTokenCache() {
122
+ _cached = null;
123
+ }
124
+
125
+ function isAdoAuthFailure(err) {
126
+ if (!err || typeof err !== 'object') return false;
127
+ const parts = [
128
+ err.message,
129
+ typeof err.stdout === 'string' ? err.stdout : '',
130
+ typeof err.stderr === 'string' ? err.stderr : '',
131
+ ].filter(Boolean).join('\n');
132
+ if (!parts) return false;
133
+ return ADO_AUTH_PATTERNS.some((p) => p.test(parts));
134
+ }
135
+
136
+ function _redactBearer(s) {
137
+ if (typeof s !== 'string') return s;
138
+ return s.replace(/Authorization:\s*Bearer\s+[A-Za-z0-9._\-]+/gi, 'Authorization: Bearer [REDACTED]');
139
+ }
140
+
141
+ function _redactErrorInPlace(err) {
142
+ if (!err || typeof err !== 'object') return err;
143
+ if (err.message) err.message = _redactBearer(err.message);
144
+ if (typeof err.stdout === 'string') err.stdout = _redactBearer(err.stdout);
145
+ if (typeof err.stderr === 'string') err.stderr = _redactBearer(err.stderr);
146
+ return err;
147
+ }
148
+
149
+ async function runAdoGit(project, gitArgs, opts = {}) {
150
+ const { acquireAdoTokenSync, _runGit, ...callerOpts } = opts;
151
+ const runGit = _runGit || shared.shellSafeGit;
152
+ const baseExtra = Array.isArray(callerOpts.gitExtraArgs) ? callerOpts.gitExtraArgs : [];
153
+ const adoOpts = { acquireAdoTokenSync };
154
+
155
+ const firstExtra = baseExtra.concat(getAdoGitExtraArgs(project, adoOpts));
156
+ const firstOpts = firstExtra.length ? { ...callerOpts, gitExtraArgs: firstExtra } : callerOpts;
157
+ try {
158
+ return await runGit(gitArgs, firstOpts);
159
+ } catch (err) {
160
+ if (!isAdoProject(project) || !isAdoAuthFailure(err)) {
161
+ throw _redactErrorInPlace(err);
162
+ }
163
+ // Token may have expired mid-dispatch — refresh and retry exactly once.
164
+ invalidateAdoTokenCache();
165
+ _backoffUntil = 0; // give the retry a chance to actually hit the token source
166
+ const refreshedExtra = getAdoGitExtraArgs(project, adoOpts);
167
+ if (!refreshedExtra.length) {
168
+ throw _redactErrorInPlace(err);
169
+ }
170
+ const retryExtra = baseExtra.concat(refreshedExtra);
171
+ try {
172
+ return await runGit(gitArgs, { ...callerOpts, gitExtraArgs: retryExtra });
173
+ } catch (err2) {
174
+ throw _redactErrorInPlace(err2);
175
+ }
176
+ }
177
+ }
178
+
179
+ // ── Test seams ──────────────────────────────────────────────────────────────
180
+
181
+ function _setTokenForTest(token) {
182
+ if (token == null) { _cached = null; _backoffUntil = 0; return; }
183
+ _cached = { token: String(token), expiresAt: Date.now() + TOKEN_TTL_MS };
184
+ _backoffUntil = 0;
185
+ }
186
+
187
+ function _clearTokenCache() {
188
+ _cached = null;
189
+ _backoffUntil = 0;
190
+ }
191
+
192
+ function _isBackedOff() { return Date.now() < _backoffUntil; }
193
+
194
+ module.exports = {
195
+ getAdoGitExtraArgs,
196
+ invalidateAdoTokenCache,
197
+ isAdoAuthFailure,
198
+ runAdoGit,
199
+ isAdoProject,
200
+ _setTokenForTest,
201
+ _clearTokenCache,
202
+ _isBackedOff,
203
+ TOKEN_TTL_MS,
204
+ ACQUIRE_BACKOFF_MS,
205
+ ADO_AUTH_PATTERNS,
206
+ };
package/engine/cli.js CHANGED
@@ -90,6 +90,32 @@ function summarizeActiveDispatchPids(activeItems = []) {
90
90
  return summary;
91
91
  }
92
92
 
93
+ // W-mpcyvff6000pf828 (#2653) — Heartbeat writer decoupled from tickInner.
94
+ // `writeHeartbeatNow` mirrors the legacy in-tick write (synchronous, fast,
95
+ // non-throwing) but is driven from the main start loop on a 15s setInterval
96
+ // instead. See engine.js#tickInner for the load-bearing rationale comment.
97
+ const HEARTBEAT_INTERVAL_MS = 15000;
98
+
99
+ function writeHeartbeatNow() {
100
+ try {
101
+ // Synchronous callback inside the lock — just `s.heartbeat = Date.now()`,
102
+ // no awaits, no other state mutation. Mirrors mutateControl's contract.
103
+ shared.mutateControl(c => { c.heartbeat = Date.now(); return c; });
104
+ } catch (err) {
105
+ try { engine().log('warn', `write heartbeat: ${err.message}`); }
106
+ catch { /* during shutdown logger can be torn down — silent */ }
107
+ }
108
+ }
109
+
110
+ // Factory used by tests to drive the heartbeat interval without spinning up
111
+ // the full engine start loop. Returns the underlying timer so callers (the
112
+ // real start handler in production, tests in unit/engine-heartbeat.test.js)
113
+ // can clearInterval(...) on shutdown. Default to the production cadence.
114
+ function createHeartbeatInterval(intervalMs = HEARTBEAT_INTERVAL_MS, writer = writeHeartbeatNow) {
115
+ return setInterval(writer, intervalMs);
116
+ }
117
+
118
+
93
119
  function createControlOwner(pid = process.pid) {
94
120
  return { pid, ownerToken: `${pid}-${shared.uid()}` };
95
121
  }
@@ -872,6 +898,18 @@ const commands = {
872
898
  // Start tick loop
873
899
  const tickTimer = setInterval(() => e.tick(), interval);
874
900
 
901
+ // W-mpcyvff6000pf828 (#2653) — Heartbeat decoupled from tickInner.
902
+ // The dashboard flips the engine badge to STALE when
903
+ // `Date.now() - control.heartbeat > 120000`; tying the write to tickInner
904
+ // meant any legitimately slow tick (cold runtime spawn, sequential
905
+ // ADO/gh polls, slow worktree create) blocked the next heartbeat and
906
+ // surfaced a healthy engine as crashed. We now write every 15s on a
907
+ // dedicated interval — 8× headroom vs the 120s threshold even under
908
+ // event-loop pressure, and orders of magnitude under TICK_TIMEOUT_MS so
909
+ // a hung tick still looks distinct from a wedged event loop.
910
+ writeHeartbeatNow(); // prime control.heartbeat immediately
911
+ const heartbeatTimer = setInterval(writeHeartbeatNow, HEARTBEAT_INTERVAL_MS);
912
+
875
913
  // Fast poll: check steering every 1s (lightweight — just fs.stat per agent)
876
914
  // and wakeup signals every 1s (control.json read)
877
915
  const { checkSteering } = require('./timeout');
@@ -948,6 +986,7 @@ const commands = {
948
986
  console.log(`\n${signal} received — initiating graceful shutdown...`);
949
987
  clearInterval(tickTimer);
950
988
  clearInterval(fastPollTimer);
989
+ clearInterval(heartbeatTimer);
951
990
  for (const f of _watchedFiles) { try { fs.unwatchFile(f); } catch { /* cleanup */ } }
952
991
  const stoppingAt = e.ts();
953
992
  const stoppingWrite = markControlStoppingForOwner(controlOwner, stoppingAt);
@@ -1799,4 +1838,8 @@ module.exports = {
1799
1838
  _readDispatchPid: readDispatchPid,
1800
1839
  _normalizeSessionBranch: normalizeSessionBranch,
1801
1840
  _dispatchSessionBranch: dispatchSessionBranch,
1841
+ // W-mpcyvff6000pf828 (#2653) — heartbeat writer + factory exported for tests
1842
+ _writeHeartbeatNow: writeHeartbeatNow,
1843
+ _createHeartbeatInterval: createHeartbeatInterval,
1844
+ _HEARTBEAT_INTERVAL_MS: HEARTBEAT_INTERVAL_MS,
1802
1845
  };
@@ -343,6 +343,7 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
343
343
  const neverRetry = new Set([
344
344
  FAILURE_CLASS.CONFIG_ERROR,
345
345
  FAILURE_CLASS.PERMISSION_BLOCKED,
346
+ FAILURE_CLASS.AUTH, // W-mpcuc8i80003a7b3 — git/network credential failure; mechanical retry won't fix missing az / GCM creds
346
347
  FAILURE_CLASS.WORKTREE_PREFLIGHT, // pre-spawn worktree validation — recompute will produce the same failure
347
348
  FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR, // W-mp6k7ywi000fa33c — keep-pids cwd is not a real git worktree; re-running won't fix the structural issue
348
349
  FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA, // W-mp7i902u000l991f — keep-pids.json failed shape validation; re-running with the same wrong file won't fix it
@@ -653,6 +654,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
653
654
  [FAILURE_CLASS.OUT_OF_CONTEXT]: 'context window exhausted',
654
655
  [FAILURE_CLASS.CONFIG_ERROR]: 'configuration error',
655
656
  [FAILURE_CLASS.PERMISSION_BLOCKED]: 'permission or auth failure',
657
+ [FAILURE_CLASS.AUTH]: 'ADO/git authentication failed (missing or expired credentials)',
656
658
  [FAILURE_CLASS.WORKTREE_PREFLIGHT]: 'worktree preflight rejected (nested in project root or rootDir collapsed to drive root)',
657
659
  [FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR]: 'keep_processes cwd is not a real git worktree (rerun in a `git worktree add` directory)',
658
660
  [FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA]: 'keep-pids.json failed shape validation (wrong keys/types/values — see inbox alert for the canonical shape)',
@@ -12,6 +12,7 @@ const { safeRead, safeJson, safeJsonNoRestore, safeWrite, mutateJsonFileLocked,
12
12
  ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
13
13
  const { trackEngineUsage } = require('./llm');
14
14
  const { resolveRuntime } = require('./runtimes');
15
+ const adoGitAuth = require('./ado-git-auth');
15
16
  const queries = require('./queries');
16
17
  const { isBranchActive } = require('./cooldown');
17
18
  const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
@@ -2104,7 +2105,11 @@ async function rebaseBranchOntoMain(pr, project, config) {
2104
2105
  }
2105
2106
  const wtRoot = path.resolve(root, config.engine?.worktreeRoot || ENGINE_DEFAULTS.worktreeRoot);
2106
2107
  const tmpWt = path.join(wtRoot, `rebase-${shared.sanitizeBranch(branch)}-${Date.now()}`).replace(/\\/g, '/');
2107
- const _gitOpts = { cwd: root, timeout: 30000, windowsHide: true };
2108
+ // W-mpcuc8i80003a7b3 thread ADO bearer-token args through every remote-touching
2109
+ // git op in the rebase flow so headless post-merge rebase against ADO origin
2110
+ // survives missing GCM creds. For GitHub/local projects this is a no-op.
2111
+ const _gitExtraArgs = adoGitAuth.getAdoGitExtraArgs(project);
2112
+ const _gitOpts = { cwd: root, timeout: 30000, windowsHide: true, gitExtraArgs: _gitExtraArgs };
2108
2113
 
2109
2114
  // Refuse to create a worktree nested in the project root — would cause
2110
2115
  // glob/grep tools running with cwd=root to match both copies of every file
@@ -2135,8 +2140,8 @@ async function rebaseBranchOntoMain(pr, project, config) {
2135
2140
  }
2136
2141
 
2137
2142
  try {
2138
- await shared.shellSafeGit(['rebase', `origin/${validatedMain}`], { cwd: tmpWt, timeout: 120000, windowsHide: true });
2139
- await shared.shellSafeGit(['push', '--force-with-lease', 'origin', validatedBranch], { cwd: tmpWt, timeout: 30000, windowsHide: true });
2143
+ await shared.shellSafeGit(['rebase', `origin/${validatedMain}`], { cwd: tmpWt, timeout: 120000, windowsHide: true, gitExtraArgs: _gitExtraArgs });
2144
+ await shared.shellSafeGit(['push', '--force-with-lease', 'origin', validatedBranch], { cwd: tmpWt, timeout: 30000, windowsHide: true, gitExtraArgs: _gitExtraArgs });
2140
2145
  log('info', `Post-merge rebase: rebased ${branch} onto ${mainBranch} and force-pushed`);
2141
2146
  return { success: true };
2142
2147
  } catch (err) {
@@ -28,6 +28,12 @@ const RECOVERY_RECIPES = new Map([
28
28
  freshSession: false,
29
29
  description: 'Permission/trust gate blocked — requires human intervention',
30
30
  }],
31
+ [FAILURE_CLASS.AUTH, {
32
+ maxAttempts: 0,
33
+ escalation: ESCALATION_POLICY.NO_RETRY,
34
+ freshSession: false,
35
+ description: 'Git/network authentication failed (missing az login, expired token, GCM prompt) — requires human credential fix before retry',
36
+ }],
31
37
  [FAILURE_CLASS.MERGE_CONFLICT, {
32
38
  maxAttempts: 2,
33
39
  escalation: ESCALATION_POLICY.RETRY_SAME,
package/engine/shared.js CHANGED
@@ -1232,8 +1232,15 @@ function shellSafeGit(args, opts = {}) {
1232
1232
  if (!Array.isArray(args)) {
1233
1233
  return Promise.reject(new TypeError('shellSafeGit: args must be an array'));
1234
1234
  }
1235
- const { timeout, ...rest } = opts;
1236
- return _execFileAsync('git', args, {
1235
+ // W-mpcuc8i80003a7b3 `gitExtraArgs` is prepended to the git argv so callers
1236
+ // can inject per-invocation `-c key=value` flags (e.g. ADO bearer-token
1237
+ // auth header) without rewriting every shellSafeGit call site. Strip the
1238
+ // key before delegating so it never reaches Node's execFile options.
1239
+ const { timeout, gitExtraArgs, ...rest } = opts;
1240
+ const finalArgs = (Array.isArray(gitExtraArgs) && gitExtraArgs.length > 0)
1241
+ ? [...gitExtraArgs, ...args]
1242
+ : args;
1243
+ return _execFileAsync('git', finalArgs, {
1237
1244
  windowsHide: true,
1238
1245
  encoding: 'utf8',
1239
1246
  shell: false,
@@ -1249,9 +1256,13 @@ function shellSafeGitSync(args, opts = {}) {
1249
1256
  if (!Array.isArray(args)) {
1250
1257
  throw new TypeError('shellSafeGitSync: args must be an array');
1251
1258
  }
1252
- const { timeout, ...rest } = opts;
1259
+ // W-mpcuc8i80003a7b3 mirror the gitExtraArgs plumbing from the async variant.
1260
+ const { timeout, gitExtraArgs, ...rest } = opts;
1261
+ const finalArgs = (Array.isArray(gitExtraArgs) && gitExtraArgs.length > 0)
1262
+ ? [...gitExtraArgs, ...args]
1263
+ : args;
1253
1264
  const { execFileSync: _execFileSync } = require('child_process');
1254
- return _execFileSync('git', args, {
1265
+ return _execFileSync('git', finalArgs, {
1255
1266
  windowsHide: true,
1256
1267
  encoding: 'utf8',
1257
1268
  shell: false,
@@ -2307,6 +2318,7 @@ const AGENT_STATUS = {
2307
2318
  const FAILURE_CLASS = {
2308
2319
  CONFIG_ERROR: 'config-error', // Exit code 78, CLI not found, bad config
2309
2320
  PERMISSION_BLOCKED: 'permission-blocked', // Trust gate, permission denied, auth failure
2321
+ AUTH: 'auth', // W-mpcuc8i80003a7b3: git/network credential failure (e.g. ADO bearer-token acquire or GCM prompt) — structural, never retryable mechanically
2310
2322
  MERGE_CONFLICT: 'merge-conflict', // Git merge conflict in worktree or dependency
2311
2323
  BUILD_FAILURE: 'build-failure', // Compilation, lint, or test failure
2312
2324
  TIMEOUT: 'timeout', // Hard runtime timeout or stale-orphan timeout
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) {
@@ -816,7 +875,12 @@ async function spawnAgent(dispatchItem, config) {
816
875
  let branchName = _preBranchName;
817
876
  const worktreeCreateTimeout = Math.max(60000, Number(engineConfig.worktreeCreateTimeout) || ENGINE_DEFAULTS.worktreeCreateTimeout);
818
877
  const worktreeCreateRetries = Math.max(0, Math.min(3, Number(engineConfig.worktreeCreateRetries) || ENGINE_DEFAULTS.worktreeCreateRetries));
819
- const _gitOpts = { stdio: 'pipe', timeout: 30000, windowsHide: true, env: shared.gitEnv() };
878
+ // W-mpcuc8i80003a7b3 for ADO projects, inject a per-invocation
879
+ // `-c http.<host>.extraHeader=Authorization: Bearer <token>` so headless
880
+ // git ops survive missing/expired Git Credential Manager state. Returns
881
+ // [] for GitHub/local projects so this is a no-op there.
882
+ const _adoGitExtraArgs = adoGitAuth.getAdoGitExtraArgs(project);
883
+ const _gitOpts = { stdio: 'pipe', timeout: 30000, windowsHide: true, env: shared.gitEnv(), gitExtraArgs: _adoGitExtraArgs };
820
884
  const _worktreeGitOpts = { ..._gitOpts, timeout: worktreeCreateTimeout };
821
885
 
822
886
  // Build the initial prompt before worktree setup, then refresh shared-branch
@@ -1173,6 +1237,12 @@ async function spawnAgent(dispatchItem, config) {
1173
1237
  let depMergeFailed = false;
1174
1238
  let depConflictBranch = null; // track which dep branch caused the conflict
1175
1239
  let depConflictFiles = []; // conflicting file names parsed from git output
1240
+ // W-mpcuc8i80003a7b3 — track whether ANY git op in the dep phase
1241
+ // failed with an ADO auth signature. If so, we escalate as
1242
+ // FAILURE_CLASS.AUTH (non-retryable + dedup'd inbox alert) instead
1243
+ // of FAILURE_CLASS.MERGE_CONFLICT (which retries 3x against the
1244
+ // same broken auth path).
1245
+ let _depAuthFailed = false;
1176
1246
  // Fetch all dependency branches in parallel (git fetches are independent)
1177
1247
  const fetchable = depBranches.filter(d => !_failedRefCache.has(d.branch));
1178
1248
  const unfetchable = depBranches.filter(d => _failedRefCache.has(d.branch));
@@ -1188,7 +1258,10 @@ async function spawnAgent(dispatchItem, config) {
1188
1258
  }
1189
1259
  const fetchResults = await Promise.allSettled(
1190
1260
  fetchable.map(({ branch: depBranch }) =>
1191
- shared.shellSafeGit(['fetch', 'origin', depBranch], { ..._gitOpts, cwd: rootDir }).then(() => depBranch)
1261
+ // runAdoGit for ADO projects auto-retries once on auth failure
1262
+ // with a refreshed bearer token (handles mid-dispatch expiry).
1263
+ // For non-ADO projects it's a thin pass-through to shellSafeGit.
1264
+ adoGitAuth.runAdoGit(project, ['fetch', 'origin', depBranch], { ..._gitOpts, cwd: rootDir }).then(() => depBranch)
1192
1265
  )
1193
1266
  );
1194
1267
  const hasFetchFailures = fetchResults.some(r => r.status === 'rejected');
@@ -1221,6 +1294,12 @@ async function spawnAgent(dispatchItem, config) {
1221
1294
  }
1222
1295
  _failedRefCache.add(failedBranch);
1223
1296
  log('warn', `Failed to fetch dependency ${failedBranch}: ${errMsg}`);
1297
+ // W-mpcuc8i80003a7b3 — detect ADO bearer-token / GCM credential
1298
+ // failures so we escalate as FAILURE_CLASS.AUTH below instead of
1299
+ // mis-classifying as a merge conflict and burning 3 retries.
1300
+ if (adoGitAuth.isAdoAuthFailure(fetchResults[i].reason)) {
1301
+ _depAuthFailed = true;
1302
+ }
1224
1303
  depMergeFailed = true;
1225
1304
  }
1226
1305
  }
@@ -1302,6 +1381,11 @@ async function spawnAgent(dispatchItem, config) {
1302
1381
  // Merge failed — possibly due to diverged history from a force-pushed (rebased) dep branch.
1303
1382
  // Abort partial merge, reset worktree to clean main base, and re-merge all deps from scratch.
1304
1383
  log('warn', `Merge of ${depBranch} into ${branchName} failed: ${mergeErr.message} — attempting reset and re-merge of all deps`);
1384
+ // W-mpcuc8i80003a7b3 — defense in depth. `git merge origin/<dep>`
1385
+ // is local so an auth failure here is unexpected, but mark the
1386
+ // flag so the final dispatch failure routes through the AUTH
1387
+ // path instead of MERGE_CONFLICT.
1388
+ if (adoGitAuth.isAdoAuthFailure(mergeErr)) _depAuthFailed = true;
1305
1389
  try { await shared.shellSafeGit(['merge', '--abort'], { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
1306
1390
  const mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
1307
1391
  try {
@@ -1316,6 +1400,7 @@ async function spawnAgent(dispatchItem, config) {
1316
1400
  } catch (resetErr) {
1317
1401
  const errOutput = (resetErr.message || '') + '\n' + (resetErr.stdout?.toString?.() || '') + '\n' + (resetErr.stderr?.toString?.() || '');
1318
1402
  log('warn', `Failed to reset and re-merge deps for ${branchName}: ${resetErr.message}`);
1403
+ if (adoGitAuth.isAdoAuthFailure(resetErr)) _depAuthFailed = true;
1319
1404
  try { await shared.shellSafeGit(['merge', '--abort'], { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
1320
1405
  // Post-mortem: incremental simulation to identify which dep caused the conflict (#958)
1321
1406
  // Uses same chained merge-tree approach as pre-flight to catch inter-dep conflicts
@@ -1366,6 +1451,58 @@ async function spawnAgent(dispatchItem, config) {
1366
1451
  _phaseT.depMergeEnd = Date.now();
1367
1452
  if (depMergeFailed) {
1368
1453
  _cleanupPromptFiles();
1454
+ // W-mpcuc8i80003a7b3 — short-circuit to AUTH classification when any
1455
+ // git op in the dep phase looked like an ADO credential failure.
1456
+ // This BYPASSES the merge-conflict path entirely: no retries (auth
1457
+ // is structural — re-running won't fix missing az / GCM creds),
1458
+ // no conflict-fix WI (would just re-trigger the same auth wall),
1459
+ // and a single dedup'd inbox alert pointing at the recovery steps.
1460
+ if (_depAuthFailed) {
1461
+ const projName = project.name || 'unknown';
1462
+ 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)`;
1463
+ try {
1464
+ writeInboxAlert(`ado-auth-${projName}`, [
1465
+ `# ADO git authentication failed — ${projName}`,
1466
+ '',
1467
+ `Work item: \`${meta?.item?.id || '(unknown)'}\``,
1468
+ `Branch: \`${branchName || '(unknown)'}\``,
1469
+ '',
1470
+ '## Symptom',
1471
+ '',
1472
+ 'Engine could not fetch dependency branches from the ADO origin. ',
1473
+ 'Git Credential Manager has no cached credentials and falls back to ',
1474
+ 'a TTY prompt, which the headless engine cannot satisfy:',
1475
+ '',
1476
+ '```',
1477
+ "fatal: could not read Username for 'https://<adoOrg>.visualstudio.com'",
1478
+ '```',
1479
+ '',
1480
+ '## Recovery',
1481
+ '',
1482
+ 'From an interactive shell on the engine host, refresh ADO credentials:',
1483
+ '',
1484
+ '```bash',
1485
+ '# Option A — Azure CLI (preferred)',
1486
+ 'az login',
1487
+ 'az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv',
1488
+ '',
1489
+ '# Option B — azureauth (corp environments)',
1490
+ 'azureauth ado token --mode iwa --mode broker --output token --timeout 1',
1491
+ '```',
1492
+ '',
1493
+ '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).',
1494
+ '',
1495
+ '## Why no auto-retry',
1496
+ '',
1497
+ '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`.',
1498
+ ].join('\n'));
1499
+ } catch (alertErr) {
1500
+ log('warn', `Failed to write ADO auth inbox alert: ${alertErr.message}`);
1501
+ }
1502
+ completeDispatch(id, DISPATCH_RESULT.ERROR, authFailReason, '', { failureClass: FAILURE_CLASS.AUTH, agentRetryable: false });
1503
+ cleanupTempAgent(agentId);
1504
+ return;
1505
+ }
1369
1506
  // Build actionable failReason identifying the conflicting branch and files (#958)
1370
1507
  const mainBranch = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
1371
1508
  let failReason = 'Dependency merge failed';
@@ -1380,39 +1517,29 @@ async function spawnAgent(dispatchItem, config) {
1380
1517
  }
1381
1518
  completeDispatch(id, DISPATCH_RESULT.ERROR, failReason, '', { failureClass: FAILURE_CLASS.MERGE_CONFLICT });
1382
1519
 
1383
- // Auto-queue conflict-fix work item when a specific dep branch is identified
1520
+ // Auto-queue conflict-fix work item when a specific dep branch is identified.
1521
+ // Routes through the shared-branch dispatch path (see buildDepConflictFixItem)
1522
+ // so commits land on the dep's existing PR branch (W-mpcwojgr000a0244).
1384
1523
  if (depConflictBranch && meta?.item?.id && project) {
1385
1524
  try {
1386
1525
  const wiPath = project.name
1387
1526
  ? projectWorkItemsPath(project)
1388
1527
  : path.join(MINIONS_DIR, 'work-items.json');
1389
- const conflictFixId = `conflict-fix-${depConflictBranch.replace(/[^a-zA-Z0-9-]/g, '-')}`;
1390
- const filesDesc = depConflictFiles.length > 0
1391
- ? `\n\nConflicting files:\n${depConflictFiles.map(f => '- ' + f).join('\n')}`
1392
- : '';
1393
- // Inter-dep conflict: rebase onto the conflicting dep; dep-vs-main: merge main (#958)
1394
- const conflictFixDesc = _isInterDepConflict && _preflightConflictPrev
1395
- ? `Branch \`${depConflictBranch}\` conflicts with dependency branch \`${_preflightConflictPrev}\`. Rebase \`${depConflictBranch}\` onto \`${_preflightConflictPrev}\` (or merge \`${_preflightConflictPrev}\` into \`${depConflictBranch}\`) and resolve conflicts, then push.`
1396
- : `Branch \`${depConflictBranch}\` conflicts with \`${mainBranch}\`. Merge ${mainBranch} into the branch and resolve conflicts, then push.`;
1528
+ const newItem = buildDepConflictFixItem({
1529
+ depConflictBranch,
1530
+ depConflictFiles,
1531
+ isInterDepConflict: _isInterDepConflict,
1532
+ preflightConflictPrev: _preflightConflictPrev,
1533
+ mainBranch,
1534
+ blockedItem: meta.item,
1535
+ projectName: project.name || null,
1536
+ });
1397
1537
  mutateWorkItems(wiPath, items => {
1398
1538
  // Don't create duplicate conflict-fix items
1399
- const existing = items.find(i => i.id === conflictFixId && i.status !== WI_STATUS.DONE && i.status !== WI_STATUS.FAILED && i.status !== WI_STATUS.CANCELLED);
1539
+ 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
1540
  if (existing) return;
1401
- items.push({
1402
- id: conflictFixId,
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})`);
1541
+ items.push(newItem);
1542
+ log('info', `Auto-queued conflict-fix work item ${newItem.id} for ${depConflictBranch} (blocked: ${meta.item.id})`);
1416
1543
  });
1417
1544
  } catch (e) { log('warn', `Failed to auto-queue conflict-fix: ${e.message}`); }
1418
1545
  }
@@ -3981,16 +4108,24 @@ async function discoverFromPrs(config, project) {
3981
4108
  const currentHeadSha = String(pr.headSha || pr._adoSourceCommit || pr._adoHeadCommit || '').trim();
3982
4109
  const lastHumanDispatch = pr._lastDispatchByCause?.[shared.PR_FIX_CAUSE.HUMAN_FEEDBACK];
3983
4110
  const currentCommentId = String(pr.humanFeedback?.lastProcessedCommentId || '');
3984
- if (lastHumanDispatch?.outcome === 'noop'
4111
+ // Issue #2632: this same-head/same-comment guard MUST be cause-local. A
4112
+ // previous `continue` here aborted the whole PR iteration and starved
4113
+ // the build-failure / re-review / conflict-fix evaluation blocks below
4114
+ // (live repro on ADO PR `office-bohemia#5215610`: had `buildStatus=failing`
4115
+ // + `buildFailureSignature` but no `build-fix-*` dispatch was ever
4116
+ // queued). Skip only the human-feedback dispatch path; leave
4117
+ // `fixDispatched=false` so downstream causes are still evaluated.
4118
+ const skipHumanFeedback = !!(lastHumanDispatch?.outcome === 'noop'
3985
4119
  && lastHumanDispatch.headSha
3986
4120
  && currentHeadSha
3987
4121
  && lastHumanDispatch.headSha === currentHeadSha
3988
4122
  && lastHumanDispatch.lastProcessedCommentId
3989
4123
  && currentCommentId
3990
- && lastHumanDispatch.lastProcessedCommentId === currentCommentId) {
4124
+ && lastHumanDispatch.lastProcessedCommentId === currentCommentId);
4125
+ if (skipHumanFeedback) {
3991
4126
  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
4127
  }
4128
+ if (!skipHumanFeedback) {
3994
4129
  const key = humanFixKey;
3995
4130
  if (isPrAutomationCauseHandledOrPending(project, pr, humanCauseKey)) continue;
3996
4131
  let staleCoalesced = [];
@@ -4035,6 +4170,7 @@ async function discoverFromPrs(config, project) {
4035
4170
  review_note: reviewNote,
4036
4171
  }, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, automationCauseKey: humanCauseKey, source: 'pr-human-feedback', pr, branch: prBranch, project: projMeta });
4037
4172
  if (item) { newWork.push(item); fixDispatched = true; }
4173
+ } // end if (!skipHumanFeedback) — cause-local guard for #2632
4038
4174
  }
4039
4175
 
4040
4176
  // Re-review after fix: trigger when a fix was pushed after the last minions review,
@@ -4150,13 +4286,18 @@ async function discoverFromPrs(config, project) {
4150
4286
  // a new commit landed (live repro on PR #2433).
4151
4287
  const currentHeadSha = String(pr.headSha || pr._adoSourceCommit || pr._adoHeadCommit || '').trim();
4152
4288
  const lastBuildDispatch = pr._lastDispatchByCause?.[shared.PR_FIX_CAUSE.BUILD_FAILURE];
4153
- if (lastBuildDispatch?.outcome === 'noop'
4289
+ // Issue #2632 audit: cause-local guard. A previous `continue` here
4290
+ // aborted the whole PR iteration and starved the conflict-fix block
4291
+ // below — symmetric to the human-feedback bug. Skip only the build-fix
4292
+ // dispatch path; downstream merge-conflict resolution must still run.
4293
+ const skipBuildFix = !!(lastBuildDispatch?.outcome === 'noop'
4154
4294
  && lastBuildDispatch.headSha
4155
4295
  && currentHeadSha
4156
- && lastBuildDispatch.headSha === currentHeadSha) {
4296
+ && lastBuildDispatch.headSha === currentHeadSha);
4297
+ if (skipBuildFix) {
4157
4298
  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
4299
  }
4300
+ if (!skipBuildFix) {
4160
4301
  const buildCauseKey = getPrAutomationCauseKey('build', pr);
4161
4302
  const key = getPrAutomationDispatchKey(`build-fix-${project?.name || 'default'}-${prDisplayId}`, buildCauseKey);
4162
4303
  if (isPrAutomationCauseHandledOrPending(project, pr, buildCauseKey)) continue;
@@ -4245,6 +4386,7 @@ async function discoverFromPrs(config, project) {
4245
4386
  });
4246
4387
  } catch (e) { log('warn', 'mark build fail notified: ' + e.message); }
4247
4388
  }
4389
+ } // end if (!skipBuildFix) — cause-local guard for #2632 audit
4248
4390
  }
4249
4391
 
4250
4392
  // PRs with merge conflicts — dispatch fix to resolve (gated by provider polling + autoFixConflicts)
@@ -5724,12 +5866,15 @@ async function tickInner() {
5724
5866
  process.exit(0);
5725
5867
  }
5726
5868
 
5727
- // Write heartbeat so dashboard can detect stale engine
5728
- try { mutateControl(c => ({ ...c, heartbeat: Date.now() })); } catch (e) { log('warn', 'write heartbeat: ' + e.message); }
5869
+ // W-mpcyvff6000pf828 (#2653) — control.heartbeat is written by a dedicated
5870
+ // 15s interval in engine/cli.js (createHeartbeatWriter), decoupled from
5871
+ // tickInner so a slow tick (cold runtime spawn, sequential PR polls, slow
5872
+ // worktree create) cannot starve heartbeats and flip the dashboard to STALE
5873
+ // on an otherwise healthy engine.
5729
5874
 
5730
5875
  // P-c2e5a1d9-a — Initial wiring guard: bail immediately if a force-release
5731
- // reclaimed our lock while the heartbeat write was in flight. Per-phase
5732
- // guards inside the rest of tickInner are sub-task -b's scope.
5876
+ // reclaimed our lock while the startup control-state read was in flight.
5877
+ // Per-phase guards inside the rest of tickInner are sub-task -b's scope.
5733
5878
  if (_isTickStale(myGeneration)) return;
5734
5879
 
5735
5880
  const config = getConfig();
@@ -6400,6 +6545,7 @@ module.exports = {
6400
6545
  // Shared helpers (used by lifecycle.js and tests)
6401
6546
  reconcileItemsWithPrs, detectDependencyCycles,
6402
6547
  parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
6548
+ buildDepConflictFixItem, // exported for testing (W-mpcwojgr000a0244)
6403
6549
  isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, // exported for testing
6404
6550
  pruneStaleWorktreeForBranch, // exported for testing
6405
6551
  _maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1984",
3
+ "version": "0.1.1985",
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"