@yemi33/minions 0.1.2379 → 0.1.2380

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 (96) hide show
  1. package/bin/minions.js +19 -9
  2. package/dashboard/js/refresh.js +2 -3
  3. package/dashboard/js/render-prd.js +1 -1
  4. package/dashboard/js/render-work-items.js +13 -6
  5. package/dashboard.js +393 -721
  6. package/docs/architecture-review-2026-07-09.md +2 -4
  7. package/docs/auto-discovery.md +36 -49
  8. package/docs/blog-first-successful-dispatch.md +1 -1
  9. package/docs/branch-derivation.md +2 -2
  10. package/docs/command-center.md +1 -1
  11. package/docs/completion-reports.md +14 -4
  12. package/docs/constellation-bridge.md +59 -10
  13. package/docs/constellation-style-telemetry.md +6 -6
  14. package/docs/cooldown-merge-semantics.md +4 -0
  15. package/docs/copilot-cli-schema.md +3 -3
  16. package/docs/cross-repo-plans.md +17 -17
  17. package/docs/deprecated.json +2 -2
  18. package/docs/design-state-storage.md +1 -1
  19. package/docs/documentation-audit-2026-07-09.md +2 -2
  20. package/docs/engine-restart.md +20 -8
  21. package/docs/harness-mode.md +1 -1
  22. package/docs/managed-spawn.md +4 -4
  23. package/docs/onboarding.md +1 -2
  24. package/docs/pr-comment-followup.md +3 -3
  25. package/docs/pr-review-fix-loop.md +1 -1
  26. package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
  27. package/docs/qa-runbook-lifecycle.md +4 -4
  28. package/docs/qa-runbooks.md +2 -2
  29. package/docs/rfc-completion-json.md +4 -1
  30. package/docs/runtime-adapters.md +1 -1
  31. package/docs/self-improvement.md +4 -5
  32. package/docs/shared-lifecycle-module-map.md +3 -1
  33. package/docs/slim-ux/architecture-suggestions.md +5 -6
  34. package/docs/slim-ux/concepts.md +23 -25
  35. package/docs/watches.md +7 -7
  36. package/docs/workspace-manifests.md +1 -1
  37. package/docs/worktree-lifecycle.md +1 -1
  38. package/engine/abandoned-pr-reconciliation.js +4 -5
  39. package/engine/ado-status.js +5 -5
  40. package/engine/ado.js +20 -25
  41. package/engine/agent-worker-pool.js +58 -1
  42. package/engine/bridge.js +260 -5
  43. package/engine/cleanup.js +48 -131
  44. package/engine/cli.js +125 -83
  45. package/engine/cooldown.js +9 -16
  46. package/engine/db/index.js +22 -9
  47. package/engine/db/migrations/009-qa.js +1 -1
  48. package/engine/db/migrations/020-qa-session-scopes.js +23 -0
  49. package/engine/db/migrations/021-archived-work-items.js +72 -0
  50. package/engine/db/migrations/022-global-cc-session.js +31 -0
  51. package/engine/db/migrations/023-engine-state.js +30 -0
  52. package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
  53. package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
  54. package/engine/dispatch-store.js +2 -7
  55. package/engine/dispatch.js +34 -41
  56. package/engine/github.js +20 -27
  57. package/engine/lifecycle.js +221 -283
  58. package/engine/llm.js +1 -1
  59. package/engine/logs-store.js +2 -2
  60. package/engine/managed-spawn.js +2 -23
  61. package/engine/meeting.js +6 -6
  62. package/engine/metrics-store.js +2 -2
  63. package/engine/note-link-backfill.js +6 -11
  64. package/engine/pipeline.js +18 -36
  65. package/engine/playbook.js +1 -1
  66. package/engine/prd-store.js +73 -54
  67. package/engine/preflight.js +2 -5
  68. package/engine/projects.js +15 -62
  69. package/engine/pull-requests-store.js +0 -17
  70. package/engine/qa-runbooks.js +2 -2
  71. package/engine/qa-runs.js +1 -8
  72. package/engine/qa-sessions.js +41 -64
  73. package/engine/queries.js +120 -219
  74. package/engine/routing.js +3 -5
  75. package/engine/scheduler.js +0 -4
  76. package/engine/shared-branch-pr-reconcile.js +2 -3
  77. package/engine/shared.js +132 -637
  78. package/engine/small-state-store.js +89 -10
  79. package/engine/state-operations.js +16 -4
  80. package/engine/stdio-timestamps.js +1 -1
  81. package/engine/timeout.js +5 -12
  82. package/engine/watch-actions.js +20 -22
  83. package/engine/watches-store.js +1 -1
  84. package/engine/watches.js +6 -10
  85. package/engine/work-item-validation.js +52 -0
  86. package/engine/work-items-store.js +127 -29
  87. package/engine/worktree-gc.js +2 -2
  88. package/engine/worktree-pool.js +8 -18
  89. package/engine.js +147 -343
  90. package/minions.js +2 -2
  91. package/package.json +1 -1
  92. package/playbooks/plan-to-prd.md +2 -2
  93. package/playbooks/shared-rules.md +3 -3
  94. package/playbooks/templates/followup-dispatch.md +1 -1
  95. package/playbooks/verify.md +1 -1
  96. package/prompts/cc-system.md +2 -2
