@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.
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
package/engine/cli.js CHANGED
@@ -247,10 +247,10 @@ const CLI_COMMAND_DOCS = Object.freeze({
247
247
  kill: { args: '', summary: 'Kill all active agents, reset to pending' },
248
248
  complete: { args: '<dispatch-id>', summary: 'Mark a dispatch as done' },
249
249
  cleanup: { args: '', summary: 'Clean temp files, worktrees, zombies' },
250
- 'mcp-sync': { args: '', summary: 'Print harness propagation diagnostic (same source as `minions doctor --harness`; read-only, no writes)' },
251
- doctor: { args: '[--harness]', summary: 'Check prerequisites and runtime health (--harness: print harness propagation diagnostic)' },
250
+ 'mcp-sync': { args: '', summary: 'Print runtime harness inventory (same source as `minions doctor --harness`; read-only, no writes)' },
251
+ doctor: { args: '[--harness]', summary: 'Check prerequisites and runtime health (--harness: print runtime harness inventory)' },
252
252
  config: { args: 'set-cli <R> [--model M]', summary: 'Persist defaultCli/defaultModel without starting' },
253
- pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--resolved] [--harness-file <f>|--harness-json <j>] [--skill-skipped-file <f>|--skill-skipped-json <j>] [--body-file <f>|--body <text>]', summary: 'Post a marker-prepended PR comment via gh (ADO: --resolved posts pre-closed)' },
253
+ pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--resolved] [--body-file <f>|--body <text>]', summary: 'Post a marker-prepended PR comment via gh (ADO: --resolved posts pre-closed)' },
254
254
  bridge: { args: 'status|health|enable|disable', summary: 'Constellation bridge: toggle and inspect the read-only cross-repo feed' },
255
255
  });
256
256
 
@@ -413,111 +413,6 @@ function _applyRuntimeFlags({ cli, model, modelExplicit }) {
413
413
  return { warnings, applied: true };
414
414
  }
415
415
 
