@yemi33/minions 0.1.2370 → 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 (71) hide show
  1. package/bin/minions.js +38 -11
  2. package/dashboard/js/render-other.js +2 -30
  3. package/dashboard/js/render-work-items.js +0 -34
  4. package/dashboard/js/settings.js +12 -27
  5. package/dashboard/pages/tools.html +1 -1
  6. package/dashboard.js +80 -257
  7. package/docs/README.md +2 -3
  8. package/docs/architecture.excalidraw +2 -2
  9. package/docs/auto-discovery.md +3 -3
  10. package/docs/completion-reports.md +0 -121
  11. package/docs/copilot-cli-schema.md +9 -17
  12. package/docs/deprecated.json +0 -51
  13. package/docs/design-state-storage.md +4 -4
  14. package/docs/harness-propagation.md +33 -263
  15. package/docs/human-vs-automated.md +1 -1
  16. package/docs/named-agents.md +0 -2
  17. package/docs/plan-lifecycle.md +5 -6
  18. package/docs/qa-runbook-lifecycle.md +10 -25
  19. package/docs/shared-lifecycle-module-map.md +2 -10
  20. package/engine/ado-comment.js +5 -11
  21. package/engine/ado.js +10 -5
  22. package/engine/cleanup.js +16 -36
  23. package/engine/cli.js +13 -175
  24. package/engine/comment-format.js +8 -182
  25. package/engine/db/index.js +60 -10
  26. package/engine/db/migrations/018-sql-only-cutover.js +56 -0
  27. package/engine/dispatch-store.js +0 -114
  28. package/engine/dispatch.js +1 -6
  29. package/engine/gh-comment.js +2 -10
  30. package/engine/github.js +8 -6
  31. package/engine/lifecycle.js +58 -173
  32. package/engine/llm.js +9 -11
  33. package/engine/managed-spawn.js +1 -6
  34. package/engine/memory-store.js +8 -6
  35. package/engine/metrics-store.js +4 -128
  36. package/engine/pipeline.js +4 -7
  37. package/engine/playbook.js +8 -196
  38. package/engine/prd-store.js +115 -141
  39. package/engine/preflight.js +8 -55
  40. package/engine/projects.js +34 -41
  41. package/engine/pull-requests-store.js +9 -234
  42. package/engine/qa-runs.js +4 -15
  43. package/engine/qa-sessions.js +5 -17
  44. package/engine/queries.js +23 -103
  45. package/engine/routing.js +1 -1
  46. package/engine/runtimes/claude.js +1 -2
  47. package/engine/runtimes/codex.js +0 -2
  48. package/engine/runtimes/copilot.js +1 -4
  49. package/engine/shared-branch-pr-reconcile.js +2 -5
  50. package/engine/shared.js +174 -533
  51. package/engine/small-state-store.js +12 -647
  52. package/engine/spawn-agent.js +5 -96
  53. package/engine/state-operations.js +166 -0
  54. package/engine/supervisor.js +0 -1
  55. package/engine/watch-actions.js +2 -3
  56. package/engine/watches-store.js +2 -128
  57. package/engine/work-items-store.js +3 -254
  58. package/engine.js +102 -334
  59. package/package.json +2 -2
  60. package/playbooks/build-fix-complex.md +0 -4
  61. package/playbooks/fix.md +0 -4
  62. package/playbooks/implement-shared.md +0 -4
  63. package/playbooks/implement.md +0 -4
  64. package/playbooks/plan-to-prd.md +16 -11
  65. package/playbooks/plan.md +0 -4
  66. package/playbooks/review.md +0 -6
  67. package/playbooks/shared-rules.md +0 -65
  68. package/docs/harness-transparency.md +0 -199
  69. package/docs/project-skills.md +0 -193
  70. package/engine/discover-project-skills.js +0 -673
  71. 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,7 +5,6 @@
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');
@@ -81,7 +80,7 @@ function getPrCommentInstructions(project) {
81
80
  const repoName = project?.repoName || '';
82
81
  const adoOrg = getProjectOrg(project);
83
82
  const adoProject = project?.adoProject || '<ado-project>';
84
- 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` +
85
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` +
86
85
  `- Pick \`--kind\` from: \`review-decision\` | \`verify-report\` | \`rebase-report\` | \`positive-signal\` | \`fix-summary\` | \`other\`\n` +
87
86
  `- Use --body-file so Markdown, quotes, and newlines pass safely\n\n` +
@@ -131,7 +130,6 @@ function getPrVoteInstructions(project, config) {
131
130
  "<your review body here>\n" +
132
131
  "```\n" +
133
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` +
134
- `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` +
135
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.`;
136
134
  }
137
135
  // Azure DevOps — prefer `az` CLI first, ADO REST API as fallback.
@@ -143,16 +141,14 @@ function getPrVoteInstructions(project, config) {
143
141
  const autoApplyReviewVote = config?.engine?.autoApplyReviewVote ?? ENGINE_DEFAULTS.autoApplyReviewVote;
144
142
  if (!autoApplyReviewVote) {
145
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` +
146
- `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` +
147
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` +
148
- `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` +
149
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.`;
150
147
  }
151
148
  return `For Azure DevOps, use the \`az\` CLI first to set your reviewer vote:\n` +
152
149
  `- \`az repos pr set-vote --id <number> --vote {approve | approve-with-suggestions | reject | reset | wait-for-author}\`\n` +
153
- `- 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` +
154
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` +
155
- `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` +
156
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.`;
157
153
  }
158
154
 
@@ -346,12 +342,6 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
346
342
  // PR-context vars on non-PR tasks (implement/explore/etc.)
347
343
  'pr_id', 'pr_number', 'pr_title', 'pr_branch', 'pr_author', 'pr_url',
348
344
  'reviewer',
349
- // W-mq16xtdx001a347e — review dispatches inject this block (or empty string
350
- // when discovery finds no project-local review skills). Optional because
351
- // non-review playbooks never reference it and review-against-skill-less
352
- // projects legitimately resolve it to ''.
353
- 'project_review_skills_block',
354
- 'skip_project_review_skills',
355
345
  // W-mq5l3f2u000i9045 — opt-out flag for the PR description audit + screenshot
356
346
  // refresh fragment inlined into fix.md / implement.md. Truthy (set when
357
347
  // item.meta.skipDescriptionAudit is true) renders the "audit suppressed"
@@ -392,26 +382,6 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
392
382
  'role', // 'primary' | 'co-service'
393
383
  'primary_project', // canonical primary project name; empty on single-project sessions
394
384
  'co_services_json', // JSON-encoded co-service project names (e.g. ["api","worker"]); empty when none
395
- // W-mq16xtdx001a347e (PR-82) — project-local review-skill discovery vars.
396
- // Always rendered (empty string when no discoveries); listed here so the
397
- // unresolved-var check doesn't complain when discovery returns nothing or
398
- // projects legitimately resolve it to ''.
399
- 'project_review_skills_block',
400
- 'skip_project_review_skills',
401
- // W-mq1cczi90006b21f — generic, intent-filtered project skills block.
402
- // Spliced by review/fix/implement/plan/etc. when discovery + intent map
403
- // produce matches. Optional for the same reason as the review-only alias.
404
- 'project_skills_block',
405
- 'skip_project_skills',
406
- // P-f52d81ba — monorepo-aware harness scoping. The engine derives a dominant
407
- // sub-project for the dispatch (explicit meta.workdir first segment, or
408
- // detectDominantSubproject over the current dispatch's changed paths) and threads
409
- // it here so playbook.js can pass it as `scopeSubproject` into project-skill
410
- // discovery. Optional with an empty-string default: single-root projects and
411
- // mixed/root diffs legitimately resolve it to '' (flat discovery), and no
412
- // playbook references {{dominant_subproject}} directly, so the unresolved-var check
413
- // must stay quiet when it is unset.
414
- 'dominant_subproject',
415
385
  // M006 — typed handoff envelope fields. Set on a follow-up dispatch meta
416
386
  // when lifecycle.js queues the item after a completing agent so the
417
387
  // receiving agent has explicit context about who handed off and why.
@@ -861,78 +831,6 @@ function renderPlaybook(type, vars) {
861
831
  } catch (e) { log('warn', `managed-spawn live-processes inject failed: ${e.message}`); }
862
832
  }
863
833
 
864
- // W-mq16xtdx001a347e (PR #82) + W-mq1cczi90006b21f (generalization) —
865
- // surface project-local skills relevant to the playbook being dispatched.
866
- //
867
- // PR-82 hard-scoped this to type === 'review'. This wiring generalizes it:
868
- // 1. Discovery walks every project skill / command / documented slash-
869
- // command (engine/discover-project-skills.js).
870
- // 2. engine/playbook-intents.js maps playbook → intent set.
871
- // 3. Discovered entries are filtered to the playbook's intent set and
872
- // rendered into `project_skills_block`. Empty intent set or zero
873
- // matches → empty string (no stray header).
874
- //
875
- // Header copy: review dispatches keep PR-82's "## Project review skills"
876
- // header (existing agents/tests grep for it). All other playbooks use the
877
- // generalized "## Project skills" header.
878
- //
879
- // Backward-compat: `project_review_skills_block` and
880
- // `vars.skip_project_review_skills` remain wired so external consumers of
881
- // the PR-82 names keep working. They alias to the generic var/flag.
882
- const skipProjectSkills = !!(vars.skip_project_skills || vars.skip_project_review_skills);
883
- let projectSkillsBlock = '';
884
- if (!skipProjectSkills) {
885
- try {
886
- const discover = require('./discover-project-skills');
887
- const { intentsForPlaybook } = require('./playbook-intents');
888
- const projectPath = matchedProject?.localPath || '';
889
- if (projectPath) {
890
- // Inherit intent set from parent dispatch type when the engine
891
- // passes vars.parent_dispatch_type (followup-dispatch flow).
892
- const effectiveType = vars.parent_dispatch_type || type;
893
- const intents = intentsForPlaybook(effectiveType);
894
- if (intents.length > 0) {
895
- // P-f52d81ba — monorepo-aware scoping. When the engine derived a
896
- // dominant sub-project for this dispatch (git diff / PR files /
897
- // references → detectDominantSubproject, threaded in via vars.dominant_subproject),
898
- // surface that sub-project's skills FIRST. Empty/unset ⇒ today's flat
899
- // global name-sort. Use the diagnostics variant so per-surface
900
- // overflow (entries dropped past the maxFilesPerSurface cap) is
901
- // observable instead of silent.
902
- const scopeSubproject = typeof vars.dominant_subproject === 'string' ? vars.dominant_subproject : '';
903
- const { entries, truncations } = discover.discoverProjectSkillsWithDiagnostics({
904
- projectPath,
905
- scopeSubproject,
906
- opts: { maxFilesPerSurface: ENGINE_DEFAULTS.projectSkillMaxFilesPerSurface },
907
- });
908
- if (Array.isArray(truncations) && truncations.length > 0) {
909
- 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('; ')}`);
910
- }
911
- // Stash discovery diagnostics on the vars object so the engine
912
- // caller (spawnAgent) can record detected sub-project + truncations onto
913
- // the dispatch record for later grounding/observability. Best-effort:
914
- // callers that don't read it simply ignore the field.
915
- vars._discoveryDiagnostics = { subproject: scopeSubproject, truncations: Array.isArray(truncations) ? truncations : [] };
916
- const filtered = discover.filterByIntents(entries, intents);
917
- // type === 'review' keeps PR-82 header copy + meta.review.* outcome
918
- // guidance; everything else uses the generic header that points at
919
- // meta.skill.* in the completion report.
920
- projectSkillsBlock = (type === WORK_TYPE.REVIEW)
921
- ? discover.renderReviewSkillsBlock(filtered)
922
- : discover.renderProjectSkillsBlock(filtered);
923
- }
924
- }
925
- } catch (e) {
926
- log('warn', `discover-project-skills failed: ${e.message}`);
927
- }
928
- }
929
- vars.project_skills_block = projectSkillsBlock;
930
- // Backward-compat alias for any consumer that still references
931
- // {{project_review_skills_block}} directly. Same content; cheaper than
932
- // re-rendering. Non-review playbooks that don't reference the alias
933
- // ignore it (it's in PLAYBOOK_OPTIONAL_VARS).
934
- vars.project_review_skills_block = projectSkillsBlock;
935
-
936
834
  // W-mpeiwz6k0005bf34-c — opt-in qa-validate context block. Injected only
937
835
  // when the dispatcher set vars.qa_run_id (truthy) from the work item's
938
836
  // `meta.qaRunId`. Mirrors the managed_spawn hint pattern: the playbook is
@@ -1188,72 +1086,13 @@ function buildSystemPrompt(agentId, config, project) {
1188
1086
  return { prompt, warnings: [] };
1189
1087
  }
1190
1088
 
1191
- // E2.b (W-mp7goxe4000p75f7): build the `## Existing Skills (do not duplicate)`
1192
- // section for buildAgentContext. Lists name + first-line description for every
1193
- // personal-scope/installed-plugin skill discovered by queries.getSkills().
1194
- // Caps at ~2KB by truncating alphabetically with a "_...and N more_" tail.
1195
- // Returns '' when there are no personal-scope skills to advertise.
1196
- const EXISTING_SKILLS_MAX_BYTES = 2048;
1197
- const PERSONAL_SKILL_SCOPES = new Set(['claude-code', 'copilot', 'agent-skill', 'plugin', 'copilot-plugin']);
1198
-
1199
- function _buildExistingSkillsSection(config) {
1200
- let skills;
1201
- try {
1202
- skills = queries.getSkills(config) || [];
1203
- } catch { return ''; }
1204
- const personal = skills
1205
- .filter(s => PERSONAL_SKILL_SCOPES.has(s.scope))
1206
- .map(s => ({
1207
- name: String(s.name || '').trim(),
1208
- description: String(s.description || '').split('\n')[0].trim(),
1209
- }))
1210
- .filter(s => s.name)
1211
- .sort((a, b) => a.name.localeCompare(b.name));
1212
- if (personal.length === 0) return '';
1213
-
1214
- 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`;
1215
- const lines = personal.map(s => s.description ? `- ${s.name} — ${s.description}` : `- ${s.name}`);
1216
-
1217
- let body = lines.join('\n') + '\n\n';
1218
- if (Buffer.byteLength(header + body, 'utf8') <= EXISTING_SKILLS_MAX_BYTES) {
1219
- return header + body;
1220
- }
1221
-
1222
- // Truncate alphabetically until we fit, leaving room for the tail line.
1223
- let kept = lines.length;
1224
- while (kept > 0) {
1225
- const dropped = lines.length - kept;
1226
- const tail = `_...and ${dropped} more (alphabetical truncation; full list on disk)_\n\n`;
1227
- body = lines.slice(0, kept).join('\n') + '\n' + tail;
1228
- if (Buffer.byteLength(header + body, 'utf8') <= EXISTING_SKILLS_MAX_BYTES) {
1229
- return header + body;
1230
- }
1231
- kept--;
1232
- }
1233
- // Pathological: header alone exceeds the cap. Return at least the header
1234
- // with a tail showing every skill was dropped, so the agent still sees the
1235
- // section heading.
1236
- return header + `_...and ${lines.length} more (alphabetical truncation; full list on disk)_\n\n`;
1237
- }
1238
-
1239
- // 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.
1240
1091
  // This is the content that grows over time and would bloat the system prompt.
1241
1092
  function buildAgentContext(agentId, config, project) {
1242
1093
  project = project || getProjects(config)[0] || {};
1243
1094
  let context = '';
1244
1095
 
1245
- function appendContextFile(heading, filePath, maxBytes, extra = '') {
1246
- if (!filePath) return;
1247
- const content = safeRead(filePath);
1248
- if (!content || !content.trim()) return;
1249
- const truncated = Buffer.byteLength(content, 'utf8') > maxBytes
1250
- ? truncateTextBytes(content, maxBytes, '\n\n_...truncated; read the full file if needed_')
1251
- : content;
1252
- context += `## ${heading}\n\n`;
1253
- if (extra) context += `${extra}\n\n`;
1254
- context += `${truncated}\n\n`;
1255
- }
1256
-
1257
1096
  function appendIndex(heading, body, maxBytes) {
1258
1097
  if (!body || !String(body).trim()) return;
1259
1098
  const truncated = Buffer.byteLength(body, 'utf8') > maxBytes
@@ -1272,28 +1111,9 @@ function buildAgentContext(agentId, config, project) {
1272
1111
  const trimmed = (header ? header + '\n' : '') + recent.join('');
1273
1112
  context += `## Your Recent History (last 5 tasks)\n\n${trimmed}\n\n`;
1274
1113
  }
1275
-
1276
- // Project conventions and instruction files — explicit, inert context for
1277
- // every runtime. Runtime-native skills/commands stay in their native indexes.
1278
- if (project.localPath) {
1279
- appendContextFile('Project Conventions (from CLAUDE.md)', path.join(project.localPath, 'CLAUDE.md'), 8192);
1280
- appendContextFile('Project Agent Instructions (from AGENTS.md)', path.join(project.localPath, 'AGENTS.md'), 8192,
1281
- '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.');
1282
- appendContextFile('Project Copilot Instructions (from .github/copilot-instructions.md)', path.join(project.localPath, '.github', 'copilot-instructions.md'), 8192,
1283
- 'Follow these repository instructions unless they conflict with the Minions task contract or playbook.');
1284
- }
1285
-
1286
- appendContextFile('User Claude Instructions (from ~/.claude/CLAUDE.md)', path.join(os.homedir(), '.claude', 'CLAUDE.md'), 8192,
1287
- '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.');
1288
-
1289
1114
  appendIndex('Knowledge Base Reference', getKnowledgeBaseIndex(), 8192);
1290
1115
 
1291
- 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`;
1292
-
1293
- // E2.b (W-mp7goxe4000p75f7): inject existing personal-scope skill catalog
1294
- // so agents can see what already exists before emitting a near-duplicate
1295
- // skill block. Caps at ~2KB; truncates alphabetically with a tail count.
1296
- 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`;
1297
1117
 
1298
1118
  // Minions awareness: what's in flight, who's doing what
1299
1119
  const dispatch = getDispatch();
@@ -1319,9 +1139,9 @@ function buildAgentContext(agentId, config, project) {
1319
1139
  const prs = getPrs(p).filter(pr => pr.status === PR_STATUS.ACTIVE);
1320
1140
  for (const pr of prs) allPrs.push({ ...pr, _project: p.name });
1321
1141
  }
1322
- // Also check central pull-requests.json
1142
+ // Also check the central SQL pull-request scope.
1323
1143
  try {
1324
- const centralPrs = safeJsonArr(path.join(MINIONS_DIR, 'pull-requests.json'));
1144
+ const centralPrs = require('./pull-requests-store').readPullRequestsForScope('central');
1325
1145
  for (const pr of centralPrs.filter(pr => pr.status === PR_STATUS.ACTIVE)) {
1326
1146
  if (!allPrs.some(p => p.id === pr.id)) allPrs.push({ ...pr, _project: 'central' });
1327
1147
  }
@@ -1432,14 +1252,6 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
1432
1252
  const vars = {
1433
1253
  ...buildBaseVars(agentId, config, project),
1434
1254
  branch_name: extraVars?.pr_branch || '',
1435
- // W-mq16xtdx001a347e + W-mq1cczi90006b21f — honor both meta flag names
1436
- // (skipProjectReviewSkills is the PR-82 alias). Default OFF: review/fix
1437
- // dispatches surface the project-local skills block.
1438
- skip_project_review_skills: !!(meta && meta.skipProjectReviewSkills),
1439
- // W-mq1cczi90006b21f — generalized project-skills suppression flag.
1440
- // Honors either the new `meta.skipProjectSkills` or the PR-82
1441
- // `meta.skipProjectReviewSkills` alias so older work items keep working.
1442
- skip_project_skills: !!(meta && (meta.skipProjectSkills || meta.skipProjectReviewSkills)),
1443
1255
  // W-mq5l3f2u000i9045 — opt-out for the inlined PR description audit
1444
1256
  // section in fix.md. Truthy renders the "audit suppressed" notice; falsy
1445
1257
  // (the default) leaves the standard audit instructions in place.