@@ -7,9 +7,6 @@
7
7
  // { wrote, result }.
8
8
  //
9
9
  // `scope` is 'central' or a project name.
10
- //
11
- // All callers go through shared.mutateWorkItems and
12
- // queries.getWorkItems — those are thin wrappers over the helpers here.
13
10
 
14
11
  function _toMs(v) {
15
12
  if (v == null) return null;
@@ -18,21 +15,6 @@ function _toMs(v) {
18
15
  return Number.isFinite(parsed) ? parsed : null;
19
16
  }
20
17
 
21
- // Map an absolute filePath → scope key. The store doesn't know about the
22
- // caller's file layout; we derive scope from the path's last two segments
23
- // because every work-items file in this codebase sits at either
24
- // <root>/work-items.json (central) or <root>/projects/<name>/work-items.json.
25
- function scopeForFilePath(filePath) {
26
- const norm = String(filePath).replace(/\\/g, '/');
27
- const m = norm.match(/projects\/([^/]+)\/work-items\.json$/);
28
- if (m) return m[1];
29
- // Anything else — including the central work-items.json under MINIONS_DIR
30
- // and any one-off test fixture file — lands in 'central'. Tests that
31
- // need to keep multiple fixture files distinct should put them under a
32
- // synthetic projects/<name>/ subtree.
33
- return 'central';
34
- }
35
-
36
18
  function _parseRow(row) {
37
19
  if (!row || !row.data) return null;
38
20
  try { return JSON.parse(row.data); }
@@ -45,7 +27,7 @@ function readWorkItemsForScope(scope) {
45
27
 
46
28
  const rows = db.prepare(`
47
29
  SELECT data FROM work_items
48
- WHERE scope = ?
30
+ WHERE scope = ? AND archived = 0
49
31
  ORDER BY rowid
50
32
  `).all(scope);
51
33
 
@@ -64,7 +46,7 @@ function readAllWorkItems() {
64
46
  const { getDb } = require('./db');
65
47
  const db = getDb();
66
48
 
67
- const rows = db.prepare('SELECT data, scope FROM work_items ORDER BY rowid').all();
49
+ const rows = db.prepare('SELECT data, scope FROM work_items WHERE archived = 0 ORDER BY rowid').all();
68
50
  const out = [];
69
51
  for (const row of rows) {
70
52
  const wi = _parseRow(row);
@@ -85,14 +67,18 @@ function _indexById(arr) {
85
67
  return out;
86
68
  }
87
69
 
88
- function _computeWorkItemsDiff(scope, before, after) {
70
+ function _computeWorkItemsDiff(scope, before, after, { allowIncomplete = false } = {}) {
89
71
  const beforeMap = _indexById(before);
90
72
  const afterMap = _indexById(after);
91
73
  const toUpsert = [];
92
74
  const toDelete = [];
93
75
  for (const [id, wi] of afterMap) {
94
76
  const prev = beforeMap.get(id);
95
- if (!prev) { toUpsert.push(wi); continue; }
77
+ if (!prev) {
78
+ if (!allowIncomplete) require('./work-item-validation').validateNewWorkItem(wi);
79
+ toUpsert.push(wi);
80
+ continue;
81
+ }
96
82
  if (JSON.stringify(prev) !== JSON.stringify(wi)) toUpsert.push(wi);
97
83
  }
98
84
  for (const [id] of beforeMap) {
@@ -105,8 +91,8 @@ function _applyWorkItemsDiff(db, diff) {
105
91
  if (diff.toUpsert.length === 0 && diff.toDelete.length === 0) return false;
106
92
  const now = Date.now();
107
93
  const upsertStmt = db.prepare(`
108
- INSERT INTO work_items (id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at)
109
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
94
+ INSERT INTO work_items (id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at, archived)
95
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
110
96
  ON CONFLICT(scope, id) DO UPDATE SET
111
97
  status = excluded.status,
112
98
  type = excluded.type,
@@ -115,9 +101,10 @@ function _applyWorkItemsDiff(db, diff) {
115
101
  created_at = excluded.created_at,
116
102
  completed_at = excluded.completed_at,
117
103
  data = excluded.data,
118
- updated_at = excluded.updated_at
104
+ updated_at = excluded.updated_at,
105
+ archived = 0
119
106
  `);
120
- const deleteStmt = db.prepare('DELETE FROM work_items WHERE scope = ? AND id = ?');
107
+ const deleteStmt = db.prepare('DELETE FROM work_items WHERE scope = ? AND id = ? AND archived = 0');
121
108
 
122
109
  for (const wi of diff.toUpsert) {
123
110
  upsertStmt.run(
@@ -147,7 +134,7 @@ function _applyWorkItemsDiff(db, diff) {
147
134
  // wrote — true iff at least one INSERT/UPDATE/DELETE landed
148
135
  // result — the post-mutation array (legacy return shape of
149
136
  // mutateJsonFileLocked)
150
- function applyWorkItemsMutation(scope, mutator) {
137
+ function applyWorkItemsMutation(scope, mutator, opts) {
151
138
  const { getDb, withTransaction } = require('./db');
152
139
  const db = getDb();
153
140
 
@@ -162,7 +149,7 @@ function applyWorkItemsMutation(scope, mutator) {
162
149
  const after = (next === undefined || next === null)
163
150
  ? before
164
151
  : (Array.isArray(next) ? next : before);
165
- const diff = _computeWorkItemsDiff(scope, beforeSnapshot, after);
152
+ const diff = _computeWorkItemsDiff(scope, beforeSnapshot, after, opts);
166
153
  const wrote = _applyWorkItemsDiff(db, diff);
167
154
  return { wrote, result: after };
168
155
  });
@@ -185,10 +172,121 @@ function dropScope(scope) {
185
172
  } catch { /* db unavailable */ }
186
173
  }
187
174
 
175
+ function readArchivedWorkItemsForScope(scope) {
176
+ const rows = require('./db').getDb().prepare(`
177
+ SELECT data FROM work_items
178
+ WHERE scope = ? AND archived = 1
179
+ ORDER BY updated_at DESC, rowid DESC
180
+ `).all(scope);
181
+ const out = [];
182
+ for (const row of rows) {
183
+ const item = _parseRow(row);
184
+ if (item) out.push(item);
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function archiveWorkItem(scope, id) {
190
+ const { getDb, withTransaction } = require('./db');
191
+ const db = getDb();
192
+ const archived = withTransaction(db, () => {
193
+ const row = db.prepare('SELECT data FROM work_items WHERE scope=? AND id=? AND archived=0').get(scope, String(id));
194
+ if (!row) return null;
195
+ const item = _parseRow(row);
196
+ if (!item) throw new Error(`work item ${id} has invalid SQL data`);
197
+ item._archived = true;
198
+ item.archivedAt = item.archivedAt || new Date().toISOString();
199
+ db.prepare('UPDATE work_items SET archived=1, data=?, updated_at=? WHERE scope=? AND id=?')
200
+ .run(JSON.stringify(item), Date.now(), scope, String(id));
201
+ return item;
202
+ });
203
+ if (archived) {
204
+ try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
205
+ try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
206
+ }
207
+ return archived;
208
+ }
209
+
210
+ function replaceArchivedWorkItems(scope, items) {
211
+ const { getDb, withTransaction } = require('./db');
212
+ const db = getDb();
213
+ const count = withTransaction(db, () => {
214
+ db.prepare('DELETE FROM work_items WHERE scope=? AND archived=1').run(scope);
215
+ const insert = db.prepare(`
216
+ INSERT OR IGNORE INTO work_items
217
+ (id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at, archived)
218
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
219
+ `);
220
+ const now = Date.now();
221
+ let inserted = 0;
222
+ for (const raw of Array.isArray(items) ? items : []) {
223
+ if (!raw || typeof raw !== 'object' || !raw.id) continue;
224
+ const item = { ...raw, _archived: true };
225
+ item.archivedAt = item.archivedAt || new Date(now).toISOString();
226
+ const result = insert.run(
227
+ String(item.id),
228
+ scope,
229
+ String(item.status || 'done'),
230
+ item.type || null,
231
+ item.dispatched_to || item.agent || null,
232
+ item.parent_id || null,
233
+ _toMs(item.created_at || item.created),
234
+ _toMs(item.completed_at || item.completedAt),
235
+ JSON.stringify(item),
236
+ now,
237
+ );
238
+ inserted += Number(result.changes) || 0;
239
+ }
240
+ return inserted;
241
+ });
242
+ try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
243
+ try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
244
+ return count;
245
+ }
246
+
247
+ function moveWorkItem(sourceScope, targetScope, id, item) {
248
+ const { getDb, withTransaction } = require('./db');
249
+ const db = getDb();
250
+ const moved = withTransaction(db, () => {
251
+ const source = db.prepare('SELECT 1 FROM work_items WHERE scope=? AND id=? AND archived=0')
252
+ .get(sourceScope, String(id));
253
+ if (!source) return false;
254
+ const target = db.prepare('SELECT 1 FROM work_items WHERE scope=? AND id=?').get(targetScope, String(id));
255
+ if (target) throw new Error(`work item ${id} already exists in scope ${targetScope}`);
256
+ const result = db.prepare(`
257
+ UPDATE work_items SET
258
+ scope=?, status=?, type=?, agent=?, parent_id=?, created_at=?,
259
+ completed_at=?, data=?, updated_at=?
260
+ WHERE scope=? AND id=? AND archived=0
261
+ `).run(
262
+ targetScope,
263
+ String(item.status || 'pending'),
264
+ item.type || null,
265
+ item.dispatched_to || item.agent || null,
266
+ item.parent_id || null,
267
+ _toMs(item.created_at || item.created),
268
+ _toMs(item.completed_at || item.completedAt),
269
+ JSON.stringify(item),
270
+ Date.now(),
271
+ sourceScope,
272
+ String(id),
273
+ );
274
+ return Number(result.changes) > 0;
275
+ });
276
+ if (moved) {
277
+ try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
278
+ try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
279
+ }
280
+ return moved;
281
+ }
282
+
188
283
  module.exports = {
189
- scopeForFilePath,
190
284
  readWorkItemsForScope,
191
285
  readAllWorkItems,
286
+ readArchivedWorkItemsForScope,
287
+ archiveWorkItem,
288
+ replaceArchivedWorkItems,
289
+ moveWorkItem,
192
290
  applyWorkItemsMutation,
193
291
  dropScope,
194
292
  };
@@ -10,7 +10,7 @@
10
10
  // anchored by live keep_processes PIDs / managed_spawn cwds.
11
11
  //
12
12
  // 2. On engine boot (engine/cli.js): scan each project's worktree root
13
- // for orphan W-* dirs whose dispatch is not in dispatch.json (active
13
+ // for orphan W-* dirs whose dispatch is not active
14
14
  // or pending) AND that are not pool entries. Such dirs are leftovers
15
15
  // from a prior crash that block retries with
16
16
  // `fatal: 'work/W-…' is already used by worktree at …`.
@@ -698,7 +698,7 @@ function gcDispatchWorktreeIfOrphan(opts) {
698
698
  * Walk each project's worktree root and evict orphan W-* dirs.
699
699
  *
700
700
  * - `projects` — array of `{ name, localPath }`
701
- * - `dispatchSnap` — `{ active: [...], pending: [...] }` (work-item dispatch.json)
701
+ * - `dispatchSnap` — `{ active: [...], pending: [...] }` dispatch state
702
702
  * - `worktreeRootRel` — relative dir under each project's localPath (default '../worktrees')
703
703
  * - `log` — log(level, msg) function (optional)
704
704
  * - `fs`/`removeWorktree`/`buildWorktreeDirName`/`listManagedSpecs` — test injection points
@@ -7,10 +7,10 @@
7
7
  * detached at `origin/<mainBranch>` and reused by the next dispatch for the
8
8
  * same project — saving the cold install/build cost on subsequent WIs.
9
9
  *
10
- * Concurrency: all state flips go through `mutateJsonFileLocked` so two
11
- * spawning ticks cannot double-borrow the same idle entry. Per CLAUDE.md, git
12
- * commands NEVER run inside the lock callback — callers must do `tryBorrow`,
13
- * then run git in their own scope, then call `evictEntry` on failure.
10
+ * Concurrency: all state flips use a SQLite transaction so two spawning ticks
11
+ * cannot double-borrow the same idle entry. Git commands never run inside the
12
+ * transaction — callers must do `tryBorrow`, then run git in their own scope,
13
+ * then call `evictEntry` on failure.
14
14
  *
15
15
  * Idle worktrees are detached at origin/<main>, NOT checked out on the local
16
16
  * `main` branch. Local-main is typically already checked out in the project
@@ -22,16 +22,12 @@
22
22
  const fs = require('fs');
23
23
  const path = require('path');
24
24
  const shared = require('./shared');
25
+ const smallStateStore = require('./small-state-store');
25
26
 
26
- const { log, ENGINE_DEFAULTS, mutateJsonFileLocked, safeJson } = shared;
27
+ const { log, ENGINE_DEFAULTS } = shared;
27
28
 
28
- const WORKTREE_POOL_FILENAME = 'worktree-pool.json';
29
29
  const POOL_STATE = Object.freeze({ IDLE: 'idle', BORROWED: 'borrowed' });
30
30
 
31
- function getPoolPath() {
32
- return path.join(shared.MINIONS_DIR, 'engine', WORKTREE_POOL_FILENAME);
33
- }
34
-
35
31
  function _normalizePath(p) {
36
32
  if (!p) return '';
37
33
  let resolved;
@@ -55,17 +51,13 @@ function _entriesArray(data) {
55
51
  }
56
52
 
57
53
  function readPool() {
58
- const data = safeJson(getPoolPath());
54
+ const data = smallStateStore.readWorktreePool();
59
55
  if (!data || !Array.isArray(data.entries)) return { entries: [] };
60
56
  return data;
61
57
  }
62
58
 
63
59
  function mutateWorktreePool(mutator) {
64
- // Phase 7: route through shared.mutateWorktreePool (SQL-backed store +
65
- // JSON mirror). Defensive wrappers around `data.entries` kept for the
66
- // legacy fallback path inside the helper (when SQLite is unavailable
67
- // and we fall through to mutateJsonFileLocked).
68
- return shared.mutateWorktreePool((data) => {
60
+ return smallStateStore.applyWorktreePoolMutation((data) => {
69
61
  if (!data || typeof data !== 'object' || Array.isArray(data)) data = { entries: [] };
70
62
  if (!Array.isArray(data.entries)) data.entries = [];
71
63
  const next = mutator(data);
@@ -320,9 +312,7 @@ function getIdlePaths() {
320
312
  }
321
313
 
322
314
  module.exports = {
323
- WORKTREE_POOL_FILENAME,
324
315
  POOL_STATE,
325
- getPoolPath,
326
316
  readPool,
327
317
  mutateWorktreePool,
328
318
  getProjectPoolSize,