@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 +0 -0
- package/engine/ado-git-auth.js +206 -0
- package/engine/cli.js +43 -0
- package/engine/dispatch.js +2 -0
- package/engine/lifecycle.js +8 -3
- package/engine/recovery.js +6 -0
- package/engine/shared.js +16 -4
- package/engine.js +183 -37
- package/package.json +1 -1
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
|
};
|
package/engine/dispatch.js
CHANGED
|
@@ -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)',
|
package/engine/lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/engine/recovery.js
CHANGED
|
@@ -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
|
-
|
|
1236
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
:
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
:
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5728
|
-
|
|
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
|
|
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.
|
|
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"
|