@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.
@@ -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-16",
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-16",
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-16",
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-16",
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 _trimTrailingPrRefPunctuation(urlMatch[0]);
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);
@@ -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.2224",
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"