416
- // resolveHarnessUsedForComment(flags) — P-7a3c9e21. Turn the `minions pr comment`
417
- // harness flags into a grounded `harnessUsed` record suitable for the posters'
418
- // `harnessUsed` param (engine/gh-comment.js / engine/ado-comment.js), which fold
419
- // it via buildMinionsCommentBody -> buildHarnessUsedSection.
420
- //
421
- // Source precedence: `--harness-file <path>` (JSON on disk) wins over the inline
422
- // `--harness-json <json>` convenience; neither supplied -> returns undefined so
423
- // the comment body is byte-identical to today (no section). Read/parse failures
424
- // are HARD errors (process.exit(2) with the offending message) — never a silent
425
- // drop. Grounding uses the spawn-time `_harnessPropagated` manifest located by
426
- // dispatch id (explicit `--dispatch-id` -> else basename(MINIONS_COMPLETION_REPORT,
427
- // '.json')); an unresolvable manifest passes null, marking every entry
428
- // grounded:false — the honest "no record" state, not a fatal condition.
429
- function resolveHarnessUsedForComment(flags) {
430
- const fileArg = flags['harness-file'];
431
- const jsonArg = flags['harness-json'];
432
-
433
- let raw;
434
- if (fileArg !== undefined) {
435
- try {
436
- raw = fs.readFileSync(fileArg, 'utf8');
437
- } catch (e) {
438
- console.error(`error: could not read --harness-file ${fileArg}: ${e.message}`);
439
- process.exit(2);
440
- }
441
- } else if (jsonArg !== undefined) {
442
- raw = String(jsonArg);
443
- } else {
444
- return undefined;
445
- }
446
-
447
- let parsed;
448
- try {
449
- parsed = JSON.parse(raw);
450
- } catch (e) {
451
- const src = fileArg !== undefined ? `--harness-file ${fileArg}` : '--harness-json';
452
- console.error(`error: invalid JSON in ${src}: ${e.message}`);
453
- process.exit(2);
454
- }
455
-
456
- // Resolve the grounding manifest best-effort: explicit id -> completion-report
457
- // basename. A missing/unresolvable manifest is non-fatal (groundHarnessUsed
458
- // with null propagated marks everything grounded:false).
459
- const dispatchId = flags['dispatch-id']
460
- || path.basename(process.env.MINIONS_COMPLETION_REPORT || '', '.json')
461
- || undefined;
462
- let propagated = null;
463
- if (dispatchId) {
464
- try {
465
- const dispatch = getDispatch();
466
- for (const queue of ['active', 'completed', 'pending']) {
467
- const list = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
468
- if (!list) continue;
469
- const found = list.find(d => d && d.id === dispatchId);
470
- if (found && found._harnessPropagated) { propagated = found._harnessPropagated; break; }
471
- }
472
- } catch { propagated = null; }
473
- }
474
-
475
- return shared.groundHarnessUsed(parsed, propagated);
476
- }
477
-
478
- // resolveSkillSkippedForComment(flags) — W-mr287sc0000uc214. Turn the
479
- // `minions pr comment` skip flags into a canonical `{name, reason}` record for
480
- // the posters' `skillSkipped` param (engine/gh-comment.js / engine/ado-comment.js),
481
- // which fold it via buildMinionsCommentBody -> buildSkippedSkillSection into a
482
- // VISIBLE callout in the same comment as the verdict.
483
- //
484
- // Source precedence mirrors the harness resolver: `--skill-skipped-file <path>`
485
- // (JSON on disk) wins over the inline `--skill-skipped-json <json>`; neither
486
- // supplied -> returns undefined so the comment body is byte-identical to today
487
- // (no callout). Read/parse failures are HARD errors (process.exit(2)). Accepts
488
- // the raw `{name, reason}` shape or a `meta.skill` / `meta.review` wrapper; a
489
- // record with no non-empty `name` yields undefined (nothing to surface).
490
- function resolveSkillSkippedForComment(flags) {
491
- const fileArg = flags['skill-skipped-file'];
492
- const jsonArg = flags['skill-skipped-json'];
493
-
494
- let raw;
495
- if (fileArg !== undefined) {
496
- try {
497
- raw = fs.readFileSync(fileArg, 'utf8');
498
- } catch (e) {
499
- console.error(`error: could not read --skill-skipped-file ${fileArg}: ${e.message}`);
500
- process.exit(2);
501
- }
502
- } else if (jsonArg !== undefined) {
503
- raw = String(jsonArg);
504
- } else {
505
- return undefined;
506
- }
507
-
508
- let parsed;
509
- try {
510
- parsed = JSON.parse(raw);
511
- } catch (e) {
512
- const src = fileArg !== undefined ? `--skill-skipped-file ${fileArg}` : '--skill-skipped-json';
513
- console.error(`error: invalid JSON in ${src}: ${e.message}`);
514
- process.exit(2);
515
- }
516
-
517
- const { _extractSkillSkipped } = require('./comment-format');
518
- return _extractSkillSkipped(parsed) || undefined;
519
- }
520
-
521
416
  const commands = {
522
417
  start(...startArgs) {
523
418
  // Apply --cli / --model fleet flags before any engine wiring touches
@@ -539,18 +434,6 @@ const commands = {
539
434
  throw err;
540
435
  }
541
436
 
542
- // Startup state-file size guard (#1167): dispatch.json / cooldowns.json
543
- // bloated past 100 MB silently OOMed V8 on JSON.parse at startup. Fail fast
544
- // with an actionable message instead.
545
- try {
546
- for (const fp of [DISPATCH_PATH, path.join(ENGINE_DIR, 'cooldowns.json')]) {
547
- shared.assertStateFileSize(fp);
548
- }
549
- } catch (stateErr) {
550
- console.error('\n[engine] STARTUP ABORTED — ' + stateErr.message + '\n');
551
- process.exit(78); // 78 = configuration error
552
- }
553
-
554
437
  // Run preflight checks (warn but don't block — engine may still be useful)
555
438
  try {
556
439
  const { runPreflight, printPreflight } = require('./preflight');
@@ -667,10 +550,7 @@ const commands = {
667
550
  }
668
551
  } catch (err) { e.log('warn', `note-link backfill failed: ${err.message}`); }
669
552
 
670
- // Backfill work_items.prd_item_id (the WI↔PRD-item FK) for rows that predate
671
- // the dual-write stamp (#546) or that a JSON re-hydrate transiently zeroed.
672
- // Set-based, only touches NULL FKs, no-op once everything is linked — safe on
673
- // every boot. Lets the Phase-10 render join move off the feature-id string.
553
+ // Backfill work_items.prd_item_id for rows created before FK stamping.
674
554
  try {
675
555
  const fk = require('./prd-store').backfillWorkItemPrdItemIds();
676
556
  const filled = (fk.byWorkItemId || 0) + (fk.bySourcePlan || 0);
@@ -1211,22 +1091,7 @@ const commands = {
1211
1091
  const _watchedFiles = new Set();
1212
1092
  let _globalDebounce = null;
1213
1093
  function watchForWorkChanges() {
1214
- const filesToWatch = [
1215
- path.join(MINIONS_DIR, 'work-items.json'),
1216
- // dispatch.json excluded — it changes every tick, causing a feedback loop
1217
- ];
1218
- // Watch project-specific work-items.json
1219
- const { getProjects } = require('./shared');
1220
- for (const p of getProjects(config)) {
1221
- filesToWatch.push(shared.projectWorkItemsPath(p));
1222
- }
1223
- // Watch PRD files
1224
- const prdDir = path.join(MINIONS_DIR, 'prd');
1225
- try {
1226
- for (const f of fs.readdirSync(prdDir).filter(f => f.endsWith('.json'))) {
1227
- filesToWatch.push(path.join(prdDir, f));
1228
- }
1229
- } catch { /* optional */ }
1094
+ const filesToWatch = [];
1230
1095
 
1231
1096
  for (const filePath of filesToWatch) {
1232
1097
  if (_watchedFiles.has(filePath)) continue;
@@ -1465,8 +1330,8 @@ const commands = {
1465
1330
  }
1466
1331
  const flagFields = [
1467
1332
  'claudeBareMode', 'claudeFallbackModel',
1468
- 'copilotDisableBuiltinMcps', 'copilotSuppressAgentsMd', 'copilotStreamMode', 'copilotReasoningSummaries',
1469
- 'harnessPropagateProjectLocal', 'claudePreApproveWorkspaceMcps', 'hermeticHarness',
1333
+ 'copilotDisableBuiltinMcps', 'copilotStreamMode', 'copilotReasoningSummaries',
1334
+ 'claudePreApproveWorkspaceMcps',
1470
1335
  'maxBudgetUsd', 'disableModelDiscovery',
1471
1336
  'ccUseWorkerPool',
1472
1337
  ];
@@ -1488,14 +1353,13 @@ const commands = {
1488
1353
  }
1489
1354
 
1490
1355
  console.log('');
1491
- const metrics = shared.safeJsonObj(path.join(ENGINE_DIR, 'metrics.json'));
1356
+ const metrics = queries.getMetrics();
1492
1357
  const lifetimeCompleted = Object.entries(metrics).filter(([k]) => !k.startsWith('_')).reduce((sum, [, m]) => sum + (m.tasksCompleted || 0) + (m.tasksErrored || 0), 0);
1493
1358
  console.log(`Dispatch: ${dispatch.pending.length} pending | ${(dispatch.active || []).length} active | ${lifetimeCompleted} completed`);
1494
1359
  const pidSummary = summarizeActiveDispatchPids(dispatch.active || []);
1495
1360
  console.log(`Active dispatch PID files: ${pidSummary.live} live | ${pidSummary.dead} dead | ${pidSummary.missing} missing`);
1496
1361
 
1497
- const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
1498
- const metricsData = safeJson(metricsPath);
1362
+ const metricsData = queries.getMetrics();
1499
1363
  if (metricsData && Object.keys(metricsData).length > 0) {
1500
1364
  console.log('\nMetrics:');
1501
1365
  console.log(` ${'Agent'.padEnd(12)} ${'Done'.padEnd(6)} ${'Err'.padEnd(6)} ${'PRs'.padEnd(6)} ${'Appr'.padEnd(6)} ${'Rej'.padEnd(6)} ${'Reviews'.padEnd(8)} ${'Cost'.padEnd(8)}`);
@@ -1989,7 +1853,7 @@ const commands = {
1989
1853
  doctor(...doctorArgs) {
1990
1854
  const { doctor, runHarnessDoctor } = require('./preflight');
1991
1855
  if (doctorArgs.includes('--harness')) {
1992
- // P-a3f9b2c1 harness propagation diagnostic. See
1856
+ // Read-only runtime harness inventory. See
1993
1857
  // docs/harness-propagation.md and the parallel branch in bin/minions.js.
1994
1858
  const ok = runHarnessDoctor(MINIONS_DIR);
1995
1859
  if (!ok) process.exit(1);
@@ -2086,19 +1950,9 @@ const commands = {
2086
1950
  console.log('Usage:');
2087
1951
  console.log(' GitHub: minions pr comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
2088
1952
  console.log(' ADO: minions pr comment <prNumber> --host ado --ado-org <org> --ado-project <proj> --repo-id <id> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
2089
- console.log('');
2090
- console.log('Optional harness self-report (folded into the collapsible "Harnesses used" section):');
2091
- console.log(' --harness-file <path> JSON file with the { skills, mcpServers, commands, docs } record (wins over --harness-json)');
2092
- console.log(' --harness-json <json> same record inline as a JSON string');
2093
- console.log(' --dispatch-id <id> grounding manifest override (else basename of $MINIONS_COMPLETION_REPORT)');
2094
- console.log('');
2095
- console.log('Skipped in-scope project skill (folded in as a VISIBLE "⚠️ Skipped project skill" callout):');
2096
- console.log(' --skill-skipped-file <path> JSON file with the { name, reason } skip record (wins over --skill-skipped-json)');
2097
- console.log(' --skill-skipped-json <json> same record inline as a JSON string (also accepts a meta.skill/meta.review wrapper)');
2098
1953
  console.log(' --resolved (ADO only) post the thread pre-resolved/closed — for non-actionable FYI/praise notes');
2099
1954
  console.log('');
2100
- console.log('Posts a PR comment with the hidden minions marker, the collapsible');
2101
- console.log('"Harnesses used" section, and the brand link folded in by the shared');
1955
+ console.log('Posts a PR comment with the hidden minions marker and brand link folded in by the shared');
2102
1956
  console.log('builder — so engine classifiers identify agent-authored comments by');
2103
1957
  console.log('structure, and the body is byte-identical on GitHub (`gh pr comment`)');
2104
1958
  console.log('and Azure DevOps (REST `…/threads`).');
@@ -2162,18 +2016,6 @@ const commands = {
2162
2016
  process.exit(2);
2163
2017
  }
2164
2018
 
2165
- // P-7a3c9e21 — thread the consulted-sources footprint into both posters so
2166
- // the auto-folded "Harnesses used" <details> section the help already
2167
- // promises is actually rendered. Resolved once (host-neutral); absent flags
2168
- // -> undefined -> body byte-identical to today.
2169
- const harnessUsed = resolveHarnessUsedForComment(flags);
2170
-
2171
- // W-mr287sc0000uc214 — thread the skipped-in-scope-project-skill self-report
2172
- // into both posters so it's surfaced as a VISIBLE callout in the same comment
2173
- // as the verdict (not buried in the completion-report JSON). Absent flags
2174
- // -> undefined -> body byte-identical to today.
2175
- const skillSkipped = resolveSkillSkippedForComment(flags);
2176
-
2177
2019
  // ── Azure DevOps: <prNumber> --host ado --ado-org --ado-project --repo-id ──
2178
2020
  if (isAdo) {
2179
2021
  const [prNumberRaw] = positional;
@@ -2193,7 +2035,7 @@ const commands = {
2193
2035
  const orgBase = flags['org-base'] || `https://dev.azure.com/${adoOrg}`;
2194
2036
  const adoComment = require('./ado-comment');
2195
2037
  adoComment.postAdoPrComment({
2196
- orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId, harnessUsed, skillSkipped,
2038
+ orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId,
2197
2039
  resolved: flags.resolved === true,
2198
2040
  }).then((result) => {
2199
2041
  if (result && result.threadId) {
@@ -2232,7 +2074,7 @@ const commands = {
2232
2074
 
2233
2075
  try {
2234
2076
  const result = ghComment.postPrComment({
2235
- repo, prNumber, body, agentId, kind, workItemId, harnessUsed, skillSkipped,
2077
+ repo, prNumber, body, agentId, kind, workItemId,
2236
2078
  });
2237
2079
  if (result.output) console.log(result.output);
2238
2080
  } catch (e) {
@@ -2371,10 +2213,6 @@ module.exports = {
2371
2213
  _readDispatchPid: readDispatchPid,
2372
2214
  _normalizeSessionBranch: normalizeSessionBranch,
2373
2215
  _dispatchSessionBranch: dispatchSessionBranch,
2374
- // P-7a3c9e21 — harness-flag resolver exported for CLI-threading tests.
2375
- _resolveHarnessUsedForComment: resolveHarnessUsedForComment,
2376
- // W-mr287sc0000uc214 — skipped-project-skill flag resolver exported for tests.
2377
- _resolveSkillSkippedForComment: resolveSkillSkippedForComment,
2378
2216
  // W-mpcyvff6000pf828 (#2653) — heartbeat writer + factory exported for tests
2379
2217
  _writeHeartbeatNow: writeHeartbeatNow,
2380
2218
  _createHeartbeatInterval: createHeartbeatInterval,
@@ -1,48 +1,10 @@
1
1
  /**
2
2
  * engine/comment-format.js — platform-neutral PR-comment body fragments.
3
3
  *
4
- * Harness Transparency, Stage 3 readout #1 (P-e4b7a6d5). The other readouts —
5
- * the work-item modal (P-d5a6f7c4) and the notes/inbox digest — live in the
6
- * dashboard renderer and the consolidation path respectively. This module owns
7
- * the *PR-comment* surface.
8
- *
9
- * `buildHarnessUsedSection(harnessUsed)` renders the grounded harness-usage
10
- * record (the canonical `{ skills, mcpServers, commands, docs }` shape produced
11
- * by `shared.groundHarnessUsed` / `shared.normalizeHarnessUsed`; each entry may
12
- * carry `grounded: true | false`) as a collapsible Markdown `<details>` block.
13
- *
14
- * It is deliberately platform-NEUTRAL: the output is plain Markdown with no
15
- * GitHub- or ADO-specific syntax, so the GitHub comment path
16
- * (`engine/gh-comment.js buildMinionsCommentBody`) and the ADO comment path
17
- * (agent-authored via `az repos pr comment` per `playbooks/shared-rules.md`)
18
- * fold in a **byte-identical** section. Both consume this one helper — there is
19
- * no second renderer to drift.
20
- *
21
- * Returns '' when the record is absent, malformed, or has no renderable entries
22
- * so callers can append it unconditionally without emitting an empty section.
4
+ * Owns the platform-neutral Minions marker and brand-link formatting shared by
5
+ * GitHub and Azure DevOps comment transports.
23
6
  */
24
7
 
25
- // Kind metadata mirrors the dashboard work-item modal (P-d5a6f7c4,
26
- // dashboard/js/render-work-items.js): same four kinds, same icons, same
27
- // primary/secondary field mapping, so the PR-comment and modal readouts stay
28
- // visually consistent.
29
- const HARNESS_KINDS = [
30
- { key: 'skills', icon: '\u{1F9E9}', primary: 'name', secondary: 'source' }, // 🧩
31
- { key: 'mcpServers', icon: '\u{1F50C}', primary: 'name', secondary: 'scope' }, // 🔌
32
- { key: 'commands', icon: '\u{2318}', primary: 'name', secondary: 'scope' }, // ⌘
33
- { key: 'docs', icon: '\u{1F4C4}', primary: 'path', secondary: 'why' }, // 📄
34
- ];
35
-
36
- const HARNESS_SUMMARY_ICON = '\u{1F9F0}'; // 🧰
37
- const HARNESS_WARN_ICON = '⚠️'; // ⚠️
38
-
39
- // W-mr287sc0000uc214 — the visible "skipped project skill" callout. The label is
40
- // a stable literal so buildMinionsCommentBody can dedupe (never stack two
41
- // callouts) and tests can assert on it. Clamps mirror the harness field caps.
42
- const SKILL_SKIPPED_LABEL = '**Skipped project skill:**';
43
- const SKILL_SKIPPED_MAX_NAME_LEN = 200;
44
- const SKILL_SKIPPED_MAX_REASON_LEN = 1000;
45
-
46
8
  // Canonical Minions site URL. shared-rules.md ("Brand link (MANDATORY)") requires
47
9
  // the human-readable "Minions" mention in a PR/ADO comment to render as a markdown
48
10
  // hyperlink to this destination. Keep this in sync with the literal in
@@ -76,118 +38,10 @@ function linkifyBrandTrailer(text) {
76
38
  return text.replace(BRAND_TRAILER_RE, `by [Minions](${MINIONS_BRAND_URL})`);
77
39
  }
78
40
 
79
- // Collapse internal whitespace/newlines to single spaces and neutralize
80
- // backticks so each entry renders on one line and can't break the inline-code
81
- // span. Values arrive already trimmed + length-clamped by normalizeHarnessUsed,
82
- // but may still contain internal newlines.
83
- function _clean(value) {
84
- return String(value).replace(/`/g, "'").replace(/\s+/g, ' ').trim();
85
- }
86
-
87
- /**
88
- * Render the grounded harnessUsed record as a collapsible Markdown section.
89
- * @param {object|null} harnessUsed canonical { skills, mcpServers, commands, docs }
90
- * @returns {string} '' when empty, else a `<details>` block (no trailing newline)
91
- */
92
- function buildHarnessUsedSection(harnessUsed) {
93
- if (!harnessUsed || typeof harnessUsed !== 'object' || Array.isArray(harnessUsed)) return '';
94
-
95
- const lines = [];
96
- let anyUngrounded = false;
97
-
98
- for (const kind of HARNESS_KINDS) {
99
- const entries = Array.isArray(harnessUsed[kind.key]) ? harnessUsed[kind.key] : [];
100
- for (const entry of entries) {
101
- if (!entry || typeof entry !== 'object') continue;
102
- const primaryRaw = entry[kind.primary];
103
- if (typeof primaryRaw !== 'string' || !primaryRaw.trim()) continue;
104
- const primary = _clean(primaryRaw);
105
-
106
- const grounded = entry.grounded === true;
107
- if (!grounded) anyUngrounded = true;
108
-
109
- const secondaryRaw = entry[kind.secondary];
110
- const secondary = (typeof secondaryRaw === 'string' && secondaryRaw.trim())
111
- ? _clean(secondaryRaw) : '';
112
-
113
- const tail = secondary ? ` — ${secondary}` : ''; // em dash
114
- const warn = grounded ? '' : ` ${HARNESS_WARN_ICON}`;
115
- lines.push(`- ${kind.icon} \`${primary}\`${tail}${warn}`);
116
- }
117
- }
118
-
119
- if (lines.length === 0) return '';
120
-
121
- const legend = anyUngrounded
122
- ? `\n\n> ${HARNESS_WARN_ICON} agent-reported, not verified by the engine`
123
- : '';
124
-
125
- return `<details>\n<summary>${HARNESS_SUMMARY_ICON} Harnesses used (${lines.length})</summary>\n\n`
126
- + `${lines.join('\n')}${legend}\n\n</details>`;
127
- }
128
-
129
- /**
130
- * Extract a canonical `{ name, reason }` skip record from a self-reported skip
131
- * value. Accepts the raw `{ name, reason }` shape directly, or a `meta` wrapper
132
- * that carries it — `meta.skill` (`{ skipped: { name, reason } }`) or
133
- * `meta.review` (`{ skillSkipped: { name, reason } }`) — so callers can pass the
134
- * completion-report block verbatim. Returns null when no non-empty `name` is
135
- * present so callers treat "no skip" as a single falsy case.
136
- * @param {*} input
137
- * @returns {{name:string, reason:string}|null}
138
- */
139
- function _extractSkillSkipped(input) {
140
- if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
141
- let node = input;
142
- if (input.skipped && typeof input.skipped === 'object' && !Array.isArray(input.skipped)) {
143
- node = input.skipped; // meta.skill shape
144
- } else if (input.skillSkipped && typeof input.skillSkipped === 'object' && !Array.isArray(input.skillSkipped)) {
145
- node = input.skillSkipped; // meta.review shape
146
- }
147
- const name = typeof node.name === 'string' ? node.name.trim() : '';
148
- if (!name) return null;
149
- const reason = typeof node.reason === 'string' ? node.reason.trim() : '';
150
- return { name, reason };
151
- }
152
-
153
- /**
154
- * Render the "skipped project skill" self-report as a VISIBLE Markdown
155
- * blockquote callout (W-mr287sc0000uc214), e.g.:
156
- * > ⚠️ **Skipped project skill:** `FMF-review-context` — small 2-file diff…
157
- *
158
- * A review/fix agent may self-report that it intentionally skipped an available,
159
- * in-scope project (review) skill via `meta.review.skillSkipped` /
160
- * `meta.skill.skipped` (shape `{name, reason}`). That skip used to live ONLY in
161
- * the completion-report JSON — invisible to a human reading the PR before merge.
162
- * This is a sibling of buildHarnessUsedSection (same neutral chokepoint, same
163
- * `_clean` hygiene) but a VISIBLE blockquote rather than a collapsed <details>:
164
- * the whole point is that a human reviewing the PR sees the skip rationale in the
165
- * same comment as the APPROVE / REQUEST_CHANGES verdict, before merge.
166
- *
167
- * Deliberately platform-NEUTRAL Markdown so the GitHub and ADO posters fold in a
168
- * byte-identical callout — there is no second renderer to drift. Accepts either
169
- * the raw `{name, reason}` record or a `meta.skill` / `meta.review` wrapper.
170
- *
171
- * Returns '' when the record is absent/malformed (no skip name), so callers can
172
- * append it unconditionally without emitting a stray callout — reports without
173
- * meta.review.skillSkipped / meta.skill.skipped render identically to today.
174
- *
175
- * @param {object|null} skillSkipped `{name, reason}` or a meta.skill/meta.review wrapper
176
- * @returns {string} '' when empty, else a one-line `> ⚠️ …` blockquote
177
- */
178
- function buildSkippedSkillSection(skillSkipped) {
179
- const parsed = _extractSkillSkipped(skillSkipped);
180
- if (!parsed) return '';
181
- const name = _clean(parsed.name).slice(0, SKILL_SKIPPED_MAX_NAME_LEN);
182
- const reasonClean = parsed.reason ? _clean(parsed.reason).slice(0, SKILL_SKIPPED_MAX_REASON_LEN) : '';
183
- const tail = reasonClean ? ` — ${reasonClean}` : ''; // em dash
184
- return `> ${HARNESS_WARN_ICON} ${SKILL_SKIPPED_LABEL} \`${name}\`${tail}`;
185
- }
186
-
187
41
  // ── Minions marker + neutral comment-body builder ───────────────────────────
188
42
  //
189
43
  // The hidden HTML-comment marker and the body composition (marker + harness
190
- // section + brand link) are platform-NEUTRAL: GitHub (engine/gh-comment.js) and
44
+ // brand link) are platform-NEUTRAL: GitHub (engine/gh-comment.js) and
191
45
  // Azure DevOps (engine/ado-comment.js) both post the byte-identical body built
192
46
  // here, so there is one builder and no drift. The host modules own only the
193
47
  // transport (gh CLI vs ADO REST) and host-specific validation (repo slug vs
@@ -246,42 +100,21 @@ function _buildMarker({ agentId, kind, workItemId }) {
246
100
  }
247
101
 
248
102
  /**
249
- * Compose a minions PR-comment body: prepend the hidden marker, fold in the
250
- * collapsible "Harnesses used" section, and hyperlink the first bare brand
251
- * trailer. Platform-neutral — GitHub and ADO posters both call this.
103
+ * Compose a minions PR-comment body: prepend the hidden marker and hyperlink the
104
+ * first bare brand trailer. Platform-neutral — GitHub and ADO posters both call this.
252
105
  *
253
106
  * Idempotency: if `body` already starts with a minions marker it is returned
254
- * with the harness/brand transforms applied but no second marker prepended.
107
+ * with the brand transform applied but no second marker prepended.
255
108
  *
256
- * @param {{agentId:string, kind:string, workItemId?:string, body?:string, harnessUsed?:object, skillSkipped?:object}} args
109
+ * @param {{agentId:string, kind:string, workItemId?:string, body?:string}} args
257
110
  * @returns {string} the final comment body
258
111
  */
259
- function buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped }) {
112
+ function buildMinionsCommentBody({ agentId, kind, workItemId, body }) {
260
113
  // Validate the inputs even when the body is pre-marked, so callers can't
261
114
  // silently bypass validation by pre-marking their body.
262
115
  _validateMarkerInputs({ agentId, kind, workItemId });
263
116
  let safeBody = body == null ? '' : String(body);
264
117
 
265
- // Fold in the VISIBLE "skipped project skill" callout (W-mr287sc0000uc214)
266
- // BEFORE the collapsed harness section so a human sees the skip rationale in
267
- // the same comment as the verdict — not buried in a <details> or reachable
268
- // only via a separate API call. Dedup on the stable label so a re-marked body
269
- // already carrying the callout doesn't stack a second one. Absent/malformed
270
- // skip → '' → no-op (byte-identical to today).
271
- const skipSection = buildSkippedSkillSection(skillSkipped);
272
- if (skipSection && !safeBody.includes(SKILL_SKIPPED_LABEL)) {
273
- safeBody = safeBody ? `${safeBody}\n\n${skipSection}` : skipSection;
274
- }
275
-
276
- // Fold the grounded harness-usage digest in as a collapsible <details>
277
- // section. Append at the END so it survives the marker-idempotency path;
278
- // dedup on the summary line so a re-marked body that already carries the
279
- // section doesn't stack a second one.
280
- const harnessSection = buildHarnessUsedSection(harnessUsed);
281
- if (harnessSection && !safeBody.includes('<summary>\u{1F9F0} Harnesses used')) {
282
- safeBody = safeBody ? `${safeBody}\n\n${harnessSection}` : harnessSection;
283
- }
284
-
285
118
  // Brand-link safety net: deterministically hyperlink the first bare
286
119
  // "… by Minions" signature trailer (no-op when the agent already linked it).
287
120
  safeBody = linkifyBrandTrailer(safeBody);
@@ -307,8 +140,6 @@ function parseMinionsMarker(body) {
307
140
  }
308
141
 
309
142
  module.exports = {
310
- buildHarnessUsedSection,
311
- buildSkippedSkillSection,
312
143
  linkifyBrandTrailer,
313
144
  MINIONS_BRAND_URL,
314
145
  // Neutral marker + body builder (consumed by gh-comment.js and ado-comment.js).
@@ -322,9 +153,4 @@ module.exports = {
322
153
  // Internal helpers exported for tests / host posters.
323
154
  _buildMarker,
324
155
  _validateMarkerInputs,
325
- // Exported for tests / advanced callers that want to mirror the kind mapping.
326
- HARNESS_KINDS,
327
- // W-mr287sc0000uc214 — skipped-project-skill callout helpers.
328
- _extractSkillSkipped,
329
- SKILL_SKIPPED_LABEL,
330
156
  };
@@ -21,6 +21,45 @@ let _db = null;
21
21
  let _dbPath = null;
22
22
  let _dbInitError = null;
23
23
  let _exitCheckpointRegistered = false;
24
+ let _cleanedDbPath = null;
25
+
26
+ function _cleanupRetiredJsonFiles(db, dbPath) {
27
+ if (_cleanedDbPath === dbPath) return;
28
+ const marker = db.prepare("SELECT value FROM state_markers WHERE key='sql_only_cutover'").get();
29
+ if (!marker) return;
30
+ const minionsDir = path.dirname(path.dirname(dbPath));
31
+ const engineDir = path.join(minionsDir, 'engine');
32
+ const engineFiles = [
33
+ 'dispatch.json', 'log.json', 'metrics.json', 'watches.json',
34
+ 'schedule-runs.json', 'pipeline-runs.json', 'managed-processes.json',
35
+ 'worktree-pool.json', 'qa-runs.json', 'qa-sessions.json', 'pr-links.json',
36
+ 'cooldowns.json', 'pending-rebases.json', 'cc-sessions.json', 'doc-sessions.json',
37
+ ];
38
+ const targets = engineFiles.map(name => path.join(engineDir, name));
39
+ targets.push(path.join(minionsDir, 'work-items.json'), path.join(minionsDir, 'pull-requests.json'));
40
+ try {
41
+ for (const project of fs.readdirSync(path.join(minionsDir, 'projects'), { withFileTypes: true })) {
42
+ if (!project.isDirectory() || project.name === '.archived') continue;
43
+ targets.push(
44
+ path.join(minionsDir, 'projects', project.name, 'work-items.json'),
45
+ path.join(minionsDir, 'projects', project.name, 'pull-requests.json'),
46
+ );
47
+ }
48
+ } catch { /* projects directory is optional */ }
49
+ for (const dir of [path.join(minionsDir, 'prd'), path.join(minionsDir, 'prd', 'archive')]) {
50
+ try {
51
+ for (const name of fs.readdirSync(dir)) {
52
+ if (name.endsWith('.json')) targets.push(path.join(dir, name));
53
+ }
54
+ } catch { /* PRD directory is optional */ }
55
+ }
56
+ for (const target of targets) {
57
+ for (const suffix of ['', '.backup', '.lock']) {
58
+ try { fs.unlinkSync(target + suffix); } catch (e) { if (e.code !== 'ENOENT') throw e; }
59
+ }
60
+ }
61
+ _cleanedDbPath = dbPath;
62
+ }
24
63
 
25
64
  // DATA-LOSS GUARD: register a one-time process-exit hook that checkpoints +
26
65
  // closes the DB. Registered by getDb() on first successful open so EVERY DB
@@ -68,14 +107,6 @@ function _installExperimentalWarningFilter() {
68
107
  }
69
108
 
70
109
  function getDb() {
71
- // Hard opt-out (issue #3035, option #3): MINIONS_FORCE_JSON=1 makes
72
- // every getDb() call throw, forcing the entire stack onto the JSON
73
- // fallback path. Useful for users who can't enable
74
- // --experimental-sqlite on Node 22.x and for regression tests that
75
- // need to exercise the JSON-fallback branches deterministically.
76
- if (process.env.MINIONS_FORCE_JSON === '1') {
77
- throw new Error('engine/db: SQL disabled via MINIONS_FORCE_JSON=1');
78
- }
79
110
  // Re-resolve the DB path on every call so tests that swap MINIONS_TEST_DIR
80
111
  // (or production-side configuration that changes MINIONS_HOME between
81
112
  // operations) get a fresh connection to the new location instead of
@@ -143,12 +174,13 @@ function getDb() {
143
174
  if (lastErr) throw lastErr;
144
175
  const { runMigrations } = require('./migrate');
145
176
  runMigrations(_db);
177
+ _cleanupRetiredJsonFiles(_db, dbPath);
146
178
  _registerExitCheckpoint();
147
179
  return _db;
148
180
  } catch (e) {
149
181
  const nodeMajor = parseInt(String(process.versions.node).split('.')[0], 10);
150
182
  const flagHint = nodeMajor === 22
151
- ? ` On Node 22.x, node:sqlite is gated behind --experimental-sqlite. Restart with NODE_OPTIONS=--experimental-sqlite, upgrade to Node 24+, or set MINIONS_FORCE_JSON=1 to bypass SQL.`
183
+ ? ` On Node 22.x, node:sqlite is gated behind --experimental-sqlite. Restart with NODE_OPTIONS=--experimental-sqlite or upgrade to Node 24+.`
152
184
  : ` Node 22.5+ (with --experimental-sqlite on 22.x) or 24+ required for built-in 'node:sqlite' support.`;
153
185
  _dbInitError = new Error(`engine/db: failed to open SQLite — ${e.message}.${flagHint}`);
154
186
  throw _dbInitError;
@@ -200,4 +232,22 @@ function withTransaction(db, fn) {
200
232
  }
201
233
  }
202
234
 
203
- module.exports = { getDb, closeDb, withTransaction };
235
+ // Cache compiled statements on the db connection itself so hot, fixed-shape
236
+ // read paths (e.g. FTS5 memory search run repeatedly per dispatch) don't pay
237
+ // SQLite's parse/plan cost on every call — `db.prepare()` recompiles from
238
+ // scratch each time it's invoked, node:sqlite does not dedupe by text. Keyed
239
+ // per-connection (not module-level) so closeDb()/getDb() reopening against a
240
+ // new path (test isolation) naturally starts with an empty cache instead of
241
+ // holding stale statements bound to a closed handle.
242
+ function prepareCached(db, sql) {
243
+ if (!db) throw new Error('engine/db.prepareCached: db is required');
244
+ if (!db._minionsStmtCache) db._minionsStmtCache = new Map();
245
+ let stmt = db._minionsStmtCache.get(sql);
246
+ if (!stmt) {
247
+ stmt = db.prepare(sql);
248
+ db._minionsStmtCache.set(sql, stmt);
249
+ }
250
+ return stmt;
251
+ }
252
+
253
+ module.exports = { getDb, closeDb, withTransaction, prepareCached };