@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 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 without
101
- // blocking the event loop on the per-project git shell-outs. Fire-and-forget,
102
- // boot-only: re-warming on every reloadConfig() would spawn git probes on
103
- // every 10s status-poll cache miss. Project add/remove handlers explicitly
104
- // invoke this when the project list actually changes.
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 { queries.warmProjectGitStatus(p.localPath, p.mainBranch); } catch { /* swallow — warming is opportunistic */ }
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
- server.listen(PORT, '127.0.0.1', () => {
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') {
@@ -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
- if (!refreshedExtra.length) {
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);
@@ -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 (detection could not
1977
- // prove the branch advanced — fetch failed, worktree gone, missing baseline)
1978
- // from confirmed noops (remote head was verified to equal beforeHead). The
1979
- // same-head guard in engine.js MUST NOT permanently suppress on indeterminate
1980
- // records, otherwise a single failed detection locks out future fix
1981
- // dispatches on that head+comment until the SHA moves and the SHA cannot
1982
- // move unless the suppressed fix runs. The `_noOpFixes[cause].count` +
1983
- // `prNoOpFixPauseAttempts` (default 3) safety valve still applies; only the
1984
- // same-head guard is relaxed.
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
- const PROJECT_GIT_STATUS_TTL = 300000; // 5 minutes
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 engine boot to
1780
- // pre-warm the cache for every configured project so the first /api/status
1781
- // after restart already has data. Also used by tests to settle the async
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.2021",
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"