@yemi33/minions 0.1.2370 → 0.1.2371

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 (71) hide show
  1. package/bin/minions.js +38 -11
  2. package/dashboard/js/render-other.js +2 -30
  3. package/dashboard/js/render-work-items.js +0 -34
  4. package/dashboard/js/settings.js +12 -27
  5. package/dashboard/pages/tools.html +1 -1
  6. package/dashboard.js +80 -257
  7. package/docs/README.md +2 -3
  8. package/docs/architecture.excalidraw +2 -2
  9. package/docs/auto-discovery.md +3 -3
  10. package/docs/completion-reports.md +0 -121
  11. package/docs/copilot-cli-schema.md +9 -17
  12. package/docs/deprecated.json +0 -51
  13. package/docs/design-state-storage.md +4 -4
  14. package/docs/harness-propagation.md +33 -263
  15. package/docs/human-vs-automated.md +1 -1
  16. package/docs/named-agents.md +0 -2
  17. package/docs/plan-lifecycle.md +5 -6
  18. package/docs/qa-runbook-lifecycle.md +10 -25
  19. package/docs/shared-lifecycle-module-map.md +2 -10
  20. package/engine/ado-comment.js +5 -11
  21. package/engine/ado.js +10 -5
  22. package/engine/cleanup.js +16 -36
  23. package/engine/cli.js +13 -175
  24. package/engine/comment-format.js +8 -182
  25. package/engine/db/index.js +60 -10
  26. package/engine/db/migrations/018-sql-only-cutover.js +56 -0
  27. package/engine/dispatch-store.js +0 -114
  28. package/engine/dispatch.js +1 -6
  29. package/engine/gh-comment.js +2 -10
  30. package/engine/github.js +8 -6
  31. package/engine/lifecycle.js +58 -173
  32. package/engine/llm.js +9 -11
  33. package/engine/managed-spawn.js +1 -6
  34. package/engine/memory-store.js +8 -6
  35. package/engine/metrics-store.js +4 -128
  36. package/engine/pipeline.js +4 -7
  37. package/engine/playbook.js +8 -196
  38. package/engine/prd-store.js +115 -141
  39. package/engine/preflight.js +8 -55
  40. package/engine/projects.js +34 -41
  41. package/engine/pull-requests-store.js +9 -234
  42. package/engine/qa-runs.js +4 -15
  43. package/engine/qa-sessions.js +5 -17
  44. package/engine/queries.js +23 -103
  45. package/engine/routing.js +1 -1
  46. package/engine/runtimes/claude.js +1 -2
  47. package/engine/runtimes/codex.js +0 -2
  48. package/engine/runtimes/copilot.js +1 -4
  49. package/engine/shared-branch-pr-reconcile.js +2 -5
  50. package/engine/shared.js +174 -533
  51. package/engine/small-state-store.js +12 -647
  52. package/engine/spawn-agent.js +5 -96
  53. package/engine/state-operations.js +166 -0
  54. package/engine/supervisor.js +0 -1
  55. package/engine/watch-actions.js +2 -3
  56. package/engine/watches-store.js +2 -128
  57. package/engine/work-items-store.js +3 -254
  58. package/engine.js +102 -334
  59. package/package.json +2 -2
  60. package/playbooks/build-fix-complex.md +0 -4
  61. package/playbooks/fix.md +0 -4
  62. package/playbooks/implement-shared.md +0 -4
  63. package/playbooks/implement.md +0 -4
  64. package/playbooks/plan-to-prd.md +16 -11
  65. package/playbooks/plan.md +0 -4
  66. package/playbooks/review.md +0 -6
  67. package/playbooks/shared-rules.md +0 -65
  68. package/docs/harness-transparency.md +0 -199
  69. package/docs/project-skills.md +0 -193
  70. package/engine/discover-project-skills.js +0 -673
  71. package/engine/playbook-intents.js +0 -76
@@ -1,21 +1,10 @@
1
- // engine/pull-requests-store.js — SQL-backed implementation of the
2
- // per-file pull-requests array. Same external contract as the legacy
3
- // JSON-file reader, modeled directly on work-items-store.js.
1
+ // engine/pull-requests-store.js — SQL-backed scoped pull-request storage.
4
2
  //
5
3
  // readPullRequestsForScope(scope) -> [pr, pr, ...]
6
4
  // applyPullRequestsMutation(scope, fn) -> diff-then-apply,
7
5
  // returns { wrote, result }
8
6
  //
9
- // `scope` is 'central' for the top-level pull-requests.json or a project
10
- // name for projects/<name>/pull-requests.json.
11
- //
12
- // External-edit detection mirrors work-items-store: per-scope last-mirror
13
- // mtime is recorded after every write; if the JSON file's mtime advances
14
- // outside our knowledge (test re-seed, operator hand-edit), we treat the
15
- // JSON as canonical and rebuild SQL before the next read or mutation.
16
-
17
- const path = require('path');
18
- const fs = require('fs');
7
+ // `scope` is 'central' or a project name.
19
8
 
