@yemi33/minions 0.1.2224 → 0.1.2226
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/deprecated.json +8 -8
- package/engine/shared.js +1 -5
- package/engine/supervisor.js +144 -1
- package/package.json +1 -1
package/docs/deprecated.json
CHANGED
|
@@ -119,8 +119,8 @@
|
|
|
119
119
|
{ "file": "engine.js", "note": "discoverFromPrs reads canonical `pr.contextOnly` only." }
|
|
120
120
|
],
|
|
121
121
|
"deprecated": "2026-06-08",
|
|
122
|
-
"targetRemovalDate": "2026-06-
|
|
123
|
-
"notes": "The write side already shipped out — there is no dual-write to retire. What remains is the read-fallback (`_contextOnly === true`) plus the one-shot boot migration migratePrGateFlags (engine/shared.js:6833) that copies + deletes the alias from any pre-migration on-disk record. Pair this entry's removal with `pr-record-autoObserve-underscore-alias`, `pr-record-manual-underscore-alias`, and `pr-link-autoObserve-body-param` — they share the same boot migration. Removal scope (DEFERRED — gated on a positive signal that every live pull-requests.json has been swept of underscore keys, not on the expired calendar date): delete the `_contextOnly` read-fallback in engine/shared.js and the migratePrGateFlags handling of it."
|
|
122
|
+
"targetRemovalDate": "2026-06-25",
|
|
123
|
+
"notes": "The write side already shipped out — there is no dual-write to retire. What remains is the read-fallback (`_contextOnly === true`) plus the one-shot boot migration migratePrGateFlags (engine/shared.js:6833) that copies + deletes the alias from any pre-migration on-disk record. Pair this entry's removal with `pr-record-autoObserve-underscore-alias`, `pr-record-manual-underscore-alias`, and `pr-link-autoObserve-body-param` — they share the same boot migration. Removal scope (DEFERRED — gated on a positive signal that every live pull-requests.json has been swept of underscore keys, not on the expired calendar date): delete the `_contextOnly` read-fallback in engine/shared.js and the migratePrGateFlags handling of it. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
"id": "pr-record-autoObserve-underscore-alias",
|
|
@@ -130,8 +130,8 @@
|
|
|
130
130
|
{ "file": "dashboard.js", "note": "Zero `_autoObserve` references — link/observe handlers accept the canonical `contextOnly` body param and write no underscore alias onto the record." }
|
|
131
131
|
],
|
|
132
132
|
"deprecated": "2026-06-08",
|
|
133
|
-
"targetRemovalDate": "2026-06-
|
|
134
|
-
"notes": "The write side already shipped out — there is no dual-write to retire. Removal is paired with `pr-record-contextOnly-underscore-alias` — same boot migration, same read-bridge. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_autoObserve`, not on the expired calendar date): delete the `_autoObserve === true` read in _prRecordIsLegacyManaged (engine/shared.js:6823) and the migratePrGateFlags handling of it."
|
|
133
|
+
"targetRemovalDate": "2026-06-25",
|
|
134
|
+
"notes": "The write side already shipped out — there is no dual-write to retire. Removal is paired with `pr-record-contextOnly-underscore-alias` — same boot migration, same read-bridge. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_autoObserve`, not on the expired calendar date): delete the `_autoObserve === true` read in _prRecordIsLegacyManaged (engine/shared.js:6823) and the migratePrGateFlags handling of it. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
"id": "pr-record-manual-underscore-alias",
|
|
@@ -142,8 +142,8 @@
|
|
|
142
142
|
{ "file": "dashboard.js", "note": "Zero `_manual` references — the link handler no longer writes the provenance alias onto the record." }
|
|
143
143
|
],
|
|
144
144
|
"deprecated": "2026-06-08",
|
|
145
|
-
"targetRemovalDate": "2026-06-
|
|
146
|
-
"notes": "Disambiguation: this entry covers ONLY the PR-record `_manual` flag (engine/<scope>/pull-requests.json). Other `_manual`-named flags elsewhere in the engine (work items, dispatch records, etc.) are NOT covered by this entry and remain in active use. The write side already shipped out — there is no write site or dashboard badge left to remove. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_manual`, not on the expired calendar date): drop the `_manual` handling in normalizePrRecord and migratePrGateFlags (engine/shared.js) once the on-disk sweep is confirmed."
|
|
145
|
+
"targetRemovalDate": "2026-06-25",
|
|
146
|
+
"notes": "Disambiguation: this entry covers ONLY the PR-record `_manual` flag (engine/<scope>/pull-requests.json). Other `_manual`-named flags elsewhere in the engine (work items, dispatch records, etc.) are NOT covered by this entry and remain in active use. The write side already shipped out — there is no write site or dashboard badge left to remove. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_manual`, not on the expired calendar date): drop the `_manual` handling in normalizePrRecord and migratePrGateFlags (engine/shared.js) once the on-disk sweep is confirmed. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
149
|
"id": "pr-link-autoObserve-body-param",
|
|
@@ -152,8 +152,8 @@
|
|
|
152
152
|
{ "file": "dashboard.js", "note": "linkPullRequestForTracking resolves `contextOnly` from `body.contextOnly` when boolean, else `autoObserve === undefined ? false : !autoObserve` (dashboard.js:1055-1057). Route registry params string still lists `autoObserve?` (dashboard.js:12680)." }
|
|
153
153
|
],
|
|
154
154
|
"deprecated": "2026-06-08",
|
|
155
|
-
"targetRemovalDate": "2026-06-
|
|
156
|
-
"notes": "Unlike the three record-field aliases, nothing is written here — this is purely an input read-fallback. Removal scope (DEFERRED — gated on confirming no client still POSTs `autoObserve`, not on the expired calendar date): drop the `!body.autoObserve` fallback in the link handler in dashboard.js, drop `autoObserve?` from the route registry `params` string, and update any client (dashboard JS, ops scripts) that still POSTs `autoObserve`. After removal, callers that still send `autoObserve` will see their value silently ignored."
|
|
155
|
+
"targetRemovalDate": "2026-06-25",
|
|
156
|
+
"notes": "Unlike the three record-field aliases, nothing is written here — this is purely an input read-fallback. Removal scope (DEFERRED — gated on confirming no client still POSTs `autoObserve`, not on the expired calendar date): drop the `!body.autoObserve` fallback in the link handler in dashboard.js, drop `autoObserve?` from the route registry `params` string, and update any client (dashboard JS, ops scripts) that still POSTs `autoObserve`. After removal, callers that still send `autoObserve` will see their value silently ignored. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the actual code removal stays a separate human-reviewed PR)."
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"id": "worktreemode-field-rename",
|
package/engine/shared.js
CHANGED
|
@@ -6163,15 +6163,11 @@ function parsePrUrl(url) {
|
|
|
6163
6163
|
// `isPrTargeted` paths intact and prevents PR-feedback fix WIs from being
|
|
6164
6164
|
// dispatched on a fresh `work/<wi-id>` branch when only the description /
|
|
6165
6165
|
// references[] carried the PR pointer (issue #2999).
|
|
6166
|
-
function _trimTrailingPrRefPunctuation(value) {
|
|
6167
|
-
return String(value || '').replace(/[),.;:]+$/g, '');
|
|
6168
|
-
}
|
|
6169
|
-
|
|
6170
6166
|
function extractPrRefFromText(value) {
|
|
6171
6167
|
const text = String(value || '');
|
|
6172
6168
|
if (!text.trim()) return null;
|
|
6173
6169
|
const urlMatch = text.match(/https?:\/\/[^\s<>()]+(?:\/pull\/\d+|\/pullrequest\/\d+)[^\s<>()]*/i);
|
|
6174
|
-
if (urlMatch) return
|
|
6170
|
+
if (urlMatch) return String(urlMatch[0] || '').replace(/[),.;:]+$/g, '');
|
|
6175
6171
|
const canonicalMatch = text.match(/\b(?:github|ado):[^\s#]+#\d+\b/i);
|
|
6176
6172
|
if (canonicalMatch) return canonicalMatch[0];
|
|
6177
6173
|
const legacyMatch = text.match(/\bPR-(\d+)\b/i);
|
package/engine/supervisor.js
CHANGED
|
@@ -27,7 +27,7 @@ require('./stdio-timestamps').installIfNotInstalled();
|
|
|
27
27
|
const fs = require('fs');
|
|
28
28
|
const path = require('path');
|
|
29
29
|
const os = require('os');
|
|
30
|
-
const { spawn, execSync } = require('child_process');
|
|
30
|
+
const { spawn, spawnSync, execSync } = require('child_process');
|
|
31
31
|
|
|
32
32
|
// Lazy path getters route through engine/shared.js when available so the
|
|
33
33
|
// supervisor's notion of "the engine dir" honors MINIONS_TEST_DIR (used by
|
|
@@ -128,6 +128,139 @@ function listeningPidsForPort(port) {
|
|
|
128
128
|
} catch { return []; }
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Reap-before-respawn (supervisor respawn-storm fix).
|
|
132
|
+
//
|
|
133
|
+
// checkEngine/checkDashboard only spawn a replacement — they never killed the
|
|
134
|
+
// orphan. When the engine's liveness signal (control.json `pid`) goes stale
|
|
135
|
+
// because a transient EPERM blocked the heartbeat tmp->rename on Windows, the
|
|
136
|
+
// supervisor kept respawning engines WITHOUT reaping the dead-but-still-running
|
|
137
|
+
// ones. Each extra engine contends harder on control.json -> more EPERM -> the
|
|
138
|
+
// PID stays stale -> unbounded accumulation (observed: 36 engines + 41
|
|
139
|
+
// dashboards, dozens of console-window popups per minute). Reaping every stray
|
|
140
|
+
// process that runs THIS install's engine.js/dashboard.js before spawning the
|
|
141
|
+
// replacement bounds the live count to ~1: a persistent EPERM degrades into a
|
|
142
|
+
// slow 1-in-1-out restart cycle instead of a runaway storm.
|
|
143
|
+
//
|
|
144
|
+
// Only ever called from the respawn branch, i.e. AFTER we've already concluded
|
|
145
|
+
// the watched process is dead — so every match is by definition an orphan that
|
|
146
|
+
// is safe to kill. Scoped to MINIONS_DIR's script path so a second Minions
|
|
147
|
+
// checkout on the same machine is never touched. Opt out via
|
|
148
|
+
// MINIONS_SUPERVISOR_REAP=0.
|
|
149
|
+
const REAP_ENABLED = process.env.MINIONS_SUPERVISOR_REAP !== '0';
|
|
150
|
+
|
|
151
|
+
function _normPath(p) { return String(p).replace(/\\/g, '/').toLowerCase(); }
|
|
152
|
+
|
|
153
|
+
// Split a command line into argv tokens, honoring double-quote grouping
|
|
154
|
+
// (Windows CommandLine quotes any path containing spaces, e.g.
|
|
155
|
+
// `"C:\Program Files\nodejs\node.exe"`). We only need this accurate enough to
|
|
156
|
+
// tell which token is the *script* being executed.
|
|
157
|
+
function _tokenizeCmdline(cmdline) {
|
|
158
|
+
const toks = [];
|
|
159
|
+
let cur = '';
|
|
160
|
+
let inQuote = false;
|
|
161
|
+
for (const ch of String(cmdline)) {
|
|
162
|
+
if (ch === '"') { inQuote = !inQuote; continue; }
|
|
163
|
+
if (!inQuote && (ch === ' ' || ch === '\t')) { if (cur) { toks.push(cur); cur = ''; } continue; }
|
|
164
|
+
cur += ch;
|
|
165
|
+
}
|
|
166
|
+
if (cur) toks.push(cur);
|
|
167
|
+
return toks;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// True iff `cmdline` RUNS `target` as its script — not merely mentions it as an
|
|
171
|
+
// argument. The script is the first `.js` token after the node executable, so
|
|
172
|
+
// `node .../engine.js start` matches but `node eslint.js .../engine.js` (where
|
|
173
|
+
// engine.js is an argument to a different script) does NOT. Exact, normalized
|
|
174
|
+
// equality also rejects longer-name false positives (`.../engine.js.bak`,
|
|
175
|
+
// `.../engine.json`) that a plain substring `includes` would catch. `target` is
|
|
176
|
+
// already normalized by the caller.
|
|
177
|
+
//
|
|
178
|
+
// This is intentionally stricter than substring matching: every orphan the
|
|
179
|
+
// supervisor needs to reap was spawned by spawnEngine()/spawnDashboard() with a
|
|
180
|
+
// byte-identical `path.join(MINIONS_DIR, '<script>.js')`, so exact script-token
|
|
181
|
+
// match catches all of them while a foreign process that only *names* the path
|
|
182
|
+
// is left alone (fail-safe: never kill on a loose match).
|
|
183
|
+
function _cmdRunsScript(cmdline, target) {
|
|
184
|
+
const toks = _tokenizeCmdline(cmdline);
|
|
185
|
+
// toks[0] is the node executable; the script is the first later .js token.
|
|
186
|
+
for (let i = 1; i < toks.length; i++) {
|
|
187
|
+
const norm = _normPath(toks[i]);
|
|
188
|
+
if (norm.endsWith('.js')) return norm === target;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// PIDs of `node` processes that RUN `needle` (a script path) as their script.
|
|
194
|
+
// Cross-platform: PowerShell CIM on Windows, `ps` elsewhere. Best effort —
|
|
195
|
+
// returns [] on any enumeration failure (fail-open: leak rather than risk
|
|
196
|
+
// killing the wrong process).
|
|
197
|
+
//
|
|
198
|
+
// The OS does the coarse filtering (WQL `CommandLine LIKE` / a `grep` on the
|
|
199
|
+
// script basename) so only candidate rows — not the machine's entire node/
|
|
200
|
+
// process table — cross into the JS heap; `_cmdRunsScript` then applies the
|
|
201
|
+
// exact script-position match. On a busy host (CI runners, many node procs)
|
|
202
|
+
// this keeps the buffered stdout to a handful of rows instead of megabytes.
|
|
203
|
+
// `maxBuffer` caps the read defensively in case the coarse filter still matches
|
|
204
|
+
// a lot. The basename is an internal constant (`engine.js`/`dashboard.js`), so
|
|
205
|
+
// it carries no shell/WQL-injection risk.
|
|
206
|
+
const _ENUM_MAX_BUFFER = 8 * 1024 * 1024;
|
|
207
|
+
function listNodePidsMatching(needle) {
|
|
208
|
+
const target = _normPath(needle);
|
|
209
|
+
const base = path.basename(needle); // coarse prefilter, e.g. 'engine.js'
|
|
210
|
+
const pids = [];
|
|
211
|
+
try {
|
|
212
|
+
let out = '';
|
|
213
|
+
if (isWin) {
|
|
214
|
+
// spawnSync (no shell) + -EncodedCommand sidesteps cmd.exe quoting — an
|
|
215
|
+
// inline `powershell -Command "... $(...) | ..."` via execSync gets its
|
|
216
|
+
// `$(...)`/`|` mangled by cmd.exe before PowerShell ever sees them.
|
|
217
|
+
const ps = `Get-CimInstance Win32_Process -Filter "name='node.exe' AND CommandLine LIKE '%${base}%'" | `
|
|
218
|
+
+ 'ForEach-Object { "$($_.ProcessId)|$($_.CommandLine)" }';
|
|
219
|
+
const res = spawnSync('powershell',
|
|
220
|
+
['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(ps, 'utf16le').toString('base64')],
|
|
221
|
+
{ encoding: 'utf8', timeout: 8000, windowsHide: true, maxBuffer: _ENUM_MAX_BUFFER });
|
|
222
|
+
out = (res && res.stdout) || '';
|
|
223
|
+
} else {
|
|
224
|
+
// grep prefilters to candidate rows; no match exits 1 -> execSync throws
|
|
225
|
+
// -> caught below -> [] (correct: nothing to reap).
|
|
226
|
+
out = execSync(`ps -eo pid=,args= | grep -iF -- '${base}'`,
|
|
227
|
+
{ encoding: 'utf8', timeout: 8000, maxBuffer: _ENUM_MAX_BUFFER });
|
|
228
|
+
}
|
|
229
|
+
for (const line of out.split('\n')) {
|
|
230
|
+
const trimmed = line.trim();
|
|
231
|
+
if (!trimmed) continue;
|
|
232
|
+
// Windows rows are "PID|<full command line>"; ps rows are "PID <args>".
|
|
233
|
+
const m = isWin ? trimmed.match(/^(\d+)\|(.*)$/) : trimmed.match(/^(\d+)\s+(.*)$/);
|
|
234
|
+
if (!m) continue;
|
|
235
|
+
const pid = Number(m[1]);
|
|
236
|
+
if (pid && _cmdRunsScript(m[2], target)) pids.push(pid);
|
|
237
|
+
}
|
|
238
|
+
} catch { /* enumeration failed — fail-open with whatever we collected */ }
|
|
239
|
+
return pids;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function _killHard(pid) {
|
|
243
|
+
const shared = _sharedOrNull();
|
|
244
|
+
try {
|
|
245
|
+
if (shared && typeof shared.killImmediate === 'function') { shared.killImmediate({ pid }); return; }
|
|
246
|
+
} catch { /* fall through to platform kill */ }
|
|
247
|
+
try {
|
|
248
|
+
if (isWin) execSync(`taskkill /F /T /PID ${pid}`, { timeout: 5000, windowsHide: true, stdio: 'ignore' });
|
|
249
|
+
else process.kill(pid, 'SIGKILL');
|
|
250
|
+
} catch { /* already gone */ }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Kill every stray process running `scriptPath`, except this supervisor.
|
|
254
|
+
// Returns the number reaped. No-op when MINIONS_SUPERVISOR_REAP=0.
|
|
255
|
+
function reapStrayProcesses(scriptPath, label) {
|
|
256
|
+
if (!REAP_ENABLED) return 0;
|
|
257
|
+
const pids = listNodePidsMatching(scriptPath).filter(p => p !== process.pid);
|
|
258
|
+
if (!pids.length) return 0;
|
|
259
|
+
console.log(`[supervisor] Reaping ${pids.length} stray ${label} process(es) before respawn: ${pids.join(', ')}`);
|
|
260
|
+
for (const pid of pids) _killHard(pid);
|
|
261
|
+
return pids.length;
|
|
262
|
+
}
|
|
263
|
+
|
|
131
264
|
function openAppendFd(name) {
|
|
132
265
|
// Try to use the same append-fd helper the rest of the codebase uses, fall
|
|
133
266
|
// back to a plain fs.openSync(..., 'a') when it isn't available. We re-route
|
|
@@ -226,6 +359,9 @@ function checkEngine(now) {
|
|
|
226
359
|
}
|
|
227
360
|
|
|
228
361
|
console.log(`[supervisor] Engine PID ${control.pid || '(none)'} is dead — respawning...`);
|
|
362
|
+
// Reap any orphan engines first so a stale control.json PID can't snowball
|
|
363
|
+
// into an unbounded pile of contending engine processes.
|
|
364
|
+
reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
|
|
229
365
|
const newPid = spawnEngine();
|
|
230
366
|
_lastEngineRespawnAt = now;
|
|
231
367
|
console.log(`[supervisor] Engine respawned (new PID: ${newPid})`);
|
|
@@ -238,6 +374,10 @@ function checkDashboard(now) {
|
|
|
238
374
|
if (pids.length > 0) return;
|
|
239
375
|
|
|
240
376
|
console.log(`[supervisor] Dashboard not listening on port ${dashPort} — respawning...`);
|
|
377
|
+
// Reap stray dashboards first. Besides bounding the count, this collapses the
|
|
378
|
+
// port-desync case: orphan dashboards holding other ports are killed, freeing
|
|
379
|
+
// the canonical port for the fresh one whose beacon _resolveDashPort() reads.
|
|
380
|
+
reapStrayProcesses(path.join(MINIONS_DIR, 'dashboard.js'), 'dashboard');
|
|
241
381
|
const newPid = spawnDashboard();
|
|
242
382
|
_lastDashboardRespawnAt = now;
|
|
243
383
|
console.log(`[supervisor] Dashboard respawned (new PID: ${newPid})`);
|
|
@@ -313,6 +453,9 @@ module.exports = {
|
|
|
313
453
|
isStopIntentSet,
|
|
314
454
|
isPidAlive,
|
|
315
455
|
listeningPidsForPort,
|
|
456
|
+
listNodePidsMatching,
|
|
457
|
+
reapStrayProcesses,
|
|
458
|
+
_cmdRunsScript,
|
|
316
459
|
openAppendFd,
|
|
317
460
|
checkEngine,
|
|
318
461
|
checkDashboard,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2226",
|
|
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"
|