@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
@@ -10,17 +10,6 @@
10
10
  // applyMetricsMutation(fn) -> mutator receives + returns the
11
11
  // legacy-shape object; diff-then-apply
12
12
  // into per-row UPSERT/DELETE
13
- // _mirrorJsonFromSql(filePath) -> writes the SQL state back to
14
- // metrics.json for legacy direct-readers
15
- //
16
- // Like work-items-store and pull-requests-store, an external-edit
17
- // detection layer (mtime tracking + resync) catches direct fs.writeFile
18
- // edits from tests or operators so the next read/mutation rebuilds SQL
19
- // from the JSON canonical state.
20
-
21
- const fs = require('fs');
22
- const path = require('path');
23
-
24
13
  // Reserved top-level keys → kind sentinel in the table.
25
14
  const RESERVED_KEYS = new Map([
26
15
  ['_engine', 'engine_category'],
@@ -33,50 +22,12 @@ const RESERVED_KEY_BY_KIND = new Map([
33
22
  ['context_pressure', '_contextPressure'],
34
23
  ]);
35
24
 
36
- function _metricsFilePath() {
37
- const shared = require('./shared');
38
- return path.join(shared.MINIONS_DIR, 'engine', 'metrics.json');
39
- }
40
-
41
25
  function _parseRow(row) {
42
26
  if (!row || !row.data) return null;
43
27
  try { return JSON.parse(row.data); }
44
28
  catch { return null; }
45
29
  }
46
30
 
47
- function _readJsonObjectFallback() {
48
- const fp = _metricsFilePath();
49
- let raw;
50
- try { raw = fs.readFileSync(fp, 'utf8'); }
51
- catch { return {}; }
52
- try {
53
- const parsed = JSON.parse(raw);
54
- return (parsed && typeof parsed === 'object') ? parsed : {};
55
- } catch (e) {
56
- try {
57
- // eslint-disable-next-line no-console
58
- console.warn(`[metrics-store] corrupt JSON in ${fp}: ${e.message}`);
59
- } catch { /* console may be wrapped */ }
60
- return {};
61
- }
62
- }
63
-
64
- // Content-hash fingerprint of the JSON mirror so external edits are
65
- // detected unambiguously. (mtime, size) is too coarse: ISO timestamps
66
- // have fixed byte length, so a `last_*` swap inside the same ms tick
67
- // preserves both axes. SHA-1 on a few-KB file is sub-ms.
68
- let _lastMirrorHash = null;
69
-
70
- const crypto = require('crypto');
71
-
72
- function _fileContentHash(filePath) {
73
- try {
74
- const buf = fs.readFileSync(filePath);
75
- return crypto.createHash('sha1').update(buf).digest('hex');
76
- }
77
- catch { return null; }
78
- }
79
-
80
31
  // Convert legacy-shape metrics object into (kind, key, data) rows.
81
32
  function _flatten(metrics) {
82
33
  const out = [];
@@ -120,38 +71,6 @@ function _inflate(rows) {
120
71
  return out;
121
72
  }
122
73
 
123
- function _hydrateFromJson(db) {
124
- const json = _readJsonObjectFallback();
125
- db.prepare('DELETE FROM metrics').run();
126
- const rows = _flatten(json);
127
- if (rows.length === 0) return;
128
- const now = Date.now();
129
- const ins = db.prepare(`
130
- INSERT INTO metrics (kind, key, data, updated_at)
131
- VALUES (?, ?, ?, ?)
132
- ON CONFLICT(kind, key) DO NOTHING
133
- `);
134
- for (const { kind, key, value } of rows) {
135
- ins.run(kind, key, JSON.stringify(value), now);
136
- }
137
- }
138
-
139
- function _resyncIfJsonDiverged(db) {
140
- const jsonPath = _metricsFilePath();
141
- const currentHash = _fileContentHash(jsonPath);
142
- if (currentHash == null) return;
143
- if (_lastMirrorHash != null && currentHash === _lastMirrorHash) return;
144
- if (_lastMirrorHash == null) {
145
- const sqlHas = db.prepare('SELECT 1 FROM metrics LIMIT 1').get();
146
- if (sqlHas) {
147
- _lastMirrorHash = currentHash;
148
- return;
149
- }
150
- }
151
- _hydrateFromJson(db);
152
- _lastMirrorHash = currentHash;
153
- }
154
-
155
74
  function _readAllRows(db) {
156
75
  const raw = db.prepare('SELECT kind, key, data FROM metrics ORDER BY rowid').all();
157
76
  const rows = [];
@@ -163,26 +82,15 @@ function _readAllRows(db) {
163
82
  return rows;
164
83
  }
165
84
 
166
- // Read the full legacy-shape metrics object. Fall back to JSON when SQL
167
- // is empty AND JSON has content (mirrors the work-items-store fallback
168
- // for fresh installs + legacy test seeds).
85
+ // Read the full legacy-shape metrics object from SQL.
169
86
  function readMetrics() {
170
87
  const { getDb } = require('./db');
171
- let db;
172
- try { db = getDb(); }
173
- catch { return _readJsonObjectFallback(); }
88
+ const db = getDb();
174
89
 
175
90
  migrateWatchCcTriageKeysOnce();
176
91
 
177
- _resyncIfJsonDiverged(db);
178
-
179
92
  const rows = _readAllRows(db);
180
- if (rows.length === 0) {
181
- const fallback = _readJsonObjectFallback();
182
- if (fallback && Object.keys(fallback).length > 0) return fallback;
183
- return {};
184
- }
185
- return _inflate(rows);
93
+ return rows.length === 0 ? {} : _inflate(rows);
186
94
  }
187
95
 
188
96
  function _indexByCompositeKey(rows) {
@@ -229,7 +137,7 @@ function _applyMetricsDiff(db, diff) {
229
137
  return true;
230
138
  }
231
139
 
232
- function applyMetricsMutation(mutator, options = {}) {
140
+ function applyMetricsMutation(mutator) {
233
141
  const { getDb, withTransaction } = require('./db');
234
142
  let db;
235
143
  try { db = getDb(); }
@@ -238,7 +146,6 @@ function applyMetricsMutation(mutator, options = {}) {
238
146
  }
239
147
 
240
148
  return withTransaction(db, () => {
241
- _resyncIfJsonDiverged(db);
242
149
  const beforeRows = _readAllRows(db);
243
150
  const beforeObj = _inflate(beforeRows);
244
151
  const beforeSnapshot = JSON.parse(JSON.stringify(beforeObj));
@@ -248,43 +155,13 @@ function applyMetricsMutation(mutator, options = {}) {
248
155
  const beforeRowsSnap = _flatten(beforeSnapshot);
249
156
  const diff = _computeMetricsDiff(beforeRowsSnap, afterRows);
250
157
  const wrote = _applyMetricsDiff(db, diff);
251
- // W-mradg74d000rfd45 — phantom-record race fix: mirror the JSON
252
- // sidecar INSIDE this same SQL write transaction, BEFORE it commits,
253
- // instead of after (as the caller used to do post-return). Engine and
254
- // dashboard are separate Node processes, each with its own in-memory
255
- // mirror-hash cache; mirroring post-commit left a window where a
256
- // sibling process could observe the newly committed SQL rows before
257
- // the JSON file caught up and read stale/missing metrics from the
258
- // sidecar. Mirroring pre-commit means "SQL committed" and "JSON
259
- // mirror has the data" can never observably disagree across
260
- // processes. Mirrors the work-items-store.js fix from PR #720.
261
- if (wrote) {
262
- try { _mirrorJsonFromSql(options.mirrorPath); } catch { /* best-effort — mirror failure must not block the SQL write */ }
263
- }
264
158
  return { wrote, result: afterObj };
265
159
  });
266
160
  }
267
161
 
268
- function _mirrorJsonFromSql(filePath) {
269
- try {
270
- const shared = require('./shared');
271
- const { getDb } = require('./db');
272
- // Read SQL directly — bypass JSON fallback so a mutation that
273
- // empties all metrics rows doesn't resurrect stale JSON content.
274
- const obj = _inflate(_readAllRows(getDb()));
275
- const target = filePath || _metricsFilePath();
276
- shared.safeWrite(target, obj);
277
- const h = _fileContentHash(target);
278
- if (h != null) _lastMirrorHash = h;
279
- } catch {
280
- // Mirror failures are non-fatal — SQL has already committed.
281
- }
282
- }
283
-
284
162
  function _resetMetricsTableForTest() {
285
163
  const { getDb } = require('./db');
286
164
  try { getDb().exec('DELETE FROM metrics'); } catch { /* not initialized */ }
287
- _lastMirrorHash = null;
288
165
  }
289
166
 
290
167
  // W-mrb1a3950003d9ea — one-time collapse of legacy `watch-cc-triage:<watchId>`
@@ -351,7 +228,6 @@ function _resetWatchCcTriageMigrationFlagForTest() {
351
228
  module.exports = {
352
229
  readMetrics,
353
230
  applyMetricsMutation,
354
- _mirrorJsonFromSql,
355
231
  _resetMetricsTableForTest,
356
232
  migrateWatchCcTriageKeys,
357
233
  migrateWatchCcTriageKeysOnce,
@@ -575,13 +575,11 @@ const _VALID_EXISTING_PRD_STATUSES = new Set(Object.values(PLAN_STATUS));
575
575
  // a usable PRD. Require both a recognized top-level status AND an array
576
576
  // `missing_features` field before counting the PRD as an existing conversion.
577
577
  function _findExistingPrdForPlan(planFile, prdDir) {
578
- if (!fs.existsSync(prdDir)) return null;
579
- const prdFiles = safeReadDir(prdDir).filter(f => f.endsWith('.json'));
578
+ const prdFiles = require('./prd-store').listPrdRows().filter(row => !row.archived);
580
579
  const planKey = _canonicalPlanName(planFile);
581
- for (const pf of prdFiles) {
580
+ for (const { filename: pf, plan: prd } of prdFiles) {
582
581
  // safeJsonNoRestore: PRDs are terminal artifacts — never restore archived
583
582
  // PRDs from a stale .backup sidecar (W-mouptdh1000h9f39).
584
- const prd = safeJsonNoRestore(path.join(prdDir, pf));
585
583
  if (!prd?.source_plan || _canonicalPlanName(path.basename(String(prd.source_plan))) !== planKey) continue;
586
584
  if (!(_VALID_EXISTING_PRD_STATUSES.has(prd.status) || isPrdArchived(prd)) || !Array.isArray(prd.missing_features)) {
587
585
  log('warn', `Pipeline plan-to-prd: found PRD "${pf}" for plan "${planFile}" but it lacks a valid status/missing_features (garbage/incomplete conversion) — treating plan-to-prd as not yet done`);
@@ -992,12 +990,11 @@ function isStageComplete(stage, stageState, run, config) {
992
990
  const plans = artifacts.plans || [];
993
991
  const discoveredPrds = [];
994
992
  const discoveredWiIds = [];
995
- const prdFiles = fs.existsSync(prdDir) ? safeReadDir(prdDir).filter(f => f.endsWith('.json')) : [];
993
+ const prdRows = require('./prd-store').listPrdRows().filter(row => !row.archived);
996
994
  for (const planFile of plans) {
997
995
  const planKey = _canonicalPlanName(planFile);
998
- for (const pf of prdFiles) {
996
+ for (const { filename: pf, plan: prd } of prdRows) {
999
997
  // safeJsonNoRestore — see _findExistingPrdForPlan above (W-mouptdh1000h9f39).
1000
- const prd = safeJsonNoRestore(path.join(prdDir, pf));
1001
998
  // Canonical match: a collision-bumped run plan (<name>-<date>-2.md)
1002
999
  // still links to the PRD written for <name>-<date>.md, so the stage's
1003
1000
  // autoApprove reaches the PRD instead of stranding it awaiting-approval.
@@ -5,12 +5,14 @@
5
5
  */
6
6
 
7
7
  const fs = require('fs');
8
- const os = require('os');
9
8
  const path = require('path');
10
9
  const shared = require('./shared');
11
10
  const queries = require('./queries');
12
11
  const { wrapUntrusted, buildSource } = require('./untrusted-fence');
13
12
  const { MINIONS_BRAND_URL } = require('./comment-format');
13
+ const features = require('./features');
14
+ const memoryRetrieval = require('./memory-retrieval');
15
+ const memoryStore = require('./memory-store');
14
16
 
15
17
  const { safeJsonArr, safeRead, getProjects, log, ts, dateStamp, truncateTextBytes, ENGINE_DEFAULTS, WI_STATUS, WORK_TYPE, PR_STATUS, DISPATCH_RESULT, getProjectOrg } = shared;
16
18
  const { getConfig, getDispatch, getNotes, getPrs, getKnowledgeBaseIndex, AGENTS_DIR } = queries;
@@ -78,7 +80,7 @@ function getPrCommentInstructions(project) {
78
80
  const repoName = project?.repoName || '';
79
81
  const adoOrg = getProjectOrg(project);
80
82
  const adoProject = project?.adoProject || '<ado-project>';
81
- return `Post the comment via \`minions pr comment --host ado\` (preferred) — it prepends the hidden \`<!-- minions:agent=… kind=… -->\` marker, folds in the "Harnesses used" section, and hyperlinks the Minions brand mention for you, exactly like the GitHub path:\n` +
83
+ return `Post the comment via \`minions pr comment --host ado\` (preferred) — it prepends the hidden \`<!-- minions:agent=… kind=… -->\` marker and hyperlinks the Minions brand mention for you, exactly like the GitHub path:\n` +
82
84
  `- \`minions pr comment <number> --host ado --ado-org ${adoOrg} --ado-project ${adoProject} --repo-id ${repoId || '<repo-id>'} --agent <your-agent-id> --kind <kind> [--wi <work-item-id>] --body-file <body-file.md>\`\n` +
83
85
  `- Pick \`--kind\` from: \`review-decision\` | \`verify-report\` | \`rebase-report\` | \`positive-signal\` | \`fix-summary\` | \`other\`\n` +
84
86
  `- Use --body-file so Markdown, quotes, and newlines pass safely\n\n` +
@@ -128,7 +130,6 @@ function getPrVoteInstructions(project, config) {
128
130
  "<your review body here>\n" +
129
131
  "```\n" +
130
132
  `Then run: \`gh pr review <number> --comment --body-file <verdict.md> --repo ${org}/${repo}\`. Do NOT use \`--approve\` or \`--request-changes\` flags — they will fail.\n\n` +
131
- `When you author the summary body, append the collapsible **"Harnesses used"** \`<details>\` section described in the shared rules (mirror of your \`harnessUsed\` completion field) so reviewers see which skills / MCPs / docs informed the change; omit it when you used none.\n\n` +
132
133
  `**Brand link:** whenever the body names "Minions" (e.g. the \`Reviewed by Minions (…)\` sign-off), render it as \`[Minions](${MINIONS_BRAND_URL})\` per the shared rules. On the GitHub path the engine also auto-links the first bare \`… by Minions\` trailer as a backstop (\`engine/comment-format.js#linkifyBrandTrailer\`), but write the link yourself so the first occurrence is correct.`;
133
134
  }
134
135
  // Azure DevOps — prefer `az` CLI first, ADO REST API as fallback.
@@ -140,16 +141,14 @@ function getPrVoteInstructions(project, config) {
140
141
  const autoApplyReviewVote = config?.engine?.autoApplyReviewVote ?? ENGINE_DEFAULTS.autoApplyReviewVote;
141
142
  if (!autoApplyReviewVote) {
142
143
  return `**Do NOT cast a platform reviewer vote.** The operator has "Auto-apply review vote to PR" disabled, so Minions record verdicts as comments only — the human casts the final vote. Do NOT run \`az repos pr set-vote\` or PUT a reviewer vote via the REST API.\n\n` +
143
- `Post your verdict as a thread comment. **Preferred:** \`minions pr comment <number> --host ado --ado-org ${getProjectOrg(project)} --ado-project ${project?.adoProject || '<ado-project>'} --repo-id ${repoId || '<repo-id>'} --agent <your-agent-id> --kind review-decision [--wi <work-item-id>] --body-file <verdict.md>\` (applies the marker + harness section + brand link for you). Fallback: \`az repos pr comment create --pull-request-id <number> --content @<verdict.md>\`\n\n` +
144
+ `Post your verdict as a thread comment. **Preferred:** \`minions pr comment <number> --host ado --ado-org ${getProjectOrg(project)} --ado-project ${project?.adoProject || '<ado-project>'} --repo-id ${repoId || '<repo-id>'} --agent <your-agent-id> --kind review-decision [--wi <work-item-id>] --body-file <verdict.md>\` (applies the marker + brand link for you). Fallback: \`az repos pr comment create --pull-request-id <number> --content @<verdict.md>\`\n\n` +
144
145
  `The verdict body file's first non-marker line MUST be \`VERDICT: APPROVE\` or \`VERDICT: REQUEST_CHANGES\` — the engine parses this to record your verdict in local state.\n\n` +
145
- `When you author the comment body, append the collapsible **"Harnesses used"** \`<details>\` section described in the shared rules (mirror of your \`harnessUsed\` completion field); omit it when you used none.\n\n` +
146
146
  `**Brand link:** whenever the body names "Minions" (e.g. the \`Reviewed by Minions (…)\` sign-off), render it as \`Reviewed by [Minions](${MINIONS_BRAND_URL}) (…)\` per the shared rules. \`minions pr comment --host ado\` auto-links the first bare \`… by Minions\` trailer for you; the raw \`az repos pr comment\` fallback does NOT, so if you use it you MUST link it yourself.`;
147
147
  }
148
148
  return `For Azure DevOps, use the \`az\` CLI first to set your reviewer vote:\n` +
149
149
  `- \`az repos pr set-vote --id <number> --vote {approve | approve-with-suggestions | reject | reset | wait-for-author}\`\n` +
150
- `- Pair the vote with a thread comment. **Preferred:** \`minions pr comment <number> --host ado --ado-org ${getProjectOrg(project)} --ado-project ${project?.adoProject || '<ado-project>'} --repo-id ${repoId || '<repo-id>'} --agent <your-agent-id> --kind review-decision [--wi <work-item-id>] --body-file <verdict.md>\` (applies the marker + harness section + brand link for you). Fallback: \`az repos pr comment create --pull-request-id <number> --content @<verdict.md>\`\n\n` +
150
+ `- Pair the vote with a thread comment. **Preferred:** \`minions pr comment <number> --host ado --ado-org ${getProjectOrg(project)} --ado-project ${project?.adoProject || '<ado-project>'} --repo-id ${repoId || '<repo-id>'} --agent <your-agent-id> --kind review-decision [--wi <work-item-id>] --body-file <verdict.md>\` (applies the marker + brand link for you). Fallback: \`az repos pr comment create --pull-request-id <number> --content @<verdict.md>\`\n\n` +
151
151
  `If \`az\` is unavailable, fall back to the ADO REST API: get a token with \`az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv\` and \`PUT .../_apis/git/repositories/${repoId}/pullrequests/<number>/reviewers/<reviewerId>?api-version=7.1\` with body \`{"vote":10}\` (10=approve, 5=approve-with-suggestions, 0=no-vote, -5=wait-for-author, -10=reject; Bearer auth). Do not use \`gh\` for Azure DevOps repositories.\n\n` +
152
- `When you author the comment body, append the collapsible **"Harnesses used"** \`<details>\` section described in the shared rules (mirror of your \`harnessUsed\` completion field) so reviewers see which skills / MCPs / docs informed the change; render it identically to the GitHub path and omit it when you used none.\n\n` +
153
152
  `**Brand link:** whenever the body names "Minions" (e.g. the \`Reviewed by Minions (…)\` sign-off), render it as \`Reviewed by [Minions](${MINIONS_BRAND_URL}) (…)\` per the shared rules. \`minions pr comment --host ado\` auto-links the first bare \`… by Minions\` trailer for you (same backstop as GitHub); the raw \`az repos pr comment\` / REST fallbacks do NOT, so if you use those you MUST link it yourself.`;
154
153
  }
155
154
 
@@ -343,12 +342,6 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
343
342
  // PR-context vars on non-PR tasks (implement/explore/etc.)
344
343
  'pr_id', 'pr_number', 'pr_title', 'pr_branch', 'pr_author', 'pr_url',
345
344
  'reviewer',
346
- // W-mq16xtdx001a347e — review dispatches inject this block (or empty string
347
- // when discovery finds no project-local review skills). Optional because
348
- // non-review playbooks never reference it and review-against-skill-less
349
- // projects legitimately resolve it to ''.
350
- 'project_review_skills_block',
351
- 'skip_project_review_skills',
352
345
  // W-mq5l3f2u000i9045 — opt-out flag for the PR description audit + screenshot
353
346
  // refresh fragment inlined into fix.md / implement.md. Truthy (set when
354
347
  // item.meta.skipDescriptionAudit is true) renders the "audit suppressed"
@@ -389,26 +382,6 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
389
382
  'role', // 'primary' | 'co-service'
390
383
  'primary_project', // canonical primary project name; empty on single-project sessions
391
384
  'co_services_json', // JSON-encoded co-service project names (e.g. ["api","worker"]); empty when none
392
- // W-mq16xtdx001a347e (PR-82) — project-local review-skill discovery vars.
393
- // Always rendered (empty string when no discoveries); listed here so the
394
- // unresolved-var check doesn't complain when discovery returns nothing or
395
- // projects legitimately resolve it to ''.
396
- 'project_review_skills_block',
397
- 'skip_project_review_skills',
398
- // W-mq1cczi90006b21f — generic, intent-filtered project skills block.
399
- // Spliced by review/fix/implement/plan/etc. when discovery + intent map
400
- // produce matches. Optional for the same reason as the review-only alias.
401
- 'project_skills_block',
402
- 'skip_project_skills',
403
- // P-f52d81ba — monorepo-aware harness scoping. The engine derives a dominant
404
- // sub-project for the dispatch (explicit meta.workdir first segment, or
405
- // detectDominantSubproject over the current dispatch's changed paths) and threads
406
- // it here so playbook.js can pass it as `scopeSubproject` into project-skill
407
- // discovery. Optional with an empty-string default: single-root projects and
408
- // mixed/root diffs legitimately resolve it to '' (flat discovery), and no
409
- // playbook references {{dominant_subproject}} directly, so the unresolved-var check
410
- // must stay quiet when it is unset.
411
- 'dominant_subproject',
412
385
  // M006 — typed handoff envelope fields. Set on a follow-up dispatch meta
413
386
  // when lifecycle.js queues the item after a completing agent so the
414
387
  // receiving agent has explicit context about who handed off and why.
@@ -716,10 +689,61 @@ function renderPlaybook(type, vars) {
716
689
  inertAppendices.push('\n\n---\n\n## Pinned Context (CRITICAL — READ FIRST)\n\n' + (fenced || pinnedContent));
717
690
  }
718
691
 
692
+ // Structured memory rollout is deliberately two-stage. The feature flag
693
+ // turns retrieval on; shadow mode (default) records selection telemetry but
694
+ // leaves the legacy notes/personal-memory prompt unchanged. Once shadow mode
695
+ // is disabled, a non-empty bounded retrieval pack replaces those broad
696
+ // appendices. Pinned operator context always remains fully visible.
697
+ let relevantMemoryInjected = false;
698
+ if (features.isFeatureOn('memoryRetrieval', configEarly)) {
699
+ const engineConfig = configEarly.engine || {};
700
+ const shadow = engineConfig.memoryRetrievalShadowMode ?? ENGINE_DEFAULTS.memoryRetrievalShadowMode;
701
+ try {
702
+ const retrieved = memoryRetrieval.retrieveRelevantMemory({
703
+ workItemId: vars.item_id || vars.task_id || '',
704
+ title: vars.item_name || vars.pr_title || '',
705
+ description: vars.item_description || vars.task_description || vars.pr_description || '',
706
+ type: vars.work_type || type,
707
+ project: projectName || '',
708
+ agent: vars.agent_id || '',
709
+ failureClass: vars.failure_class || '',
710
+ sourcePlan: vars.source_plan || '',
711
+ references: vars.references || '',
712
+ prTitle: vars.pr_title || '',
713
+ prBranch: vars.pr_branch || '',
714
+ }, {
715
+ topK: engineConfig.memoryRetrievalTopK ?? ENGINE_DEFAULTS.memoryRetrievalTopK,
716
+ maxBytes: engineConfig.memoryRetrievalMaxBytes ?? ENGINE_DEFAULTS.memoryRetrievalMaxBytes,
717
+ candidateLimit: engineConfig.memoryRetrievalCandidateLimit ?? ENGINE_DEFAULTS.memoryRetrievalCandidateLimit,
718
+ });
719
+ memoryStore.recordRetrievalRun({
720
+ workItemId: vars.item_id || vars.task_id || null,
721
+ project: projectName,
722
+ agent: vars.agent_id || null,
723
+ query: retrieved.query,
724
+ candidateCount: retrieved.candidates.length,
725
+ selectedCount: retrieved.selected.length,
726
+ selectedBytes: retrieved.bytes,
727
+ durationMs: retrieved.durationMs,
728
+ shadow,
729
+ });
730
+ if (!shadow && retrieved.text) {
731
+ const fenced = wrapUntrusted(
732
+ retrieved.text,
733
+ buildSource('memory-retrieval', { item: vars.item_id || vars.task_id || 'unknown' })
734
+ );
735
+ inertAppendices.push('\n\n---\n\n## Relevant Memory (task-selected; verify against live code)\n\n' + (fenced || retrieved.text));
736
+ relevantMemoryInjected = true;
737
+ }
738
+ } catch (e) {
739
+ log('warn', `Memory retrieval failed for ${vars.item_id || vars.task_id || type}; using legacy memory: ${e.message}`);
740
+ }
741
+ }
742
+
719
743
  // Inject team notes (single injection point — not in buildAgentContext) — capped via ENGINE_DEFAULTS.
720
744
  // F5: wrap in <UNTRUSTED-INPUT> fence — notes.md is an LLM-consolidated mix
721
745
  // of agent inbox notes (semi-trusted) and human edits.
722
- let notes = getNotes();
746
+ let notes = relevantMemoryInjected ? '' : getNotes();
723
747
  if (notes) {
724
748
  if (Buffer.byteLength(notes, 'utf8') > ENGINE_DEFAULTS.maxNotesPromptBytes) {
725
749
  const sections = notes.split(/(?=^### )/m);
@@ -738,7 +762,7 @@ function renderPlaybook(type, vars) {
738
762
  // notes budget; missing file degrades gracefully (silent skip).
739
763
  // F5: fence — agent-authored inbox notes routed into this file; any agent
740
764
  // could include attacker-controlled quoted material.
741
- const agentIdForMemory = vars.agent_id;
765
+ const agentIdForMemory = relevantMemoryInjected ? '' : vars.agent_id;
742
766
  if (agentIdForMemory && /^[a-z][a-z0-9-]{0,40}$/i.test(agentIdForMemory) && !String(agentIdForMemory).toLowerCase().startsWith('temp-')) {
743
767
  const agentMemRel = `knowledge/agents/${String(agentIdForMemory).toLowerCase()}.md`;
744
768
  const agentMemPath = path.join(MINIONS_DIR, agentMemRel);
@@ -807,78 +831,6 @@ function renderPlaybook(type, vars) {
807
831
  } catch (e) { log('warn', `managed-spawn live-processes inject failed: ${e.message}`); }
808
832
  }
809
833
 
810
- // W-mq16xtdx001a347e (PR #82) + W-mq1cczi90006b21f (generalization) —
811
- // surface project-local skills relevant to the playbook being dispatched.
812
- //
813
- // PR-82 hard-scoped this to type === 'review'. This wiring generalizes it:
814
- // 1. Discovery walks every project skill / command / documented slash-
815
- // command (engine/discover-project-skills.js).
816
- // 2. engine/playbook-intents.js maps playbook → intent set.
817
- // 3. Discovered entries are filtered to the playbook's intent set and
818
- // rendered into `project_skills_block`. Empty intent set or zero
819
- // matches → empty string (no stray header).
820
- //
821
- // Header copy: review dispatches keep PR-82's "## Project review skills"
822
- // header (existing agents/tests grep for it). All other playbooks use the
823
- // generalized "## Project skills" header.
824
- //
825
- // Backward-compat: `project_review_skills_block` and
826
- // `vars.skip_project_review_skills` remain wired so external consumers of
827
- // the PR-82 names keep working. They alias to the generic var/flag.
828
- const skipProjectSkills = !!(vars.skip_project_skills || vars.skip_project_review_skills);
829
- let projectSkillsBlock = '';
830
- if (!skipProjectSkills) {
831
- try {
832
- const discover = require('./discover-project-skills');
833
- const { intentsForPlaybook } = require('./playbook-intents');
834
- const projectPath = matchedProject?.localPath || '';
835
- if (projectPath) {
836
- // Inherit intent set from parent dispatch type when the engine
837
- // passes vars.parent_dispatch_type (followup-dispatch flow).
838
- const effectiveType = vars.parent_dispatch_type || type;
839
- const intents = intentsForPlaybook(effectiveType);
840
- if (intents.length > 0) {
841
- // P-f52d81ba — monorepo-aware scoping. When the engine derived a
842
- // dominant sub-project for this dispatch (git diff / PR files /
843
- // references → detectDominantSubproject, threaded in via vars.dominant_subproject),
844
- // surface that sub-project's skills FIRST. Empty/unset ⇒ today's flat
845
- // global name-sort. Use the diagnostics variant so per-surface
846
- // overflow (entries dropped past the maxFilesPerSurface cap) is
847
- // observable instead of silent.
848
- const scopeSubproject = typeof vars.dominant_subproject === 'string' ? vars.dominant_subproject : '';
849
- const { entries, truncations } = discover.discoverProjectSkillsWithDiagnostics({
850
- projectPath,
851
- scopeSubproject,
852
- opts: { maxFilesPerSurface: ENGINE_DEFAULTS.projectSkillMaxFilesPerSurface },
853
- });
854
- if (Array.isArray(truncations) && truncations.length > 0) {
855
- log('warn', `discover-project-skills: ${truncations.length} surface(s) exceeded maxFilesPerSurface (entries dropped) for project ${matchedProject?.name || projectPath}${scopeSubproject ? ', subproject=' + scopeSubproject : ''}: ${truncations.map(t => `${t.dir} (${t.scanned}/${t.total})`).join('; ')}`);
856
- }
857
- // Stash discovery diagnostics on the vars object so the engine
858
- // caller (spawnAgent) can record detected sub-project + truncations onto
859
- // the dispatch record for later grounding/observability. Best-effort:
860
- // callers that don't read it simply ignore the field.
861
- vars._discoveryDiagnostics = { subproject: scopeSubproject, truncations: Array.isArray(truncations) ? truncations : [] };
862
- const filtered = discover.filterByIntents(entries, intents);
863
- // type === 'review' keeps PR-82 header copy + meta.review.* outcome
864
- // guidance; everything else uses the generic header that points at
865
- // meta.skill.* in the completion report.
866
- projectSkillsBlock = (type === WORK_TYPE.REVIEW)
867
- ? discover.renderReviewSkillsBlock(filtered)
868
- : discover.renderProjectSkillsBlock(filtered);
869
- }
870
- }
871
- } catch (e) {
872
- log('warn', `discover-project-skills failed: ${e.message}`);
873
- }
874
- }
875
- vars.project_skills_block = projectSkillsBlock;
876
- // Backward-compat alias for any consumer that still references
877
- // {{project_review_skills_block}} directly. Same content; cheaper than
878
- // re-rendering. Non-review playbooks that don't reference the alias
879
- // ignore it (it's in PLAYBOOK_OPTIONAL_VARS).
880
- vars.project_review_skills_block = projectSkillsBlock;
881
-
882
834
  // W-mpeiwz6k0005bf34-c — opt-in qa-validate context block. Injected only
883
835
  // when the dispatcher set vars.qa_run_id (truthy) from the work item's
884
836
  // `meta.qaRunId`. Mirrors the managed_spawn hint pattern: the playbook is
@@ -1134,72 +1086,13 @@ function buildSystemPrompt(agentId, config, project) {
1134
1086
  return { prompt, warnings: [] };
1135
1087
  }
1136
1088
 
1137
- // E2.b (W-mp7goxe4000p75f7): build the `## Existing Skills (do not duplicate)`
1138
- // section for buildAgentContext. Lists name + first-line description for every
1139
- // personal-scope/installed-plugin skill discovered by queries.getSkills().
1140
- // Caps at ~2KB by truncating alphabetically with a "_...and N more_" tail.
1141
- // Returns '' when there are no personal-scope skills to advertise.
1142
- const EXISTING_SKILLS_MAX_BYTES = 2048;
1143
- const PERSONAL_SKILL_SCOPES = new Set(['claude-code', 'copilot', 'agent-skill', 'plugin', 'copilot-plugin']);
1144
-
1145
- function _buildExistingSkillsSection(config) {
1146
- let skills;
1147
- try {
1148
- skills = queries.getSkills(config) || [];
1149
- } catch { return ''; }
1150
- const personal = skills
1151
- .filter(s => PERSONAL_SKILL_SCOPES.has(s.scope))
1152
- .map(s => ({
1153
- name: String(s.name || '').trim(),
1154
- description: String(s.description || '').split('\n')[0].trim(),
1155
- }))
1156
- .filter(s => s.name)
1157
- .sort((a, b) => a.name.localeCompare(b.name));
1158
- if (personal.length === 0) return '';
1159
-
1160
- const header = `## Existing Skills (do not duplicate)\n\nThese personal-scope skills already exist on disk. Do NOT emit a \`\`\`skill block whose name duplicates or near-duplicates one of these — the engine will flag it for human review and skip the write.\n\n`;
1161
- const lines = personal.map(s => s.description ? `- ${s.name} — ${s.description}` : `- ${s.name}`);
1162
-
1163
- let body = lines.join('\n') + '\n\n';
1164
- if (Buffer.byteLength(header + body, 'utf8') <= EXISTING_SKILLS_MAX_BYTES) {
1165
- return header + body;
1166
- }
1167
-
1168
- // Truncate alphabetically until we fit, leaving room for the tail line.
1169
- let kept = lines.length;
1170
- while (kept > 0) {
1171
- const dropped = lines.length - kept;
1172
- const tail = `_...and ${dropped} more (alphabetical truncation; full list on disk)_\n\n`;
1173
- body = lines.slice(0, kept).join('\n') + '\n' + tail;
1174
- if (Buffer.byteLength(header + body, 'utf8') <= EXISTING_SKILLS_MAX_BYTES) {
1175
- return header + body;
1176
- }
1177
- kept--;
1178
- }
1179
- // Pathological: header alone exceeds the cap. Return at least the header
1180
- // with a tail showing every skill was dropped, so the agent still sees the
1181
- // section heading.
1182
- return header + `_...and ${lines.length} more (alphabetical truncation; full list on disk)_\n\n`;
1183
- }
1184
-
1185
- // Bulk context: history, notes, conventions, skills — prepended to user/task prompt.
1089
+ // Bulk Minions context: history, notes, and orchestration state prepended to
1090
+ // the user/task prompt. Repository harness discovery remains runtime-native.
1186
1091
  // This is the content that grows over time and would bloat the system prompt.
1187
1092
  function buildAgentContext(agentId, config, project) {
1188
1093
  project = project || getProjects(config)[0] || {};
1189
1094
  let context = '';
1190
1095
 
1191
- function appendContextFile(heading, filePath, maxBytes, extra = '') {
1192
- if (!filePath) return;
1193
- const content = safeRead(filePath);
1194
- if (!content || !content.trim()) return;
1195
- const truncated = Buffer.byteLength(content, 'utf8') > maxBytes
1196
- ? truncateTextBytes(content, maxBytes, '\n\n_...truncated; read the full file if needed_')
1197
- : content;
1198
- context += `## ${heading}\n\n`;
1199
- if (extra) context += `${extra}\n\n`;
1200
- context += `${truncated}\n\n`;
1201
- }
1202
-
1203
1096
  function appendIndex(heading, body, maxBytes) {
1204
1097
  if (!body || !String(body).trim()) return;
1205
1098
  const truncated = Buffer.byteLength(body, 'utf8') > maxBytes
@@ -1218,28 +1111,9 @@ function buildAgentContext(agentId, config, project) {
1218
1111
  const trimmed = (header ? header + '\n' : '') + recent.join('');
1219
1112
  context += `## Your Recent History (last 5 tasks)\n\n${trimmed}\n\n`;
1220
1113
  }
1221
-
1222
- // Project conventions and instruction files — explicit, inert context for
1223
- // every runtime. Runtime-native skills/commands stay in their native indexes.
1224
- if (project.localPath) {
1225
- appendContextFile('Project Conventions (from CLAUDE.md)', path.join(project.localPath, 'CLAUDE.md'), 8192);
1226
- appendContextFile('Project Agent Instructions (from AGENTS.md)', path.join(project.localPath, 'AGENTS.md'), 8192,
1227
- 'These instructions are explicitly injected because some runtimes suppress automatic AGENTS.md loading. Follow them unless they conflict with the Minions task contract or playbook.');
1228
- appendContextFile('Project Copilot Instructions (from .github/copilot-instructions.md)', path.join(project.localPath, '.github', 'copilot-instructions.md'), 8192,
1229
- 'Follow these repository instructions unless they conflict with the Minions task contract or playbook.');
1230
- }
1231
-
1232
- appendContextFile('User Claude Instructions (from ~/.claude/CLAUDE.md)', path.join(os.homedir(), '.claude', 'CLAUDE.md'), 8192,
1233
- 'These are the user-level Claude Code instructions available in regular Claude usage. Follow them unless they conflict with the Minions task contract or playbook.');
1234
-
1235
1114
  appendIndex('Knowledge Base Reference', getKnowledgeBaseIndex(), 8192);
1236
1115
 
1237
- context += `## Reference Files\n\nKnowledge base entries are in \`knowledge/{category}/*.md\`, and project-local playbooks live in \`projects/<project>/playbooks/\`. Runtime-native skills and commands live in their native indexes (the catalog of names + descriptions is injected below as "Existing Skills"; full skill bodies are not).\n\n`;
1238
-
1239
- // E2.b (W-mp7goxe4000p75f7): inject existing personal-scope skill catalog
1240
- // so agents can see what already exists before emitting a near-duplicate
1241
- // skill block. Caps at ~2KB; truncates alphabetically with a tail count.
1242
- context += _buildExistingSkillsSection(config);
1116
+ context += `## Reference Files\n\nKnowledge base entries are in \`knowledge/{category}/*.md\`, and project-local playbooks live in \`projects/<project>/playbooks/\`.\n\n`;
1243
1117
 
1244
1118
  // Minions awareness: what's in flight, who's doing what
1245
1119
  const dispatch = getDispatch();
@@ -1265,9 +1139,9 @@ function buildAgentContext(agentId, config, project) {
1265
1139
  const prs = getPrs(p).filter(pr => pr.status === PR_STATUS.ACTIVE);
1266
1140
  for (const pr of prs) allPrs.push({ ...pr, _project: p.name });
1267
1141
  }
1268
- // Also check central pull-requests.json
1142
+ // Also check the central SQL pull-request scope.
1269
1143
  try {
1270
- const centralPrs = safeJsonArr(path.join(MINIONS_DIR, 'pull-requests.json'));
1144
+ const centralPrs = require('./pull-requests-store').readPullRequestsForScope('central');
1271
1145
  for (const pr of centralPrs.filter(pr => pr.status === PR_STATUS.ACTIVE)) {
1272
1146
  if (!allPrs.some(p => p.id === pr.id)) allPrs.push({ ...pr, _project: 'central' });
1273
1147
  }
@@ -1378,14 +1252,6 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
1378
1252
  const vars = {
1379
1253
  ...buildBaseVars(agentId, config, project),
1380
1254
  branch_name: extraVars?.pr_branch || '',
1381
- // W-mq16xtdx001a347e + W-mq1cczi90006b21f — honor both meta flag names
1382
- // (skipProjectReviewSkills is the PR-82 alias). Default OFF: review/fix
1383
- // dispatches surface the project-local skills block.
1384
- skip_project_review_skills: !!(meta && meta.skipProjectReviewSkills),
1385
- // W-mq1cczi90006b21f — generalized project-skills suppression flag.
1386
- // Honors either the new `meta.skipProjectSkills` or the PR-82
1387
- // `meta.skipProjectReviewSkills` alias so older work items keep working.
1388
- skip_project_skills: !!(meta && (meta.skipProjectSkills || meta.skipProjectReviewSkills)),
1389
1255
  // W-mq5l3f2u000i9045 — opt-out for the inlined PR description audit
1390
1256
  // section in fix.md. Truthy renders the "audit suppressed" notice; falsy
1391
1257
  // (the default) leaves the standard audit instructions in place.