@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.
- package/bin/minions.js +38 -11
- package/dashboard/js/render-other.js +2 -30
- package/dashboard/js/render-work-items.js +0 -34
- package/dashboard/js/settings.js +12 -27
- package/dashboard/pages/tools.html +1 -1
- package/dashboard.js +80 -257
- package/docs/README.md +2 -3
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +3 -3
- package/docs/completion-reports.md +0 -121
- package/docs/copilot-cli-schema.md +9 -17
- package/docs/deprecated.json +0 -51
- package/docs/design-state-storage.md +4 -4
- package/docs/harness-propagation.md +33 -263
- package/docs/human-vs-automated.md +1 -1
- package/docs/named-agents.md +0 -2
- package/docs/plan-lifecycle.md +5 -6
- package/docs/qa-runbook-lifecycle.md +10 -25
- package/docs/shared-lifecycle-module-map.md +2 -10
- package/engine/ado-comment.js +5 -11
- package/engine/ado.js +10 -5
- package/engine/cleanup.js +16 -36
- package/engine/cli.js +13 -175
- package/engine/comment-format.js +8 -182
- package/engine/db/index.js +60 -10
- package/engine/db/migrations/018-sql-only-cutover.js +56 -0
- package/engine/dispatch-store.js +0 -114
- package/engine/dispatch.js +1 -6
- package/engine/gh-comment.js +2 -10
- package/engine/github.js +8 -6
- package/engine/lifecycle.js +58 -173
- package/engine/llm.js +9 -11
- package/engine/managed-spawn.js +1 -6
- package/engine/memory-store.js +8 -6
- package/engine/metrics-store.js +4 -128
- package/engine/pipeline.js +4 -7
- package/engine/playbook.js +8 -196
- package/engine/prd-store.js +115 -141
- package/engine/preflight.js +8 -55
- package/engine/projects.js +34 -41
- package/engine/pull-requests-store.js +9 -234
- package/engine/qa-runs.js +4 -15
- package/engine/qa-sessions.js +5 -17
- package/engine/queries.js +23 -103
- package/engine/routing.js +1 -1
- package/engine/runtimes/claude.js +1 -2
- package/engine/runtimes/codex.js +0 -2
- package/engine/runtimes/copilot.js +1 -4
- package/engine/shared-branch-pr-reconcile.js +2 -5
- package/engine/shared.js +174 -533
- package/engine/small-state-store.js +12 -647
- package/engine/spawn-agent.js +5 -96
- package/engine/state-operations.js +166 -0
- package/engine/supervisor.js +0 -1
- package/engine/watch-actions.js +2 -3
- package/engine/watches-store.js +2 -128
- package/engine/work-items-store.js +3 -254
- package/engine.js +102 -334
- package/package.json +2 -2
- package/playbooks/build-fix-complex.md +0 -4
- package/playbooks/fix.md +0 -4
- package/playbooks/implement-shared.md +0 -4
- package/playbooks/implement.md +0 -4
- package/playbooks/plan-to-prd.md +16 -11
- package/playbooks/plan.md +0 -4
- package/playbooks/review.md +0 -6
- package/playbooks/shared-rules.md +0 -65
- package/docs/harness-transparency.md +0 -199
- package/docs/project-skills.md +0 -193
- package/engine/discover-project-skills.js +0 -673
- package/engine/playbook-intents.js +0 -76
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
// engine/pull-requests-store.js — SQL-backed
|
|
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'
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
/**
|
package/engine/qa-sessions.js
CHANGED
|
@@ -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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
182
|
+
return shared.safeJsonNoRestore(filePath);
|
|
184
183
|
} catch (e) {
|
|
185
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
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 = ''; //
|
|
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(
|
|
2090
|
+
function _getPrdInputHash() {
|
|
2144
2091
|
const mtimes = [];
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
2284
|
-
for (const { filename, archived, plan } of prdStore.listPrdRows()) {
|
|
2211
|
+
for (const { filename, archived, plan, updatedAt } of prdStore.listPrdRows()) {
|
|
2285
2212
|
try {
|
|
2286
|
-
|
|
2287
|
-
|
|
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:
|
|
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,
|
|
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 =
|
|
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
|
-
* `
|
|
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
|
}
|