@yemi33/minions 0.1.2369 → 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 (79) hide show
  1. package/bin/minions.js +38 -11
  2. package/dashboard/js/memory-search.js +112 -0
  3. package/dashboard/js/render-kb.js +15 -0
  4. package/dashboard/js/render-other.js +2 -30
  5. package/dashboard/js/render-work-items.js +0 -34
  6. package/dashboard/js/settings.js +27 -27
  7. package/dashboard/pages/inbox.html +1 -1
  8. package/dashboard/pages/tools.html +1 -1
  9. package/dashboard.js +148 -258
  10. package/docs/README.md +2 -3
  11. package/docs/architecture.excalidraw +2 -2
  12. package/docs/auto-discovery.md +3 -3
  13. package/docs/completion-reports.md +0 -121
  14. package/docs/copilot-cli-schema.md +9 -17
  15. package/docs/deprecated.json +0 -51
  16. package/docs/design-state-storage.md +4 -4
  17. package/docs/harness-propagation.md +33 -263
  18. package/docs/human-vs-automated.md +1 -1
  19. package/docs/named-agents.md +0 -2
  20. package/docs/plan-lifecycle.md +5 -6
  21. package/docs/qa-runbook-lifecycle.md +10 -25
  22. package/docs/shared-lifecycle-module-map.md +2 -10
  23. package/docs/team-memory.md +18 -4
  24. package/engine/ado-comment.js +5 -11
  25. package/engine/ado.js +10 -5
  26. package/engine/cleanup.js +16 -36
  27. package/engine/cli.js +13 -175
  28. package/engine/comment-format.js +8 -182
  29. package/engine/consolidation.js +72 -1
  30. package/engine/db/index.js +60 -10
  31. package/engine/db/migrations/017-agent-memory.js +208 -0
  32. package/engine/db/migrations/018-sql-only-cutover.js +56 -0
  33. package/engine/dispatch-store.js +0 -114
  34. package/engine/dispatch.js +1 -6
  35. package/engine/features.js +6 -0
  36. package/engine/gh-comment.js +2 -10
  37. package/engine/github.js +8 -6
  38. package/engine/lifecycle.js +141 -173
  39. package/engine/llm.js +9 -11
  40. package/engine/managed-spawn.js +1 -6
  41. package/engine/memory-retrieval.js +165 -0
  42. package/engine/memory-store.js +367 -0
  43. package/engine/metrics-store.js +4 -128
  44. package/engine/pipeline.js +4 -7
  45. package/engine/playbook.js +64 -198
  46. package/engine/prd-store.js +115 -141
  47. package/engine/preflight.js +8 -55
  48. package/engine/projects.js +34 -41
  49. package/engine/pull-requests-store.js +9 -234
  50. package/engine/qa-runs.js +4 -15
  51. package/engine/qa-sessions.js +5 -17
  52. package/engine/queries.js +23 -103
  53. package/engine/routing.js +1 -1
  54. package/engine/runtimes/claude.js +1 -2
  55. package/engine/runtimes/codex.js +0 -2
  56. package/engine/runtimes/copilot.js +1 -4
  57. package/engine/shared-branch-pr-reconcile.js +2 -5
  58. package/engine/shared.js +179 -533
  59. package/engine/small-state-store.js +12 -647
  60. package/engine/spawn-agent.js +5 -96
  61. package/engine/state-operations.js +166 -0
  62. package/engine/supervisor.js +0 -1
  63. package/engine/watch-actions.js +2 -3
  64. package/engine/watches-store.js +2 -128
  65. package/engine/work-items-store.js +3 -254
  66. package/engine.js +102 -334
  67. package/package.json +2 -2
  68. package/playbooks/build-fix-complex.md +0 -4
  69. package/playbooks/fix.md +0 -4
  70. package/playbooks/implement-shared.md +0 -4
  71. package/playbooks/implement.md +0 -4
  72. package/playbooks/plan-to-prd.md +16 -11
  73. package/playbooks/plan.md +0 -4
  74. package/playbooks/review.md +0 -6
  75. package/playbooks/shared-rules.md +0 -65
  76. package/docs/harness-transparency.md +0 -199
  77. package/docs/project-skills.md +0 -193
  78. package/engine/discover-project-skills.js +0 -673
  79. package/engine/playbook-intents.js +0 -76
