@yemi33/minions 0.1.2021 → 0.1.2023

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.
@@ -152,8 +152,19 @@ function _processStatusUpdate(data) {
152
152
  var _workItemsChanged = _changed('workItems', data.workItems);
153
153
  if (_prsChanged) renderPrs(data.pullRequests || []);
154
154
  if (_changed('archivedPrds', data.archivedPrds)) renderArchiveButtons(data.archivedPrds || []);
155
+ // renderEngineStatus is NOT gated by _changed('engine'): heartbeat
156
+ // staleness is wall-clock-relative (Date.now() - engine.heartbeat), so
157
+ // even when the snapshot bytes are byte-for-byte identical between polls
158
+ // (e.g. control.json mtime is intentionally excluded from the fast-state
159
+ // mtime tracker — see engine/queries.js#getStatusFastStateMtimePaths)
160
+ // the staleness verdict can flip both directions purely from time
161
+ // passing. Gating the call made the STALE banner stick after a CLI
162
+ // restart until some unrelated slice changed. The render is cheap and
163
+ // renderEngineAlert is idempotent (hides itself when state !== 'stale').
164
+ // The quick-stats block IS still gated to avoid pointless DOM writes
165
+ // when the underlying numbers haven't moved.
166
+ if (data.engine) renderEngineStatus(data.engine);
155
167
  if (_changed('engine', data.engine)) {
156
- renderEngineStatus(data.engine);
157
168
  var qs = document.getElementById('engine-quick-stats');
158
169
  if (qs && data.engine) {
159
170
  var wt = data.engine.worktreeCount != null ? data.engine.worktreeCount : '-';
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') {
@@ -1,6 +1,6 @@
1
1
  # Auto-Discovery & Execution Pipeline
2
2
 
3
- > Last verified: 2026-05-17 against `engine.js` `tickInner()` (lines 5360-5993).
3
+ > Last verified: 2026-05-22 against `engine.js` `tickInner()` (lines 6068-6425).
4
4
 
5
5
  How the minions engine finds work and dispatches agents automatically.
6
6
 
@@ -39,7 +39,7 @@ The engine does not cap review→fix cycles or build-fix attempts. Each trigger
39
39
  ### A. Human comments (`humanFeedback.pendingFix`)
40
40
 
41
41
  - Gate: `pendingFix || coalescedFeedback` + not already dispatched/on cooldown
42
- - Agent comments filtered out via `/\bMinions\s*\(/i` regex on comment body
42
+ - Agent comments filtered out via `_isNonActionableComment()` (`engine/github.js`, `engine/ado.js`) composes preview/bot body checks and the structural Minions-author check from `engine/comment-classifier.js`. The structural check matches the `<!-- minions:agent=<id> kind=<kind> -->` HTML marker on the body's first line (`hasMinionsMarker`); on GitHub it must combine with `viewerDidAuthor === true` (anti-spoof), on ADO with a `config.engine.minionsAdoIdentities` author match. Marker presence alone never classifies a comment as agent-authored on either platform.
43
43
  - Coalesces multiple comments arriving during cooldown into single fix
44
44
  - Routes to author
45
45
  - Fix agents must treat human comments as claims to verify, not commands. They inspect or reproduce each claimed issue, make the smallest correct fix only when the claim is valid, and otherwise reply with evidence-backed rationale.
@@ -108,6 +108,7 @@ The engine does not cap review→fix cycles or build-fix attempts. Each trigger
108
108
  | `engine/lifecycle.js` | `syncPrsFromOutput()`, `updatePrAfterReview()`, `updatePrAfterFix()` |
109
109
  | `engine/github.js` | `pollPrStatus()`, `pollPrHumanComments()`, `fetchGhBuildErrorLog()` |
110
110
  | `engine/ado.js` | `pollPrStatus()`, `pollPrHumanComments()`, `fetchAdoBuildErrorLog()` |
111
+ | `engine/comment-classifier.js` | Host-agnostic body-only predicates (`isPreviewStatusBody`, `hasMinionsMarker`, `hasVerdictPrefix`) shared by GH + ADO pollers (extracted in F2+F3+F4) |
111
112
  | `engine/dispatch.js` | `addToDispatch()` — dedup by work item ID and dispatchKey |
112
113
  | `engine/cooldown.js` | `isBranchActive()`, cooldown management |
113
114
  | `playbooks/review.md` | Reviewer playbook |
@@ -174,10 +174,10 @@ Human comments on PR with "@minions fix the error handling here"
174
174
 
175
175
  ### How it works
176
176
 
177
- - **Trigger:** If you're the only human commenter, **any** comment triggers a fix. If multiple humans are commenting, `@minions` keyword is required to avoid noise
178
- - **Agent detection:** Comments matching `/\bMinions\s*\(/i` are skipped (agent signature pattern)
177
+ - **Trigger:** Any human comment newer than the per-PR cutoff that survives `_isNonActionableComment()` (preview/CI/Minions-author filter) flips `pr.humanFeedback.pendingFix = true` and queues a fix. There is no "@minions keyword required" gate — `@minions` mentions are merely stripped from `feedbackContent` before it is handed to the fix agent.
178
+ - **Agent detection:** Comments are filtered via `_isNonActionableComment()` in `engine/github.js` / `engine/ado.js`, which composes the body-only predicates from `engine/comment-classifier.js` (`isPreviewStatusBody`, `hasMinionsMarker`) with an identity gate (`viewerDidAuthor === true` on GitHub, `config.engine.minionsAdoIdentities` author match on ADO). Marker presence alone never classifies a comment as agent-authored on either platform.
179
179
  - **Dedup:** Only comments newer than `pr.humanFeedback.lastProcessedCommentDate` are processed
180
- - **Multiple comments:** All new `@minions` comments are concatenated into a single fix task
180
+ - **Multiple comments:** All new actionable comments since the cutoff are concatenated chronologically into a single fix task (each body individually fenced in `<UNTRUSTED-INPUT>` per F5 / W-mpeklod3000we69c)
181
181
  - **After fix:** `pendingFix` is cleared; PR re-enters normal review cycle
182
182
 
183
183
  ## 5. Quality Metrics
package/docs/watches.md CHANGED
@@ -49,7 +49,7 @@ When `stopAfter === 0`, these are **fire-once** — the engine flips the watch t
49
49
  > **Per-target override (W-mp7hg58e000b5212):** the global `WATCH_ABSOLUTE_CONDITIONS` set is the legacy fallback. Each target type now declares its own `absoluteConditions: [...]` array in its spec; `registerTargetType` normalizes that into a `Set` that takes precedence at evaluation time. The plugin contract (see below) uses this to keep absolute-vs-change semantics local to each target type. Plugins that omit `absoluteConditions` get an empty set (all change-based).
50
50
 
51
51
  ### Change-based conditions
52
- `status-change`, `any`, `new-comments`, `vote-change`, `stage-complete`, `ran`, `enabled`, `disabled`, `activity-change`, plus the predicate conditions added under P-w4e2f6a1 / P-w5b8d2c9 for the `pr`, `work-item`, `plan`, and `pipeline` target types (`head-commit-change`, `mergeable-flipped`, `behind-master`, `draft-flipped`, `stalled`, `dependency-met`, `stage-advanced`, `stuck-in-stage`). See `engine/shared.js:1891-1929` for the canonical enum.
52
+ `status-change`, `any`, `new-comments`, `vote-change`, `stage-complete`, `ran`, `enabled`, `disabled`, `activity-change`, plus the predicate conditions added under P-w4e2f6a1 / P-w5b8d2c9 for the `pr`, `work-item`, `plan`, and `pipeline` target types (`head-commit-change`, `mergeable-flipped`, `behind-master`, `draft-flipped`, `stalled`, `dependency-met`, `stage-advanced`, `stuck-in-stage`). See `engine/shared.js:2422-2460` for the canonical enum.
53
53
 
54
54
  These compare the live entity against the watch's `_lastState` snapshot and run forever when `stopAfter === 0`. Baseline `_lastState` is captured on the first check so the very next change triggers the watch *(source: `engine/watches.js:434, 520`)*.
55
55
 
@@ -89,7 +89,7 @@ Canonical example: `watches.d/http.js` (W-mp7i22mu00191b07) — a generic HTTP p
89
89
 
90
90
  ### Built-in target types
91
91
 
92
- The eight built-ins are registered at module load *(source: `engine/watches.js:672-1311`)*. Constants live at `engine/shared.js:1881-1890` (`WATCH_TARGET_TYPE`).
92
+ The eight built-ins are registered at module load *(source: `engine/watches.js:672-1313`)*. Constants live at `engine/shared.js:2412-2421` (`WATCH_TARGET_TYPE`).
93
93
 
94
94
  | `targetType` | Target value | Conditions | Notes |
95
95
  |---------------|--------------------------------------|----------------------------------------------------------------------------|-------|
@@ -174,7 +174,7 @@ I/O happens **outside the lock**: notifications via `writeToInbox`, follow-up ac
174
174
  | `archive-plan` | Set PRD `status="archived"` + `archivedAt` |
175
175
  | `resume-plan` | Set PRD `status=PLAN_STATUS.ACTIVE` and clear `planStale` |
176
176
 
177
- Constants live in `WATCH_ACTION_TYPE` (`engine/shared.js:1960`); handlers in `engine/watch-actions.js`.
177
+ Constants live in `WATCH_ACTION_TYPE` (`engine/shared.js:2491`); handlers in `engine/watch-actions.js`.
178
178
 
179
179
  ### Templating
180
180
 
@@ -245,7 +245,7 @@ Absolute conditions firing under `stopAfter === 0` flip `status` to `expired`; `
245
245
 
246
246
  ## See Also
247
247
 
248
- - `engine/shared.js:1875-1960` — `WATCH_STATUS`, `WATCH_TARGET_TYPE`, `WATCH_CONDITION`, `WATCH_ABSOLUTE_CONDITIONS`, `WATCH_ACTION_TYPE` constants
248
+ - `engine/shared.js:2406-2500` — `WATCH_STATUS`, `WATCH_TARGET_TYPE`, `WATCH_CONDITION`, `WATCH_ABSOLUTE_CONDITIONS`, `WATCH_ACTION_TYPE` constants
249
249
  - `engine/watches.js` — registry, lifecycle, tick integration, `watches.d/` plugin loader
250
250
  - `engine/watch-actions.js` — action registry and built-in handlers (including `minions-api`)
251
251
  - `watches.d/http.js` — canonical user-extensible target type plugin
@@ -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.2023",
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"