@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.
- package/dashboard/js/refresh.js +12 -1
- package/dashboard.js +43 -7
- package/docs/auto-discovery.md +1 -1
- package/docs/pr-review-fix-loop.md +2 -1
- package/docs/self-improvement.md +3 -3
- package/docs/watches.md +4 -4
- 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/refresh.js
CHANGED
|
@@ -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
|
|
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/docs/auto-discovery.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auto-Discovery & Execution Pipeline
|
|
2
2
|
|
|
3
|
-
> Last verified: 2026-05-
|
|
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
|
|
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 |
|
package/docs/self-improvement.md
CHANGED
|
@@ -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:**
|
|
178
|
-
- **Agent detection:** Comments
|
|
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
|
|
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:
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
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.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"
|