@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,17 +1,9 @@
1
1
  // engine/prd-store.js
2
2
  //
3
- // Phase 10 step 2: SQL mirror for PRD state. PRD content writes go to the
4
- // canonical prd/*.json file AND are mirrored into the plans/prds/prd_items/
5
- // prd_verify_prs tables created by migration 015. JSON stays canonical and is
6
- // still the only thing read in this step — the SQL side is a passive mirror so
7
- // the read-flip (step 3) has a trustworthy source.
8
- //
9
- // All entry points are BEST-EFFORT: a SQLite failure (Node < 22.5, locked DB,
10
- // schema not yet migrated) must never break a PRD JSON write. Callers wrap via
11
- // the thin `mirror*`/`remove*` helpers which swallow and return {ok:false}.
3
+ // SQL-authoritative PRD state. Paths are retained as stable identifiers for
4
+ // existing callers, but no runtime PRD JSON is read or written here.
12
5
 
13
6
  const path = require('path');
14
- const fs = require('fs');
15
7
 
16
8
  function _toMs(v) {
17
9
  if (v == null) return null;
@@ -75,7 +67,7 @@ function _upsertPrd(db, filename, archived, prd, now) {
75
67
  JSON.stringify(prd), now, prdId,
76
68
  );
77
69
  // NOTE: prd_items are NOT bulk-deleted here — they are upserted by their
78
- // stable (prd_id, feature_id) key below so prd_items.id survives a re-mirror
70
+ // stable (prd_id, feature_id) key below so prd_items.id survives an update
79
71
  // (work_items.prd_item_id is an FK to it). prd_verify_prs carries no FK, so
80
72
  // a cheap delete+reinsert is fine.
81
73
  db.prepare('DELETE FROM prd_verify_prs WHERE prd_id=?').run(prdId);
@@ -96,8 +88,8 @@ function _upsertPrd(db, filename, archived, prd, now) {
96
88
 
97
89
  // Upsert prd_items by their STABLE natural key (prd_id, feature_id) instead of
98
90
  // delete-all-then-reinsert, so the surrogate prd_items.id is durable across
99
- // re-mirrors. work_items.prd_item_id is an FK to this id; churning it (via
100
- // AUTOINCREMENT on every mirror) dangled every stamped row — the Phase-10
91
+ // updates. work_items.prd_item_id is an FK to this id; churning it (via
92
+ // AUTOINCREMENT on every write) dangled every stamped row — the Phase-10
101
93
  // step-4 join-flip blocker. feature_id-less legacy items can't be keyed (NULL
102
94
  // != NULL under UNIQUE), so they are swept + reinserted each time; they are
103
95
  // never FK targets, so their churn is harmless.
@@ -128,15 +120,19 @@ function _upsertPrd(db, filename, archived, prd, now) {
128
120
  `);
129
121
  const selItemId = db.prepare('SELECT id FROM prd_items WHERE prd_id=? AND feature_id=?');
130
122
  // Stamp the materialized work item(s) → prd_items.id. The two backref paths
131
- // mirror the migration-015 backfill: an explicit feature.workItemId (set by
123
+ // match the migration-015 backfill: an explicit feature.workItemId (set by
132
124
  // lifecycle.stampPrdItemWorkItemId), and the sourcePlan(=PRD filename) +
133
- // feature-id equality the legacy JSON model joined on. Self-heals on every
134
- // mirror, so a WI materialized after this PRD was first seen still gets its FK.
125
+ // feature-id equality the legacy model joined on. Every update self-heals a
126
+ // WI materialized after the PRD was first written.
127
+ // Live PRDs take precedence over same-basename archived rows. Archived
128
+ // imports may fill an empty FK but must never steal one already stamped by
129
+ // the live PRD.
130
+ const fkPredicate = archived ? 'prd_item_id IS NULL' : '(prd_item_id IS NULL OR prd_item_id<>?)';
135
131
  const stampWiById = db.prepare(
136
- 'UPDATE work_items SET prd_item_id=? WHERE id=? AND (prd_item_id IS NULL OR prd_item_id<>?)'
132
+ `UPDATE work_items SET prd_item_id=? WHERE id=? AND ${fkPredicate}`
137
133
  );
138
134
  const stampWiBySourcePlan = db.prepare(
139
- `UPDATE work_items SET prd_item_id=? WHERE json_extract(data,'$.sourcePlan')=? AND json_extract(data,'$.id')=? AND (prd_item_id IS NULL OR prd_item_id<>?)`
135
+ `UPDATE work_items SET prd_item_id=? WHERE json_extract(data,'$.sourcePlan')=? AND json_extract(data,'$.id')=? AND ${fkPredicate}`
140
136
  );
141
137
  for (const ft of features) {
142
138
  if (!ft || typeof ft !== 'object') continue;
@@ -158,9 +154,15 @@ function _upsertPrd(db, filename, archived, prd, now) {
158
154
  try { prdItemId = selItemId.get(prdId, featureId)?.id; } catch { /* ignore */ }
159
155
  if (!prdItemId) continue;
160
156
  if (explicitWi) {
161
- try { stampWiById.run(prdItemId, String(explicitWi), prdItemId); } catch { /* best-effort */ }
157
+ try {
158
+ if (archived) stampWiById.run(prdItemId, String(explicitWi));
159
+ else stampWiById.run(prdItemId, String(explicitWi), prdItemId);
160
+ } catch { /* best-effort */ }
162
161
  }
163
- try { stampWiBySourcePlan.run(prdItemId, filename, featureId, prdItemId); } catch { /* best-effort */ }
162
+ try {
163
+ if (archived) stampWiBySourcePlan.run(prdItemId, filename, featureId);
164
+ else stampWiBySourcePlan.run(prdItemId, filename, featureId, prdItemId);
165
+ } catch { /* best-effort */ }
164
166
  }
165
167
 
166
168
  const insVp = db.prepare(`
@@ -179,127 +181,101 @@ function _upsertPrd(db, filename, archived, prd, now) {
179
181
  return prdId;
180
182
  }
181
183
 
182
- // Mirror a PRD object to SQL for the given prd/*.json path. Best-effort.
183
- function mirrorPrdToSql(filePath, prd) {
184
+ function writePrd(filePath, prd) {
184
185
  const parsed = parsePrdPath(filePath);
185
- if (!parsed || !prd || typeof prd !== 'object' || Array.isArray(prd)) return { ok: false, reason: 'not-a-prd' };
186
- try {
187
- const { getDb, withTransaction } = require('./db');
188
- const db = getDb();
189
- withTransaction(db, () => _upsertPrd(db, parsed.filename, parsed.archived, prd, Date.now()));
190
- try { require('./db-events').emitStateEvent('prds', { filename: parsed.filename, archived: parsed.archived }); } catch { /* event optional */ }
191
- return { ok: true };
192
- } catch (e) {
193
- return { ok: false, reason: e && e.message };
186
+ if (!parsed || !prd || typeof prd !== 'object' || Array.isArray(prd)) {
187
+ throw new TypeError('prd-store.writePrd requires a PRD path and object');
194
188
  }
189
+ const { getDb, withTransaction } = require('./db');
190
+ const db = getDb();
191
+ const prdId = withTransaction(db, () => _upsertPrd(db, parsed.filename, parsed.archived, prd, Date.now()));
192
+ require('./db-events').emitStateEvent('prds', { filename: parsed.filename, archived: parsed.archived });
193
+ return { ok: true, id: prdId, plan: prd };
195
194
  }
196
195
 
197
- // Remove a PRD (and its items/verify rows) from SQL for the given path. Best-effort.
198
- function removePrdFromSql(filePath) {
196
+ function mutatePrd(filePath, mutator, { defaultValue = {} } = {}) {
199
197
  const parsed = parsePrdPath(filePath);
200
- if (!parsed) return { ok: false, reason: 'not-a-prd' };
201
- try {
202
- const { getDb, withTransaction } = require('./db');
203
- const db = getDb();
204
- withTransaction(db, () => {
205
- const row = db.prepare('SELECT id FROM prds WHERE filename=? AND archived=?').get(parsed.filename, parsed.archived);
206
- if (!row) return;
207
- db.prepare('DELETE FROM prd_items WHERE prd_id=?').run(row.id);
208
- db.prepare('DELETE FROM prd_verify_prs WHERE prd_id=?').run(row.id);
209
- db.prepare('DELETE FROM prds WHERE id=?').run(row.id);
210
- });
211
- try { require('./db-events').emitStateEvent('prds', { filename: parsed.filename, archived: parsed.archived, removed: true }); } catch { /* optional */ }
212
- return { ok: true };
213
- } catch (e) {
214
- return { ok: false, reason: e && e.message };
198
+ if (!parsed || typeof mutator !== 'function') {
199
+ throw new TypeError('prd-store.mutatePrd requires a PRD path and mutator');
215
200
  }
201
+ const { getDb, withTransaction } = require('./db');
202
+ const db = getDb();
203
+ const result = withTransaction(db, () => {
204
+ const row = db.prepare('SELECT data FROM prds WHERE filename=? AND archived=?').get(parsed.filename, parsed.archived);
205
+ let current = row ? JSON.parse(row.data) : structuredClone(defaultValue);
206
+ if (!current || typeof current !== 'object' || Array.isArray(current)) current = structuredClone(defaultValue);
207
+ const next = mutator(current);
208
+ const finalData = next === undefined ? current : next;
209
+ if (!finalData || typeof finalData !== 'object' || Array.isArray(finalData)) {
210
+ throw new TypeError('PRD mutator must return an object or mutate in place');
211
+ }
212
+ _upsertPrd(db, parsed.filename, parsed.archived, finalData, Date.now());
213
+ return finalData;
214
+ });
215
+ require('./db-events').emitStateEvent('prds', { filename: parsed.filename, archived: parsed.archived });
216
+ return result;
216
217
  }
217
218
 
218
- // Read one PRD back from SQL as the original object (the `data` blob). Used by
219
- // the divergence test now and the read-flip in step 3.
220
- function readPrdFromSql(filePath) {
219
+ function deletePrd(filePath) {
221
220
  const parsed = parsePrdPath(filePath);
222
- if (!parsed) return null;
223
- try {
224
- const { getDb } = require('./db');
225
- const row = getDb().prepare('SELECT data FROM prds WHERE filename=? AND archived=?').get(parsed.filename, parsed.archived);
226
- return row ? JSON.parse(row.data) : null;
227
- } catch { return null; }
221
+ if (!parsed) throw new TypeError('prd-store.deletePrd requires a PRD path');
222
+ const { getDb, withTransaction } = require('./db');
223
+ const db = getDb();
224
+ const removed = withTransaction(db, () => {
225
+ const row = db.prepare('SELECT id FROM prds WHERE filename=? AND archived=?').get(parsed.filename, parsed.archived);
226
+ if (!row) return false;
227
+ db.prepare('DELETE FROM prd_items WHERE prd_id=?').run(row.id);
228
+ db.prepare('DELETE FROM prd_verify_prs WHERE prd_id=?').run(row.id);
229
+ db.prepare('DELETE FROM prds WHERE id=?').run(row.id);
230
+ return true;
231
+ });
232
+ if (removed) require('./db-events').emitStateEvent('prds', { filename: parsed.filename, archived: parsed.archived, removed: true });
233
+ return removed;
228
234
  }
229
235
 
230
- // Per-process mtime cache so reconciliation re-mirrors only files that actually
231
- // changed on disk since this process last saw them (empty after restart → the
232
- // first read re-mirrors everything once). Keyed `${archived} ${filename}`.
233
- const _reconcileMtimes = new Map();
234
-
235
- function _prdDirsFor(minionsDir) {
236
- return [
237
- { dir: path.join(minionsDir, 'prd'), archived: 0 },
238
- { dir: path.join(minionsDir, 'prd', 'archive'), archived: 1 },
239
- ];
236
+ function readPrd(filePath) {
237
+ const parsed = parsePrdPath(filePath);
238
+ if (!parsed) return null;
239
+ const row = require('./db').getDb()
240
+ .prepare('SELECT data FROM prds WHERE filename=? AND archived=?')
241
+ .get(parsed.filename, parsed.archived);
242
+ return row ? JSON.parse(row.data) : null;
240
243
  }
241
244
 
242
- // Reconcile the SQL mirror from the prd/ + prd/archive/ directories: re-mirror
243
- // files that are new or changed on disk (catches PRDs written OUTSIDE the
244
- // mutateJsonFileLocked chokepoint e.g. the plan-to-prd agent writing the JSON
245
- // directly, or an archive file-move), and drop SQL rows whose backing file is
246
- // gone. Best-effort; safe to call on every read. Returns a small stats object.
247
- function reconcilePrdsFromDisk() {
248
- let minionsDir;
249
- try { minionsDir = require('./shared').MINIONS_DIR; } catch { return { ok: false }; }
250
- if (!minionsDir) return { ok: false };
251
- let db;
252
- try { db = require('./db').getDb(); } catch { return { ok: false }; }
253
-
254
- let mirrored = 0, removed = 0;
255
- const seen = new Set(); // `${archived} ${filename}` present on disk
256
- for (const { dir, archived } of _prdDirsFor(minionsDir)) {
257
- let files;
258
- try { files = fs.readdirSync(dir).filter(f => f.endsWith('.json')); } catch { continue; }
259
- for (const f of files) {
260
- const key = `${archived} ${f}`;
261
- seen.add(key);
262
- const full = path.join(dir, f);
263
- let mtime;
264
- try { mtime = Math.round(fs.statSync(full).mtimeMs); } catch { continue; }
265
- if (_reconcileMtimes.get(key) === mtime) continue; // unchanged since last seen
266
- const prd = require('./shared').safeJsonNoRestore(full);
267
- if (!prd) continue;
268
- try {
269
- const { withTransaction } = require('./db');
270
- withTransaction(db, () => _upsertPrd(db, f, archived, prd, Date.now()));
271
- _reconcileMtimes.set(key, mtime);
272
- mirrored += 1;
273
- } catch { /* best-effort */ }
274
- }
245
+ // Atomically restore a legacy physically-archived PRD (archived=1 row) into
246
+ // the live bucket (archived=0). Unlike a caller doing readPrd(archivePath)
247
+ // then writePrd(livePath, snapshot), the archived row's data is read FRESH
248
+ // inside the SAME transaction that upserts the live row and deletes the
249
+ // archived one so a concurrent engine write to the archived row (e.g.
250
+ // lifecycle updating completion/verify state between an operator's unarchive
251
+ // request and this write) can never be silently clobbered by a stale
252
+ // in-memory snapshot (W-mrenu075000a94c9). Returns the restored PRD data, or
253
+ // null if no archived row exists for `filename`.
254
+ function restorePrdFromArchive(filename) {
255
+ if (!filename || typeof filename !== 'string') {
256
+ throw new TypeError('prd-store.restorePrdFromArchive requires a filename');
275
257
  }
276
- // Drop rows whose backing file disappeared (delete/move not caught by a hook).
277
- try {
278
- const rows = db.prepare('SELECT id, filename, archived FROM prds').all();
279
- for (const r of rows) {
280
- const key = `${r.archived} ${r.filename}`;
281
- if (seen.has(key)) continue;
282
- try {
283
- require('./db').withTransaction(db, () => {
284
- db.prepare('DELETE FROM prd_items WHERE prd_id=?').run(r.id);
285
- db.prepare('DELETE FROM prd_verify_prs WHERE prd_id=?').run(r.id);
286
- db.prepare('DELETE FROM prds WHERE id=?').run(r.id);
287
- });
288
- _reconcileMtimes.delete(key);
289
- removed += 1;
290
- } catch { /* best-effort */ }
291
- }
292
- } catch { /* best-effort */ }
293
- return { ok: true, mirrored, removed };
258
+ const { getDb, withTransaction } = require('./db');
259
+ const db = getDb();
260
+ const restored = withTransaction(db, () => {
261
+ const row = db.prepare('SELECT id, data FROM prds WHERE filename=? AND archived=1').get(filename);
262
+ if (!row) return null;
263
+ const prd = JSON.parse(row.data);
264
+ _upsertPrd(db, filename, 0, prd, Date.now());
265
+ db.prepare('DELETE FROM prd_items WHERE prd_id=?').run(row.id);
266
+ db.prepare('DELETE FROM prd_verify_prs WHERE prd_id=?').run(row.id);
267
+ db.prepare('DELETE FROM prds WHERE id=?').run(row.id);
268
+ return prd;
269
+ });
270
+ if (restored) {
271
+ require('./db-events').emitStateEvent('prds', { filename, archived: 0, restored: true });
272
+ }
273
+ return restored;
294
274
  }
295
275
 
296
- // Idempotent boot sweep: populate work_items.prd_item_id for rows that predate
297
- // the dual-write stamp (everything materialized before #546), or that a JSON
298
- // re-hydrate transiently zeroed. The per-mirror stamp in _upsertPrd only fires
299
- // for PRDs that are re-mirrored, and reconcile is mtime-gated — so a PRD that
300
- // never changes again never re-stamps its historical WIs. This catch-up is
301
- // set-based (two SQL UPDATEs, no per-row loop), only touches NULL FKs, and
302
- // prefers the live (archived=0) prd_item when a basename exists in both buckets.
276
+ // Idempotent boot sweep for work items that predate PRD FK stamping.
277
+ // This is set-based, only touches NULL FKs, and prefers live PRDs when a
278
+ // basename exists in both live and archived buckets.
303
279
  // Safe to run on every boot — a no-op once every matchable WI is linked.
304
280
  // Returns { ok, byWorkItemId, bySourcePlan } counts.
305
281
  function backfillWorkItemPrdItemIds() {
@@ -344,27 +320,25 @@ function backfillWorkItemPrdItemIds() {
344
320
  // data blob. Sorted by (archived, filename) for deterministic consumer order
345
321
  // (matches the disk-scan order once the caller also sorts). Read-flip source.
346
322
  function listPrdRows() {
347
- try {
348
- const db = require('./db').getDb();
349
- const rows = db.prepare('SELECT filename, archived, data FROM prds ORDER BY archived, filename').all();
350
- const out = [];
351
- for (const r of rows) {
352
- let plan;
353
- try { plan = JSON.parse(r.data); } catch { continue; }
354
- out.push({ filename: r.filename, archived: !!r.archived, plan });
355
- }
356
- return out;
357
- } catch { return []; }
323
+ const db = require('./db').getDb();
324
+ const rows = db.prepare('SELECT filename, archived, data, updated_at FROM prds ORDER BY archived, filename').all();
325
+ const out = [];
326
+ for (const r of rows) {
327
+ let plan;
328
+ try { plan = JSON.parse(r.data); } catch { continue; }
329
+ out.push({ filename: r.filename, archived: !!r.archived, plan, updatedAt: Number(r.updated_at) || 0 });
330
+ }
331
+ return out;
358
332
  }
359
333
 
360
334
  module.exports = {
361
335
  parsePrdPath,
362
- mirrorPrdToSql,
363
- removePrdFromSql,
364
- readPrdFromSql,
365
- reconcilePrdsFromDisk,
336
+ writePrd,
337
+ mutatePrd,
338
+ deletePrd,
339
+ readPrd,
340
+ restorePrdFromArchive,
366
341
  backfillWorkItemPrdItemIds,
367
342
  listPrdRows,
368
- _reconcileMtimes, // exported for tests (reset between cases)
369
343
  _upsertPrd, // exported for tests
370
344
  };
@@ -343,7 +343,6 @@ const _FEATURE_FLAG_DEFAULTS = {
343
343
  claudeBareMode: false,
344
344
  claudeFallbackModel: undefined,
345
345
  copilotDisableBuiltinMcps: true,
346
- copilotSuppressAgentsMd: true,
347
346
  copilotStreamMode: 'on',
348
347
  copilotReasoningSummaries: false,
349
348
  maxBudgetUsd: undefined,
@@ -716,23 +715,10 @@ function doctor(minionsHome) {
716
715
  }
717
716
 
718
717
  /**
719
- * Harness propagation diagnostic for `minions doctor --harness` (plan item #1
720
- * of `seamless-user-repo-harness-invocation`, PRD `P-a3f9b2c1`). Iterates
721
- * every registered runtime and prints every dir / file the engine would
722
- * surface to a spawned agent today, grouped by scope label
723
- * (`user` / `project:<name>` / `personal` / `minions`). Missing on-disk paths
724
- * are flagged with `⚠` but do not fail the command — they're listed so the
725
- * operator can decide whether to create / populate the dir or leave it as-is
726
- * for this host.
718
+ * Read-only inventory for `minions doctor --harness`.
727
719
  *
728
- * The diagnostic is intentionally read-only and side-effect-free: no mutation
729
- * of config, cache files, or worktrees. Exits non-zero only on a true
730
- * runtime/config error (e.g. registry refuses to resolve a registered
731
- * runtime). See `docs/harness-propagation.md` for the full contract.
732
- *
733
- * Pure-ish: `homeDir` and `existsFn` are injectable for tests so a temp
734
- * sandbox can assert the "missing path" branch without touching the real
735
- * `~/.claude` / `~/.copilot` dirs.
720
+ * Reports native locations declared by each adapter without changing runtime
721
+ * discovery. Missing paths are warnings; runtime/config errors fail the command.
736
722
  */
737
723
  function _resolveProjectsForHarness(minionsHome) {
738
724
  const configPath = path.join(minionsHome, 'config.json');
@@ -867,21 +853,6 @@ function _runtimeHarnessRows(runtime, { homeDir, projects, existsFn }) {
867
853
  return rows;
868
854
  }
869
855
 
870
- function _computeAddDirSnapshot(runtime, { minionsHome, homeDir, existsFn }) {
871
- let computeAddDirs;
872
- try { ({ computeAddDirs } = require('./spawn-agent')); }
873
- catch { return null; }
874
- if (typeof computeAddDirs !== 'function') return null;
875
- let dirs = [];
876
- try { dirs = computeAddDirs({ runtime, minionsDir: minionsHome, homeDir, exists: existsFn }) || []; }
877
- catch { return null; }
878
- return dirs.map(d => {
879
- let scope = 'user';
880
- if (path.resolve(d) === path.resolve(minionsHome)) scope = 'minions';
881
- return { path: d, scope, exists: existsFn(d) };
882
- });
883
- }
884
-
885
856
  function runHarnessDoctor(minionsHome, opts = {}) {
886
857
  const homeDir = opts.homeDir || os.homedir();
887
858
  const existsFn = opts.existsFn || fs.existsSync;
@@ -890,15 +861,14 @@ function runHarnessDoctor(minionsHome, opts = {}) {
890
861
  let registry;
891
862
  try { registry = require('./runtimes'); }
892
863
  catch (e) {
893
- out(`Minions Harness Propagation`);
864
+ out(`Minions Runtime Harness Inventory`);
894
865
  out(` Could not load runtime registry: ${e.message}`);
895
866
  return false;
896
867
  }
897
868
  const runtimeNames = registry.listRuntimes();
898
869
  const { config, projects } = _resolveProjectsForHarness(minionsHome);
899
- const defaultCli = (config && config.engine && config.engine.defaultCli) || 'copilot';
900
-
901
- out('Minions Harness Propagation');
870
+ out('Minions Runtime Harness Inventory');
871
+ out(' Minions does not propagate these paths; each CLI discovers its native harness from cwd and user config.');
902
872
  if (projects.length === 0) {
903
873
  out(' (no real projects configured — project-scope rows will be empty)');
904
874
  } else {
@@ -918,7 +888,7 @@ function runHarnessDoctor(minionsHome, opts = {}) {
918
888
  }
919
889
  const rows = _runtimeHarnessRows(runtime, { homeDir, projects, existsFn });
920
890
 
921
- out(' User asset dirs (--add-dir to agents):');
891
+ out(' User asset dirs (adapter inventory only):');
922
892
  if (rows.userAssetDirs.length === 0) out(' (adapter does not expose getUserAssetDirs)');
923
893
  for (const r of rows.userAssetDirs) out(_formatHarnessLine(r.path, r.scope, r.exists));
924
894
 
@@ -950,22 +920,6 @@ function runHarnessDoctor(minionsHome, opts = {}) {
950
920
  for (const r of rows.mcpConfigProject) out(_formatHarnessLine(r.path, r.scope, r.exists));
951
921
  }
952
922
 
953
- out('');
954
- out(` Worktree --add-dir snapshot (engine fleet default: ${defaultCli}):`);
955
- let defaultRuntime = null;
956
- try { defaultRuntime = registry.resolveRuntime(defaultCli); }
957
- catch { /* preflight handles unknown-runtime errors; here we just skip */ }
958
- if (!defaultRuntime) {
959
- out(' (cannot resolve fleet-default runtime — see `minions doctor` for details)');
960
- } else {
961
- const snapshot = _computeAddDirSnapshot(defaultRuntime, { minionsHome, homeDir, existsFn });
962
- if (!snapshot || snapshot.length === 0) {
963
- out(' (no dirs attached — engine/spawn-agent.js unavailable or adapter has no asset dirs)');
964
- } else {
965
- for (const r of snapshot) out(_formatHarnessLine(r.path, r.scope, r.exists));
966
- }
967
- }
968
-
969
923
  out('');
970
924
  out(' All harness paths surveyed. Missing on-disk paths are warnings, not failures —');
971
925
  out(' they\'re listed so you can decide whether to create the dir, populate it, or');
@@ -991,8 +945,7 @@ module.exports = {
991
945
  _fetchCliHelpText,
992
946
  _checkBypassFlagSupported,
993
947
  _bypassFlagResults,
994
- // Exposed for harness-propagation doctor unit tests (P-a3f9b2c1).
948
+ // Exposed for runtime harness inventory unit tests.
995
949
  _runtimeHarnessRows,
996
- _computeAddDirSnapshot,
997
950
  _formatHarnessLine,
998
951
  };
@@ -100,7 +100,6 @@ function _collectProjectlessCentralDispatchItemIds(removedProject, projects) {
100
100
  function _requeueProjectlessCentralWorkItems(itemIds) {
101
101
  if (!itemIds || itemIds.size === 0) return 0;
102
102
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
103
- if (!fs.existsSync(wiPath)) return 0;
104
103
  let requeued = 0;
105
104
  shared.mutateWorkItems(wiPath, items => {
106
105
  if (!Array.isArray(items)) return items;
@@ -236,16 +235,15 @@ function removeProject(target, options = {}) {
236
235
  // from re-triggering plan completion (mirror of f28162b0 fix).
237
236
  const plansDir = path.join(MINIONS_DIR, 'plans');
238
237
  const plansArchive = path.join(plansDir, 'archive');
239
- const prdDir = path.join(MINIONS_DIR, 'prd');
240
238
  const escName = project.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
241
239
  const projectLineRe = new RegExp('^[*_>\\s]{0,4}\\*{0,2}_{0,2}Project:?\\*{0,2}_{0,2}\\s*' + escName + '\\b', 'im');
242
240
  const archivedSourcePlans = new Set();
243
241
  try {
244
- if (fs.existsSync(prdDir)) {
242
+ {
245
243
  const { archivePlan } = require('./lifecycle');
246
- for (const f of fs.readdirSync(prdDir).filter(f => f.endsWith('.json'))) {
244
+ for (const { filename: f, plan: prd, archived } of require('./prd-store').listPrdRows()) {
245
+ if (archived) continue;
247
246
  try {
248
- const prd = JSON.parse(fs.readFileSync(path.join(prdDir, f), 'utf8'));
249
247
  if (!_projectRefMatches(prd?.project, project, projects)) continue;
250
248
  archivePlan(f, prd, [project], config);
251
249
  summary.archivedPlans.push('prd/' + f);
@@ -361,49 +359,44 @@ function removeProject(target, options = {}) {
361
359
  });
362
360
  } catch (e) { summary.warnings.push('orphan-tag dispatch: ' + e.message); }
363
361
 
364
- // (b) MINIONS_DIR/pull-requests.json — central PR registry. PRs in this
365
- // file lack an explicit project field; match by canonical scope
362
+ // (b) Central PR registry. PRs in this scope lack an explicit project field; match by canonical scope
366
363
  // (e.g. `github:owner/repo`) against the removed project's PR scope.
367
364
  const centralPrPath = shared.centralPullRequestsPath();
368
- if (fs.existsSync(centralPrPath)) {
369
- try {
370
- const projectScope = shared.getProjectPrScope(project);
371
- if (projectScope) {
372
- shared.mutatePullRequests(centralPrPath, (prs) => {
373
- for (const pr of prs) {
374
- if (!pr || pr._orphaned_at) continue;
375
- const canonical = shared.parseCanonicalPrId(pr.id);
376
- if (canonical?.scope !== projectScope) continue;
377
- pr._orphaned_at = orphanedAt;
378
- pr._orphaned_project = orphanedProject;
379
- summary.orphanedRefs.pullRequests++;
380
- }
381
- return prs;
382
- });
383
- }
384
- } catch (e) { summary.warnings.push('orphan-tag pull-requests: ' + e.message); }
385
- }
365
+ try {
366
+ const projectScope = shared.getProjectPrScope(project);
367
+ if (projectScope) {
368
+ shared.mutatePullRequests(centralPrPath, (prs) => {
369
+ for (const pr of prs) {
370
+ if (!pr || pr._orphaned_at) continue;
371
+ const canonical = shared.parseCanonicalPrId(pr.id);
372
+ if (canonical?.scope !== projectScope) continue;
373
+ pr._orphaned_at = orphanedAt;
374
+ pr._orphaned_project = orphanedProject;
375
+ summary.orphanedRefs.pullRequests++;
376
+ }
377
+ return prs;
378
+ });
379
+ }
380
+ } catch (e) { summary.warnings.push('orphan-tag pull-requests: ' + e.message); }
386
381
 
387
- // (c) MINIONS_DIR/work-items.json — central WI list. Pending/queued WIs
382
+ // (c) Central WI scope. Pending/queued WIs
388
383
  // were already CANCELLED by cancelPendingWorkItems in step 1; tag
389
384
  // the remaining matchers (dispatched / done / failed / etc.) so
390
385
  // downstream consumers can filter them.
391
386
  const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
392
- if (fs.existsSync(centralWiPath)) {
393
- try {
394
- shared.mutateWorkItems(centralWiPath, (items) => {
395
- for (const w of items) {
396
- if (!w || w._orphaned_at) continue;
397
- if (w.status === shared.WI_STATUS.CANCELLED) continue;
398
- if (!_workItemMatchesProject(w, project, projects)) continue;
399
- w._orphaned_at = orphanedAt;
400
- w._orphaned_project = orphanedProject;
401
- summary.orphanedRefs.workItems++;
402
- }
403
- return items;
404
- });
405
- } catch (e) { summary.warnings.push('orphan-tag work-items: ' + e.message); }
406
- }
387
+ try {
388
+ shared.mutateWorkItems(centralWiPath, (items) => {
389
+ for (const w of items) {
390
+ if (!w || w._orphaned_at) continue;
391
+ if (w.status === shared.WI_STATUS.CANCELLED) continue;
392
+ if (!_workItemMatchesProject(w, project, projects)) continue;
393
+ w._orphaned_at = orphanedAt;
394
+ w._orphaned_project = orphanedProject;
395
+ summary.orphanedRefs.workItems++;
396
+ }
397
+ return items;
398
+ });
399
+ } catch (e) { summary.warnings.push('orphan-tag work-items: ' + e.message); }
407
400
  } catch (e) { summary.warnings.push('orphan-sweep: ' + e.message); }
408
401
 
409
402
  // 8. Move (or purge) projects/<name>/ — preserves PR/work-item history by