20
9
  function _toMs(v) {
21
10
  if (v == null) return null;
@@ -31,12 +20,12 @@ function scopeForFilePath(filePath) {
31
20
  return 'central';
32
21
  }
33
22
 
23
+ // Compatibility adapter for callers that still pass a path-shaped scope token
24
+ // into shared.mutatePullRequests. No file is read or written.
34
25
  function _filePathForScope(scope) {
35
26
  const shared = require('./shared');
36
- if (scope === 'central') {
37
- return path.join(shared.MINIONS_DIR, 'pull-requests.json');
38
- }
39
- return path.join(shared.MINIONS_DIR, 'projects', scope, 'pull-requests.json');
27
+ if (!scope || scope === 'central') return shared.centralPullRequestsPath(shared.MINIONS_DIR);
28
+ return require('path').join(shared.MINIONS_DIR, 'projects', scope, 'pull-requests.json');
40
29
  }
41
30
 
42
31
  function _parseRow(row) {
@@ -45,143 +34,16 @@ function _parseRow(row) {
45
34
  catch { return null; }
46
35
  }
47
36
 
48
- function _readJsonArrayFallback(scope) {
49
- const fp = _filePathForScope(scope);
50
- let raw;
51
- try { raw = fs.readFileSync(fp, 'utf8'); }
52
- catch { return []; }
53
- try {
54
- const parsed = JSON.parse(raw);
55
- return Array.isArray(parsed) ? parsed : [];
56
- } catch (e) {
57
- try {
58
- // eslint-disable-next-line no-console
59
- console.warn(`[pull-requests-store] corrupt JSON in ${fp}: ${e.message}`);
60
- } catch { /* console may be wrapped in tests */ }
61
- return [];
62
- }
63
- }
64
-
65
- // Content-hash fingerprint per scope: same-length swaps (timestamps,
66
- // reviewStatus enums) collide on (mtime, size) but never on SHA-1.
67
- //
68
- // W-mrcg7j9k000ja233 (#759): this used to be an in-memory-only Map, which
69
- // resets to empty on every process restart. Persist it in SQL instead
70
- // (`pr_mirror_hashes`) so a restart can't blank out the "does JSON already
71
- // agree with SQL" trust state — that reset, combined with a restart landing
72
- // mid-flight of another process's mirror write, is exactly what let a stale
73
- // JSON snapshot get treated as canonical and silently wipe SQL rows for a
74
- // scope (see _resyncScopeIfJsonDiverged / _hydrateScopeFromJson below).
75
- function _getLastMirrorHash(db, scope) {
76
- try {
77
- const row = db.prepare('SELECT hash FROM pr_mirror_hashes WHERE scope = ?').get(scope);
78
- return row ? row.hash : null;
79
- } catch { return null; }
80
- }
81
-
82
- function _setLastMirrorHash(db, scope, hash) {
83
- try {
84
- db.prepare(`
85
- INSERT INTO pr_mirror_hashes (scope, hash, updated_at) VALUES (?, ?, ?)
86
- ON CONFLICT(scope) DO UPDATE SET hash = excluded.hash, updated_at = excluded.updated_at
87
- `).run(scope, hash, Date.now());
88
- } catch { /* best-effort — a missed persisted-hash write just means the next resync re-checks */ }
89
- }
90
-
91
- function _forgetLastMirrorHash(db, scope) {
92
- try { db.prepare('DELETE FROM pr_mirror_hashes WHERE scope = ?').run(scope); } catch { /* best-effort */ }
93
- }
94
-
95
- const crypto = require('crypto');
96
-
97
- function _fileContentHash(filePath) {
98
- try {
99
- const buf = fs.readFileSync(filePath);
100
- return crypto.createHash('sha1').update(buf).digest('hex');
101
- }
102
- catch { return null; }
103
- }
104
-
105
- function _hydrateScopeFromJson(db, scope) {
106
- const jsonItems = _readJsonArrayFallback(scope);
107
- const beforeCount = db.prepare('SELECT COUNT(*) AS n FROM pull_requests WHERE scope = ?').get(scope).n;
108
- const beforeIds = db.prepare('SELECT id FROM pull_requests WHERE scope = ?').all(scope).map(r => r.id);
109
- db.prepare('DELETE FROM pull_requests WHERE scope = ?').run(scope);
110
- // #759 — this DELETE used to be completely silent. Log every hydrate at
111
- // warn level with before/after counts + the ids that are about to be
112
- // dropped, so a JSON<->SQL resync that removes rows the operator didn't
113
- // expect leaves a trace in engine-stdio.log instead of vanishing without
114
- // explanation.
115
- try {
116
- const afterIds = new Set(jsonItems.filter(pr => pr && pr.id).map(pr => String(pr.id)));
117
- const droppedIds = beforeIds.filter(id => !afterIds.has(id));
118
- // eslint-disable-next-line no-console
119
- console.warn(
120
- `[pull-requests-store] _hydrateScopeFromJson(scope=${scope}): rebuilding SQL from JSON mirror — `
121
- + `before=${beforeCount} row(s), after=${jsonItems.length} row(s)`
122
- + (droppedIds.length ? `, dropping id(s): ${droppedIds.join(', ')}` : ''),
123
- );
124
- } catch { /* logging must never block the resync */ }
125
- if (jsonItems.length === 0) return;
126
- const now = Date.now();
127
- const ins = db.prepare(`
128
- INSERT INTO pull_requests (id, scope, status, review_status, agent, branch, pr_number, created_at, last_polled_at, data, updated_at)
129
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
130
- ON CONFLICT(scope, id) DO NOTHING
131
- `);
132
- for (const pr of jsonItems) {
133
- if (!pr || !pr.id) continue;
134
- ins.run(
135
- String(pr.id),
136
- scope,
137
- String(pr.status || 'active'),
138
- pr.reviewStatus || null,
139
- pr.agent || null,
140
- pr.branch || null,
141
- Number.isFinite(pr.prNumber) ? pr.prNumber : null,
142
- _toMs(pr.created),
143
- _toMs(pr.lastPolledAt || pr.lastPolled),
144
- JSON.stringify(pr),
145
- now,
146
- );
147
- }
148
- }
149
-
150
- function _resyncScopeIfJsonDiverged(db, scope) {
151
- const jsonPath = _filePathForScope(scope);
152
- const currentHash = _fileContentHash(jsonPath);
153
- const lastHash = _getLastMirrorHash(db, scope);
154
- if (currentHash == null) return;
155
- if (lastHash != null && currentHash === lastHash) return;
156
- if (lastHash == null) {
157
- const sqlHas = db.prepare('SELECT 1 FROM pull_requests WHERE scope = ? LIMIT 1').get(scope);
158
- if (sqlHas) {
159
- _setLastMirrorHash(db, scope, currentHash);
160
- return;
161
- }
162
- }
163
- _hydrateScopeFromJson(db, scope);
164
- _setLastMirrorHash(db, scope, currentHash);
165
- }
166
-
167
37
  function readPullRequestsForScope(scope) {
168
38
  const { getDb } = require('./db');
169
39
  const db = getDb();
170
40
 
171
- _resyncScopeIfJsonDiverged(db, scope);
172
-
173
41
  const rows = db.prepare(`
174
42
  SELECT data FROM pull_requests
175
43
  WHERE scope = ?
176
44
  ORDER BY rowid
177
45
  `).all(scope);
178
46
 
179
- if (rows.length === 0) {
180
- const fallback = _readJsonArrayFallback(scope);
181
- if (fallback.length > 0) return fallback;
182
- return [];
183
- }
184
-
185
47
  const out = [];
186
48
  for (const row of rows) {
187
49
  const pr = _parseRow(row);
@@ -190,36 +52,10 @@ function readPullRequestsForScope(scope) {
190
52
  return out;
191
53
  }
192
54
 
193
- // Enumerate scopes that have a JSON file on disk — used as the
194
- // SQL-unavailable fallback for `readAllPullRequests`. Mirrors
195
- // work-items-store's `_enumerateJsonScopes` shape.
196
- function _enumerateJsonScopes() {
197
- const shared = require('./shared');
198
- const scopes = [];
199
- const centralPath = path.join(shared.MINIONS_DIR, 'pull-requests.json');
200
- if (fs.existsSync(centralPath)) scopes.push('central');
201
- const projectsDir = path.join(shared.MINIONS_DIR, 'projects');
202
- try {
203
- for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
204
- if (!d.isDirectory()) continue;
205
- const fp = path.join(projectsDir, d.name, 'pull-requests.json');
206
- if (fs.existsSync(fp)) scopes.push(d.name);
207
- }
208
- } catch { /* projects dir missing on fresh install */ }
209
- return scopes;
210
- }
211
-
212
55
  function readAllPullRequests() {
213
56
  const { getDb } = require('./db');
214
57
  const db = getDb();
215
58
 
216
- try { _resyncScopeIfJsonDiverged(db, 'central'); } catch {}
217
- const knownScopes = db.prepare('SELECT DISTINCT scope FROM pull_requests').all().map(r => r.scope);
218
- for (const s of knownScopes) {
219
- if (s === 'central') continue;
220
- try { _resyncScopeIfJsonDiverged(db, s); } catch {}
221
- }
222
-
223
59
  const rows = db.prepare('SELECT data, scope FROM pull_requests ORDER BY rowid').all();
224
60
  const out = [];
225
61
  for (const row of rows) {
@@ -296,12 +132,11 @@ function _applyPullRequestsDiff(db, diff) {
296
132
  return true;
297
133
  }
298
134
 
299
- function applyPullRequestsMutation(scope, mutator, options = {}) {
135
+ function applyPullRequestsMutation(scope, mutator) {
300
136
  const { getDb, withTransaction } = require('./db');
301
137
  const db = getDb();
302
138
 
303
139
  return withTransaction(db, () => {
304
- _resyncScopeIfJsonDiverged(db, scope);
305
140
  const before = readPullRequestsForScope(scope);
306
141
  // Strip the _scope decoration that readAllPullRequests adds, in case
307
142
  // a caller round-trips records from there back through here.
@@ -313,56 +148,10 @@ function applyPullRequestsMutation(scope, mutator, options = {}) {
313
148
  : (Array.isArray(next) ? next : before);
314
149
  const diff = _computePullRequestsDiff(scope, beforeSnapshot, after);
315
150
  const wrote = _applyPullRequestsDiff(db, diff);
316
- // W-mrcg7j9k000ja233 (#759) — mirror the JSON sidecar INSIDE this same
317
- // SQL write transaction, BEFORE it commits, instead of after (as the
318
- // caller in shared.js used to do post-return). This mirrors the fix
319
- // already applied to work-items-store.js (W-mr9vcxvy000cdb4e): SQLite
320
- // only allows one writer transaction at a time, so mirroring here
321
- // guarantees the on-disk JSON file reflects this commit by the time it
322
- // becomes visible to any other reader/writer. Without this ordering, a
323
- // sibling process (dashboard vs. engine — separate Node processes, each
324
- // with its own resync state) could observe the freshly committed SQL
325
- // rows before the mirror file caught up, decide its own cached hash no
326
- // longer matched the (still-stale) JSON file, and call
327
- // _hydrateScopeFromJson — which DELETEs + reinserts the whole scope
328
- // from that stale file, silently dropping the row this transaction just
329
- // committed.
330
- if (wrote) {
331
- try { _mirrorJsonFromSql(scope, options.mirrorPath); } catch { /* best-effort — mirror failure must not block the SQL write */ }
332
- }
333
151
  return { wrote, result: after };
334
152
  });
335
153
  }
336
154
 
337
- function _mirrorJsonFromSql(scope, filePath) {
338
- try {
339
- const shared = require('./shared');
340
- const { getDb } = require('./db');
341
- // Read SQL directly for this scope — bypass JSON fallback so a
342
- // mutation that empties SQL for the scope doesn't resurrect stale
343
- // JSON content.
344
- const db = getDb();
345
- const rows = db.prepare('SELECT data FROM pull_requests WHERE scope = ? ORDER BY rowid').all(scope);
346
- const items = [];
347
- for (const row of rows) {
348
- const pr = _parseRow(row);
349
- if (!pr) continue;
350
- if (pr._scope) delete pr._scope;
351
- items.push(pr);
352
- }
353
- const target = filePath || _filePathForScope(scope);
354
- shared.safeWrite(target, items);
355
- const h = _fileContentHash(target);
356
- if (h != null) _setLastMirrorHash(db, scope, h);
357
- } catch {
358
- // Mirror failures are non-fatal — SQL has already committed.
359
- }
360
- }
361
-
362
- // Drop every SQL row for `scope` and forget its mirror-hash. Called by
363
- // removeProject after the JSON file is archived so safeJson shim reads no
364
- // longer surface the removed project's records.
365
- //
366
155
  // Per CLAUDE.md's mutator contract, every mutator wrapper must call
367
156
  // emitStateEvent() so the dashboard's MAX(events.id) cache-version
368
157
  // advances — otherwise the dashboard keeps serving stale cached
@@ -372,20 +161,7 @@ function _mirrorJsonFromSql(scope, filePath) {
372
161
  function dropScope(scope) {
373
162
  try {
374
163
  const { getDb } = require('./db');
375
- const db = getDb();
376
- const result = db.prepare('DELETE FROM pull_requests WHERE scope = ?').run(scope);
377
- // Mirror the work-items-store.js fix (W-mr96m3hm000fd5d7): don't blindly
378
- // forget the mirror-hash. _resyncScopeIfJsonDiverged treats "no recorded
379
- // hash" + "zero SQL rows" as a first-install hydrate and rebuilds SQL from
380
- // whatever JSON is on disk. removeProject with dataMode:'keep' leaves this
381
- // scope's JSON file untouched, so forgetting the hash here would let the
382
- // very next read/write resurrect the just-dropped project's stale PR rows.
383
- // Adopt the file's current hash instead (or forget it only if the file is
384
- // actually gone, e.g. dataMode: 'purge'/'archive').
385
- const jsonPath = _filePathForScope(scope);
386
- const currentHash = _fileContentHash(jsonPath);
387
- if (currentHash == null) _forgetLastMirrorHash(db, scope);
388
- else _setLastMirrorHash(db, scope, currentHash);
164
+ const result = getDb().prepare('DELETE FROM pull_requests WHERE scope = ?').run(scope);
389
165
  if (result && result.changes > 0) {
390
166
  try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
391
167
  }
@@ -393,11 +169,10 @@ function dropScope(scope) {
393
169
  }
394
170
 
395
171
  module.exports = {
172
+ _filePathForScope,
396
173
  scopeForFilePath,
397
174
  readPullRequestsForScope,
398
175
  readAllPullRequests,
399
176
  applyPullRequestsMutation,
400
177
  dropScope,
401
- _filePathForScope,
402
- _mirrorJsonFromSql,
403
178
  };
package/engine/qa-runs.js CHANGED
@@ -5,18 +5,14 @@
5
5
  * a managed target. Records carry status, timing, work-item linkage, and an
6
6
  * artifacts array (screenshots, logs, videos captured during the run).
7
7
  *
8
- * Persistence: SQLite (engine/state.db, `qa_runs` table) as source of truth
9
- * since Phase 8 (PRD: migrate-qa-state-to-sqlite). The legacy JSON sidecar
10
- * at engine/qa-runs.json is dual-written on every mutation when
11
- * `engine.qaDualWriteJson` is true (default) so external tooling and the
12
- * rollback path keep working. Artifacts live on disk under
8
+ * Persistence: SQLite (engine/state.db, `qa_runs` table). Artifacts live on disk under
13
9
  * engine/qa-artifacts/<runId>/ — the run record stores relative paths
14
10
  * (`type`, `path`, `label`, `capturedAt`), and the dashboard exposes them
15
11
  * through GET /api/qa/artifacts/<runId>/<file>.
16
12
  *
17
13
  * Concurrency: every mutation goes through shared.mutateQaRuns, which routes
18
14
  * to small-state-store.applyQaRunsMutation inside a SQLite transaction
19
- * (BEGIN IMMEDIATE) plus a content-hash divergence rehydrate on read.
15
+ * (BEGIN IMMEDIATE).
20
16
  * Callbacks are synchronous and never await.
21
17
  *
22
18
  * Status state machine:
@@ -35,7 +31,7 @@ const path = require('path');
35
31
  const shared = require('./shared');
36
32
  const { mutateQaRuns, uid, ts, log } = shared;
37
33
 
38
- // Cap qa-runs.json so the file (and qa_runs table) doesn't grow unboundedly
34
+ // Cap qa_runs so the table doesn't grow unboundedly
39
35
  // over months of nightly QA dispatch. Mirrors the 2500-entry cap on
40
36
  // engine/log.json. createRun trims oldest-by-createdAt when crossing the
41
37
  // threshold; terminal-status runs that have already shipped completion
@@ -252,15 +248,8 @@ function getRun(id) {
252
248
  return run || null;
253
249
  }
254
250
 
255
- // Internal helper — reads via the small-state store (SQL → JSON fallback)
256
- // when available, falling back to direct JSON read if SQLite is unavailable.
257
251
  function _readRuns() {
258
- try {
259
- const store = require('./small-state-store');
260
- const arr = store.readQaRuns();
261
- if (Array.isArray(arr)) return arr;
262
- } catch { /* fall back */ }
263
- return shared.safeJsonArr(qaRunsPath());
252
+ return require('./small-state-store').readQaRuns();
264
253
  }
265
254
 
266
255
  /**
@@ -30,8 +30,7 @@
30
30
  *
31
31
  * Concurrency: every mutation goes through shared.mutateQaSessions, which
32
32
  * routes to small-state-store.applyQaSessionsMutation inside a SQLite
33
- * transaction (BEGIN IMMEDIATE) plus a content-hash divergence rehydrate on
34
- * read. Callbacks are synchronous and never await. Slow filesystem work
33
+ * transaction (BEGIN IMMEDIATE). Callbacks are synchronous and never await. Slow filesystem work
35
34
  * (qa-tests/<id>/ scaffolding, dispatch enqueueing) runs OUTSIDE the lock.
36
35
  *
37
36
  * Path-traversal hardening: sessionId is generated by createSession() with a
@@ -40,10 +39,7 @@
40
39
  * a filesystem path or a session lookup. Mirrors engine/qa-runbooks.js
41
40
  * _isSafeId (PR #2694 review feedback).
42
41
  *
43
- * Persistence: SQLite (engine/state.db, `qa_sessions` table) as source of
44
- * truth since Phase 8 (PRD: migrate-qa-state-to-sqlite). The legacy JSON
45
- * sidecar at engine/qa-sessions.json is dual-written on every mutation when
46
- * `engine.qaDualWriteJson` is true (default). Capped at
42
+ * Persistence: SQLite (engine/state.db, `qa_sessions` table). Capped at
47
43
  * QA_SESSIONS_MAX_RECORDS via createSession-time rotation.
48
44
  */
49
45
 
@@ -52,22 +48,14 @@ const path = require('path');
52
48
  const shared = require('./shared');
53
49
  const { mutateQaSessions, uid, ts, log } = shared;
54
50
 
55
- // Internal helper — read sessions via the small-state store when available
56
- // (SQL → JSON fallback inside the store), else direct JSON read.
57
51
  function _readSessions() {
58
- try {
59
- const store = require('./small-state-store');
60
- const arr = store.readQaSessions();
61
- if (Array.isArray(arr)) return arr;
62
- } catch { /* fall back */ }
63
- return _readSessions();
52
+ return require('./small-state-store').readQaSessions();
64
53
  }
65
54
 
66
- // Cap engine/qa-sessions.json. Sessions cost more than runs (3 WIs, a
55
+ // Sessions cost more than runs (3 WIs, a
67
56
  // managed-spawn, artifacts) so the operational steady state is meaningfully
68
57
  // smaller than QA_RUNS_MAX_RECORDS (2000). 500 covers ~2 months of nightly +
69
- // ad-hoc sessions without ballooning the JSON parse cost on /api/status's
70
- // fast-state slice (W-mpehsyhv event-loop budget — see qa-runs.js cap notes).
58
+ // ad-hoc sessions.
71
59
  const QA_SESSIONS_MAX_RECORDS = 500;
72
60
 
73
61
  const QA_SESSION_STATE = Object.freeze({
package/engine/queries.js CHANGED
@@ -178,19 +178,10 @@ function timeSince(ms) {
178
178
  }
179
179
 
180
180
  function readJsonNoRestore(filePath) {
181
- let raw;
182
181
  try {
183
- raw = fs.readFileSync(filePath, 'utf8');
182
+ return shared.safeJsonNoRestore(filePath);
184
183
  } catch (e) {
185
- if (e && e.code !== 'ENOENT') {
186
- console.warn(`[queries] failed to read ${_relativeStatePath(filePath)}: ${e.message}`);
187
- }
188
- return null;
189
- }
190
- try {
191
- return JSON.parse(raw);
192
- } catch (e) {
193
- console.warn(`[queries] corrupt JSON in ${_relativeStatePath(filePath)}: ${e.message}`);
184
+ console.warn(`[queries] failed to read ${_relativeStatePath(filePath)}: ${e.message}`);
194
185
  return null;
195
186
  }
196
187
  }
@@ -398,7 +389,7 @@ function getDispatchQueue() {
398
389
  const d = getDispatch();
399
390
  const allCompleted = d.completed || [];
400
391
  // Lifetime total from metrics (dispatch.completed is capped at 100)
401
- const metrics = readJsonNoRestore(path.join(ENGINE_DIR, 'metrics.json')) || {};
392
+ const metrics = require('./metrics-store').readMetrics() || {};
402
393
  // Periodically prune cache entries for dispatches that have rotated out of the queue.
403
394
  if (_completionReportCache.size >= 200) {
404
395
  const activeIds = new Set();
@@ -846,7 +837,8 @@ function getAgentDetail(id) {
846
837
 
847
838
  function getPrs(project) {
848
839
  if (project) {
849
- const prs = readJsonNoRestore(projectPrPath(project)) || [];
840
+ const store = require('./pull-requests-store');
841
+ const prs = store.readPullRequestsForScope(store.scopeForFilePath(projectPrPath(project))) || [];
850
842
  shared.normalizePrRecords(prs, project);
851
843
  return prs;
852
844
  }
@@ -1217,10 +1209,9 @@ function _skillsCacheKeyFor(config, homeDir) {
1217
1209
  // it via `runtimes: [name, ...]` so diagnostics can show provenance.
1218
1210
  //
1219
1211
  // Consumers: dashboard `/api/status` skills/MCP slices (via collectSkillFiles
1220
- // / collectCommandFiles refactor), the planned `/api/harness/diagnostics`
1221
- // endpoint, `engine.js#spawnAgent` for `computeAddDirs` project-local asset
1222
- // propagation, and `minions doctor --harness` (preflight.js still calls the
1223
- // per-adapter methods directly there to label by adapter — that's intentional).
1212
+ // / collectCommandFiles refactor), the `/api/harness/diagnostics` inventory,
1213
+ // and `minions doctor --harness` (preflight.js calls the per-adapter methods
1214
+ // directly there to label by adapter that's intentional).
1224
1215
  //
1225
1216
  // Return shape:
1226
1217
  // { skills: HarnessEntry[], commands: HarnessEntry[], mcps: HarnessEntry[] }
@@ -1321,49 +1312,6 @@ function getProjectHarnesses(project) {
1321
1312
  );
1322
1313
  }
1323
1314
 
1324
- // buildHarnessPropagatedManifest (P-a8f3c2d1) — pure mapper that folds the
1325
- // getUserHarnesses + getProjectHarnesses aggregation (HarnessEntry roots) and a
1326
- // resolved `--add-dir` set into the canonical grounding-manifest shape persisted
1327
- // onto the dispatch record as `_harnessPropagated`:
1328
- // { skills:[{name,source,path}], mcpServers:[{name,scope}],
1329
- // commands:[{name,scope}], addDirs:[...] }
1330
- // Unions BOTH user-scope and project-scope contributions (the CRITICAL note in
1331
- // P-a8f3c2d1: omitting user-scope harnesses wrongly flags ~/.agents / ~/.claude
1332
- // skill usage grounded:false downstream) and dedups by absolute path / scoped
1333
- // name. Pure: no fs, no os, no runtime registry — all inputs are injected, so it
1334
- // unit-tests trivially against a fixture project.
1335
- function buildHarnessPropagatedManifest({ userHarnesses, projectHarnesses, addDirs } = {}) {
1336
- const u = userHarnesses || { skills: [], commands: [], mcps: [] };
1337
- const p = projectHarnesses || { skills: [], commands: [], mcps: [] };
1338
- const mapSkill = (e) => ({ name: path.basename(e.dir || ''), source: e.scope || 'unknown', path: e.dir || '' });
1339
- const mapCommand = (e) => ({ name: path.basename(e.dir || ''), scope: e.scope || 'unknown' });
1340
- const mapMcp = (e) => ({ name: path.basename(e.file || ''), scope: e.scope || 'unknown' });
1341
- const dedupBy = (arr, keyFn) => {
1342
- const seen = new Set();
1343
- const out = [];
1344
- for (const item of arr) {
1345
- const k = keyFn(item);
1346
- if (seen.has(k)) continue;
1347
- seen.add(k);
1348
- out.push(item);
1349
- }
1350
- return out;
1351
- };
1352
- const skills = dedupBy(
1353
- [...(u.skills || []), ...(p.skills || [])].filter(e => e && e.dir).map(mapSkill),
1354
- (s) => s.path,
1355
- );
1356
- const commands = dedupBy(
1357
- [...(u.commands || []), ...(p.commands || [])].filter(e => e && e.dir).map(mapCommand),
1358
- (c) => `${c.scope}:${c.name}`,
1359
- );
1360
- const mcpServers = dedupBy(
1361
- [...(u.mcps || []), ...(p.mcps || [])].filter(e => e && e.file).map(mapMcp),
1362
- (m) => `${m.scope}:${m.name}`,
1363
- );
1364
- return { skills, mcpServers, commands, addDirs: Array.isArray(addDirs) ? addDirs : [] };
1365
- }
1366
-
1367
1315
  function collectSkillFiles(config) {
1368
1316
  const now = Date.now();
1369
1317
  config = config || getConfig();
@@ -2132,33 +2080,19 @@ function getWorkItems(config, opts) {
2132
2080
  // ── PRD Progress ────────────────────────────────────────────────────────────
2133
2081
 
2134
2082
  // Module-level caches for getPrdInfo() — avoids re-reading unchanged PRD data
2135
- let _prdDirMtimes = { prd: 0, archive: 0 }; // directory mtimes to detect new/deleted files
2136
2083
  let _prdResultCache = null; // cached final result
2137
- let _prdResultInputHash = ''; // hash of all input mtimes to detect any change
2084
+ let _prdResultInputHash = ''; // SQL event version + authored-plan mtimes
2138
2085
 
2139
2086
  /**
2140
2087
  * Collect mtimes of all input files that affect getPrdInfo() output.
2141
2088
  * Returns a string hash for quick equality check plus the dir mtimes.
2142
2089
  */
2143
- function _getPrdInputHash(projects) {
2090
+ function _getPrdInputHash() {
2144
2091
  const mtimes = [];
2145
- // PRD directory mtimes (detect new/deleted files)
2146
- let prdDirMtime = 0, archiveDirMtime = 0;
2147
- try { prdDirMtime = fs.statSync(PRD_DIR).mtimeMs; } catch { /* optional */ }
2148
- const archiveDir = path.join(PRD_DIR, 'archive');
2149
- try { archiveDirMtime = fs.statSync(archiveDir).mtimeMs; } catch { /* optional */ }
2150
- mtimes.push(prdDirMtime, archiveDirMtime);
2151
- // Work-items file mtimes (affect status display)
2152
- for (const project of projects) {
2153
- try { mtimes.push(fs.statSync(projectWorkItemsPath(project)).mtimeMs); } catch { mtimes.push(0); }
2154
- }
2155
- try { mtimes.push(fs.statSync(path.join(MINIONS_DIR, 'work-items.json')).mtimeMs); } catch { mtimes.push(0); }
2156
- // PR file mtimes (affect PR links)
2157
- for (const project of projects) {
2158
- try { mtimes.push(fs.statSync(projectPrPath(project)).mtimeMs); } catch { mtimes.push(0); }
2159
- }
2160
- // Static pr-links.json overrides (affect shared.getPrLinks(); missing project mtimes otherwise)
2161
- try { mtimes.push(fs.statSync(path.join(MINIONS_DIR, 'engine', 'pr-links.json')).mtimeMs); } catch { mtimes.push(0); }
2092
+ try {
2093
+ const row = require('./db').getDb().prepare('SELECT COALESCE(MAX(id), 0) AS version FROM events').get();
2094
+ mtimes.push(Number(row?.version) || 0);
2095
+ } catch { mtimes.push(0); }
2162
2096
  // W-mqacrzis0003df4a — Source-plan markdown mtimes. Without this, the
2163
2097
  // per-PRD planStale fresh-computation at lines ~1568-1576 is never reached
2164
2098
  // after the user revises plans/<x>.md until the engine tick (~10s) flips
@@ -2174,7 +2108,7 @@ function _getPrdInputHash(projects) {
2174
2108
  try { mtimes.push(fs.statSync(path.join(PLANS_DIR, f)).mtimeMs); } catch { mtimes.push(0); }
2175
2109
  }
2176
2110
  } catch { /* plans/ may not exist in some test/init states */ }
2177
- return { hash: mtimes.join(','), prdDirMtime, archiveDirMtime };
2111
+ return mtimes.join(',');
2178
2112
  }
2179
2113
 
2180
2114
  function getPrdInfo(config) {
@@ -2182,7 +2116,7 @@ function getPrdInfo(config) {
2182
2116
  const projects = getProjects(config);
2183
2117
 
2184
2118
  // Quick mtime check — return cached result if nothing changed
2185
- const { hash, prdDirMtime, archiveDirMtime } = _getPrdInputHash(projects);
2119
+ const hash = _getPrdInputHash();
2186
2120
  if (_prdResultCache && hash === _prdResultInputHash) return _prdResultCache;
2187
2121
 
2188
2122
  let allPrdItems = [];
@@ -2192,10 +2126,7 @@ function getPrdInfo(config) {
2192
2126
  // filename so render-prd.js#renderE2eSection can merge them with the live
2193
2127
  // tracker and keep rendering a merged aggregate after its live record is swept.
2194
2128
  const verifyPrsByPlan = {};
2195
- let latestStat = null;
2196
-
2197
- // Check if directory listings need refresh (used by mtime hash)
2198
- _prdDirMtimes = { prd: prdDirMtime, archive: archiveDirMtime };
2129
+ let latestUpdatedAt = 0;
2199
2130
 
2200
2131
  // Phase 10 step 4.3 — FK-based WI↔PRD-item join scaffolding. `prdItemIdByKey`
2201
2132
  // maps `${physicalArchived} ${filename} ${feature_id}` → prd_items.id so each
@@ -2275,22 +2206,12 @@ function getPrdInfo(config) {
2275
2206
  } catch { prdItemIdByKey = null; }
2276
2207
  }
2277
2208
 
2278
- // Phase 10 step 3 (read-flip complete) — SQL mirror is the unconditional
2279
- // source. reconcilePrdsFromDisk() catches PRDs written outside the dual-write
2280
- // chokepoint (plan-to-prd agent direct writes, archive moves, deletes). Files
2281
- // still live on disk in this phase, so stat them for mtime fields + `age`.
2209
+ // SQLite is the unconditional PRD source.
2282
2210
  const prdStore = require('./prd-store');
2283
- prdStore.reconcilePrdsFromDisk();
2284
- for (const { filename, archived, plan } of prdStore.listPrdRows()) {
2211
+ for (const { filename, archived, plan, updatedAt } of prdStore.listPrdRows()) {
2285
2212
  try {
2286
- const dir = archived ? path.join(PRD_DIR, 'archive') : PRD_DIR;
2287
- let mtimeMs = 0;
2288
- try {
2289
- const stat = fs.statSync(path.join(dir, filename));
2290
- mtimeMs = stat.mtimeMs;
2291
- if (!latestStat || stat.mtimeMs > latestStat.mtimeMs) latestStat = stat;
2292
- } catch { /* file may have just moved/deleted between reconcile and read */ }
2293
- processPrd(filename, archived, plan, mtimeMs);
2213
+ latestUpdatedAt = Math.max(latestUpdatedAt, updatedAt);
2214
+ processPrd(filename, archived, plan, updatedAt);
2294
2215
  } catch { /* optional */ }
2295
2216
  }
2296
2217
  if (allPrdItems.length === 0) return { progress: null, status: null };
@@ -2527,7 +2448,7 @@ function getPrdInfo(config) {
2527
2448
  };
2528
2449
 
2529
2450
  const status = {
2530
- exists: true, age: latestStat ? timeSince(latestStat.mtimeMs) : 'unknown',
2451
+ exists: true, age: latestUpdatedAt ? timeSince(latestUpdatedAt) : 'unknown',
2531
2452
  existing: existingPrds, missing: items.filter(i => i.status === 'missing').length, questions: 0, summary: '',
2532
2453
  missingList: items.filter(i => i.status === 'missing').map(f => ({ id: f.id, name: f.name || f.title, priority: f.priority, complexity: f.estimated_complexity || f.size })),
2533
2454
  };
@@ -2540,7 +2461,6 @@ function getPrdInfo(config) {
2540
2461
 
2541
2462
  /** Reset PRD info cache — exported for testing */
2542
2463
  function resetPrdInfoCache() {
2543
- _prdDirMtimes = { prd: 0, archive: 0 };
2544
2464
  _prdResultCache = null;
2545
2465
  _prdResultInputHash = '';
2546
2466
  }
@@ -3454,7 +3374,7 @@ module.exports = {
3454
3374
 
3455
3375
  // Harness aggregation (P-f5a91c30) — shared union over per-runtime
3456
3376
  // getSkillRoots / getCommandRoots / getMcpConfigPaths.
3457
- getUserHarnesses, getProjectHarnesses, buildHarnessPropagatedManifest,
3377
+ getUserHarnesses, getProjectHarnesses,
3458
3378
 
3459
3379
  // Knowledge base
3460
3380
  getKnowledgeBaseEntries, getKnowledgeBaseEntriesSnapshot, getKnowledgeBaseIndex,
package/engine/routing.js CHANGED
@@ -64,7 +64,7 @@ function getRoutingTableCached() {
64
64
  // ─── Budget ──────────────────────────────────────────────────────────────────
65
65
 
66
66
  function getMonthlySpend(agentId) {
67
- const metrics = safeJson(path.join(ENGINE_DIR, 'metrics.json')) || {};
67
+ const metrics = queries.getMetrics() || {};
68
68
  const daily = metrics._daily || {};
69
69
  const now = new Date();
70
70
  const monthPrefix = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
@@ -206,7 +206,7 @@ const MODELS_CACHE = path.join(_CACHE_DIR, 'claude-models.json');
206
206
  *
207
207
  * Conditional flags are emitted ONLY when their corresponding capability is
208
208
  * truthy. Copilot-only flags (`stream`, `disableBuiltinMcps`,
209
- * `suppressAgentsMd`, `reasoningSummaries`) are silently ignored on the Claude
209
+ * `reasoningSummaries`) are silently ignored on the Claude
210
210
  * path — runtime adapters MUST be tolerant of unknown opts so engine code can
211
211
  * pass the same option bag to every adapter without branching.
212
212
  */
@@ -264,7 +264,6 @@ function buildSpawnFlags(opts = {}) {
264
264
  if (opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
265
265
  if (opts.stream != null && opts.stream !== '') flags.push('--stream', String(opts.stream));
266
266
  if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
267
- if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
268
267
  if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
269
268
  return flags;
270
269
  }