@yemi33/minions 0.1.2021 → 0.1.2022
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/dashboard.js +43 -7
- package/engine/ado-git-auth.js +42 -6
- package/engine/lifecycle.js +12 -9
- package/engine/queries.js +12 -4
- package/engine.js +31 -13
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -97,17 +97,33 @@ function reloadConfig() {
|
|
|
97
97
|
ensureConfiguredProjectStateFiles();
|
|
98
98
|
|
|
99
99
|
// Pre-warm git-status cache for every configured project so the first
|
|
100
|
-
// /api/status after dashboard boot already has branch/dirty data
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
100
|
+
// /api/status after dashboard boot already has branch/dirty data instead of
|
|
101
|
+
// the ~8s `gitState=pending` gap that hides the "on: <branch>" chips in the
|
|
102
|
+
// projects bar (#2754, W-mpgr4bu8). Returns a Promise that settles once every
|
|
103
|
+
// per-project probe finishes; the boot block awaits this before `server.listen`
|
|
104
|
+
// so the first poll never sees pending. The module-level call below is
|
|
105
|
+
// fire-and-forget so module-import paths (tests) still get opportunistic
|
|
106
|
+
// warming without blocking. Project add handlers re-invoke this when the
|
|
107
|
+
// project list changes; re-warming on every `reloadConfig()` would spawn 4×
|
|
108
|
+
// git probes on every 10s status-poll cache miss.
|
|
105
109
|
function warmProjectGitStatusCache() {
|
|
110
|
+
const probes = [];
|
|
106
111
|
for (const p of PROJECTS) {
|
|
107
112
|
if (p && p.localPath) {
|
|
108
|
-
try {
|
|
113
|
+
try {
|
|
114
|
+
const probe = queries.warmProjectGitStatus(p.localPath, p.mainBranch);
|
|
115
|
+
if (probe && typeof probe.then === 'function') {
|
|
116
|
+
probes.push(probe.catch(e => {
|
|
117
|
+
console.warn(`[boot] warm git status failed for ${p.name}: ${e && e.message}`);
|
|
118
|
+
return null;
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.warn(`[boot] warm git status threw for ${p.name}: ${e && e.message}`);
|
|
123
|
+
}
|
|
109
124
|
}
|
|
110
125
|
}
|
|
126
|
+
return Promise.all(probes);
|
|
111
127
|
}
|
|
112
128
|
warmProjectGitStatusCache();
|
|
113
129
|
|
|
@@ -10233,6 +10249,7 @@ module.exports = {
|
|
|
10233
10249
|
_resetPreambleCache,
|
|
10234
10250
|
_installCrashHandlers,
|
|
10235
10251
|
_mergeSettingsConfigUpdate: mergeSettingsConfigUpdate,
|
|
10252
|
+
_warmProjectGitStatusCache: warmProjectGitStatusCache,
|
|
10236
10253
|
// /api/status event-loop isolation surface (W-mpehsyhv0017085a)
|
|
10237
10254
|
refreshStatusAsync,
|
|
10238
10255
|
handleStatus: _handleStatusRequest,
|
|
@@ -10251,7 +10268,25 @@ if (require.main === module) {
|
|
|
10251
10268
|
// engine reads, port binding) is captured rather than dying silently.
|
|
10252
10269
|
_installCrashHandlers();
|
|
10253
10270
|
|
|
10254
|
-
|
|
10271
|
+
// Pre-warm the per-project git-status cache before accepting requests so
|
|
10272
|
+
// the first /api/status after restart already returns gitState='ok' with a
|
|
10273
|
+
// real branch instead of the ~8s pending gap that hides the projects-bar
|
|
10274
|
+
// "on: <branch>" chips (#2754, W-mpgr4bu8). Wrapped in an async IIFE so
|
|
10275
|
+
// the await happens before server.listen(); per-project failures are
|
|
10276
|
+
// swallowed inside warmProjectGitStatusCache() so one bad project cannot
|
|
10277
|
+
// block boot.
|
|
10278
|
+
(async () => {
|
|
10279
|
+
const _warmStartedAt = Date.now();
|
|
10280
|
+
try {
|
|
10281
|
+
await warmProjectGitStatusCache();
|
|
10282
|
+
const ms = Date.now() - _warmStartedAt;
|
|
10283
|
+
const n = Array.isArray(PROJECTS) ? PROJECTS.length : 0;
|
|
10284
|
+
console.log(`[boot] pre-warmed git status for ${n} project${n === 1 ? '' : 's'} in ${ms}ms`);
|
|
10285
|
+
} catch (e) {
|
|
10286
|
+
console.warn(`[boot] pre-warm git status failed: ${e && e.message}`);
|
|
10287
|
+
}
|
|
10288
|
+
|
|
10289
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
10255
10290
|
console.log(`\n Minions Mission Control`);
|
|
10256
10291
|
console.log(` -----------------------------------`);
|
|
10257
10292
|
console.log(` http://localhost:${PORT}`);
|
|
@@ -10331,6 +10366,7 @@ if (require.main === module) {
|
|
|
10331
10366
|
}, 30000).unref();
|
|
10332
10367
|
console.log(` Engine watchdog: active (checks every 30s)`);
|
|
10333
10368
|
});
|
|
10369
|
+
})();
|
|
10334
10370
|
|
|
10335
10371
|
server.on('error', e => {
|
|
10336
10372
|
if (e.code === 'EADDRINUSE') {
|
package/engine/ado-git-auth.js
CHANGED
|
@@ -47,6 +47,22 @@ const TOKEN_TTL_MS = 30 * 60 * 1000; // 30 min — matches gh-token cadence
|
|
|
47
47
|
const ACQUIRE_BACKOFF_MS = 10 * 60 * 1000; // 10 min — same backoff as gh-token
|
|
48
48
|
const ACQUIRE_TIMEOUT_MS = 30000; // 30s — generous for slow IWA/broker auth
|
|
49
49
|
|
|
50
|
+
// #2762 — Suppress Windows credential helpers (GCM/WAM broker) and any
|
|
51
|
+
// interactive credential prompt for the lifetime of each ADO git invocation.
|
|
52
|
+
// Without these, when the bearer extraHeader doesn't cover a needed host
|
|
53
|
+
// (e.g. Scalar/GVFS cache-server like *.gvfscache.dev.azure.com), git falls
|
|
54
|
+
// through to `credential.helper=git-credential-manager`. On Windows with
|
|
55
|
+
// `credential.msauthusebroker=true` in the user's global config, GCM invokes
|
|
56
|
+
// the WAM broker which pops a focus-stealing "Account Manager" dialog mid-task.
|
|
57
|
+
// Setting credential.helper to an empty string disables ALL helpers for this
|
|
58
|
+
// command only (no global config mutation); credential.interactive=false
|
|
59
|
+
// keeps git from falling back to TTY prompts. Together they convert an
|
|
60
|
+
// otherwise-silent hang behind a UI dialog into an immediate, debuggable 401.
|
|
61
|
+
const CREDENTIAL_DISABLE_ARGS = [
|
|
62
|
+
'-c', 'credential.helper=',
|
|
63
|
+
'-c', 'credential.interactive=false',
|
|
64
|
+
];
|
|
65
|
+
|
|
50
66
|
let _cached = null; // { token, expiresAt }
|
|
51
67
|
let _backoffUntil = 0; // epoch ms — no acquire attempts before this
|
|
52
68
|
|
|
@@ -127,6 +143,14 @@ function _buildHeaderArgs(token, scopeUrls) {
|
|
|
127
143
|
return args;
|
|
128
144
|
}
|
|
129
145
|
|
|
146
|
+
// #2762 — Always-on credential-helper suppression for ADO git ops, paired with
|
|
147
|
+
// (optional) bearer-header injection. The credential disable flags come FIRST
|
|
148
|
+
// so they're effective even when token acquisition failed and we have nothing
|
|
149
|
+
// else to add — preventing the WAM popup is more important than authenticating.
|
|
150
|
+
function _composeAdoArgs(headerArgs) {
|
|
151
|
+
return [...CREDENTIAL_DISABLE_ARGS, ...headerArgs];
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
function _acquireToken(opts = {}) {
|
|
131
155
|
const exec = opts.acquireAdoTokenSync || adoToken.acquireAdoTokenSync;
|
|
132
156
|
const result = exec({ timeout: opts.timeout || ACQUIRE_TIMEOUT_MS });
|
|
@@ -137,23 +161,28 @@ function getAdoGitExtraArgs(project, opts = {}) {
|
|
|
137
161
|
if (!isAdoProject(project)) return [];
|
|
138
162
|
const now = Date.now();
|
|
139
163
|
if (_cached && _cached.expiresAt > now) {
|
|
140
|
-
return _buildHeaderArgs(_cached.token, _resolveScopeUrls(project));
|
|
164
|
+
return _composeAdoArgs(_buildHeaderArgs(_cached.token, _resolveScopeUrls(project)));
|
|
165
|
+
}
|
|
166
|
+
if (now < _backoffUntil) {
|
|
167
|
+
// #2762 — Still suppress the credential helper even when we have no token.
|
|
168
|
+
// Without these flags, git falls through to GCM/WAM and pops the account
|
|
169
|
+
// picker; with them, the fetch fails fast with a 401 in agent logs.
|
|
170
|
+
return [...CREDENTIAL_DISABLE_ARGS];
|
|
141
171
|
}
|
|
142
|
-
if (now < _backoffUntil) return [];
|
|
143
172
|
try {
|
|
144
173
|
const token = _acquireToken(opts);
|
|
145
174
|
if (!token) {
|
|
146
175
|
_backoffUntil = now + ACQUIRE_BACKOFF_MS;
|
|
147
176
|
log('warn', `ado-git-auth: acquireAdoTokenSync returned empty token; backing off ${ACQUIRE_BACKOFF_MS / 1000}s`);
|
|
148
|
-
return [];
|
|
177
|
+
return [...CREDENTIAL_DISABLE_ARGS];
|
|
149
178
|
}
|
|
150
179
|
_cached = { token, expiresAt: now + TOKEN_TTL_MS };
|
|
151
|
-
return _buildHeaderArgs(token, _resolveScopeUrls(project));
|
|
180
|
+
return _composeAdoArgs(_buildHeaderArgs(token, _resolveScopeUrls(project)));
|
|
152
181
|
} catch (e) {
|
|
153
182
|
_backoffUntil = now + ACQUIRE_BACKOFF_MS;
|
|
154
183
|
const firstLine = String(e && e.message || e).split('\n')[0];
|
|
155
184
|
log('warn', `ado-git-auth: token acquire failed (${firstLine}); backing off ${ACQUIRE_BACKOFF_MS / 1000}s`);
|
|
156
|
-
return [];
|
|
185
|
+
return [...CREDENTIAL_DISABLE_ARGS];
|
|
157
186
|
}
|
|
158
187
|
}
|
|
159
188
|
|
|
@@ -203,7 +232,14 @@ async function runAdoGit(project, gitArgs, opts = {}) {
|
|
|
203
232
|
invalidateAdoTokenCache();
|
|
204
233
|
_backoffUntil = 0; // give the retry a chance to actually hit the token source
|
|
205
234
|
const refreshedExtra = getAdoGitExtraArgs(project, adoOpts);
|
|
206
|
-
|
|
235
|
+
// #2762 — `refreshedExtra` now always includes the credential-disable
|
|
236
|
+
// flags even when token acquisition fails, so length alone no longer
|
|
237
|
+
// signals "got a usable token." Detect a real bearer header instead;
|
|
238
|
+
// without one, retrying just re-runs the same unauthenticated request.
|
|
239
|
+
const hasBearer = refreshedExtra.some(
|
|
240
|
+
(a) => typeof a === 'string' && a.includes('Authorization: Bearer '),
|
|
241
|
+
);
|
|
242
|
+
if (!hasBearer) {
|
|
207
243
|
throw _redactErrorInPlace(err);
|
|
208
244
|
}
|
|
209
245
|
const retryExtra = baseExtra.concat(refreshedExtra);
|
package/engine/lifecycle.js
CHANGED
|
@@ -1973,15 +1973,18 @@ function recordPrNoOpFixAttempt(target, cause, source, dispatchItem, branchChang
|
|
|
1973
1973
|
).slice(0, 500);
|
|
1974
1974
|
target._lastDispatchByCause = target._lastDispatchByCause
|
|
1975
1975
|
&& typeof target._lastDispatchByCause === 'object' ? target._lastDispatchByCause : {};
|
|
1976
|
-
// W-mpg6wptq0011cc68: distinguish indeterminate noops
|
|
1977
|
-
// prove the branch advanced — fetch failed, worktree
|
|
1978
|
-
// from confirmed noops (remote head was verified
|
|
1979
|
-
//
|
|
1980
|
-
//
|
|
1981
|
-
//
|
|
1982
|
-
//
|
|
1983
|
-
//
|
|
1984
|
-
//
|
|
1976
|
+
// W-mpg6wptq0011cc68 / W-mpgq8vve0001b8e3: distinguish indeterminate noops
|
|
1977
|
+
// (detection could not prove the branch advanced — fetch failed, worktree
|
|
1978
|
+
// gone, missing baseline) from confirmed noops (remote head was verified
|
|
1979
|
+
// to equal beforeHead). The flag is recorded on `_lastDispatchByCause`
|
|
1980
|
+
// for diagnostics and dashboards; the same-head/same-comment guard in
|
|
1981
|
+
// engine.js NO LONGER treats it as a bypass (it used to, but that produced
|
|
1982
|
+
// tight retry loops on PRs like ado:office/iss/constellation#5227039 where
|
|
1983
|
+
// the indeterminate flag forced a retry every poll while the evidence
|
|
1984
|
+
// fingerprint kept drifting just enough to reset `_noOpFixes.count`).
|
|
1985
|
+
// Recovery story without the bypass: if a fix actually landed but
|
|
1986
|
+
// detection failed, the next ADO/GH poll refreshes `pr.headSha` and the
|
|
1987
|
+
// guard's `===` check stops matching — re-dispatch resumes automatically.
|
|
1985
1988
|
const indeterminate = !!branchChange && branchChange.changed === null;
|
|
1986
1989
|
target._lastDispatchByCause[cause] = {
|
|
1987
1990
|
outcome: 'noop',
|
package/engine/queries.js
CHANGED
|
@@ -1632,7 +1632,13 @@ function resetPrdInfoCache() {
|
|
|
1632
1632
|
// Folding state + in-flight into one map keeps reads/writes atomic and removes
|
|
1633
1633
|
// the second-Map sync hazard.
|
|
1634
1634
|
const _projectGitStatusCache = new Map();
|
|
1635
|
-
|
|
1635
|
+
// 15 s TTL — keeps external `git checkout` visible on the projects bar within
|
|
1636
|
+
// ~one /api/status cycle. The probe is 4 cheap local `git` commands (<100 ms on
|
|
1637
|
+
// a warm repo) and is already backgrounded + single-flighted by
|
|
1638
|
+
// _scheduleProjectGitStatusRefresh, so a tighter TTL doesn't add load: at
|
|
1639
|
+
// /api/status's 4 s poll cadence we trigger at most one background refresh per
|
|
1640
|
+
// project every ~4 polls (#2760).
|
|
1641
|
+
const PROJECT_GIT_STATUS_TTL = 15000;
|
|
1636
1642
|
// Default null comparator fields so callers can blindly spread the result into
|
|
1637
1643
|
// the /api/status payload without per-state branching (W-mpg3whgp000d09ec).
|
|
1638
1644
|
// `remoteDefaultBranch` / `ahead` / `behind` are populated only by the `ok`
|
|
@@ -1776,9 +1782,11 @@ function getProjectGitStatus(localPath, configuredMainBranch = null) {
|
|
|
1776
1782
|
return cached ? cached.value : PROJECT_GIT_STATUS_PENDING;
|
|
1777
1783
|
}
|
|
1778
1784
|
|
|
1779
|
-
// Force a refresh now and wait for completion. Called from
|
|
1780
|
-
//
|
|
1781
|
-
//
|
|
1785
|
+
// Force a refresh now and wait for completion. Called from dashboard boot
|
|
1786
|
+
// (see `warmProjectGitStatusCache()` in dashboard.js) to pre-warm the cache
|
|
1787
|
+
// for every configured project before `server.listen()` accepts requests, so
|
|
1788
|
+
// the first /api/status after restart already has data instead of a ~8s
|
|
1789
|
+
// `gitState=pending` gap (#2754). Also used by tests to settle the async
|
|
1782
1790
|
// probe deterministically.
|
|
1783
1791
|
function warmProjectGitStatus(localPath, configuredMainBranch = null) {
|
|
1784
1792
|
const norm = String(localPath || '').replace(/\\/g, '/');
|
package/engine.js
CHANGED
|
@@ -4281,14 +4281,28 @@ async function discoverFromPrs(config, project) {
|
|
|
4281
4281
|
// + `buildFailureSignature` but no `build-fix-*` dispatch was ever
|
|
4282
4282
|
// queued). Skip only the human-feedback dispatch path; leave
|
|
4283
4283
|
// `fixDispatched=false` so downstream causes are still evaluated.
|
|
4284
|
+
// W-mpgq8vve0001b8e3: the same-head/same-comment guard now suppresses
|
|
4285
|
+
// BOTH confirmed AND indeterminate noops. The original W-mpg6wptq0011cc68
|
|
4286
|
+
// bypass for `indeterminate` produced a tight retry loop on
|
|
4287
|
+
// ado:office/iss/constellation#5227039 (4 dispatches on identical head
|
|
4288
|
+
// `c182a517` and comment id `2` within a single day) because the
|
|
4289
|
+
// indeterminate flag forced a retry every poll while the evidence
|
|
4290
|
+
// fingerprint (`feedbackContent`) kept drifting just enough to reset
|
|
4291
|
+
// `_noOpFixes.count` to 1, preventing the `prNoOpFixPauseAttempts` valve
|
|
4292
|
+
// from ever tripping. Rationale for suppressing indeterminate too:
|
|
4293
|
+
// - `currentHeadSha` reads from `pr.headSha`, which is refreshed by
|
|
4294
|
+
// the ADO/GH poller every `prPollStatusEvery` ticks. If the agent
|
|
4295
|
+
// had genuinely pushed (and lifecycle-side fetch detection just
|
|
4296
|
+
// failed), the next poll cycle would advance `pr.headSha` and the
|
|
4297
|
+
// `===` check would no longer match → re-dispatch naturally
|
|
4298
|
+
// resumes.
|
|
4299
|
+
// - If `currentHeadSha` still equals `lastHumanDispatch.headSha` AND
|
|
4300
|
+
// the comment id is unchanged, there is no new evidence for the
|
|
4301
|
+
// agent to act on — retrying produces another indeterminate noop
|
|
4302
|
+
// and accomplishes nothing. New activity (new comment, head moves)
|
|
4303
|
+
// is the only thing that can unstick the loop, and both naturally
|
|
4304
|
+
// unstick the guard.
|
|
4284
4305
|
const skipHumanFeedback = !!(lastHumanDispatch?.outcome === 'noop'
|
|
4285
|
-
// W-mpg6wptq0011cc68: indeterminate noops (detection could not verify
|
|
4286
|
-
// branch advance — fetch failed, worktree gone) must NOT permanently
|
|
4287
|
-
// suppress re-dispatch. lifecycle.js:2043-2048 explicitly comments
|
|
4288
|
-
// that a future tick with working detection must be free to re-fire.
|
|
4289
|
-
// The `_noOpFixes` count + `prNoOpFixPauseAttempts` (default 3) safety
|
|
4290
|
-
// valve still triggers pause if detection keeps failing.
|
|
4291
|
-
&& !lastHumanDispatch.indeterminate
|
|
4292
4306
|
&& lastHumanDispatch.headSha
|
|
4293
4307
|
&& currentHeadSha
|
|
4294
4308
|
&& lastHumanDispatch.headSha === currentHeadSha
|
|
@@ -4472,13 +4486,17 @@ async function discoverFromPrs(config, project) {
|
|
|
4472
4486
|
// aborted the whole PR iteration and starved the conflict-fix block
|
|
4473
4487
|
// below — symmetric to the human-feedback bug. Skip only the build-fix
|
|
4474
4488
|
// dispatch path; downstream merge-conflict resolution must still run.
|
|
4489
|
+
// W-mpgq8vve0001b8e3: symmetric to the human-feedback guard above —
|
|
4490
|
+
// suppress BOTH confirmed AND indeterminate noops on an unchanged head.
|
|
4491
|
+
// The original W-mpg6wptq0011cc68 bypass let indeterminate build-fix
|
|
4492
|
+
// noops retry indefinitely; in practice that produces the same tight
|
|
4493
|
+
// loop the human-feedback path hit (4 noop dispatches on identical
|
|
4494
|
+
// head). Recovery story is the same: if a fix actually landed but
|
|
4495
|
+
// fetch-side detection failed, the next ADO/GH poll cycle refreshes
|
|
4496
|
+
// `pr.headSha` and the `===` check stops matching → re-dispatch
|
|
4497
|
+
// resumes naturally. If the head genuinely hasn't moved, there is
|
|
4498
|
+
// nothing for the agent to do.
|
|
4475
4499
|
const skipBuildFix = !!(lastBuildDispatch?.outcome === 'noop'
|
|
4476
|
-
// W-mpg6wptq0011cc68: symmetric protection — indeterminate noops here
|
|
4477
|
-
// (detection couldn't verify branch advance) must NOT permanently
|
|
4478
|
-
// suppress build-fix either. Same rationale as the human-feedback
|
|
4479
|
-
// guard above; the per-cause `_noOpFixes` count + pause-after-N valve
|
|
4480
|
-
// still applies.
|
|
4481
|
-
&& !lastBuildDispatch.indeterminate
|
|
4482
4500
|
&& lastBuildDispatch.headSha
|
|
4483
4501
|
&& currentHeadSha
|
|
4484
4502
|
&& lastBuildDispatch.headSha === currentHeadSha);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2022",
|
|
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"
|