@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/dashboard.js CHANGED
@@ -82,24 +82,6 @@ const IS_DEV_MODE = fs.existsSync(path.join(MINIONS_DIR, '.git'));
82
82
  const FAVICON_EMOJI = IS_DEV_MODE ? '🚧' : '👽';
83
83
  const TITLE_SUFFIX = IS_DEV_MODE ? ' [DEV]' : '';
84
84
 
85
- // Startup size guard (#1167): fail fast with a clear error when dispatch.json /
86
- // cooldowns.json have ballooned past ENGINE_DEFAULTS.maxStateFileBytes. Without
87
- // this, V8 silently OOMs on JSON.parse(~1 GB) and the operator has no hint as to
88
- // which file is bloated. The thrown error names the file and directs to
89
- // engine/contexts/ where sidecars live.
90
- (() => {
91
- const stateFiles = [
92
- DISPATCH_PATH,
93
- path.join(ENGINE_DIR, 'cooldowns.json'),
94
- ];
95
- for (const fp of stateFiles) {
96
- try { shared.assertStateFileSize(fp); } catch (e) {
97
- console.error('\n[dashboard] STARTUP ABORTED — ' + e.message + '\n');
98
- process.exit(78); // 78 = configuration error; consistent with spawn-agent.js
99
- }
100
- }
101
- })();
102
-
103
85
  let CONFIG = queries.getConfig();
104
86
  let PROJECTS = _getProjects(CONFIG);
105
87
  // PORT resolution chain (W-mq5nwl9l): explicit CLI arg (argv[2]) > env
@@ -469,6 +451,18 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
469
451
  // `worktreeMode`. Drop any stale legacy key on every settings save so a
470
452
  // migrated project never carries both fields.
471
453
  delete currentProject.worktreeMode;
454
+ // W-mrchrfe100067d3a — mirror the per-project additionalRepoScopes
455
+ // allowlist the same way: present on candidate → copy; absent → clear so
456
+ // the endpoint falls back to single-scope matching. Without this branch
457
+ // the validated update (set/deleted on `candidate` in
458
+ // handleSettingsUpdate) is silently dropped before reaching disk — the
459
+ // endpoint would return 200 but persist nothing (issue: fix before
460
+ // configuring minions-opg to poll the secondary yemi33/minions scope).
461
+ if (Object.prototype.hasOwnProperty.call(candidateProject, 'additionalRepoScopes')) {
462
+ currentProject.additionalRepoScopes = candidateProject.additionalRepoScopes;
463
+ } else {
464
+ delete currentProject.additionalRepoScopes;
465
+ }
472
466
  }
473
467
  }
474
468
  shared.pruneDefaultClaudeConfig(current);
@@ -579,6 +573,9 @@ function normalizePrMetadata(metadata) {
579
573
  description: typeof metadata.description === 'string' ? metadata.description : '',
580
574
  branch: typeof metadata.branch === 'string' ? metadata.branch.trim().replace(/^refs\/heads\//i, '') : '',
581
575
  author: typeof metadata.author === 'string' ? metadata.author.trim() : '',
576
+ // W-mrezh0yb0007f733 — carry the platform's real creation timestamp through
577
+ // so the manual-link path can seed `created` with it instead of now().
578
+ created: typeof metadata.created === 'string' ? metadata.created.trim() : '',
582
579
  };
583
580
  }
584
581
 
