@yemi33/minions 0.1.2370 → 0.1.2372
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
package/engine/prd-store.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
// engine/prd-store.js
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
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
|
-
//
|
|
100
|
-
// AUTOINCREMENT on every
|
|
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
|
-
//
|
|
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
|
|
134
|
-
//
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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))
|
|
186
|
-
|
|
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
|
-
|
|
198
|
-
function removePrdFromSql(filePath) {
|
|
196
|
+
function mutatePrd(filePath, mutator, { defaultValue = {} } = {}) {
|
|
199
197
|
const parsed = parsePrdPath(filePath);
|
|
200
|
-
if (!parsed
|
|
201
|
-
|
|
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
|
-
|
|
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)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
297
|
-
//
|
|
298
|
-
//
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
};
|
package/engine/preflight.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
729
|
-
*
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
948
|
+
// Exposed for runtime harness inventory unit tests.
|
|
995
949
|
_runtimeHarnessRows,
|
|
996
|
-
_computeAddDirSnapshot,
|
|
997
950
|
_formatHarnessLine,
|
|
998
951
|
};
|
package/engine/projects.js
CHANGED
|
@@ -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
|
-
|
|
242
|
+
{
|
|
245
243
|
const { archivePlan } = require('./lifecycle');
|
|
246
|
-
for (const
|
|
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)
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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)
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|