@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.
Files changed (44) hide show
  1. package/bin/minions.js +24 -11
  2. package/dashboard/js/command-parser.js +1 -1
  3. package/dashboard/js/memory-panel.js +262 -0
  4. package/dashboard/js/qa.js +2 -2
  5. package/dashboard/js/refresh.js +9 -1
  6. package/dashboard/js/render-dispatch.js +92 -0
  7. package/dashboard/js/render-other.js +1 -1
  8. package/dashboard/js/render-plans.js +82 -13
  9. package/dashboard/js/render-prs.js +2 -1
  10. package/dashboard/js/render-schedules.js +1 -1
  11. package/dashboard/js/render-watches.js +1 -1
  12. package/dashboard/js/settings.js +100 -11
  13. package/dashboard/layout.html +6 -0
  14. package/dashboard/pages/engine-memory-panel.html +49 -0
  15. package/dashboard/pages/engine.html +1 -0
  16. package/dashboard/slim/js/link-pr.js +5 -5
  17. package/dashboard/slim/js/modals-tiles.js +44 -3
  18. package/dashboard/slim/js/projects.js +8 -6
  19. package/dashboard/slim/styles.css +20 -0
  20. package/dashboard/styles.css +39 -0
  21. package/dashboard-build.js +17 -2
  22. package/dashboard.js +469 -21
  23. package/docs/README.md +8 -1
  24. package/docs/auto-discovery.md +40 -0
  25. package/docs/branch-derivation.md +13 -1
  26. package/docs/cross-repo-plans.md +292 -0
  27. package/docs/deprecated.json +4 -4
  28. package/docs/pr-auto-fix-dispatch.md +64 -0
  29. package/docs/pr-review-fix-loop.md +1 -1
  30. package/docs/watches.md +1 -0
  31. package/engine/ado.js +1 -10
  32. package/engine/diagnostics-memory.js +190 -0
  33. package/engine/dispatch.js +53 -0
  34. package/engine/lifecycle.js +155 -191
  35. package/engine/meeting.js +30 -0
  36. package/engine/playbook.js +15 -0
  37. package/engine/queries.js +165 -5
  38. package/engine/runtimes/copilot.js +19 -0
  39. package/engine/shared.js +303 -3
  40. package/engine/watchdog.js +6 -0
  41. package/engine.js +576 -113
  42. package/package.json +2 -2
  43. package/playbooks/plan-to-prd.md +25 -2
  44. 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
+ };
@@ -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
  };