@@ -1,6 +1,4 @@
1
- // engine/work-items-store.js — SQL-backed implementation of the
2
- // per-file work-items array. Same external contract as the legacy
3
- // JSON-file reader:
1
+ // engine/work-items-store.js — SQL-backed scoped work-item storage.
4
2
  //
5
3
  // readWorkItemsForScope(scope) -> [wi, wi, ...]
6
4
  // applyWorkItemsMutation(scope, fn) -> runs fn against the array,
@@ -8,16 +6,11 @@
8
6
  // transaction, returns
9
7
  // { wrote, result }.
10
8
  //
11
- // `scope` is 'central' for the top-level work-items.json or a project
12
- // name for projects/<name>/work-items.json. Mirrors the file convention
13
- // established by migration 003.
9
+ // `scope` is 'central' or a project name.
14
10
  //
15
11
  // All callers go through shared.mutateWorkItems and
16
12
  // queries.getWorkItems — those are thin wrappers over the helpers here.
17
13
 
18
- const path = require('path');
19
- const fs = require('fs');
20
-
21
14
  function _toMs(v) {
22
15
  if (v == null) return null;
23
16
  if (typeof v === 'number') return Number.isFinite(v) ? v : null;
@@ -40,60 +33,22 @@ function scopeForFilePath(filePath) {
40
33
  return 'central';
41
34
  }
42
35
 
43
- // JSON fallback path resolution. Used both for the empty-SQL fallback
44
- // (test seeding pattern from Phase 1) and for the post-write JSON mirror.
45
- function _filePathForScope(scope) {
46
- const shared = require('./shared');
47
- if (scope === 'central') {
48
- return path.join(shared.MINIONS_DIR, 'work-items.json');
49
- }
50
- return path.join(shared.MINIONS_DIR, 'projects', scope, 'work-items.json');
51
- }
52
-
53
36
  function _parseRow(row) {
54
37
  if (!row || !row.data) return null;
55
38
  try { return JSON.parse(row.data); }
56
39
  catch { return null; }
57
40
  }
58
41
 
59
- function _readJsonArrayFallback(scope) {
60
- const fp = _filePathForScope(scope);
61
- let raw;
62
- try { raw = fs.readFileSync(fp, 'utf8'); }
63
- catch { return []; }
64
- try {
65
- const parsed = JSON.parse(raw);
66
- return Array.isArray(parsed) ? parsed : [];
67
- } catch (e) {
68
- try {
69
- // eslint-disable-next-line no-console
70
- console.warn(`[work-items-store] corrupt JSON in ${fp}: ${e.message}`);
71
- } catch { /* console may be wrapped in tests */ }
72
- return [];
73
- }
74
- }
75
-
76
42
  function readWorkItemsForScope(scope) {
77
43
  const { getDb } = require('./db');
78
44
  const db = getDb();
79
45
 
80
- _resyncScopeIfJsonDiverged(db, scope);
81
-
82
46
  const rows = db.prepare(`
83
47
  SELECT data FROM work_items
84
48
  WHERE scope = ?
85
49
  ORDER BY rowid
86
50
  `).all(scope);
87
51
 
88
- if (rows.length === 0) {
89
- // SQL empty AND JSON has content means a test seeded via fs.writeFileSync
90
- // (legacy helper) or a fresh install pre-migration. Returning the JSON
91
- // keeps those call sites working without touching every helper.
92
- const fallback = _readJsonArrayFallback(scope);
93
- if (fallback.length > 0) return fallback;
94
- return [];
95
- }
96
-
97
52
  const out = [];
98
53
  for (const row of rows) {
99
54
  const wi = _parseRow(row);
@@ -102,58 +57,6 @@ function readWorkItemsForScope(scope) {
102
57
  return out;
103
58
  }
104
59
 
105
- // If the JSON mirror's mtime advanced since our last mirror write, the
106
- // file was rewritten outside the store (test cleanup + safeWrite, manual
107
- // operator edit, external tool). Treat the JSON as canonical and rebuild
108
- // SQL for that scope from it. Called from BOTH read and write paths so
109
- // readers don't see stale SQL state and writers don't compute diffs
110
- // against a SQL snapshot that no longer matches what callers expect.
111
- //
112
- // Safe to call inside `withTransaction` — DELETE + INSERT against a
113
- // scope is bounded by the JSON's row count.
114
- function _resyncScopeIfJsonDiverged(db, scope) {
115
- const jsonPath = _filePathForScope(scope);
116
- const currentHash = _fileContentHash(jsonPath);
117
- const lastHash = _lastMirrorHashByScope.get(scope);
118
- if (currentHash == null) return;
119
- if (lastHash != null && currentHash === lastHash) return;
120
- // First touch in this process (lastHash == null): if SQL already holds rows
121
- // for the scope, SQL is the established source of truth (post-Phase-9.4) —
122
- // adopt the current JSON hash and DO NOT hydrate. Hydrating here would let a
123
- // crash-stale / .backup-restored / git-checked-out older JSON mirror DELETE +
124
- // overwrite newer SQL. Only an empty-in-SQL scope (genuine first-install
125
- // migration) falls through to hydrate-from-JSON, which is purely additive.
126
- if (lastHash == null) {
127
- const sqlHas = db.prepare('SELECT 1 FROM work_items WHERE scope = ? LIMIT 1').get(scope);
128
- if (sqlHas) {
129
- _lastMirrorHashByScope.set(scope, currentHash);
130
- return;
131
- }
132
- }
133
- _hydrateScopeFromJson(db, scope);
134
- _lastMirrorHashByScope.set(scope, currentHash);
135
- }
136
-
137
- // Enumerate scopes that have a JSON file on disk — used as the SQL-
138
- // unavailable fallback for `readAllWorkItems`. Walks `<MINIONS_DIR>/
139
- // projects/*/work-items.json` plus the central `<MINIONS_DIR>/work-
140
- // items.json`. Silently swallows missing dirs (fresh installs).
141
- function _enumerateJsonScopes() {
142
- const shared = require('./shared');
143
- const scopes = [];
144
- const centralPath = path.join(shared.MINIONS_DIR, 'work-items.json');
145
- if (fs.existsSync(centralPath)) scopes.push('central');
146
- const projectsDir = path.join(shared.MINIONS_DIR, 'projects');
147
- try {
148
- for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
149
- if (!d.isDirectory()) continue;
150
- const fp = path.join(projectsDir, d.name, 'work-items.json');
151
- if (fs.existsSync(fp)) scopes.push(d.name);
152
- }
153
- } catch { /* projects dir missing on fresh install */ }
154
- return scopes;
155
- }
156
-
157
60
  // Read all rows across all scopes — used by queries.getWorkItems which
158
61
  // needs to surface central + every project's items in a single shot,
159
62
  // tagged with their source scope.
@@ -161,17 +64,6 @@ function readAllWorkItems() {
161
64
  const { getDb } = require('./db');
162
65
  const db = getDb();
163
66
 
164
- // Pick up any external JSON edits for every scope SQL knows about.
165
- // Also resync 'central' explicitly so first-time reads on a JSON-only
166
- // install hydrate the central scope. Per-project scopes get checked
167
- // lazily on next read or mutation of that scope.
168
- try { _resyncScopeIfJsonDiverged(db, 'central'); } catch {}
169
- const knownScopes = db.prepare('SELECT DISTINCT scope FROM work_items').all().map(r => r.scope);
170
- for (const s of knownScopes) {
171
- if (s === 'central') continue;
172
- try { _resyncScopeIfJsonDiverged(db, s); } catch {}
173
- }
174
-
175
67
  const rows = db.prepare('SELECT data, scope FROM work_items ORDER BY rowid').all();
176
68
  const out = [];
177
69
  for (const row of rows) {
@@ -255,83 +147,11 @@ function _applyWorkItemsDiff(db, diff) {
255
147
  // wrote — true iff at least one INSERT/UPDATE/DELETE landed
256
148
  // result — the post-mutation array (legacy return shape of
257
149
  // mutateJsonFileLocked)
258
- // Content-hash fingerprint of the JSON mirror per scope, so external
259
- // edits are detected unambiguously. (mtime, size) is too coarse: same-
260
- // length timestamp swaps (last_checked / last_triggered / completed_at)
261
- // preserve both axes. SHA-1 is sub-ms on a few-hundred-KB file.
262
- const _lastMirrorHashByScope = new Map();
263
-
264
- const crypto = require('crypto');
265
-
266
- function _fileContentHash(filePath) {
267
- try {
268
- const buf = fs.readFileSync(filePath);
269
- return crypto.createHash('sha1').update(buf).digest('hex');
270
- }
271
- catch { return null; }
272
- }
273
-
274
- function _hydrateScopeFromJson(db, scope) {
275
- const jsonItems = _readJsonArrayFallback(scope);
276
- // Preserve the SQL-only prd_item_id FK across the delete+reinsert. It is owned
277
- // by the PRD dual-write (prd-store.js#_upsertPrd), NOT represented in the JSON
278
- // mirror, so a naive rebuild-from-JSON silently zeroes it for the whole scope —
279
- // regressing the WI↔PRD link every time the JSON diverges (the count would
280
- // oscillate as the PRD mirror re-stamps and the next hydrate wipes it again).
281
- // Snapshot before the DELETE, restore onto the rows that survive the rebuild.
282
- const fkById = new Map();
283
- try {
284
- for (const r of db.prepare('SELECT id, prd_item_id FROM work_items WHERE scope = ? AND prd_item_id IS NOT NULL').all(scope)) {
285
- fkById.set(r.id, r.prd_item_id);
286
- }
287
- } catch { /* column absent on a pre-v15 schema — nothing to preserve */ }
288
- // DELETE before re-insert: callers that wrote a smaller JSON file
289
- // (test cleanup() removing items) must end up with a smaller SQL state.
290
- db.prepare('DELETE FROM work_items WHERE scope = ?').run(scope);
291
- if (jsonItems.length === 0) return;
292
- const now = Date.now();
293
- const ins = db.prepare(`
294
- INSERT INTO work_items (id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at)
295
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
296
- ON CONFLICT(scope, id) DO NOTHING
297
- `);
298
- for (const wi of jsonItems) {
299
- if (!wi || !wi.id) continue;
300
- ins.run(
301
- String(wi.id),
302
- scope,
303
- String(wi.status || 'pending'),
304
- wi.type || null,
305
- wi.dispatched_to || wi.agent || null,
306
- wi.parent_id || null,
307
- _toMs(wi.created_at),
308
- _toMs(wi.completed_at || wi.completedAt),
309
- JSON.stringify(wi),
310
- now,
311
- );
312
- }
313
- // Restore the FK for rows that still exist after the rebuild (a WI dropped
314
- // from the JSON is gone, and its link goes with it — correct).
315
- if (fkById.size) {
316
- const restore = db.prepare('UPDATE work_items SET prd_item_id = ? WHERE scope = ? AND id = ?');
317
- for (const [id, fk] of fkById) {
318
- try { restore.run(fk, scope, id); } catch { /* best effort */ }
319
- }
320
- }
321
- }
322
-
323
- function applyWorkItemsMutation(scope, mutator, options = {}) {
150
+ function applyWorkItemsMutation(scope, mutator) {
324
151
  const { getDb, withTransaction } = require('./db');
325
152
  const db = getDb();
326
153
 
327
154
  return withTransaction(db, () => {
328
- // Re-hydrate SQL from JSON if the file was touched outside the
329
- // store (external edit, test re-seed, first-time hydrate after a
330
- // fresh install). Without this, the diff would compare against a
331
- // stale SQL snapshot and the mirror write would overwrite the JSON
332
- // with that stale state.
333
- _resyncScopeIfJsonDiverged(db, scope);
334
-
335
155
  const before = readWorkItemsForScope(scope);
336
156
  // Strip the _source decoration before handing to the mutator — it's
337
157
  // added by readAllWorkItems only and would round-trip as garbage into
@@ -344,63 +164,10 @@ function applyWorkItemsMutation(scope, mutator, options = {}) {
344
164
  : (Array.isArray(next) ? next : before);
345
165
  const diff = _computeWorkItemsDiff(scope, beforeSnapshot, after);
346
166
  const wrote = _applyWorkItemsDiff(db, diff);
347
- // W-mr9vcxvy000cdb4e — phantom-WI race fix: mirror the JSON sidecar
348
- // INSIDE this same SQL write transaction, BEFORE it commits, instead
349
- // of after (as the caller used to do post-return). SQLite only allows
350
- // one writer transaction at a time (BEGIN IMMEDIATE holds an
351
- // exclusive writer lock), so mirroring here guarantees the on-disk
352
- // JSON file is fully up to date by the time this commit becomes
353
- // visible to any other reader/writer — dashboard and engine are
354
- // SEPARATE Node processes, each with its own in-memory
355
- // `_lastMirrorHashByScope` cache, so without this ordering a sibling
356
- // process could observe a freshly committed SQL row before the mirror
357
- // file caught up, decide (via its own stale hash) that the JSON file
358
- // was "externally edited", and call `_hydrateScopeFromJson` — which
359
- // DELETEs + reinserts the whole scope from that stale file, silently
360
- // dropping the row a POST /api/work-items caller had just been told
361
- // was created successfully (visible to the engine's dispatch loop
362
- // one moment, gone from work_items the next). Mirroring pre-commit
363
- // means "SQL says the row exists" and "JSON mirror has the row" can
364
- // never observably disagree across processes.
365
- if (wrote) {
366
- try { _mirrorJsonFromSql(scope, options.mirrorPath); } catch { /* best-effort — mirror failure must not block the SQL write */ }
367
- }
368
167
  return { wrote, result: after };
369
168
  });
370
169
  }
371
170
 
372
- // Mirror SQL → JSON file. Phase 2 keeps the file as a read-only derivative
373
- // so legacy direct-readers keep working; SQL is the source of truth.
374
- function _mirrorJsonFromSql(scope, filePath) {
375
- try {
376
- const shared = require('./shared');
377
- const { getDb } = require('./db');
378
- // Read SQL directly for this scope — bypass the JSON fallback so a
379
- // mutation that empties SQL for the scope doesn't resurrect stale
380
- // JSON content. (See pull-requests-store and watches-store for the
381
- // same defense.)
382
- const db = getDb();
383
- const rows = db.prepare('SELECT data FROM work_items WHERE scope = ? ORDER BY rowid').all(scope);
384
- const items = [];
385
- for (const row of rows) {
386
- const wi = _parseRow(row);
387
- if (!wi) continue;
388
- if (wi._source) delete wi._source;
389
- items.push(wi);
390
- }
391
- const target = filePath || _filePathForScope(scope);
392
- shared.safeWrite(target, items);
393
- const h = _fileContentHash(target);
394
- if (h != null) _lastMirrorHashByScope.set(scope, h);
395
- } catch {
396
- // Mirror failures are non-fatal: SQL has already committed.
397
- }
398
- }
399
-
400
- // Drop every SQL row for `scope` and forget its mirror-hash. Called by
401
- // removeProject after the JSON file is archived so safeJson shim reads no
402
- // longer surface the removed project's records.
403
- //
404
171
  // Per CLAUDE.md's mutator contract, every mutator wrapper must call
405
172
  // emitStateEvent() so the dashboard's MAX(events.id) cache-version
406
173
  // advances — otherwise the dashboard keeps serving stale cached
@@ -411,21 +178,6 @@ function dropScope(scope) {
411
178
  try {
412
179
  const { getDb } = require('./db');
413
180
  const result = getDb().prepare('DELETE FROM work_items WHERE scope = ?').run(scope);
414
- // Do NOT simply forget the mirror-hash here. _resyncScopeIfJsonDiverged
415
- // treats "no recorded hash" + "zero SQL rows for the scope" as a genuine
416
- // first-install hydrate and rebuilds SQL from whatever JSON is on disk.
417
- // When the caller is removeProject with dataMode:'keep', the JSON file for
418
- // this scope is deliberately left untouched — so the very next read/write
419
- // against this scope would silently resurrect the just-dropped project's
420
- // stale rows straight back into SQL. Instead, adopt the JSON file's
421
- // *current* hash (if it still exists) so the next resync correctly reads
422
- // that as "unchanged" and skips the rehydrate. If the file is genuinely
423
- // gone (dataMode: 'purge'/'archive'), forgetting the hash is safe since
424
- // _resyncScopeIfJsonDiverged no-ops when the file can't be hashed.
425
- const jsonPath = _filePathForScope(scope);
426
- const currentHash = _fileContentHash(jsonPath);
427
- if (currentHash == null) _lastMirrorHashByScope.delete(scope);
428
- else _lastMirrorHashByScope.set(scope, currentHash);
429
181
  if (result && result.changes > 0) {
430
182
  try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
431
183
  try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
@@ -439,7 +191,4 @@ module.exports = {
439
191
  readAllWorkItems,
440
192
  applyWorkItemsMutation,
441
193
  dropScope,
442
- _filePathForScope,
443
- _mirrorJsonFromSql,
444
- _hydrateScopeFromJson, // exported for testing (prd_item_id-preservation)
445
194
  };