@@ -1365,7 +1362,12 @@ function linkPullRequestForTracking({ url, title, project: projectName, contextO
1365
1362
  branch: metadata?.branch || '',
1366
1363
  reviewStatus: 'pending',
1367
1364
  status: 'active',
1368
- created: new Date().toISOString(),
1365
+ // W-mrezh0yb0007f733 — prefer the platform's real creationDate (fetched into
1366
+ // metadata for ADO before linking) over link-time now(). When it isn't
1367
+ // available yet (e.g. GitHub, where enrichment runs async after this insert,
1368
+ // or an ADO metadata-fetch failure), fall back to now() — the next PR poll
1369
+ // corrects it from the platform via shared.shouldAdoptPlatformCreated.
1370
+ created: metadata?.created || new Date().toISOString(),
1369
1371
  url,
1370
1372
  prdItems: linkedWorkItemId ? [linkedWorkItemId] : [],
1371
1373
  contextOnly: resolvedContextOnly,
@@ -1499,161 +1501,10 @@ function _resolveSkillReadPath({ file, dir, source, config, skillFiles } = {}) {
1499
1501
  return null;
1500
1502
  }
1501
1503
 
1502
- // ── Harness propagation diagnostic (P-c601f9a2) ──────────────────────────────
1503
- // Per-runtime view of where the CLI looks for skills/commands/MCPs, what the
1504
- // engine actually attaches via --add-dir, which assets are explicitly
1505
- // suppressed (e.g. AGENTS.md auto-load, Copilot's bundled github-mcp-server),
1506
- // and the canonical footgun: per-project assets that live as uncommitted files
1507
- // in the operator's main checkout but won't propagate to fresh worktrees.
1508
- //
1509
- // Surfaced via GET /api/harness/diagnostics and rendered on the Tools page so
1510
- // operators can spot a missing or unpropagated harness asset without leaving
1511
- // the dashboard. See docs/harness-propagation.md for the full contract.
1512
- const HARNESS_FOOTGUN_DIRS = [
1513
- '.claude/skills',
1514
- '.claude/commands',
1515
- '.copilot/skills',
1516
- '.copilot/commands',
1517
- '.agents/skills',
1518
- '.agents/commands',
1519
- ];
1520
- const HARNESS_FOOTGUN_FILES = [
1521
- '.claude/.mcp.json',
1522
- '.copilot/.mcp.json',
1523
- '.mcp.json',
1524
- ];
1525
-
1526
- const _PROJECT_LOCAL_FOOTGUN_WARNING =
1527
- 'These files exist in your main checkout but are uncommitted or untracked in ' +
1528
- 'git. A fresh `git worktree add` for a mutating dispatch will NOT see them, so ' +
1529
- 'the dispatched agent silently underperforms. Fix: commit them, move them to ' +
1530
- 'user scope (~/.claude/skills/..., ~/.copilot/skills/...), or flip the project ' +
1531
- 'to live-checkout mode (checkoutMode: live).';
1532
-
1533
- function _walkUncommittedHarnessAssets(absStart, rootDir, projectName) {
1534
- const results = [];
1535
- const stack = [absStart];
1536
- let count = 0;
1537
- while (stack.length > 0 && count < 500) {
1538
- const cur = stack.pop();
1539
- let entries;
1540
- try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
1541
- catch { continue; }
1542
- for (const ent of entries) {
1543
- if (ent.name === '.git' || ent.name === 'node_modules') continue;
1544
- const full = path.join(cur, ent.name);
1545
- if (ent.isDirectory()) { stack.push(full); continue; }
1546
- if (!ent.isFile()) continue;
1547
- const rel = path.relative(rootDir, full);
1548
- const normalized = rel.replace(/\\/g, '/');
1549
- let kind = null;
1550
- if (
1551
- normalized.startsWith('.claude/skills/') ||
1552
- normalized.startsWith('.copilot/skills/') ||
1553
- normalized.startsWith('.agents/skills/')
1554
- ) kind = 'skill';
1555
- else if (
1556
- normalized.startsWith('.claude/commands/') ||
1557
- normalized.startsWith('.copilot/commands/') ||
1558
- normalized.startsWith('.agents/commands/')
1559
- ) kind = 'command';
1560
- else if (
1561
- normalized === '.mcp.json' ||
1562
- normalized === '.claude/.mcp.json' ||
1563
- normalized === '.copilot/.mcp.json'
1564
- ) kind = 'mcp';
1565
- if (!kind) continue;
1566
- results.push({ file: rel, kind, scope: `project:${projectName}` });
1567
- count++;
1568
- if (count >= 500) break;
1569
- }
1570
- }
1571
- return results;
1572
- }
1573
-
1574
- function _scanProjectLocalHarnessFootgun(project) {
1575
- const out = {
1576
- project: project && project.name ? project.name : '',
1577
- localPath: project && project.localPath ? project.localPath : '',
1578
- uncommittedAssets: [],
1579
- footgunWarning: _PROJECT_LOCAL_FOOTGUN_WARNING,
1580
- };
1581
- if (!project || !project.localPath) return out;
1582
- // Skip the synchronous `git status` for UNC / WSL-from-Windows paths
1583
- // (\\wsl.localhost\..., \\wsl$\..., or any \\server\share). git/stat over a
1584
- // WSL UNC mount can hang for minutes; even capped at the 10s timeout below it
1585
- // hard-blocks the dashboard event loop on every poll, freezing the whole
1586
- // dashboard. Such projects can't carry uncommitted project-local harness
1587
- // assets the engine would propagate anyway, so returning the empty shell here
1588
- // is correct, not lossy. (Load-reduction audit 2026-06-24.)
1589
- const _lp = String(project.localPath);
1590
- if (_lp.startsWith('\\\\') || _lp.startsWith('//')) return out;
1591
- let raw = '';
1592
- try {
1593
- const { execFileSync } = require('child_process');
1594
- raw = execFileSync('git', ['status', '--porcelain', '-z'], {
1595
- cwd: project.localPath,
1596
- encoding: 'utf8',
1597
- stdio: ['ignore', 'pipe', 'ignore'],
1598
- timeout: 10000,
1599
- // W-mqecdoot — MUST set windowsHide. This runs per-project on every
1600
- // GET /api/harness/diagnostics hit (Tools/Harness page, polled), and a
1601
- // detached dashboard has no console — so without this each call pops a
1602
- // visible git.exe console window per project ("infinite git windows").
1603
- // This was the only git spawn in the codebase missing windowsHide.
1604
- windowsHide: true,
1605
- });
1606
- } catch { return out; }
1607
- if (!raw) return out;
1608
- const records = raw.split('\0').filter(Boolean);
1609
- const seen = new Set();
1610
- for (const rec of records) {
1611
- if (rec.length < 4) continue;
1612
- const filePath = rec.slice(3);
1613
- const normalized = filePath.replace(/\\/g, '/');
1614
- const isDirRecord = normalized.endsWith('/');
1615
- const trimmed = isDirRecord ? normalized.slice(0, -1) : normalized;
1616
- let overlaps = false;
1617
- for (const dir of HARNESS_FOOTGUN_DIRS) {
1618
- if (
1619
- trimmed === dir ||
1620
- trimmed.startsWith(dir + '/') ||
1621
- dir.startsWith(trimmed + '/')
1622
- ) { overlaps = true; break; }
1623
- }
1624
- if (!overlaps) {
1625
- for (const file of HARNESS_FOOTGUN_FILES) {
1626
- if (trimmed === file || file.startsWith(trimmed + '/')) { overlaps = true; break; }
1627
- }
1628
- }
1629
- if (!overlaps) continue;
1630
-
1631
- const absStart = path.join(project.localPath, filePath);
1632
- let stat;
1633
- try { stat = fs.statSync(absStart); } catch { continue; }
1634
- if (stat.isDirectory()) {
1635
- for (const asset of _walkUncommittedHarnessAssets(absStart, project.localPath, out.project)) {
1636
- if (seen.has(asset.file)) continue;
1637
- seen.add(asset.file);
1638
- out.uncommittedAssets.push(asset);
1639
- }
1640
- } else {
1641
- let kind = 'other';
1642
- if (normalized.includes('/skills/')) kind = 'skill';
1643
- else if (normalized.includes('/commands/')) kind = 'command';
1644
- else if (normalized.endsWith('.mcp.json')) kind = 'mcp';
1645
- if (seen.has(filePath)) continue;
1646
- seen.add(filePath);
1647
- out.uncommittedAssets.push({ file: filePath, kind, scope: `project:${out.project}` });
1648
- }
1649
- }
1650
- return out;
1651
- }
1652
-
1653
- // TTL cache for the harness diagnostic. The build runs a synchronous `git
1654
- // status` + directory walks per project; the Tools page polls it while open,
1655
- // so without a cache it re-runs the whole sweep every 4s and blocks the event
1656
- // loop each time. The diagnostic is rare-change, so a short TTL is ample.
1504
+ // ── Runtime harness inventory ─────────────────────────────────────────────────
1505
+ // Read-only view of native skill, command, and MCP locations declared by each
1506
+ // adapter. It does not influence spawning or attest what a runtime loaded.
1507
+ // The inventory is rare-change, so a short TTL is ample.
1657
1508
  // Bypassed whenever explicit opts are supplied (unit tests pass homeDir/
1658
1509
  // projects/engineConfig) so test determinism is unaffected.
1659
1510
  // (Load-reduction audit 2026-06-24.)
@@ -1730,29 +1581,7 @@ function _buildHarnessDiagnosticsUncached(opts = {}) {
1730
1581
  collectMissing(mcpConfigPaths, runtimeName, 'mcp');
1731
1582
  }
1732
1583
 
1733
- let addDirSnapshot = [];
1734
- if (preflight && registry) {
1735
- let fleetRuntime = null;
1736
- try { fleetRuntime = registry.resolveRuntime(fleetDefaultCli); }
1737
- catch { fleetRuntime = null; }
1738
- if (fleetRuntime) {
1739
- const snapshot = preflight._computeAddDirSnapshot(fleetRuntime, {
1740
- minionsHome: MINIONS_DIR,
1741
- homeDir,
1742
- existsFn,
1743
- });
1744
- if (Array.isArray(snapshot)) addDirSnapshot = snapshot.map(slimRow);
1745
- }
1746
- }
1747
-
1748
1584
  const suppressed = [];
1749
- if (engineConfig.copilotSuppressAgentsMd !== false) {
1750
- suppressed.push({
1751
- flag: 'copilotSuppressAgentsMd',
1752
- value: true,
1753
- effect: 'Copilot --no-custom-instructions: in-tree AGENTS.md is NOT auto-loaded, so Minions playbook prompts are not overridden by repo-local instructions.',
1754
- });
1755
- }
1756
1585
  if (engineConfig.copilotDisableBuiltinMcps !== false) {
1757
1586
  suppressed.push({
1758
1587
  flag: 'copilotDisableBuiltinMcps',
@@ -1760,22 +1589,10 @@ function _buildHarnessDiagnosticsUncached(opts = {}) {
1760
1589
  effect: 'Copilot --disable-builtin-mcps: bundled github-mcp-server is stripped so dispatched agents do not open a parallel PR alongside the Minions PR.',
1761
1590
  });
1762
1591
  }
1763
- if (engineConfig.hermeticHarness === true) {
1764
- suppressed.push({
1765
- flag: 'hermeticHarness',
1766
- value: true,
1767
- effect: 'Hermetic harness mode: only the engine playbook + system prompt are surfaced; user-scope skills, commands, and MCP configs are NOT propagated to this dispatch.',
1768
- });
1769
- }
1770
-
1771
- const projectLocalOnMain = projects.map(_scanProjectLocalHarnessFootgun);
1772
-
1773
1592
  return {
1774
1593
  fleetDefaultCli,
1775
1594
  runtimes,
1776
- addDirSnapshot,
1777
1595
  suppressed,
1778
- projectLocalOnMain,
1779
1596
  missingDirs,
1780
1597
  };
1781
1598
  }
@@ -1841,6 +1658,11 @@ let _plansCacheTs = 0;
1841
1658
  const PLANS_CACHE_TTL_MS = 5000;
1842
1659
  function invalidatePlansCache() { _plansCache = null; _plansCacheTs = 0; }
1843
1660
 
1661
+ function statePathExists(filePath) {
1662
+ if (prdStore.parsePrdPath(filePath)) return prdStore.readPrd(filePath) != null;
1663
+ return fs.existsSync(filePath);
1664
+ }
1665
+
1844
1666
  // Resolve a plan/PRD file path: .json files live in prd/, .md files in plans/
1845
1667
  // Validates that the file stays within the expected directory to prevent path traversal.
1846
1668
  function resolvePlanPath(file) {
@@ -1848,11 +1670,12 @@ function resolvePlanPath(file) {
1848
1670
  // Validate against both prd/ and prd/archive/
1849
1671
  shared.sanitizePath(file, PRD_DIR);
1850
1672
  const active = path.join(PRD_DIR, file);
1851
- if (fs.existsSync(active)) return active;
1673
+ if (prdStore.readPrd(active)) return active;
1852
1674
  const archived = path.join(PRD_DIR, 'archive', file);
1853
- if (fs.existsSync(archived)) return archived;
1675
+ if (prdStore.readPrd(archived)) return archived;
1854
1676
  return active;
1855
1677
  }
1678
+
1856
1679
  // Validate against both plans/ and plans/archive/
1857
1680
  shared.sanitizePath(file, PLANS_DIR);
1858
1681
  const active = path.join(PLANS_DIR, file);
@@ -1881,7 +1704,7 @@ function _archivePrdPostProcess({ planFile, archivePath, planPath, plansDir, _mu
1881
1704
 
1882
1705
  // (a) Flip the archived FLAG + status + archivedAt. Phase 10 step 4.2b:
1883
1706
  // archived-ness is the flag now (not the directory), so set `archived:true`
1884
- // explicitly. The mutate dual-writes the change into SQL via the chokepoint.
1707
+ // explicitly. The path-shaped mutator routes PRDs into SQL.
1885
1708
  // When archivePath === planPath (in-place archive) this flags the PRD where it
1886
1709
  // sits; when they differ (.md-cascade / legacy move) it flags the moved copy.
1887
1710
  try {
@@ -3245,7 +3068,8 @@ function serveFreshJson(req, res, opts) {
3245
3068
  return;
3246
3069
  }
3247
3070
  const mtime = _maxInputMtimeMs(inputs);
3248
- const etag = '"' + tag + '-' + mtime + '"';
3071
+ const eventVersion = _getCurrentEventVersion();
3072
+ const etag = '"' + tag + '-' + mtime + '-' + eventVersion + '"';
3249
3073
  res.setHeader('ETag', etag);
3250
3074
  res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
3251
3075
  if (req && req.headers && req.headers['if-none-match'] === etag) {
@@ -6254,11 +6078,11 @@ const server = http.createServer(async (req, res) => {
6254
6078
  const prdDir = path.join(MINIONS_DIR, 'prd');
6255
6079
  let prdPath = path.join(prdDir, body.file);
6256
6080
  let fromArchive = false;
6257
- if (!fs.existsSync(prdPath)) {
6081
+ if (!statePathExists(prdPath)) {
6258
6082
  prdPath = path.join(prdDir, 'archive', body.file);
6259
6083
  fromArchive = true;
6260
6084
  }
6261
- if (!fs.existsSync(prdPath)) return jsonReply(res, 404, { error: 'PRD not found' });
6085
+ if (!statePathExists(prdPath)) return jsonReply(res, 404, { error: 'PRD not found' });
6262
6086
 
6263
6087
  // If archived, temporarily restore to active so checkPlanCompletion can find it
6264
6088
  const activePath = path.join(prdDir, body.file);
@@ -7218,14 +7042,9 @@ const server = http.createServer(async (req, res) => {
7218
7042
  const manualPrd = buildManualPrdItemPlan(body);
7219
7043
  if (manualPrd.error) return jsonReply(res, 400, { error: manualPrd.error });
7220
7044
 
7221
- if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
7222
-
7223
7045
  const planFile = 'manual-' + shared.uid() + '.json';
7224
7046
  const manualPrdPath = path.join(PRD_DIR, planFile);
7225
7047
  safeWrite(manualPrdPath, manualPrd.plan);
7226
- // Phase 10 step 2 — this create uses safeWrite (not mutateJsonFileLocked),
7227
- // so mirror to SQL explicitly. Best-effort.
7228
- prdStore.mirrorPrdToSql(manualPrdPath, manualPrd.plan);
7229
7048
  invalidatePlansCache();
7230
7049
  return jsonReply(res, 200, { ok: true, id: manualPrd.id, file: planFile });
7231
7050
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
@@ -7239,7 +7058,7 @@ const server = http.createServer(async (req, res) => {
7239
7058
  // the file with JSON, destroying source plans (see /api/plans/approve trap).
7240
7059
  if (!body.source.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename in `source` (got `' + body.source + '`). Pass prd/<plan>.json, not plans/<plan>.md.' });
7241
7060
  const planPath = resolvePlanPath(body.source);
7242
- if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
7061
+ if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
7243
7062
  // Pre-check: verify item exists before taking the lock
7244
7063
  const preCheck = safeJsonObj(planPath);
7245
7064
  const preItem = (preCheck.missing_features || []).find(f => f.id === body.itemId);
@@ -7296,7 +7115,7 @@ const server = http.createServer(async (req, res) => {
7296
7115
  if (!body.source || !body.itemId) return jsonReply(res, 400, { error: 'source and itemId required' });
7297
7116
  if (!body.source.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename in `source` (got `' + body.source + '`). Pass prd/<plan>.json, not plans/<plan>.md.' });
7298
7117
  const planPath = resolvePlanPath(body.source);
7299
- if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
7118
+ if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
7300
7119
  let removed = false;
7301
7120
  mutateJsonFileLocked(planPath, (plan) => {
7302
7121
  if (!plan || Array.isArray(plan) || typeof plan !== 'object') plan = { missing_features: [] };
@@ -8041,9 +7860,9 @@ const server = http.createServer(async (req, res) => {
8041
7860
  { dir: PRD_DIR, archived: false },
8042
7861
  { dir: path.join(PRD_DIR, 'archive'), archived: true },
8043
7862
  ];
8044
- // Load work items to check for completed plan-to-prd conversions.
8045
- // safeJsonArr is sync but reads a single small file — leave as is.
8046
- const centralWi = safeJsonArr(path.join(MINIONS_DIR, 'work-items.json'));
7863
+ // Load central work items from the SQL-backed store to check for completed
7864
+ // plan-to-prd conversions.
7865
+ const centralWi = require('./engine/work-items-store').readWorkItemsForScope('central');
8047
7866
  const completedPrdFiles = new Set(
8048
7867
  centralWi.filter(w => w.type === WORK_TYPE.PLAN_TO_PRD && DONE_STATUSES.has(w.status) && w.planFile)
8049
7868
  .map(w => w.planFile)
@@ -8427,8 +8246,8 @@ const server = http.createServer(async (req, res) => {
8427
8246
  if (!body.file.endsWith('.md')) return jsonReply(res, 400, { error: 'only .md plans can be executed' });
8428
8247
  shared.sanitizePath(body.file, PLANS_DIR);
8429
8248
  const planPath = path.join(MINIONS_DIR, 'plans', body.file);
8430
- if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
8431
- const planContent = fs.readFileSync(planPath, 'utf8');
8249
+ if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
8250
+ const planContent = safeRead(planPath);
8432
8251
  const declaredProject = shared.extractPlanDeclaredProject(planContent);
8433
8252
 
8434
8253
  const feedback = (body.feedback || '').toString().trim();
@@ -8485,7 +8304,7 @@ const server = http.createServer(async (req, res) => {
8485
8304
  const body = await readBody(req);
8486
8305
  if (!body.source) return jsonReply(res, 400, { error: 'source required' });
8487
8306
  const planPath = resolvePlanPath(body.source);
8488
- if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
8307
+ if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
8489
8308
  const plan = safeJsonObj(planPath);
8490
8309
  const planItems = plan.missing_features || [];
8491
8310
 
@@ -8581,7 +8400,7 @@ const server = http.createServer(async (req, res) => {
8581
8400
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
8582
8401
  shared.sanitizePath(body.file, body.file.endsWith('.json') ? PRD_DIR : PLANS_DIR);
8583
8402
  const planPath = resolvePlanPath(body.file);
8584
- if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
8403
+ if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
8585
8404
  // Read plan content before deleting — needed for worktree cleanup and source_plan
8586
8405
  let planObj = null;
8587
8406
  let prdSourcePlan = null;
@@ -8591,13 +8410,13 @@ const server = http.createServer(async (req, res) => {
8591
8410
  // Clean up worktrees before deleting work items (needs branch info from work items)
8592
8411
  try {
8593
8412
  const { cleanupPlanWorktrees } = require('./engine/lifecycle');
8594
- cleanupPlanWorktrees(body.file, planObj || {}, PROJECTS, getConfig());
8413
+ cleanupPlanWorktrees(body.file, planObj || {}, PROJECTS, queries.getConfig());
8595
8414
  } catch (e) { console.error('plan worktree cleanup:', e.message); }
8596
8415
  safeUnlink(planPath);
8597
8416
  // Phase 10 step 2 — a deleted PRD must also leave SQL, or the read-flip
8598
8417
  // (step 3) would resurrect it from a stale mirror row. Best-effort; no-op
8599
8418
  // for non-PRD paths (parsePrdPath returns null for plan .md deletes).
8600
- if (body.file.endsWith('.json')) prdStore.removePrdFromSql(planPath);
8419
+ if (body.file.endsWith('.json')) prdStore.deletePrd(planPath);
8601
8420
  // Neutralize the `.backup` sidecar so `safeJson` auto-restore can't
8602
8421
  // RESURRECT the PRD we just deleted (the live file is gone, but a stray
8603
8422
  // `prd/<plan>.json.backup` would be auto-restored on the next safeJson read
@@ -8667,7 +8486,7 @@ const server = http.createServer(async (req, res) => {
8667
8486
  const isPrd = body.file.endsWith('.json');
8668
8487
  shared.sanitizePath(body.file, isPrd ? PRD_DIR : PLANS_DIR);
8669
8488
  const planPath = resolvePlanPath(body.file);
8670
- if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
8489
+ if (!statePathExists(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
8671
8490
 
8672
8491
  // Phase 10 step 4.2b: a PRD is archived IN PLACE (flag flip via
8673
8492
  // _archivePrdPostProcess — the json stays in prd/, no move, so the
@@ -8710,19 +8529,7 @@ const server = http.createServer(async (req, res) => {
8710
8529
  // shipped for the PRD branch in W-mqa13ulk0002def5 (PR #3222) — the
8711
8530
  // helper's source-plan move is a safe no-op here because the outer
8712
8531
  // handler already renamed body.file into plans/archive/.
8713
- let prdFiles = [];
8714
- try {
8715
- prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json'));
8716
- } catch (e) {
8717
- // ENOENT on prd/ is expected for projects without a PRD dir yet —
8718
- // don't surface as a user-visible warning. Other errors (EACCES,
8719
- // EIO) get logged + warned.
8720
- if (e.code !== 'ENOENT') {
8721
- const warning = `Archive could not enumerate ${PRD_DIR}: ${e.message}`;
8722
- archiveWarnings.push(warning);
8723
- console.warn(warning);
8724
- }
8725
- }
8532
+ const prdFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
8726
8533
  for (const prdFile of prdFiles) {
8727
8534
  const prdLivePath = path.join(PRD_DIR, prdFile);
8728
8535
  let prd = null;
@@ -8777,7 +8584,7 @@ const server = http.createServer(async (req, res) => {
8777
8584
 
8778
8585
  try {
8779
8586
  const { cleanupPlanWorktrees } = require('./engine/lifecycle');
8780
- cleanupPlanWorktrees(body.file, plan, PROJECTS, getConfig());
8587
+ cleanupPlanWorktrees(body.file, plan, PROJECTS, queries.getConfig());
8781
8588
  } catch (e) { console.error('plan worktree cleanup:', e.message); }
8782
8589
 
8783
8590
  invalidateStatusCache();
@@ -8804,12 +8611,23 @@ const server = http.createServer(async (req, res) => {
8804
8611
  // (physically in <dir>/archive/) or in-place (in <dir>/ with the archived
8805
8612
  // flag set). Restore from whichever it is.
8806
8613
  let liveDest;
8807
- if (fs.existsSync(archivePath)) {
8614
+ // Existence probes only — NEVER used as the write payload. The actual
8615
+ // restore below (prdStore.restorePrdFromArchive) re-reads the archived
8616
+ // row fresh inside its own transaction immediately before writing, so a
8617
+ // concurrent engine write to that row between this probe and the write
8618
+ // can't be clobbered by a stale in-memory snapshot (W-mrenu075000a94c9).
8619
+ const archivedPrdExists = isJson ? prdStore.readPrd(archivePath) != null : false;
8620
+ const activePrdData = isJson ? prdStore.readPrd(livePath) : null;
8621
+ if (isJson && archivedPrdExists) {
8622
+ const restored = prdStore.restorePrdFromArchive(body.file);
8623
+ if (!restored) return jsonReply(res, 404, { error: 'File not found in archive' });
8624
+ liveDest = livePath;
8625
+ } else if (fs.existsSync(archivePath)) {
8808
8626
  // DATA-LOSS GUARD: restoring must not clobber a LIVE plan/PRD of the same
8809
8627
  // basename (renameSync overwrites). moveFileNoClobber bumps the restored
8810
8628
  // name instead so the existing live file survives.
8811
8629
  liveDest = shared.moveFileNoClobber(archivePath, targetDir, body.file);
8812
- } else if (isJson && fs.existsSync(livePath) && shared.isPrdArchived(safeJsonNoRestore(livePath))) {
8630
+ } else if (isJson && activePrdData && shared.isPrdArchived(activePrdData)) {
8813
8631
  liveDest = livePath; // in-place flag-archived — already in prd/, just clear the flag below
8814
8632
  } else {
8815
8633
  return jsonReply(res, 404, { error: 'File not found in archive' });
@@ -13610,10 +13428,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13610
13428
  },
13611
13429
  });
13612
13430
  }},
13613
- { method: 'GET', path: '/api/schedules', desc: 'Schedule definitions merged with schedule-runs.json overlay (_lastRun/_lastResult/_lastCompletedAt)', handler: (req, res) => {
13431
+ { method: 'GET', path: '/api/schedules', desc: 'Schedule definitions merged with SQL schedule-run state (_lastRun/_lastResult/_lastCompletedAt)', handler: (req, res) => {
13614
13432
  return serveFreshJson(req, res, {
13615
13433
  tag: 'schedules',
13616
- inputs: [CONFIG_PATH, path.join(ENGINE_DIR, 'schedule-runs.json')],
13434
+ inputs: [CONFIG_PATH],
13617
13435
  builder: () => {
13618
13436
  // Read config.json directly via queries.getConfig() rather than
13619
13437
  // calling reloadConfig() — the latter cascades into
@@ -13625,7 +13443,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13625
13443
  // (Round-2 review finding #3.)
13626
13444
  const cfg = queries.getConfig() || {};
13627
13445
  const scheds = cfg.schedules || [];
13628
- const runs = shared.safeJsonObj(path.join(ENGINE_DIR, 'schedule-runs.json'));
13446
+ const runs = require('./engine/small-state-store').readScheduleRuns();
13629
13447
  return scheds.map(s => {
13630
13448
  const runEntry = runs[s.id];
13631
13449
  const _lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || runEntry?.lastCompletedAt || null);
@@ -13642,12 +13460,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13642
13460
  },
13643
13461
  });
13644
13462
  }},
13645
- { method: 'GET', path: '/api/pipelines', desc: 'Pipeline definitions merged with last-5 runs each from pipeline-runs.json', handler: (req, res) => {
13463
+ { method: 'GET', path: '/api/pipelines', desc: 'Pipeline definitions merged with last-5 SQL-backed runs', handler: (req, res) => {
13646
13464
  return serveFreshJson(req, res, {
13647
13465
  tag: 'pipelines',
13648
13466
  inputs: [
13649
13467
  path.join(MINIONS_DIR, 'pipelines'),
13650
- path.join(ENGINE_DIR, 'pipeline-runs.json'),
13651
13468
  ],
13652
13469
  builder: () => {
13653
13470
  try {
@@ -13662,14 +13479,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13662
13479
  { method: 'GET', path: '/api/qa-runs-summary', desc: 'Sidebar-counter summary {total, sig} for QA runs (sidebar activity-dot signal)', handler: (req, res) => {
13663
13480
  return serveFreshJson(req, res, {
13664
13481
  tag: 'qa-runs-summary',
13665
- inputs: [path.join(ENGINE_DIR, 'qa-runs.json')],
13482
+ inputs: [],
13666
13483
  builder: () => qaRunsMod.summarizeRunsForStatus(),
13667
13484
  });
13668
13485
  }},
13669
13486
  { method: 'GET', path: '/api/qa-sessions-summary', desc: 'Sidebar-counter summary {active, sig} for non-terminal QA sessions (drives the QA nav badge — issue #717)', handler: (req, res) => {
13670
13487
  return serveFreshJson(req, res, {
13671
13488
  tag: 'qa-sessions-summary',
13672
- inputs: [qaSessionsMod.qaSessionsPath()],
13489
+ inputs: [],
13673
13490
  builder: () => qaSessionsMod.summarizeActiveSessionsForStatus(),
13674
13491
  });
13675
13492
  }},
@@ -13956,7 +13773,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13956
13773
  if (token) ghOpts.env = { ...process.env, GH_TOKEN: token };
13957
13774
  const result = await shared.shellSafeGh(['api', `repos/${shared.validateGhSlug(slug)}/pulls/${String(prNum)}`], ghOpts);
13958
13775
  const d = JSON.parse(result);
13959
- prData = { title: d.title, description: d.body, branch: d.head?.ref, author: d.user?.login };
13776
+ prData = { title: d.title, description: d.body, branch: d.head?.ref, author: d.user?.login, created: d.created_at };
13960
13777
  } else if (adoTarget && !initialPrData) {
13961
13778
  try {
13962
13779
  prData = await ado.fetchAdoPrMetadata(adoTarget.prNum, adoTarget.adoOrg, adoTarget.adoProj, adoTarget.adoRepo);
@@ -13983,6 +13800,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13983
13800
  }
13984
13801
  }
13985
13802
  if (pr.agent === 'human' && prData.author) pr.agent = prData.author;
13803
+ // W-mrezh0yb0007f733 — for GitHub (and ADO metadata-fetch-retry)
13804
+ // links, `created` was seeded with link-time now(); adopt the
13805
+ // platform's real creation timestamp now that enrichment fetched it.
13806
+ if (prData.created && shared.shouldAdoptPlatformCreated(pr.created, prData.created)) {
13807
+ pr.created = prData.created;
13808
+ }
13986
13809
  return prs;
13987
13810
  }, { defaultValue: [] });
13988
13811
  invalidateStatusCache();
@@ -14663,8 +14486,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
14663
14486
  // Skills
14664
14487
  { method: 'GET', path: '/api/skill', desc: 'Read a skill file', params: 'file, source?, dir?', handler: handleSkillRead },
14665
14488
 
14666
- // Harness propagation diagnostic (P-c601f9a2)
14667
- { method: 'GET', path: '/api/harness/diagnostics', desc: 'Per-runtime harness diagnostic (add-dirs, suppressed flags, project-local-on-main footgun, missing dirs)', handler: handleHarnessDiagnostics },
14489
+ // Runtime harness inventory
14490
+ { method: 'GET', path: '/api/harness/diagnostics', desc: 'Read-only inventory of runtime-declared harness locations and explicit orchestration policies', handler: handleHarnessDiagnostics },
14668
14491
 
14669
14492
  // Projects
14670
14493
  { method: 'POST', path: '/api/projects/browse', desc: 'Open folder picker dialog, return selected path', handler: handleProjectsBrowse },
package/docs/README.md CHANGED
@@ -43,8 +43,8 @@ Architecture, design proposals, and lifecycle references for people working on t
43
43
  - [deprecated-process.md](deprecated-process.md) — Schema for `docs/deprecated.json` and the weekly `cleanup-deprecated` audit walk that retires entries past their removal signal.
44
44
  - [design-state-storage.md](design-state-storage.md) — Design proposal evaluating five database options for replacing Minions' file-based JSON state; recommends `node:sqlite` (accepted; implementation tracked in CHANGELOG.md Phases 0–10).
45
45
  - [harness-mode.md](harness-mode.md) — Tri-Agent Harness Mode (`harness_mode: "tri_agent"` on scheduled tasks): Planner → Generator → Evaluator loop that iterates a shared on-disk artifact until a rubric passes or the iteration cap fires.
46
- - [harness-propagation.md](harness-propagation.md) — How user-level and project-local harness assets (skills, slash-commands, MCP config, `CLAUDE.md` / `AGENTS.md`) propagate into an agent's worktree via `--add-dir`, the `harnessPropagateProjectLocal` flag, and the project-local-on-main worktree-visibility footgun.
47
- - [harness-transparency.md](harness-transparency.md) — The `harnessUsed` self-report contract: capture (agent reports the skills / MCPs / commands / docs it used) → ground (engine cross-checks against `_harnessPropagated` and annotates `grounded:true\|false`, never dropping) → surface (PR comment, final agent note, work-item modal).
46
+ - [harness-propagation.md](harness-propagation.md) — The native runtime
47
+ contract for repository instructions, skills, commands, and MCPs.
48
48
  - [kb-dedup-duplicate-pair-investigation.md](kb-dedup-duplicate-pair-investigation.md) — Investigation-only pass diffing suspected duplicate KB filename groups (content-hash dedup for agent-authored reviews/build-reports) to confirm true full-body duplication before any fix is proposed.
49
49
  - [kb-pr3223-cascade-archiving.md](kb-pr3223-cascade-archiving.md) — Findings from PR #3223: cascade-archiving PRDs when their linked markdown plan is archived, including the per-concern try/catch pattern and `source_plan` basename-matching invariant in `handlePlansArchive` (`dashboard.js`).
50
50
  - [kb-pr696-merge-conflict-docs.md](kb-pr696-merge-conflict-docs.md) — Findings from PR #696: resolving a merge conflict in `docs/kb-sweep.md` with stale line-number citations to `engine/kb-sweep.js`.
@@ -57,7 +57,6 @@ Architecture, design proposals, and lifecycle references for people working on t
57
57
  - [pr-auto-fix-dispatch.md](pr-auto-fix-dispatch.md) — Short reference table mapping each PR auto-fix / review dispatch site in `engine.js#discoverFromPrs` to its gate flag, plus the `pollingPaused` / `autoFixPaused` master kill-switches and the per-provider polling gates.
58
58
  - [pr-comment-followup.md](pr-comment-followup.md) — PR-comment follow-up dispatch contract: fix/review agents may spin off a new WI via `POST /api/work-items` with `meta.pr_followup` instead of broadening the current PR or rebutting the comment.
59
59
  - [pr-review-fix-loop.md](pr-review-fix-loop.md) — How the engine moves a PR from creation through review, fix dispatch, and re-review, including stale-status guards.
60
- - [project-skills.md](project-skills.md) — Project-local skill discovery (`.claude/skills/`, `.claude/commands/`, `CLAUDE.md` / `.github/copilot-instructions.md` slash-command mentions): how dispatched agents see and steer toward purpose-built tooling the project ships, plus the intent-vocabulary contract.
61
60
  - [proposals/repo-pool-for-live-checkout.md](proposals/repo-pool-for-live-checkout.md) — Review-only artifact reproducing the approved plan + PRD for a repo pool covering live-checkout projects (multi-enlistment dispatch), so the design has a git-tracked surface for human comment before the dependent implementation work items land as code PRs.
62
61
  - [qa-runbook-lifecycle.md](qa-runbook-lifecycle.md) — End-to-end QA runbook lifecycle (W-mpeiwz6k0005bf34): runbook + run-record storage, `POST /api/qa/runbooks/run` dispatch into the `qa-validate` playbook, artifact contract, and how the `/qa` page mirrors managed-spawn observability.
63
62
  - [qa-runbooks.md](qa-runbooks.md) — Per-project QA runbook schema, storage layout (`projects/<name>/runbooks/<id>.json`), CRUD endpoints, run-record lifecycle, and the `qa-validate` agent sidecar contract.
@@ -1551,11 +1551,11 @@
1551
1551
  "versionNonce": 1820013027,
1552
1552
  "fontSize": 12,
1553
1553
  "fontFamily": 3,
1554
- "text": "copilot adapter\n--autopilot --allow-all\n--disable-builtin-mcps\n--no-custom-instructions",
1554
+ "text": "copilot adapter\n--autopilot --allow-all\n--disable-builtin-mcps",
1555
1555
  "textAlign": "center",
1556
1556
  "verticalAlign": "middle",
1557
1557
  "containerId": "NSvcvc9cT4YYzxMebEDz",
1558
- "originalText": "copilot adapter\n--autopilot --allow-all\n--disable-builtin-mcps\n--no-custom-instructions",
1558
+ "originalText": "copilot adapter\n--autopilot --allow-all\n--disable-builtin-mcps",
1559
1559
  "lineHeight": 1.25,
1560
1560
  "autoResize": false
1561
1561
  },
@@ -105,9 +105,9 @@ Flip via Dashboard → Settings → Polling → "Granular work-discovery control
105
105
 
106
106
  ### Source 2: PRD Gap Analysis (via `materializePlansAsWorkItems`)
107
107
 
108
- PRD items flow through `materializePlansAsWorkItems()`, which scans `~/.minions/prd/*.json` for PRD files with `missing` / `updated` / `planned` items and creates work items in the target project's queue.
108
+ PRD items flow through `materializePlansAsWorkItems()`, which queries SQL PRDs for `missing` / `updated` / `planned` items and creates work items in the target project's queue.
109
109
 
110
- **Reads:** `~/.minions/prd/*.json` the central PRD directory. (Legacy `<project>/docs/prd-gaps.json` paths are no longer scanned.)
110
+ **Reads:** SQL `prds` and `prd_items`.
111
111
 
112
112
  | Item State | Action | Dispatch Type |
113
113
  |------------|--------|---------------|
@@ -182,7 +182,7 @@ priority: high
182
182
  ...
183
183
  ```
184
184
 
185
- **Plans** (`materializePlansAsWorkItems`): Scans `~/.minions/prd/*.json` for PRD files with `missing`/`planned` items. Creates work items in the target project's queue with `createdBy: 'engine:plan-discovery'`. Work item ID = PRD item ID (e.g. `P-43e5ac28`). Deduped by `id`.
185
+ **Plans** (`materializePlansAsWorkItems`): Queries SQL PRDs for `missing`/`planned` items. Creates work items in the target project's queue with `createdBy: 'engine:plan-discovery'`. Work item ID = PRD item ID (e.g. `P-43e5ac28`). Deduped by `id`.
186
186
 
187
187
  Both write to `work-items.json` and are picked up by Source 3 on the same or next tick.
188
188