@yemi33/minions 0.1.2308 → 0.1.2310
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/bin/minions.js +22 -2
- package/dashboard/js/refresh.js +2 -1
- package/dashboard/js/render-other.js +56 -21
- package/dashboard/styles.css +10 -4
- package/dashboard.js +4 -1
- package/docs/README.md +1 -0
- package/docs/diagnostics-crash-reports.md +132 -0
- package/engine/cleanup.js +13 -0
- package/engine/lifecycle.js +53 -31
- package/engine/shared.js +133 -10
- package/engine/supervisor.js +13 -0
- package/package.json +1 -1
- package/prompts/cc-system.md +1 -1
package/bin/minions.js
CHANGED
|
@@ -391,6 +391,19 @@ function _resolveRestartHealthTimeoutMs() {
|
|
|
391
391
|
return Math.min(300000, Math.max(15000, resolved));
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Read config.json best-effort for callers that spawn engine.js before any
|
|
396
|
+
* config is loaded into memory. Mirrors _resolveRestartHealthTimeoutMs's
|
|
397
|
+
* inline read.
|
|
398
|
+
*/
|
|
399
|
+
function _readConfigJsonSafe() {
|
|
400
|
+
const home = process.env.MINIONS_HOME || (typeof MINIONS_HOME !== 'undefined' ? MINIONS_HOME : null);
|
|
401
|
+
try {
|
|
402
|
+
if (home) return JSON.parse(fs.readFileSync(path.join(home, 'config.json'), 'utf8'));
|
|
403
|
+
} catch { /* config may not exist yet */ }
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
394
407
|
/**
|
|
395
408
|
* Spawn engine + dashboard + supervisor, verify health, optionally open browser.
|
|
396
409
|
* Shared by `minions start` and `minions restart` — restart layers a kill phase
|
|
@@ -401,8 +414,14 @@ function _resolveRestartHealthTimeoutMs() {
|
|
|
401
414
|
function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs }) {
|
|
402
415
|
const engineOut = _openStdioLog('engine-stdio.log');
|
|
403
416
|
const engineErr = _openStdioLog('engine-stdio.log');
|
|
417
|
+
// W-mr2c4i8m0004da94: enable Node's crash-diagnostics report for the
|
|
418
|
+
// engine.js process so a future silent death (the class investigated in
|
|
419
|
+
// W-mr2azk6i) leaves a JSON report under engine/diagnostics/crash-reports
|
|
420
|
+
// instead of vanishing with no trace.
|
|
421
|
+
const crashDiagEnv = shared.getEngineCrashDiagnosticsEnv(_readConfigJsonSafe());
|
|
404
422
|
const engineProc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
|
|
405
|
-
cwd: MINIONS_HOME, stdio: ['ignore', engineOut, engineErr], detached: true, windowsHide: true
|
|
423
|
+
cwd: MINIONS_HOME, stdio: ['ignore', engineOut, engineErr], detached: true, windowsHide: true,
|
|
424
|
+
env: { ...process.env, ...crashDiagEnv }
|
|
406
425
|
});
|
|
407
426
|
engineProc.unref();
|
|
408
427
|
console.log(`\n Engine started (PID: ${engineProc.pid})`);
|
|
@@ -934,7 +953,8 @@ function init() {
|
|
|
934
953
|
? `\n Upgrade complete (${pkgVersion}). Restarting engine and dashboard...\n`
|
|
935
954
|
: '\n Starting engine and dashboard...\n');
|
|
936
955
|
const engineProc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine.js'), 'start'], {
|
|
937
|
-
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
|
956
|
+
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true,
|
|
957
|
+
env: { ...process.env, ...shared.getEngineCrashDiagnosticsEnv(_readConfigJsonSafe()) }
|
|
938
958
|
});
|
|
939
959
|
engineProc.unref();
|
|
940
960
|
console.log(` Engine started (PID: ${engineProc.pid})`);
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -135,7 +135,8 @@ const RENDER_VERSIONS = {
|
|
|
135
135
|
prdPrs: 1,
|
|
136
136
|
inbox: 2,
|
|
137
137
|
// Bumped 4→5 for the clickable checkout-mode pill + picker (W-mr1b67zi0006b788).
|
|
138
|
-
|
|
138
|
+
// Bumped 5→6 for multi-select hybrid liveValidation.type support (W-mr2m1ute000a9c01).
|
|
139
|
+
projects: 6,
|
|
139
140
|
notes: 1,
|
|
140
141
|
prd: 3,
|
|
141
142
|
prs: 3,
|
|
@@ -108,9 +108,11 @@ function _renderWorktreeModePill(p) {
|
|
|
108
108
|
const name = escHtml(p.name);
|
|
109
109
|
const common = ' data-checkout-pill="' + name + '" role="button" tabindex="0" aria-haspopup="true" aria-label="Change checkout mode"';
|
|
110
110
|
if (p.checkoutMode === 'live') {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const types = _liveValidationTypesArray(p);
|
|
112
|
+
if (types.length > 0) {
|
|
113
|
+
const typesText = types.join(', ');
|
|
114
|
+
const vt = escapeHtml(typesText);
|
|
115
|
+
return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid live-validation — coding work items author in isolated worktrees; only the "' + vt + '" validation type(s) run in-place on the live checkout (capped to one mutating dispatch, refused on a dirty tree). Click to change.">⚡ Hybrid · ' + vt + '</span>';
|
|
114
116
|
}
|
|
115
117
|
return ' <span class="project-mode-pill project-mode-live project-mode-pill-clickable"' + common + ' title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch and refused on a dirty tree. Click to change.">⚡ Live checkout</span>';
|
|
116
118
|
}
|
|
@@ -130,6 +132,16 @@ function _findProjectInLastStatus(name) {
|
|
|
130
132
|
return projects.find(function(p) { return p.name === name; }) || null;
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
// Normalize p.liveValidationType (single string legacy shape, OR array of
|
|
136
|
+
// strings — W-mr2m1ute000a9c01 multi-select) into an array of non-empty
|
|
137
|
+
// strings for uniform handling by menu/pill rendering code.
|
|
138
|
+
function _liveValidationTypesArray(p) {
|
|
139
|
+
const raw = p && p.liveValidationType;
|
|
140
|
+
if (!raw) return [];
|
|
141
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
142
|
+
return arr.filter(function(t) { return typeof t === 'string' && t.length > 0; });
|
|
143
|
+
}
|
|
144
|
+
|
|
133
145
|
function _closeCheckoutModeMenu() {
|
|
134
146
|
const existing = document.getElementById('checkout-mode-menu');
|
|
135
147
|
if (existing) existing.remove();
|
|
@@ -183,8 +195,8 @@ function _openCheckoutModeMenu(projectName, anchorEl) {
|
|
|
183
195
|
item.appendChild(desc);
|
|
184
196
|
}
|
|
185
197
|
const isActive = (mode === 'worktree' && project.checkoutMode !== 'live')
|
|
186
|
-
|| (mode === 'live' && project.checkoutMode === 'live' &&
|
|
187
|
-
|| (mode === 'hybrid' && project.checkoutMode === 'live' &&
|
|
198
|
+
|| (mode === 'live' && project.checkoutMode === 'live' && _liveValidationTypesArray(project).length === 0)
|
|
199
|
+
|| (mode === 'hybrid' && project.checkoutMode === 'live' && _liveValidationTypesArray(project).length > 0);
|
|
188
200
|
if (isActive) {
|
|
189
201
|
item.setAttribute('aria-checked', 'true');
|
|
190
202
|
item.classList.add('checkout-mode-menu-item-active');
|
|
@@ -221,9 +233,12 @@ function _openCheckoutModeMenu(projectName, anchorEl) {
|
|
|
221
233
|
if (first) first.focus();
|
|
222
234
|
}
|
|
223
235
|
|
|
224
|
-
// Second step of the picker for hybrid mode: pick which work-item type
|
|
236
|
+
// Second step of the picker for hybrid mode: pick which work-item type(s) run
|
|
225
237
|
// in-place on the live checkout while everything else authors in isolated
|
|
226
|
-
// worktrees. Renders a
|
|
238
|
+
// worktrees. Renders a checkbox list (multi-select, W-mr2m1ute000a9c01) +
|
|
239
|
+
// Back/Apply into the existing menu element. A checkbox list (rather than a
|
|
240
|
+
// native <select multiple>) keeps the existing "click one row" affordance
|
|
241
|
+
// discoverable while allowing more than one type to be selected at once.
|
|
227
242
|
function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
228
243
|
const project = _findProjectInLastStatus(projectName);
|
|
229
244
|
while (menu.firstChild) menu.removeChild(menu.firstChild);
|
|
@@ -235,21 +250,31 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
235
250
|
|
|
236
251
|
const label = document.createElement('div');
|
|
237
252
|
label.className = 'checkout-mode-menu-item';
|
|
238
|
-
label.textContent = 'Work-item type to validate live:';
|
|
253
|
+
label.textContent = 'Work-item type(s) to validate live:';
|
|
239
254
|
menu.appendChild(label);
|
|
240
255
|
|
|
241
|
-
const
|
|
242
|
-
|
|
256
|
+
const currentTypes = _liveValidationTypesArray(project);
|
|
257
|
+
const preselect = currentTypes.length > 0 ? currentTypes : [CHECKOUT_MODE_HYBRID_DEFAULT_TYPE];
|
|
258
|
+
|
|
259
|
+
const checkboxList = document.createElement('div');
|
|
260
|
+
checkboxList.className = 'checkout-mode-menu-checkboxes';
|
|
261
|
+
const checkboxes = [];
|
|
243
262
|
CHECKOUT_MODE_HYBRID_TYPES.forEach(function(t) {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
263
|
+
const row = document.createElement('label');
|
|
264
|
+
row.className = 'checkout-mode-menu-checkbox-row';
|
|
265
|
+
const cb = document.createElement('input');
|
|
266
|
+
cb.type = 'checkbox';
|
|
267
|
+
cb.value = t;
|
|
268
|
+
cb.className = 'checkout-mode-menu-checkbox';
|
|
269
|
+
cb.checked = preselect.includes(t);
|
|
270
|
+
checkboxes.push(cb);
|
|
271
|
+
const text = document.createElement('span');
|
|
272
|
+
text.textContent = t;
|
|
273
|
+
row.appendChild(cb);
|
|
274
|
+
row.appendChild(text);
|
|
275
|
+
checkboxList.appendChild(row);
|
|
248
276
|
});
|
|
249
|
-
|
|
250
|
-
? project.liveValidationType
|
|
251
|
-
: CHECKOUT_MODE_HYBRID_DEFAULT_TYPE;
|
|
252
|
-
menu.appendChild(select);
|
|
277
|
+
menu.appendChild(checkboxList);
|
|
253
278
|
|
|
254
279
|
const actions = document.createElement('div');
|
|
255
280
|
actions.className = 'checkout-mode-menu-actions';
|
|
@@ -270,9 +295,10 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
270
295
|
applyBtn.className = 'checkout-mode-menu-btn checkout-mode-menu-btn-primary';
|
|
271
296
|
applyBtn.textContent = 'Apply';
|
|
272
297
|
applyBtn.addEventListener('click', function() {
|
|
273
|
-
const
|
|
298
|
+
const types = checkboxes.filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.value; });
|
|
299
|
+
if (types.length === 0) return; // require at least one selected type
|
|
274
300
|
_closeCheckoutModeMenu();
|
|
275
|
-
_applyCheckoutModeChange(projectName, 'live',
|
|
301
|
+
_applyCheckoutModeChange(projectName, 'live', types);
|
|
276
302
|
});
|
|
277
303
|
actions.appendChild(applyBtn);
|
|
278
304
|
|
|
@@ -284,12 +310,21 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
284
310
|
// call (mirrors projectChipRemove / removePinnedNote), then POSTs to
|
|
285
311
|
// /api/settings. On failure, reverts the optimistic flip, re-renders, and
|
|
286
312
|
// shows an error toast.
|
|
313
|
+
//
|
|
314
|
+
// `liveValidationType` accepts a single type string (legacy call sites) OR an
|
|
315
|
+
// array of type strings (W-mr2m1ute000a9c01 — multi-select hybrid types);
|
|
316
|
+
// whichever shape is passed through is what gets POSTed as
|
|
317
|
+
// liveValidation.type, so shared.validateLiveValidation on the server decides
|
|
318
|
+
// the final persisted shape.
|
|
287
319
|
async function _applyCheckoutModeChange(projectName, newMode, liveValidationType) {
|
|
288
320
|
const project = _findProjectInLastStatus(projectName);
|
|
289
321
|
if (!project) return;
|
|
322
|
+
const typesForLabel = liveValidationType
|
|
323
|
+
? (Array.isArray(liveValidationType) ? liveValidationType : [liveValidationType])
|
|
324
|
+
: [];
|
|
290
325
|
const label = newMode === 'worktree'
|
|
291
326
|
? 'Worktrees'
|
|
292
|
-
: (
|
|
327
|
+
: (typesForLabel.length > 0 ? ('Hybrid (' + typesForLabel.join(', ') + ')') : 'Live checkout');
|
|
293
328
|
const ok = await confirmDialog({
|
|
294
329
|
title: 'Change checkout mode?',
|
|
295
330
|
message: 'Switch "' + projectName + '" to ' + label + '? This changes how every future dispatch on this project runs.',
|
package/dashboard/styles.css
CHANGED
|
@@ -1330,11 +1330,17 @@
|
|
|
1330
1330
|
font-size: var(--text-xs); color: var(--muted); font-weight: 400;
|
|
1331
1331
|
margin-top: 2px; line-height: 1.35;
|
|
1332
1332
|
}
|
|
1333
|
-
.checkout-mode-menu-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
border-radius: 4px;
|
|
1333
|
+
.checkout-mode-menu-checkboxes {
|
|
1334
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
1335
|
+
margin: 4px 0 8px; max-height: 220px; overflow-y: auto;
|
|
1337
1336
|
}
|
|
1337
|
+
.checkout-mode-menu-checkbox-row {
|
|
1338
|
+
display: flex; align-items: center; gap: 6px;
|
|
1339
|
+
padding: 4px 6px; border-radius: 4px; cursor: pointer;
|
|
1340
|
+
color: var(--text); font-size: var(--text-sm);
|
|
1341
|
+
}
|
|
1342
|
+
.checkout-mode-menu-checkbox-row:hover { background: var(--surface); }
|
|
1343
|
+
.checkout-mode-menu-checkbox { cursor: pointer; }
|
|
1338
1344
|
.checkout-mode-menu-actions {
|
|
1339
1345
|
display: flex; justify-content: flex-end; gap: 6px; padding-top: 4px;
|
|
1340
1346
|
}
|
package/dashboard.js
CHANGED
|
@@ -4300,7 +4300,10 @@ function _resetPreambleCache() {
|
|
|
4300
4300
|
function _projectCheckoutModeLabel(p) {
|
|
4301
4301
|
const mode = shared.resolveCheckoutMode(p);
|
|
4302
4302
|
if (mode === 'live' && p && p.liveValidation && p.liveValidation.type) {
|
|
4303
|
-
|
|
4303
|
+
// liveValidation.type may be a single string (legacy) or an array of
|
|
4304
|
+
// strings (W-mr2m1ute000a9c01 — multi-select hybrid types); render all.
|
|
4305
|
+
const types = Array.isArray(p.liveValidation.type) ? p.liveValidation.type : [p.liveValidation.type];
|
|
4306
|
+
return `hybrid (live-validation: ${types.join(', ')})`;
|
|
4304
4307
|
}
|
|
4305
4308
|
return mode;
|
|
4306
4309
|
}
|
package/docs/README.md
CHANGED
|
@@ -61,6 +61,7 @@ Operational runbooks for engine operators and fleet maintainers.
|
|
|
61
61
|
- [notes.md](notes.md) — Consolidated team knowledge: patterns, conventions, bugs, and build findings accumulated from agent inbox notes. Read by the engine and injected into agent prompts as shared context.
|
|
62
62
|
- [auto-discovery.md](auto-discovery.md) — Auto-discovery and execution pipeline: the per-tick orchestration loop and the four work-discovery sources.
|
|
63
63
|
- [diagnostics-memory.md](diagnostics-memory.md) — Operator runbook for the in-process memory + perf observability surface: `/api/diagnostics/memory[/history]`, `/api/diagnostics/heap-snapshot` guard-token capture, `MEMORY_BASELINE` log emissions, `--cpu-prof`/`--heap-prof` capture, and the `test/perf/soak.test.js` heap-growth regression gate.
|
|
64
|
+
- [diagnostics-crash-reports.md](diagnostics-crash-reports.md) — Proactive Node crash-diagnostics reports (`--report-on-fatalerror --report-on-signal --diagnostic-dir=...`) for the engine.js process on Windows: where reports land, config/opt-out, and retention.
|
|
64
65
|
- [engine-restart.md](engine-restart.md) — How agents survive an engine restart: state persistence, the 20-minute startup grace period, and orphan reattachment via PID files and `live-output.log`.
|
|
65
66
|
- [human-vs-automated.md](human-vs-automated.md) — Quick reference table of which features humans start, run, decide, and recover, and the two human approval gates.
|
|
66
67
|
- [kb-sweep.md](kb-sweep.md) — Knowledge-base sweep runbook: how `engine/kb-sweep.js` consolidates `notes/inbox/` into `knowledge/` and survives `minions restart`.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Diagnostics: crash reports for the engine.js process (Windows)
|
|
2
|
+
|
|
3
|
+
Operator runbook for the proactive crash-diagnostics feature shipped for
|
|
4
|
+
`W-mr2c4i8m0004da94`. Follow-up from `W-mr2azk6i` (2026-07-01 engine.js
|
|
5
|
+
crash-loop investigation): a run of 4 silent `engine.js` deaths left no
|
|
6
|
+
exception/stack trace in `engine-stdio.log`, no Windows Event Viewer
|
|
7
|
+
entries (Application/System logs were empty for the window), and no crash
|
|
8
|
+
dumps in `C:\Windows\Minidump` or `%LOCALAPPDATA%\CrashDumps`. The box had
|
|
9
|
+
no crash-event logging wired up at all, so a genuine native/access-violation
|
|
10
|
+
crash of `node.exe` would have vanished without a trace.
|
|
11
|
+
|
|
12
|
+
## What this does
|
|
13
|
+
|
|
14
|
+
Every spawn of `engine.js` — via `minions start`/`restart` (`bin/minions.js`)
|
|
15
|
+
**and** the supervisor's respawn-on-death path (`engine/supervisor.js`) —
|
|
16
|
+
now gets Node's own diagnostic-report flags folded into `NODE_OPTIONS`:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
--report-on-fatalerror --report-on-signal --diagnostic-dir=<MINIONS_HOME>/engine/diagnostics/crash-reports
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This is a zero-dependency, built-in Node feature (no Sysinternals ProcDump
|
|
23
|
+
or other external tool needed):
|
|
24
|
+
|
|
25
|
+
- **`--report-on-fatalerror`** — writes a JSON diagnostic report the moment
|
|
26
|
+
the process hits an unrecoverable V8 error: out-of-memory, JS stack
|
|
27
|
+
overflow, or a native-addon crash. This is exactly the class of death a
|
|
28
|
+
silent `node.exe` disappearance on Windows would otherwise hide.
|
|
29
|
+
- **`--report-on-signal`** — lets an operator request a report on demand by
|
|
30
|
+
sending the configured signal to the running engine PID: `SIGUSR2` on
|
|
31
|
+
POSIX, `SIGBREAK` on Windows (Node's default `--report-signal`). Useful to
|
|
32
|
+
capture a report while a hang is in progress, before deciding to kill it.
|
|
33
|
+
- **`--diagnostic-dir=...`** — where both of the above land.
|
|
34
|
+
|
|
35
|
+
Reports are plain JSON (`report.<timestamp>.<pid>.<seq>.json`) containing
|
|
36
|
+
the JS/native stack, loaded modules, resource usage, and libuv handle
|
|
37
|
+
summary at the moment of the event — see [Node's diagnostic report
|
|
38
|
+
docs](https://nodejs.org/api/report.html) for the full schema.
|
|
39
|
+
|
|
40
|
+
## Where reports land
|
|
41
|
+
|
|
42
|
+
`<MINIONS_HOME>/engine/diagnostics/crash-reports/` — a sibling of the
|
|
43
|
+
existing `engine/diagnostics/` directory used for heap snapshots and
|
|
44
|
+
CPU/heap profiles (see [docs/diagnostics-memory.md](diagnostics-memory.md)
|
|
45
|
+
§3–4), in its own subfolder so retention sweeps don't collide. The directory
|
|
46
|
+
is gitignored and created on first spawn if missing
|
|
47
|
+
(`getEngineCrashDiagnosticsEnv`, [`engine/shared.js`](../engine/shared.js)).
|
|
48
|
+
|
|
49
|
+
## Config + opt-out
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"engine": {
|
|
54
|
+
"crashDiagnostics": {
|
|
55
|
+
"enabled": true,
|
|
56
|
+
"retainCount": 20
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `enabled` (default `true`) — set `false` to disable the NODE_OPTIONS
|
|
63
|
+
injection entirely (falls back to the pre-existing no-diagnostics
|
|
64
|
+
behavior).
|
|
65
|
+
- `retainCount` (default `20`) — how many `report.*.json` files are kept
|
|
66
|
+
before the oldest are pruned. Restart the engine (`minions restart`)
|
|
67
|
+
after editing `enabled`, since it only takes effect at the next spawn.
|
|
68
|
+
|
|
69
|
+
Both live in `ENGINE_DEFAULTS.crashDiagnostics`
|
|
70
|
+
([`engine/shared.js`](../engine/shared.js)).
|
|
71
|
+
|
|
72
|
+
## Retention / cleanup
|
|
73
|
+
|
|
74
|
+
Reports are pruned by `pruneCrashDiagnosticsReports` (`engine/shared.js`),
|
|
75
|
+
called from `engine/cleanup.js#runCleanup` — the same periodic cleanup pass
|
|
76
|
+
that already runs every `ENGINE_DEFAULTS.cleanupEvery` ticks (default 60
|
|
77
|
+
ticks ≈ 10 min at the default 10 s tick interval). It keeps the newest
|
|
78
|
+
`retainCount` files by mtime and deletes the rest, mirroring the existing
|
|
79
|
+
heap-snapshot retention pattern in `dashboard.js#_heapSnapshotPrune`. Pruning
|
|
80
|
+
is best-effort: a missing directory or a transient Windows file lock is
|
|
81
|
+
swallowed and retried on the next cleanup pass.
|
|
82
|
+
|
|
83
|
+
## Idempotency across the CLI → supervisor respawn chain
|
|
84
|
+
|
|
85
|
+
Both `bin/minions.js` (initial spawn) and `engine/supervisor.js` (respawn on
|
|
86
|
+
death) call the same `getEngineCrashDiagnosticsEnv(config)` helper. It checks
|
|
87
|
+
whether the caller's `NODE_OPTIONS` already contains `--diagnostic-dir=`
|
|
88
|
+
(which a spawned child inherits from its parent's env) and returns `{}`
|
|
89
|
+
unchanged if so — so a CLI-spawned engine that later gets respawned by the
|
|
90
|
+
supervisor never accumulates duplicate flags.
|
|
91
|
+
|
|
92
|
+
## Inspecting a report after a crash
|
|
93
|
+
|
|
94
|
+
```powershell
|
|
95
|
+
Get-ChildItem "$env:USERPROFILE\.minions\engine\diagnostics\crash-reports" |
|
|
96
|
+
Sort-Object LastWriteTime -Descending | Select-Object -First 5
|
|
97
|
+
|
|
98
|
+
Get-Content "$env:USERPROFILE\.minions\engine\diagnostics\crash-reports\report.<ts>.<pid>.001.json" |
|
|
99
|
+
ConvertFrom-Json | Select-Object javascriptStack, header
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Key fields to check first: `header.event` (what triggered the report,
|
|
103
|
+
e.g. `"FatalError"` or `"Signal"`), `javascriptStack`, `nativeStack`, and
|
|
104
|
+
`resourceUsage` (memory/CPU at the moment of the event).
|
|
105
|
+
|
|
106
|
+
## Requesting a report on demand (hang investigation)
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
# Find the engine PID from engine/control.json, then:
|
|
110
|
+
# Windows: send SIGBREAK-equivalent via node's report trigger utility, or
|
|
111
|
+
# use Node's process.report.writeReport() interactively if you have a REPL
|
|
112
|
+
# attached. The simplest cross-platform trigger is `process.kill(pid, 'SIGBREAK')`
|
|
113
|
+
# from another Node process on Windows.
|
|
114
|
+
node -e "process.kill(<pid>, 'SIGBREAK')"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A fresh `report.*.json` should appear in the crash-reports directory within
|
|
118
|
+
a second or two, without killing the process.
|
|
119
|
+
|
|
120
|
+
## Related modules and tests
|
|
121
|
+
|
|
122
|
+
- Module: [`engine/shared.js`](../engine/shared.js) — `CRASH_REPORTS_DIR`,
|
|
123
|
+
`getEngineCrashDiagnosticsEnv`, `pruneCrashDiagnosticsReports`,
|
|
124
|
+
`ENGINE_DEFAULTS.crashDiagnostics`.
|
|
125
|
+
- Spawn wiring: [`bin/minions.js`](../bin/minions.js)
|
|
126
|
+
(`spawnFullStackAndVerify`, the post-install/upgrade auto-start path) and
|
|
127
|
+
[`engine/supervisor.js`](../engine/supervisor.js) (`spawnEngine`).
|
|
128
|
+
- Cleanup wiring: [`engine/cleanup.js`](../engine/cleanup.js) `runCleanup`.
|
|
129
|
+
- Tests: [`test/unit/crash-diagnostics.test.js`](../test/unit/crash-diagnostics.test.js).
|
|
130
|
+
- Related: [docs/diagnostics-memory.md](diagnostics-memory.md) (memory/GC/heap
|
|
131
|
+
observability — the sibling diagnostics surface this doc's directory lives
|
|
132
|
+
next to).
|
package/engine/cleanup.js
CHANGED
|
@@ -1633,6 +1633,19 @@ async function runCleanup(config, verbose = false) {
|
|
|
1633
1633
|
cleaned.knowledgeScratch = reapKnowledgeScratch();
|
|
1634
1634
|
} catch (e) { log('warn', `reapKnowledgeScratch: ${e.message}`); }
|
|
1635
1635
|
|
|
1636
|
+
// 18. Prune old Node crash-diagnostics reports (W-mr2c4i8m0004da94). Node
|
|
1637
|
+
// writes `report.*.json` to CRASH_REPORTS_DIR on a fatal V8 error or
|
|
1638
|
+
// `--report-on-signal` request — see getEngineCrashDiagnosticsEnv. Keep
|
|
1639
|
+
// only the newest config.engine.crashDiagnostics.retainCount (default 20)
|
|
1640
|
+
// so a crash-loop or repeated on-demand reports don't accumulate unbounded.
|
|
1641
|
+
cleaned.crashReports = 0;
|
|
1642
|
+
try {
|
|
1643
|
+
cleaned.crashReports = shared.pruneCrashDiagnosticsReports(config);
|
|
1644
|
+
if (cleaned.crashReports > 0) {
|
|
1645
|
+
log('info', `cleanup: pruned ${cleaned.crashReports} old crash-diagnostics report(s)`);
|
|
1646
|
+
}
|
|
1647
|
+
} catch (e) { log('warn', `pruneCrashDiagnosticsReports: ${e.message}`); }
|
|
1648
|
+
|
|
1636
1649
|
return cleaned;
|
|
1637
1650
|
}
|
|
1638
1651
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -6503,9 +6503,20 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6503
6503
|
|
|
6504
6504
|
// M003 — After a coding WI completes successfully, auto-dispatch a live-validation
|
|
6505
6505
|
// WI when project.liveValidation.autoDispatch === true and the completed item is
|
|
6506
|
-
// a coding WI (not the validation
|
|
6507
|
-
//
|
|
6508
|
-
//
|
|
6506
|
+
// a coding WI (not one of the validation types itself). Skips if the coding WI
|
|
6507
|
+
// has no PR.
|
|
6508
|
+
//
|
|
6509
|
+
// liveValidation.type may be a single string (legacy / back-compat) or an
|
|
6510
|
+
// array of strings (W-mr2m1ute000a9c01 — multi-select hybrid types). When an
|
|
6511
|
+
// array, ONE validation WI is dispatched per configured type (each validation
|
|
6512
|
+
// WI still carries a single canonical `type` field so downstream routing —
|
|
6513
|
+
// playbooks, agent resolution, resolveCheckoutMode's membership check — keeps
|
|
6514
|
+
// working per-type).
|
|
6515
|
+
//
|
|
6516
|
+
// Deduplicates per (codingWiId, validationType) pair: a non-terminal WI with
|
|
6517
|
+
// meta.liveValidationFor === codingWiId AND meta.liveValidationType === type
|
|
6518
|
+
// blocks a second dispatch of that specific type, so multiple configured
|
|
6519
|
+
// types don't collide or skip each other.
|
|
6509
6520
|
function autoDispatchLiveValidationWi(meta, config) {
|
|
6510
6521
|
const item = meta?.item;
|
|
6511
6522
|
if (!item?.id || !item?.type) return;
|
|
@@ -6521,8 +6532,12 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6521
6532
|
const lv = project.liveValidation;
|
|
6522
6533
|
if (!lv || lv.autoDispatch !== true || !lv.type) return;
|
|
6523
6534
|
|
|
6524
|
-
|
|
6525
|
-
|
|
6535
|
+
const lvTypes = Array.isArray(lv.type) ? lv.type : [lv.type];
|
|
6536
|
+
|
|
6537
|
+
// Only auto-dispatch for coding WIs — skip types that ARE one of the
|
|
6538
|
+
// configured validation types (avoid an infinite validate-the-validation loop).
|
|
6539
|
+
const typesToDispatch = lvTypes.filter(t => t !== item.type);
|
|
6540
|
+
if (typesToDispatch.length === 0) return;
|
|
6526
6541
|
|
|
6527
6542
|
// Resolve PR reference: prefer the canonical stamped _pr field (set by
|
|
6528
6543
|
// stampWiPrRef which runs earlier in runPostCompletionHooks), then fall
|
|
@@ -6538,34 +6553,41 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6538
6553
|
mutateWorkItems(wiPath, items => {
|
|
6539
6554
|
if (!Array.isArray(items)) return items;
|
|
6540
6555
|
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6556
|
+
for (const validationType of typesToDispatch) {
|
|
6557
|
+
// Dedup: skip if a non-terminal WI already tracks this (coding WI, type)
|
|
6558
|
+
// pair. Falls back to the WI's own `type` field when meta.liveValidationType
|
|
6559
|
+
// is absent (back-compat with validation WIs created before this field
|
|
6560
|
+
// existed, e.g. single-type configs from before W-mr2m1ute000a9c01).
|
|
6561
|
+
const existing = items.find(i =>
|
|
6562
|
+
i &&
|
|
6563
|
+
i.meta &&
|
|
6564
|
+
i.meta.liveValidationFor === codingWiId &&
|
|
6565
|
+
(i.meta.liveValidationType === validationType ||
|
|
6566
|
+
(!i.meta.liveValidationType && i.type === validationType)) &&
|
|
6567
|
+
!PLAN_TERMINAL_STATUSES.has(i.status)
|
|
6568
|
+
);
|
|
6569
|
+
if (existing) {
|
|
6570
|
+
log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} (type=${validationType}) already exists for ${codingWiId}`);
|
|
6571
|
+
continue;
|
|
6572
|
+
}
|
|
6552
6573
|
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6574
|
+
const validationWi = {
|
|
6575
|
+
id: 'W-' + shared.uid(),
|
|
6576
|
+
title: 'Validate: ' + (item.title || codingWiId),
|
|
6577
|
+
type: validationType,
|
|
6578
|
+
status: WI_STATUS.PENDING,
|
|
6579
|
+
depends_on: [codingWiId],
|
|
6580
|
+
references: [prRef],
|
|
6581
|
+
meta: { liveValidationFor: codingWiId, liveValidationType: validationType },
|
|
6582
|
+
project: projectName,
|
|
6583
|
+
priority: item.priority || 'medium',
|
|
6584
|
+
created: ts(),
|
|
6585
|
+
createdBy: 'lifecycle:live-validation-auto-dispatch',
|
|
6586
|
+
};
|
|
6566
6587
|
|
|
6567
|
-
|
|
6568
|
-
|
|
6588
|
+
items.push(validationWi);
|
|
6589
|
+
log('info', `liveValidation: auto-dispatched validation WI ${validationWi.id} (type=${validationType}) for coding WI ${codingWiId}`);
|
|
6590
|
+
}
|
|
6569
6591
|
return items;
|
|
6570
6592
|
});
|
|
6571
6593
|
} catch (err) {
|
package/engine/shared.js
CHANGED
|
@@ -58,6 +58,13 @@ const MINIONS_DIR = process.env.MINIONS_TEST_DIR || resolveMinionsHome(false, {
|
|
|
58
58
|
preferSourceCheckout: true,
|
|
59
59
|
});
|
|
60
60
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
61
|
+
// W-mr2c4i8m0004da94: Node's built-in `--diagnostic-dir` target for the
|
|
62
|
+
// engine.js process's crash-diagnostics report (see
|
|
63
|
+
// getEngineCrashDiagnosticsEnv / pruneCrashDiagnosticsReports below and
|
|
64
|
+
// docs/diagnostics-crash-reports.md). Sibling of the existing
|
|
65
|
+
// engine/diagnostics/ heap-snapshot dir, own subfolder so retention sweeps
|
|
66
|
+
// don't collide with heap-snapshot pruning.
|
|
67
|
+
const CRASH_REPORTS_DIR = path.join(ENGINE_DIR, 'diagnostics', 'crash-reports');
|
|
61
68
|
const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
|
|
62
69
|
const COOLDOWNS_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
63
70
|
// W-mp60tw0u000j3931: Persistent cross-restart engine state (migration markers,
|
|
@@ -1633,6 +1640,69 @@ function openAppendLogFd(name, dir, opts) {
|
|
|
1633
1640
|
}
|
|
1634
1641
|
}
|
|
1635
1642
|
|
|
1643
|
+
// W-mr2c4i8m0004da94: proactive crash diagnostics for the engine.js node
|
|
1644
|
+
// process on Windows. Follow-up from W-mr2azk6i (2026-07-01 crash-loop
|
|
1645
|
+
// investigation): 4 silent engine.js deaths with no exception in
|
|
1646
|
+
// engine-stdio.log, no Windows Event Viewer entries, and no crash dumps —
|
|
1647
|
+
// this box had no crash-event logging wired up for a genuine native/AV
|
|
1648
|
+
// crash. Node's own diagnostic-report feature is a zero-dependency fix:
|
|
1649
|
+
// `--report-on-fatalerror` writes a JSON report on unrecoverable V8 errors
|
|
1650
|
+
// (OOM, stack overflow, native-addon segfault) and `--report-on-signal`
|
|
1651
|
+
// lets an operator request one on demand (SIGUSR2 on POSIX, SIGBREAK on
|
|
1652
|
+
// Windows) without installing anything (e.g. Sysinternals ProcDump).
|
|
1653
|
+
//
|
|
1654
|
+
// Callers: bin/minions.js (CLI `start`/`restart` spawn) and
|
|
1655
|
+
// engine/supervisor.js (respawn-on-death). Both spawn engine.js as a fresh
|
|
1656
|
+
// process before any config is loaded into memory, so this reads config.json
|
|
1657
|
+
// directly rather than expecting an already-parsed config object — pass the
|
|
1658
|
+
// parsed object in if the caller already has it (e.g. from its own
|
|
1659
|
+
// best-effort config.json read), or `null`/`undefined` to use the defaults.
|
|
1660
|
+
//
|
|
1661
|
+
// Idempotent: if the caller's NODE_OPTIONS already carries
|
|
1662
|
+
// `--diagnostic-dir=`, returns `{}` unchanged so the CLI's spawn -> a later
|
|
1663
|
+
// supervisor respawn -> another respawn chain never piles up duplicate
|
|
1664
|
+
// flags (child processes inherit NODE_OPTIONS from their parent env).
|
|
1665
|
+
function getEngineCrashDiagnosticsEnv(config, baseEnv) {
|
|
1666
|
+
const env = baseEnv || process.env;
|
|
1667
|
+
const cfg = (config && config.engine && config.engine.crashDiagnostics) || {};
|
|
1668
|
+
const defaults = ENGINE_DEFAULTS.crashDiagnostics || {};
|
|
1669
|
+
const enabled = cfg.enabled !== undefined ? cfg.enabled !== false : defaults.enabled !== false;
|
|
1670
|
+
if (!enabled) return {};
|
|
1671
|
+
const existing = String(env.NODE_OPTIONS || '');
|
|
1672
|
+
if (existing.includes('--diagnostic-dir=')) return {};
|
|
1673
|
+
try { fs.mkdirSync(CRASH_REPORTS_DIR, { recursive: true }); } catch { /* best effort — Node still tries to write on crash */ }
|
|
1674
|
+
const flags = `--report-on-fatalerror --report-on-signal --diagnostic-dir=${CRASH_REPORTS_DIR}`;
|
|
1675
|
+
return { NODE_OPTIONS: existing ? `${existing} ${flags}` : flags };
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// Prune `report.*.json` files in CRASH_REPORTS_DIR down to the most-recent
|
|
1679
|
+
// `retainCount` (config.engine.crashDiagnostics.retainCount, default 20) by
|
|
1680
|
+
// mtime, mirroring dashboard.js's `_heapSnapshotPrune` retention pattern so
|
|
1681
|
+
// a leak-hunt session (or a genuine crash-loop) doesn't fill the disk with
|
|
1682
|
+
// reports unbounded. Best-effort: missing dir / unreadable stat / failed
|
|
1683
|
+
// unlink are swallowed. Called from engine/cleanup.js#runCleanup (fires
|
|
1684
|
+
// every ENGINE_DEFAULTS.cleanupEvery ticks, ~10 min at default tick
|
|
1685
|
+
// interval) so retention happens automatically without a dedicated sweep.
|
|
1686
|
+
function pruneCrashDiagnosticsReports(config) {
|
|
1687
|
+
const cfg = (config && config.engine && config.engine.crashDiagnostics) || {};
|
|
1688
|
+
const defaults = ENGINE_DEFAULTS.crashDiagnostics || {};
|
|
1689
|
+
const retain = Number.isFinite(cfg.retainCount) ? cfg.retainCount : (defaults.retainCount || 20);
|
|
1690
|
+
let names;
|
|
1691
|
+
try { names = fs.readdirSync(CRASH_REPORTS_DIR); } catch { return 0; }
|
|
1692
|
+
const candidates = names.filter(n => n.startsWith('report.') && n.endsWith('.json'));
|
|
1693
|
+
if (candidates.length <= retain) return 0;
|
|
1694
|
+
const stamped = candidates.map(n => {
|
|
1695
|
+
try { return { n, mtime: fs.statSync(path.join(CRASH_REPORTS_DIR, n)).mtimeMs }; }
|
|
1696
|
+
catch { return { n, mtime: 0 }; }
|
|
1697
|
+
});
|
|
1698
|
+
stamped.sort((a, b) => b.mtime - a.mtime); // newest first
|
|
1699
|
+
let removed = 0;
|
|
1700
|
+
for (const s of stamped.slice(retain)) {
|
|
1701
|
+
try { fs.unlinkSync(path.join(CRASH_REPORTS_DIR, s.n)); removed++; } catch { /* best effort */ }
|
|
1702
|
+
}
|
|
1703
|
+
return removed;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1636
1706
|
function withFileLock(lockPath, fn, {
|
|
1637
1707
|
timeoutMs = 5000,
|
|
1638
1708
|
retryDelayMs = 25,
|
|
@@ -2783,8 +2853,14 @@ function resolveCheckoutMode(project, workItemType) {
|
|
|
2783
2853
|
}
|
|
2784
2854
|
if (canonical === CHECKOUT_MODES.LIVE) {
|
|
2785
2855
|
// Apply liveValidation routing when the block is present and workItemType is provided.
|
|
2856
|
+
// liveValidation.type may be a single string (legacy / back-compat) or an
|
|
2857
|
+
// array of strings (W-mr2m1ute000a9c01, multi-select hybrid types) —
|
|
2858
|
+
// normalize to an array and do a membership check so either shape works.
|
|
2786
2859
|
if (project.liveValidation && workItemType !== undefined) {
|
|
2787
|
-
|
|
2860
|
+
const lvTypes = Array.isArray(project.liveValidation.type)
|
|
2861
|
+
? project.liveValidation.type
|
|
2862
|
+
: [project.liveValidation.type];
|
|
2863
|
+
return lvTypes.includes(workItemType)
|
|
2788
2864
|
? CHECKOUT_MODES.LIVE
|
|
2789
2865
|
: CHECKOUT_MODES.WORKTREE;
|
|
2790
2866
|
}
|
|
@@ -2840,13 +2916,26 @@ function validateCheckoutMode(value) {
|
|
|
2840
2916
|
// Validate + normalize a per-project `liveValidation` block (hybrid mode).
|
|
2841
2917
|
// Hybrid = checkoutMode:'live' + liveValidation:{ type, autoDispatch }: coding
|
|
2842
2918
|
// work items author in isolated worktrees (escaping the live cap) while only
|
|
2843
|
-
// work items whose type
|
|
2919
|
+
// work items whose type is IN liveValidation.type run in-place on the live
|
|
2844
2920
|
// checkout. See resolveCheckoutMode (which routes per work-item type) and
|
|
2845
2921
|
// docs/live-checkout-mode.md.
|
|
2846
2922
|
//
|
|
2923
|
+
// `type` may be a single work-item-type string (legacy / back-compat) OR a
|
|
2924
|
+
// non-empty array of non-empty-string work-item types
|
|
2925
|
+
// (W-mr2m1ute000a9c01 — multi-select hybrid types, e.g. both "implement" and
|
|
2926
|
+
// "fix" validate live while other types stay in worktrees). Every reader of
|
|
2927
|
+
// liveValidation.type (resolveCheckoutMode, lifecycle.autoDispatchLiveValidationWi,
|
|
2928
|
+
// dashboard project mapper / pill / CC label) must handle BOTH shapes —
|
|
2929
|
+
// existing config.json entries with a plain string `type` continue to work
|
|
2930
|
+
// unchanged, no migration required.
|
|
2931
|
+
//
|
|
2847
2932
|
// Returns:
|
|
2848
2933
|
// - undefined → caller should clear the field (empty / null / '' input)
|
|
2849
|
-
// - { type, autoDispatch? } normalized object on success
|
|
2934
|
+
// - { type, autoDispatch? } normalized object on success, where `type` is
|
|
2935
|
+
// whatever shape the caller passed in (string stays a string; array stays
|
|
2936
|
+
// a deduped array) — we intentionally do NOT force single-string input
|
|
2937
|
+
// into an array so already-persisted single-type configs round-trip
|
|
2938
|
+
// byte-identical through a Settings save that doesn't touch this field.
|
|
2850
2939
|
// Throws HTTP 400 (via _httpError) on malformed input, or when the effective
|
|
2851
2940
|
// checkout mode is not 'live' (liveValidation is meaningless without it — it is
|
|
2852
2941
|
// silently ignored at resolve time, so we refuse to persist a misconfiguration).
|
|
@@ -2863,15 +2952,33 @@ function validateLiveValidation(value, opts) {
|
|
|
2863
2952
|
if (checkoutMode !== CHECKOUT_MODES.LIVE) {
|
|
2864
2953
|
throw _httpError(400, 'liveValidation requires checkoutMode "live" (hybrid mode). Set checkoutMode to "live" first, or clear liveValidation.');
|
|
2865
2954
|
}
|
|
2866
|
-
const
|
|
2867
|
-
|
|
2868
|
-
|
|
2955
|
+
const typeErrorMsg = 'Invalid liveValidation: "type" is required and must be a non-empty string, or a non-empty array of non-empty-string work-item types — the work-item type(s) that run on the live checkout (e.g. "build-and-test" or ["implement", "fix"]).';
|
|
2956
|
+
const rawType = value.type;
|
|
2957
|
+
let out;
|
|
2958
|
+
if (typeof rawType === 'string') {
|
|
2959
|
+
const trimmed = rawType.trim();
|
|
2960
|
+
if (!trimmed) throw _httpError(400, typeErrorMsg);
|
|
2961
|
+
out = { type: trimmed };
|
|
2962
|
+
} else if (Array.isArray(rawType)) {
|
|
2963
|
+
if (!rawType.every(t => typeof t === 'string' && t.trim().length > 0)) {
|
|
2964
|
+
throw _httpError(400, typeErrorMsg);
|
|
2965
|
+
}
|
|
2966
|
+
// Dedupe (preserving first-seen order) and cap at the number of distinct
|
|
2967
|
+
// work-item types the engine recognizes — a longer array can only be
|
|
2968
|
+
// full of duplicates or invented types.
|
|
2969
|
+
const deduped = [...new Set(rawType.map(t => t.trim()))];
|
|
2970
|
+
if (deduped.length === 0) throw _httpError(400, typeErrorMsg);
|
|
2971
|
+
if (deduped.length > VALID_WORK_TYPES.size) {
|
|
2972
|
+
throw _httpError(400, `Invalid liveValidation: "type" array has ${deduped.length} entries, more than the ${VALID_WORK_TYPES.size} distinct work-item types the engine recognizes — remove duplicates or invalid types.`);
|
|
2973
|
+
}
|
|
2974
|
+
out = { type: deduped };
|
|
2975
|
+
} else {
|
|
2976
|
+
throw _httpError(400, typeErrorMsg);
|
|
2869
2977
|
}
|
|
2870
|
-
const out = { type };
|
|
2871
2978
|
// autoDispatch (optional): when true, lifecycle auto-creates a validation WI
|
|
2872
|
-
//
|
|
2873
|
-
// autoDispatchLiveValidationWi). Coerce to a strict
|
|
2874
|
-
// off so the operator opts in explicitly.
|
|
2979
|
+
// per configured `type` after each coding WI completes with a PR
|
|
2980
|
+
// (engine/lifecycle.js autoDispatchLiveValidationWi). Coerce to a strict
|
|
2981
|
+
// boolean; absent leaves it off so the operator opts in explicitly.
|
|
2875
2982
|
if (Object.prototype.hasOwnProperty.call(value, 'autoDispatch')) {
|
|
2876
2983
|
out.autoDispatch = !!value.autoDispatch;
|
|
2877
2984
|
}
|
|
@@ -3156,6 +3263,19 @@ const ENGINE_DEFAULTS = {
|
|
|
3156
3263
|
// at the default 10s tickInterval. Set to 0 (or any non-positive integer)
|
|
3157
3264
|
// to disable both the log emission and sidecar write cleanly (operator opt-out).
|
|
3158
3265
|
memoryBaselineEveryTicks: 6,
|
|
3266
|
+
// W-mr2c4i8m0004da94: proactive crash diagnostics for the engine.js
|
|
3267
|
+
// node.exe process. When enabled (default), every spawn of engine.js
|
|
3268
|
+
// (bin/minions.js `start`/`restart` AND engine/supervisor.js respawn-on-
|
|
3269
|
+
// death) gets NODE_OPTIONS += `--report-on-fatalerror --report-on-signal
|
|
3270
|
+
// --diagnostic-dir=<CRASH_REPORTS_DIR>` so a fatal V8 error (OOM, stack
|
|
3271
|
+
// overflow, native-addon crash) or an operator-sent signal
|
|
3272
|
+
// (SIGUSR2 POSIX / SIGBREAK Windows) leaves a JSON diagnostic report
|
|
3273
|
+
// instead of a silent death. See getEngineCrashDiagnosticsEnv,
|
|
3274
|
+
// pruneCrashDiagnosticsReports, and docs/diagnostics-crash-reports.md.
|
|
3275
|
+
crashDiagnostics: {
|
|
3276
|
+
enabled: true,
|
|
3277
|
+
retainCount: 20, // report.*.json files kept in CRASH_REPORTS_DIR; oldest pruned during runCleanup (cleanupEvery ticks)
|
|
3278
|
+
},
|
|
3159
3279
|
stalledDispatchSweepEvery: 120, // stalled-dispatch retry sweep (~20 min at default 10s tick) — only fires when all agents idle
|
|
3160
3280
|
// W-mp5trwh60008386d: per-PR 404 must repeat across N consecutive successful base-repo probes
|
|
3161
3281
|
// before flipping a PR to `abandoned`. A single 404 on `repos/{slug}/pulls/{n}` can be a transient
|
|
@@ -9091,6 +9211,9 @@ function createBackoffTracker({ baseMs = 30000, maxMs = 10 * 60 * 1000 } = {}) {
|
|
|
9091
9211
|
module.exports = {
|
|
9092
9212
|
MINIONS_DIR,
|
|
9093
9213
|
ENGINE_DIR,
|
|
9214
|
+
CRASH_REPORTS_DIR,
|
|
9215
|
+
getEngineCrashDiagnosticsEnv,
|
|
9216
|
+
pruneCrashDiagnosticsReports,
|
|
9094
9217
|
resolveEngineCacheDir,
|
|
9095
9218
|
openUrlInBrowser,
|
|
9096
9219
|
CONTROL_PATH,
|
package/engine/supervisor.js
CHANGED
|
@@ -347,6 +347,18 @@ function _sqliteSpawnFlags() {
|
|
|
347
347
|
return ['--experimental-sqlite'];
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
// W-mr2c4i8m0004da94: crash-diagnostics env additions for a supervisor-
|
|
351
|
+
// initiated respawn. Best-effort config.json read — the supervisor spawns
|
|
352
|
+
// engine.js before any config is loaded into memory, mirroring the same
|
|
353
|
+
// read bin/minions.js does for its own engine spawns.
|
|
354
|
+
function _engineCrashDiagnosticsEnv() {
|
|
355
|
+
const shared = _sharedOrNull();
|
|
356
|
+
if (!shared || typeof shared.getEngineCrashDiagnosticsEnv !== 'function') return {};
|
|
357
|
+
let cfg = null;
|
|
358
|
+
try { cfg = JSON.parse(fs.readFileSync(path.join(MINIONS_DIR, 'config.json'), 'utf8')); } catch { /* config may not exist */ }
|
|
359
|
+
try { return shared.getEngineCrashDiagnosticsEnv(cfg); } catch { return {}; }
|
|
360
|
+
}
|
|
361
|
+
|
|
350
362
|
function spawnEngine() {
|
|
351
363
|
const out = openAppendFd('engine-stdio.log');
|
|
352
364
|
const err = openAppendFd('engine-stdio.log');
|
|
@@ -355,6 +367,7 @@ function spawnEngine() {
|
|
|
355
367
|
stdio: ['ignore', out, err],
|
|
356
368
|
detached: true,
|
|
357
369
|
windowsHide: true,
|
|
370
|
+
env: { ...process.env, ..._engineCrashDiagnosticsEnv() },
|
|
358
371
|
});
|
|
359
372
|
proc.unref();
|
|
360
373
|
return proc.pid;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2310",
|
|
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"
|
package/prompts/cc-system.md
CHANGED
|
@@ -325,7 +325,7 @@ Every configured project has an effective **checkout mode** — surfaced in your
|
|
|
325
325
|
|
|
326
326
|
- **`worktree`** (default) — each dispatch gets its own isolated `git worktree`. Agents run fully in parallel; nothing touches the operator's working tree. Use for normal repos.
|
|
327
327
|
- **`live`** — agents run **in-place** inside the project's `localPath` (no worktree). The engine caps this to **one mutating dispatch at a time** per project and **refuses on a dirty tree**. Use only when worktrees are unworkable (e.g. Android `repo`, submodules, deep Windows paths, emulators that bind the real checkout).
|
|
328
|
-
- **`hybrid`** — `live` **plus** a `liveValidation: { type, autoDispatch }` block. Coding work items (`implement`/`fix`/`docs`/`decompose`) author in **isolated worktrees** (full parallelism), while **only** work items whose type matches `liveValidation.type` (e.g. `build-and-test`) run **in-place on the live checkout**. This is the best of both: parallel code authoring + a real on-disk build/validation that can't run in a worktree.
|
|
328
|
+
- **`hybrid`** — `live` **plus** a `liveValidation: { type, autoDispatch }` block. Coding work items (`implement`/`fix`/`docs`/`decompose`) author in **isolated worktrees** (full parallelism), while **only** work items whose type matches `liveValidation.type` (e.g. `build-and-test`) run **in-place on the live checkout**. This is the best of both: parallel code authoring + a real on-disk build/validation that can't run in a worktree. `type` accepts a single string (e.g. `"build-and-test"`) or an array of strings (e.g. `["implement","fix"]`) to route multiple work-item types to the live checkout at once.
|
|
329
329
|
|
|
330
330
|
**When to recommend hybrid:** the project's build/test/validation genuinely cannot run in an isolated worktree (it needs the real checkout — submodules, a `repo`-managed tree, an emulator/dev-server bound to `localPath`, deep-path tooling), **but** you still want coding agents to work in parallel rather than serialize through the single live checkout. If the *whole* workflow must run on the real tree, use plain `live`. If nothing needs the real tree, stay on `worktree`.
|
|
331
331
|
|