@yemi33/minions 0.1.2177 → 0.1.2179
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 +24 -11
- package/dashboard/js/command-parser.js +1 -1
- package/dashboard/js/memory-panel.js +262 -0
- package/dashboard/js/qa.js +2 -2
- package/dashboard/js/refresh.js +9 -1
- package/dashboard/js/render-dispatch.js +92 -0
- package/dashboard/js/render-other.js +1 -1
- package/dashboard/js/render-plans.js +82 -13
- package/dashboard/js/render-prs.js +2 -1
- package/dashboard/js/render-schedules.js +1 -1
- package/dashboard/js/render-watches.js +1 -1
- package/dashboard/js/settings.js +100 -11
- package/dashboard/layout.html +6 -0
- package/dashboard/pages/engine-memory-panel.html +49 -0
- package/dashboard/pages/engine.html +1 -0
- package/dashboard/slim/js/link-pr.js +5 -5
- package/dashboard/slim/js/modals-tiles.js +44 -3
- package/dashboard/slim/js/projects.js +8 -6
- package/dashboard/slim/styles.css +20 -0
- package/dashboard/styles.css +39 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +469 -21
- package/docs/README.md +8 -1
- package/docs/auto-discovery.md +40 -0
- package/docs/branch-derivation.md +13 -1
- package/docs/cross-repo-plans.md +292 -0
- package/docs/deprecated.json +4 -4
- package/docs/pr-auto-fix-dispatch.md +64 -0
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/watches.md +1 -0
- package/engine/ado.js +1 -10
- package/engine/diagnostics-memory.js +190 -0
- package/engine/dispatch.js +53 -0
- package/engine/lifecycle.js +155 -191
- package/engine/meeting.js +30 -0
- package/engine/playbook.js +15 -0
- package/engine/queries.js +165 -5
- package/engine/runtimes/copilot.js +19 -0
- package/engine/shared.js +303 -3
- package/engine/watchdog.js +6 -0
- package/engine.js +576 -113
- package/package.json +2 -2
- package/playbooks/plan-to-prd.md +25 -2
- package/playbooks/plan.md +4 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/diagnostics-memory.js — In-process memory + event-loop + GC sampler.
|
|
3
|
+
*
|
|
4
|
+
* P-a1b2c3d4 (memory + perf audit plan). Importable from both engine.js and
|
|
5
|
+
* dashboard.js — each Node process gets its own module-scope state (event-loop
|
|
6
|
+
* histogram, GC counters, ring buffer). No I/O; persistence is the caller's
|
|
7
|
+
* responsibility (see P-b2c3d4e5).
|
|
8
|
+
*
|
|
9
|
+
* Exports:
|
|
10
|
+
* - sampleSelf({ label }) → snapshot object with RSS / heap / event-loop /
|
|
11
|
+
* GC counters / pid / uptime / capturedAt.
|
|
12
|
+
* - getHistory({ limit }) → up-to-`limit` samples from the ring buffer
|
|
13
|
+
* (oldest first), defaults to all 1440.
|
|
14
|
+
* - recordSample(sample) → append to the ring buffer; rotates at the cap.
|
|
15
|
+
* - startPeriodicSampling({ intervalMs, onSample }) → setInterval-driven
|
|
16
|
+
* sampler; returns a stop() function. Dashboard.js uses this on boot;
|
|
17
|
+
* engine.js drives sampling from its own tick instead.
|
|
18
|
+
*
|
|
19
|
+
* Zero deps — all Node built-ins. The event-loop histogram is a
|
|
20
|
+
* `monitorEventLoopDelay({ resolution: 20 })` singleton lazily enabled on the
|
|
21
|
+
* first `sampleSelf()` call. The PerformanceObserver subscribes to
|
|
22
|
+
* `entryTypes: ['gc']` and accumulates pause times into module-scope counters.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const v8 = require('v8');
|
|
26
|
+
const { monitorEventLoopDelay, PerformanceObserver, constants } = require('perf_hooks');
|
|
27
|
+
|
|
28
|
+
const RING_BUFFER_CAP = 1440;
|
|
29
|
+
const HISTOGRAM_RESOLUTION_MS = 20;
|
|
30
|
+
const NS_PER_MS = 1e6;
|
|
31
|
+
|
|
32
|
+
const ringBuffer = [];
|
|
33
|
+
|
|
34
|
+
let eventLoopHistogram = null;
|
|
35
|
+
let gcObserver = null;
|
|
36
|
+
let gcPausesTotalMs = 0;
|
|
37
|
+
let gcCount = 0;
|
|
38
|
+
let lastGcPauseMs = 0;
|
|
39
|
+
let lastGcKind = null;
|
|
40
|
+
|
|
41
|
+
const GC_KIND_NAMES = {
|
|
42
|
+
[constants.NODE_PERFORMANCE_GC_MAJOR]: 'major',
|
|
43
|
+
[constants.NODE_PERFORMANCE_GC_MINOR]: 'minor',
|
|
44
|
+
[constants.NODE_PERFORMANCE_GC_INCREMENTAL]: 'incremental',
|
|
45
|
+
[constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weakcb',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function _ensureHistogramStarted() {
|
|
49
|
+
if (eventLoopHistogram) return eventLoopHistogram;
|
|
50
|
+
eventLoopHistogram = monitorEventLoopDelay({ resolution: HISTOGRAM_RESOLUTION_MS });
|
|
51
|
+
eventLoopHistogram.enable();
|
|
52
|
+
return eventLoopHistogram;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _ensureGcObserverStarted() {
|
|
56
|
+
if (gcObserver) return gcObserver;
|
|
57
|
+
gcObserver = new PerformanceObserver((list) => {
|
|
58
|
+
for (const entry of list.getEntries()) {
|
|
59
|
+
// entry.duration is in ms (fractional). entry.kind / entry.detail.kind
|
|
60
|
+
// is the numeric NODE_PERFORMANCE_GC_* constant.
|
|
61
|
+
const pauseMs = Number(entry.duration) || 0;
|
|
62
|
+
gcPausesTotalMs += pauseMs;
|
|
63
|
+
gcCount += 1;
|
|
64
|
+
lastGcPauseMs = pauseMs;
|
|
65
|
+
const kindNum = (entry.detail && entry.detail.kind) || entry.kind || null;
|
|
66
|
+
lastGcKind = (kindNum != null && GC_KIND_NAMES[kindNum]) || (kindNum != null ? String(kindNum) : null);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
gcObserver.observe({ entryTypes: ['gc'], buffered: false });
|
|
70
|
+
// Don't keep the event loop alive just to observe GC.
|
|
71
|
+
if (typeof gcObserver.unref === 'function') gcObserver.unref();
|
|
72
|
+
return gcObserver;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _nsToMs(ns) {
|
|
76
|
+
if (!Number.isFinite(ns) || ns <= 0) return 0;
|
|
77
|
+
return ns / NS_PER_MS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _readEventLoopLag(hist) {
|
|
81
|
+
// hist.percentile(p) throws when the histogram is empty (no recorded
|
|
82
|
+
// samples yet). Guard so the first sampleSelf() call after process start
|
|
83
|
+
// returns zeros instead of throwing.
|
|
84
|
+
let p50 = 0, p99 = 0, max = 0;
|
|
85
|
+
try {
|
|
86
|
+
if (hist && typeof hist.percentile === 'function') {
|
|
87
|
+
const c = typeof hist.count === 'number' ? hist.count : (hist.totalCount || 0);
|
|
88
|
+
if (c > 0) {
|
|
89
|
+
p50 = _nsToMs(hist.percentile(50));
|
|
90
|
+
p99 = _nsToMs(hist.percentile(99));
|
|
91
|
+
max = _nsToMs(hist.max);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Empty / not-yet-populated histogram. Leave zeros.
|
|
96
|
+
}
|
|
97
|
+
return { p50, p99, max };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sampleSelf({ label = null } = {}) {
|
|
101
|
+
const hist = _ensureHistogramStarted();
|
|
102
|
+
_ensureGcObserverStarted();
|
|
103
|
+
|
|
104
|
+
const mem = process.memoryUsage();
|
|
105
|
+
const heap = v8.getHeapStatistics();
|
|
106
|
+
const lag = _readEventLoopLag(hist);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
rss: mem.rss,
|
|
110
|
+
heapUsed: mem.heapUsed,
|
|
111
|
+
heapTotal: mem.heapTotal,
|
|
112
|
+
external: mem.external,
|
|
113
|
+
arrayBuffers: mem.arrayBuffers || 0,
|
|
114
|
+
heapSizeLimit: heap.heap_size_limit,
|
|
115
|
+
eventLoopLagP50: lag.p50,
|
|
116
|
+
eventLoopLagP99: lag.p99,
|
|
117
|
+
eventLoopLagMax: lag.max,
|
|
118
|
+
lastGcPauseMs,
|
|
119
|
+
lastGcKind,
|
|
120
|
+
gcPausesTotalMs,
|
|
121
|
+
gcCount,
|
|
122
|
+
uptime: process.uptime(),
|
|
123
|
+
pid: process.pid,
|
|
124
|
+
label,
|
|
125
|
+
capturedAt: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function recordSample(sample) {
|
|
130
|
+
if (!sample || typeof sample !== 'object') return;
|
|
131
|
+
ringBuffer.push(sample);
|
|
132
|
+
while (ringBuffer.length > RING_BUFFER_CAP) ringBuffer.shift();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getHistory({ limit } = {}) {
|
|
136
|
+
const n = ringBuffer.length;
|
|
137
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit >= n) {
|
|
138
|
+
return ringBuffer.slice();
|
|
139
|
+
}
|
|
140
|
+
return ringBuffer.slice(n - limit);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function startPeriodicSampling({ intervalMs = 60000, onSample = null } = {}) {
|
|
144
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
145
|
+
throw new Error('startPeriodicSampling: intervalMs must be a positive number');
|
|
146
|
+
}
|
|
147
|
+
const handle = setInterval(() => {
|
|
148
|
+
let sample;
|
|
149
|
+
try {
|
|
150
|
+
sample = sampleSelf({ label: 'periodic' });
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
recordSample(sample);
|
|
155
|
+
if (typeof onSample === 'function') {
|
|
156
|
+
try { onSample(sample); } catch { /* swallow — sampler must not crash on callback errors */ }
|
|
157
|
+
}
|
|
158
|
+
}, intervalMs);
|
|
159
|
+
if (typeof handle.unref === 'function') handle.unref();
|
|
160
|
+
return function stop() {
|
|
161
|
+
clearInterval(handle);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _resetForTest() {
|
|
166
|
+
ringBuffer.length = 0;
|
|
167
|
+
if (eventLoopHistogram && typeof eventLoopHistogram.disable === 'function') {
|
|
168
|
+
try { eventLoopHistogram.disable(); } catch {}
|
|
169
|
+
}
|
|
170
|
+
eventLoopHistogram = null;
|
|
171
|
+
if (gcObserver && typeof gcObserver.disconnect === 'function') {
|
|
172
|
+
try { gcObserver.disconnect(); } catch {}
|
|
173
|
+
}
|
|
174
|
+
gcObserver = null;
|
|
175
|
+
gcPausesTotalMs = 0;
|
|
176
|
+
gcCount = 0;
|
|
177
|
+
lastGcPauseMs = 0;
|
|
178
|
+
lastGcKind = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
sampleSelf,
|
|
183
|
+
getHistory,
|
|
184
|
+
recordSample,
|
|
185
|
+
startPeriodicSampling,
|
|
186
|
+
// Constants for callers that need to know the cap up front.
|
|
187
|
+
RING_BUFFER_CAP,
|
|
188
|
+
// exported for testing
|
|
189
|
+
_resetForTest,
|
|
190
|
+
};
|
package/engine/dispatch.js
CHANGED
|
@@ -295,6 +295,33 @@ function _routeToReviewQueue(item, evaluation) {
|
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Resolve whether the work item's parent PRD is in a "vetted" status —
|
|
300
|
+
* `approved` or `active` — for the PRD-sourced bypass (W-mq9acoo800177bcb).
|
|
301
|
+
* Returns true when the bypass should fire: PRD exists on disk and its
|
|
302
|
+
* status is one of the vetted set. Returns false on missing sourcePlan,
|
|
303
|
+
* missing/unreadable PRD file, or non-vetted status (rejected, paused,
|
|
304
|
+
* revision-requested, completed, awaiting-approval). Errors fail closed —
|
|
305
|
+
* if we can't confirm the PRD is approved/active, we let the normal
|
|
306
|
+
* validator run.
|
|
307
|
+
*
|
|
308
|
+
* @param {object} wi - the work item (item.meta.item)
|
|
309
|
+
* @returns {boolean}
|
|
310
|
+
*/
|
|
311
|
+
function _isPrdSourcedAndVetted(wi) {
|
|
312
|
+
const sourcePlan = wi && wi.sourcePlan;
|
|
313
|
+
if (!sourcePlan || typeof sourcePlan !== 'string') return false;
|
|
314
|
+
try {
|
|
315
|
+
const prdPath = path.join(queries.PRD_DIR, sourcePlan);
|
|
316
|
+
const plan = shared.safeJsonNoRestore(prdPath);
|
|
317
|
+
if (!plan || !plan.missing_features) return false;
|
|
318
|
+
const status = plan.status || shared.PLAN_STATUS.ACTIVE;
|
|
319
|
+
return status === shared.PLAN_STATUS.APPROVED || status === shared.PLAN_STATUS.ACTIVE;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
298
325
|
/**
|
|
299
326
|
* Async wrapper around addToDispatch that consults the pre-dispatch validator
|
|
300
327
|
* when ENGINE_DEFAULTS.enablePreDispatchEval is true. Validator failures are
|
|
@@ -305,6 +332,8 @@ function _routeToReviewQueue(item, evaluation) {
|
|
|
305
332
|
* @param {object} [opts.config] - engine config; defaults to queries.getConfig().
|
|
306
333
|
* @param {Function} [opts.validate] - injection point for tests; defaults to
|
|
307
334
|
* require('./pre-dispatch-eval').validateAcceptanceCriteria.
|
|
335
|
+
* @param {Function} [opts.isPrdVetted] - injection point for tests; defaults
|
|
336
|
+
* to the on-disk PRD status read via _isPrdSourcedAndVetted.
|
|
308
337
|
* @returns {Promise<string|null>} dispatch id when queued; null when routed to review.
|
|
309
338
|
*/
|
|
310
339
|
async function addToDispatchWithValidation(item, opts = {}) {
|
|
@@ -328,6 +357,29 @@ async function addToDispatchWithValidation(item, opts = {}) {
|
|
|
328
357
|
return addToDispatch(item);
|
|
329
358
|
}
|
|
330
359
|
|
|
360
|
+
// W-mq9acoo800177bcb — PRD-sourced bypass. plan-to-prd already LLM-vets
|
|
361
|
+
// every item it lands in a PRD, so re-validating each materialized item
|
|
362
|
+
// per discovery tick is the dominant cost of the workflow this
|
|
363
|
+
// optimization targets (user-observed: 2 PRDs / 11 items → ~5 min
|
|
364
|
+
// queueing). Gate behind preDispatchEvalSkipPrdSourced (default true) and
|
|
365
|
+
// only fire when the parent PRD is approved/active — rejected / paused /
|
|
366
|
+
// revision-requested / completed PRDs still flow through the normal
|
|
367
|
+
// validator path so a stale PRD reopen can't smuggle in unvetted items.
|
|
368
|
+
const skipPrdSourced = config?.engine?.preDispatchEvalSkipPrdSourced
|
|
369
|
+
?? ENGINE_DEFAULTS.preDispatchEvalSkipPrdSourced;
|
|
370
|
+
if (skipPrdSourced) {
|
|
371
|
+
const isPrdVetted = typeof opts.isPrdVetted === 'function'
|
|
372
|
+
? opts.isPrdVetted
|
|
373
|
+
: _isPrdSourcedAndVetted;
|
|
374
|
+
if (isPrdVetted(wi)) {
|
|
375
|
+
// Stamp the skip marker on the dispatch item so the entry that lands
|
|
376
|
+
// in dispatch.json carries observability — operators can spot which
|
|
377
|
+
// items the validator skipped vs which it ran.
|
|
378
|
+
item._preDispatchEvalSkipped = 'prd-sourced';
|
|
379
|
+
return addToDispatch(item);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
331
383
|
const validate = typeof opts.validate === 'function'
|
|
332
384
|
? opts.validate
|
|
333
385
|
: require('./pre-dispatch-eval').validateAcceptanceCriteria;
|
|
@@ -1073,5 +1125,6 @@ module.exports = {
|
|
|
1073
1125
|
findActivePrOrBranchLock,
|
|
1074
1126
|
normalizeRetryableDecision,
|
|
1075
1127
|
isCompletedWorkItemForFailure,
|
|
1128
|
+
_isPrdSourcedAndVetted,
|
|
1076
1129
|
NON_MUTATING_DISPATCH_TYPES,
|
|
1077
1130
|
};
|