@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
@@ -16,7 +16,6 @@ const queries = require('./queries');
16
16
  const harness = require('./harness');
17
17
  const { isBranchActive } = require('./cooldown');
18
18
  const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
19
- const { buildHarnessUsedSection } = require('./comment-format');
20
19
  const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
21
20
  MINIONS_DIR, ENGINE_DIR, PLANS_DIR, PRD_DIR, INBOX_DIR, AGENTS_DIR } = queries;
22
21
 
@@ -520,17 +519,9 @@ function checkPlanCompletion(meta, config) {
520
519
  function archivePlan(planFile, plan, projects, config) {
521
520
  const planPath = path.join(PRD_DIR, planFile);
522
521
 
523
- // Phase 10 archive the PRD IN PLACE: flag it (archived/status/archivedAt)
524
- // where it sits, rather than MOVING it to prd/archive/. This matches the
525
- // canonical dashboard (_archivePrdPostProcess, #533) + watch-action archive
526
- // paths. The legacy move was the LAST path still physically relocating a PRD,
527
- // and it reintroduced the live↔archive basename-collision class (footgun #7)
528
- // that the in-place flag model retired — plus it needed a .backup neutralize
529
- // that is moot now that prd/*.json never gets a .backup sidecar at all (#570).
530
- // mutateJsonFileLocked dual-writes the flag into SQL via the chokepoint; the
531
- // archived-PRD scanner guards (#530) already treat status:'archived' correctly.
522
+ // Archive in place by flagging the authoritative SQL PRD.
532
523
  try {
533
- if (fs.existsSync(planPath)) {
524
+ if (safeJsonNoRestore(planPath)) {
534
525
  mutateJsonFileLocked(planPath, (data) => {
535
526
  if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
536
527
  data.status = 'archived';
@@ -842,7 +833,9 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
842
833
  if (!_VALID_PRD_STATUSES.has(status)) return;
843
834
  try {
844
835
  const prdDir = path.join(MINIONS_DIR, 'prd');
845
- const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
836
+ const files = sourcePlan
837
+ ? [sourcePlan]
838
+ : require('./prd-store').listPrdRows().filter(row => !row.archived).map(row => row.filename);
846
839
  for (const pf of files) {
847
840
  const fpath = path.join(prdDir, pf);
848
841
  // Lock-free peek: most PRDs won't contain the ID, so skip the lock cost.
@@ -880,7 +873,6 @@ function stampPrdItemWorkItemId(itemId, sourcePlan) {
880
873
  if (!itemId || !sourcePlan) return;
881
874
  try {
882
875
  const fpath = path.join(PRD_DIR, sourcePlan);
883
- if (!fs.existsSync(fpath)) return;
884
876
  const plan = safeJsonNoRestore(fpath);
885
877
  const feature = plan?.missing_features?.find(f => f.id === itemId);
886
878
  if (!feature || feature.workItemId === itemId) return;
@@ -896,7 +888,6 @@ function stampPrdItemPrUrl(itemId, sourcePlan, prUrl) {
896
888
  if (!itemId || !sourcePlan || !prUrl) return;
897
889
  try {
898
890
  const fpath = path.join(PRD_DIR, sourcePlan);
899
- if (!fs.existsSync(fpath)) return;
900
891
  const plan = safeJsonNoRestore(fpath);
901
892
  const feature = plan?.missing_features?.find(f => f.id === itemId);
902
893
  if (!feature || feature.pr_url === prUrl) return;
@@ -916,9 +907,7 @@ function stampPrdItemPrUrl(itemId, sourcePlan, prUrl) {
916
907
  // different ID than the original PRD feature, leaving the PRD status stale.
917
908
 
918
909
  function reconcilePrdStatuses(config) {
919
- if (!fs.existsSync(PRD_DIR)) return;
920
- let prdFiles;
921
- try { prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json')); } catch { return; }
910
+ const prdFiles = require('./prd-store').listPrdRows().filter(row => !row.archived).map(row => row.filename);
922
911
  if (prdFiles.length === 0) return;
923
912
 
924
913
  const allWorkItems = queries.getWorkItems(config);
@@ -1537,35 +1526,17 @@ function stampWiPrRef(meta, config) {
1537
1526
  const projects = (config && Array.isArray(config.projects))
1538
1527
  ? config.projects : shared.getProjects(config || {});
1539
1528
 
1540
- // 1. Search each project's pull-requests.json.
1541
- let found = null;
1542
- for (const p of projects) {
1543
- const pr = safeJsonArr(projectPrPath(p)).find(
1544
- r => Array.isArray(r.prdItems) && r.prdItems.includes(itemId)
1545
- );
1546
- if (pr) { found = pr; break; }
1547
- }
1548
-
1549
- // 2. Fall back to central pull-requests.json.
1550
- if (!found) {
1551
- found = safeJsonArr(path.join(MINIONS_DIR, 'pull-requests.json')).find(
1552
- r => Array.isArray(r.prdItems) && r.prdItems.includes(itemId)
1553
- );
1554
- }
1529
+ const prStore = require('./pull-requests-store');
1530
+ const allPrs = prStore.readAllPullRequests();
1531
+ let found = allPrs.find(r => Array.isArray(r.prdItems) && r.prdItems.includes(itemId)) || null;
1555
1532
 
1556
- // 3. Fall back to pr-links index (catches cases where prdItems wasn't
1533
+ // Fall back to pr-links index (catches cases where prdItems wasn't
1557
1534
  // populated but a pr-links entry links the PR to this WI).
1558
1535
  if (!found) {
1559
1536
  const prLinks = getPrLinks();
1560
1537
  const linkedPrId = Object.keys(prLinks).find(id => (prLinks[id] || []).includes(itemId));
1561
1538
  if (linkedPrId) {
1562
- for (const p of projects) {
1563
- const pr = safeJsonArr(projectPrPath(p)).find(r => r.id === linkedPrId);
1564
- if (pr) { found = pr; break; }
1565
- }
1566
- if (!found) {
1567
- found = safeJsonArr(path.join(MINIONS_DIR, 'pull-requests.json')).find(r => r.id === linkedPrId);
1568
- }
1539
+ found = allPrs.find(r => r.id === linkedPrId) || null;
1569
1540
  // Worst case: only the canonical ID is known — at least stamp _pr.
1570
1541
  if (!found) found = { id: linkedPrId };
1571
1542
  }
@@ -1632,21 +1603,15 @@ function isPrAttachmentRequired(type, item, meta = {}) {
1632
1603
 
1633
1604
  function readOptionalJsonStrict(filePath, label, validate) {
1634
1605
  if (!filePath) return null;
1635
- let raw;
1636
- try {
1637
- raw = fs.readFileSync(filePath, 'utf8');
1638
- } catch (err) {
1639
- if (err.code === 'ENOENT') return null;
1640
- throw new Error(`Cannot read ${label} JSON at ${filePath}: ${err.message}`);
1641
- }
1642
1606
  let parsed;
1643
1607
  try {
1644
- parsed = JSON.parse(raw);
1608
+ parsed = shared.safeJson(filePath);
1645
1609
  } catch (err) {
1646
- throw new Error(`Corrupt ${label} JSON at ${filePath}: ${err.message}`);
1610
+ throw new Error(`Cannot read ${label} state at ${filePath}: ${err.message}`);
1647
1611
  }
1612
+ if (parsed == null) return null;
1648
1613
  if (validate && !validate(parsed)) {
1649
- throw new Error(`Invalid ${label} JSON shape at ${filePath}`);
1614
+ throw new Error(`Invalid ${label} state shape at ${filePath}`);
1650
1615
  }
1651
1616
  return parsed;
1652
1617
  }
@@ -2109,7 +2074,7 @@ function markPrAttachmentVerificationError(meta, agentId, reason, resultSummary)
2109
2074
  const wiPath = resolveWorkItemPath(meta);
2110
2075
  let syncFailedToPrd = false;
2111
2076
  if (wiPath) {
2112
- mutateJsonFileLocked(wiPath, data => {
2077
+ mutateWorkItems(wiPath, data => {
2113
2078
  if (!Array.isArray(data)) return data;
2114
2079
  const w = data.find(i => i.id === meta.item.id);
2115
2080
  if (!w) return data;
@@ -2253,7 +2218,7 @@ function enforceVerifyPrContract(type, meta, agentId, config, resultSummary) {
2253
2218
 
2254
2219
  const wiPath = resolveWorkItemPath(meta);
2255
2220
  if (wiPath) {
2256
- mutateJsonFileLocked(wiPath, data => {
2221
+ mutateWorkItems(wiPath, data => {
2257
2222
  if (!Array.isArray(data)) return data;
2258
2223
  const w = data.find(i => i.id === item.id);
2259
2224
  if (!w) return data;
@@ -3701,7 +3666,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
3701
3666
  // Mark PRD feature as implemented
3702
3667
  const prdDir = path.join(MINIONS_DIR, 'prd');
3703
3668
  try {
3704
- const planFiles = fs.readdirSync(prdDir).filter(f => f.endsWith('.json'));
3669
+ const planFiles = require('./prd-store').listPrdRows().filter(row => !row.archived).map(row => row.filename);
3705
3670
  let updated = 0;
3706
3671
  for (const pf of planFiles) {
3707
3672
  const planPath = path.join(prdDir, pf);
@@ -4247,28 +4212,6 @@ function handleInjectionFlag(dispatchItem, agentId, structuredCompletion, config
4247
4212
  return { description, sources, at };
4248
4213
  }
4249
4214
 
4250
- // resolveHarnessPropagated(dispatchItem) — P-c6f5e8b3. Locate the spawn-time
4251
- // `_harnessPropagated` manifest for a dispatch. The in-memory dispatchItem
4252
- // (spawnAgent's closure object) normally carries it directly; after an engine
4253
- // restart + re-attach the field may only live on the persisted dispatch record,
4254
- // so fall back to scanning the live dispatch queues by id. Returns null when no
4255
- // manifest was captured (grounding then marks every entry grounded:false — the
4256
- // honest "no record" state).
4257
- function resolveHarnessPropagated(dispatchItem) {
4258
- if (!dispatchItem) return null;
4259
- if (dispatchItem._harnessPropagated) return dispatchItem._harnessPropagated;
4260
- try {
4261
- const dispatch = getDispatch();
4262
- for (const queue of ['active', 'completed', 'pending']) {
4263
- const list = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
4264
- if (!list) continue;
4265
- const found = list.find(d => d && d.id === dispatchItem.id);
4266
- if (found && found._harnessPropagated) return found._harnessPropagated;
4267
- }
4268
- } catch { /* best-effort */ }
4269
- return null;
4270
- }
4271
-
4272
4215
  function parseCompletionReportFile(dispatchItem, opts = {}) {
4273
4216
  const reportPath = dispatchItem?.meta?.completionReportPath || shared.dispatchCompletionReportPath(dispatchItem?.id);
4274
4217
  if (!reportPath || !fs.existsSync(reportPath)) {
@@ -4434,19 +4377,7 @@ function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredComplet
4434
4377
  const outputLog = opts.outputLog ? String(opts.outputLog) : '';
4435
4378
  const branch = opts.branch || meta.branch || '';
4436
4379
  const resultSummary = opts.resultSummary ? String(opts.resultSummary).slice(0, 500) : '';
4437
- // P-d5a6f7c4 (Harness Transparency, Stage 3 surface). The grounded
4438
- // harnessUsed object (canonical { skills, mcpServers, commands, docs } shape
4439
- // with a strict-boolean `grounded` on each entry, produced upstream in
4440
- // runPostCompletionHooks via shared.groundHarnessUsed) rides here on
4441
- // structuredCompletion.harnessUsed. Persist it onto the work item as
4442
- // `_harnessUsed` so the dashboard work-item detail modal can render the
4443
- // "Repo harnesses used" section. `null`/absent when the agent reported no
4444
- // harness usage; only a plain-object survives normalization upstream.
4445
- const harnessUsed = shared.isPlainObject(structuredCompletion?.harnessUsed)
4446
- ? structuredCompletion.harnessUsed
4447
- : null;
4448
-
4449
- if (artifacts.length === 0 && notes.length === 0 && !outputLog && !branch && !resultSummary && !harnessUsed) {
4380
+ if (artifacts.length === 0 && notes.length === 0 && !outputLog && !branch && !resultSummary) {
4450
4381
  return { artifacts, notes };
4451
4382
  }
4452
4383
 
@@ -4465,7 +4396,6 @@ function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredComplet
4465
4396
  if (meta.item?.sourcePlan) arts.sourcePlan = meta.item.sourcePlan;
4466
4397
  if (artifacts.length > 0) wi.artifacts = mergeCompletionArtifacts(wi.artifacts, artifacts);
4467
4398
  if (resultSummary) wi.resultSummary = resultSummary;
4468
- if (harnessUsed) wi._harnessUsed = harnessUsed;
4469
4399
  wi._artifacts = arts;
4470
4400
  return data;
4471
4401
  }, { defaultValue: [], skipWriteIfUnchanged: true });
@@ -4798,11 +4728,6 @@ function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredComp
4798
4728
  const structuredLines = structuredCompletion
4799
4729
  ? Object.entries(structuredCompletion).map(([key, value]) => `- ${key}: ${value}`).join('\n')
4800
4730
  : '- none';
4801
- // #308 — fold the agent's grounded harness footprint into THIS final agent
4802
- // report instead of emitting a separate `harness-usage-*` inbox note per
4803
- // attempt. buildHarnessUsedSection returns '' for an absent/empty/malformed
4804
- // record, so the section is appended only when there is something to show.
4805
- const harnessSection = buildHarnessUsedSection(structuredCompletion?.harnessUsed);
4806
4731
  const content = [
4807
4732
  `# Agent ${outcome === 'partial' ? 'Partially Completed' : 'Reported Failure'}: ${title}`,
4808
4733
  '',
@@ -4817,7 +4742,6 @@ function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredComp
4817
4742
  structuredLines,
4818
4743
  '',
4819
4744
  resultSummary ? `## Summary\n${resultSummary}` : '## Summary\n(no agent summary captured)',
4820
- harnessSection ? `\n${harnessSection}` : '',
4821
4745
  ].filter(Boolean).join('\n');
4822
4746
  shared.writeToInbox(agentId || 'engine', `agent-${outcome}-${dispatchItem.id}`, content, null, metadata);
4823
4747
  }
@@ -5559,31 +5483,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5559
5483
  structuredCompletion.status = 'failed-injection-flagged';
5560
5484
  }
5561
5485
  }
5562
- // P-c6f5e8b3 — harness grounding cross-check (Stage 2). Cross-check the
5563
- // agent's self-reported `harnessUsed` against the spawn-time
5564
- // `_harnessPropagated` manifest and annotate each entry `grounded:true|false`.
5565
- // The grounded result replaces `structuredCompletion.harnessUsed` IN PLACE, so
5566
- // it rides the existing `structuredCompletion` storage onto the completed
5567
- // dispatch record (completeDispatch persists `item.structuredCompletion`) for
5568
- // the Stage-3 surfaces (PR comment / final agent note / WI modal) to read.
5569
- // Pure, non-destructive (grounded:false entries are kept + flagged, never
5570
- // dropped), and best-effort: any failure here must never block completion.
5571
- //
5572
- // #308 — the grounded footprint is no longer routed to its own
5573
- // `harness-usage-*` inbox note (that produced one duplicate-looking note chip
5574
- // per attempt). For clean completions the structured `_harnessUsed` WI-modal
5575
- // UI surfaces it; for non-clean outcomes writeNonCleanAgentReport folds the
5576
- // same buildHarnessUsedSection block into the single final agent note.
5577
- if (structuredCompletion && structuredCompletion.harnessUsed) {
5578
- try {
5579
- const propagated = resolveHarnessPropagated(dispatchItem);
5580
- const grounded = shared.groundHarnessUsed(structuredCompletion.harnessUsed, propagated);
5581
- structuredCompletion.harnessUsed = grounded; // canonical grounded shape, or null when nothing survived
5582
- } catch (err) {
5583
- log('warn', `Harness grounding cross-check failed for ${dispatchItem.id} (non-fatal): ${err.message}`);
5584
- }
5585
- }
5586
-
5587
5486
  const completionGateSummary = resultSummary || (typeof stdout === 'string' && !stdout.includes('"type":') ? stdout : '');
5588
5487
 
5589
5488
  // Save session for potential resume on next dispatch
@@ -5763,59 +5662,49 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5763
5662
  }
5764
5663
  }
5765
5664
 
5766
- // Verify plan-to-prd actually created the PRD file before marking done (#893)
5665
+ // Import the plan-to-prd result sidecar into the authoritative SQL store
5666
+ // before marking the work item done.
5767
5667
  // Must run BEFORE updateWorkItemStatus(DONE) — otherwise _retryCount is deleted and retries never advance
5768
5668
  if (effectiveSuccess && type === WORK_TYPE.PLAN_TO_PRD && meta?.item?.id) {
5769
5669
  let prdFound = false;
5770
5670
  let resolvedPrdFilename = null;
5771
5671
  const expectedFile = meta.item._prdFilename;
5772
- if (expectedFile) {
5773
- // Inline `fs.existsSync(path.join(PRD_DIR, expectedFile))` is the literal
5774
- // substring pinned by source-text regression test (test/unit.test.js#893).
5775
- // Do not refactor into a temp variable without updating that assertion.
5776
- if (fs.existsSync(path.join(PRD_DIR, expectedFile))) {
5777
- // Defense-in-depth (W-mozkn1bb001j66fd): a near-simultaneous plan-to-prd
5778
- // dispatch for a different plan may have clobbered our pinned filename.
5779
- // Verify the file's source_plan matches our planFile before accepting it.
5780
- // Without this check we'd mark the WI done against ANOTHER plan's PRD.
5781
- if (meta.item.planFile) {
5782
- try {
5783
- const prd = safeJson(path.join(PRD_DIR, expectedFile));
5784
- if (prd && shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
5785
- prdFound = true;
5786
- resolvedPrdFilename = expectedFile;
5787
- } else {
5788
- log('warn', `plan-to-prd ${meta.item.id}: file "${expectedFile}" exists but source_plan="${prd?.source_plan ?? '(unknown)'}" doesn't match expected planFile="${meta.item.planFile}" — possible collision, scanning by source_plan`);
5789
- }
5790
- } catch (e) {
5791
- // Unparseable PRD file — log and accept (legacy behavior). A real
5792
- // collision could hide behind a parse error, so the warn keeps the
5793
- // anomaly visible for follow-up rather than failing silently.
5794
- log('warn', `plan-to-prd ${meta.item.id}: file "${expectedFile}" exists but failed to parse (${e.message}) — accepting as legacy behavior; manual inspection recommended if this recurs`);
5795
- prdFound = true;
5796
- resolvedPrdFilename = expectedFile;
5797
- }
5798
- } else {
5799
- // No planFile to verify against — fall back to legacy existence check.
5800
- prdFound = true;
5801
- resolvedPrdFilename = expectedFile;
5672
+ const prdStore = require('./prd-store');
5673
+ const agentId = meta._agentId || meta.item.dispatched_to;
5674
+ const resultPath = agentId ? path.join(AGENTS_DIR, agentId, 'prd-result.json') : null;
5675
+ if (expectedFile && resultPath && fs.existsSync(resultPath)) {
5676
+ try {
5677
+ const envelope = shared.safeJsonNoRestore(resultPath);
5678
+ const filename = envelope?.filename || expectedFile;
5679
+ const prd = envelope?.prd || envelope;
5680
+ if (filename !== expectedFile) {
5681
+ throw new Error(`sidecar filename "${filename}" does not match expected "${expectedFile}"`);
5682
+ }
5683
+ if (!prd || !shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
5684
+ throw new Error(`sidecar source_plan "${prd?.source_plan ?? '(unknown)'}" does not match "${meta.item.planFile}"`);
5802
5685
  }
5686
+ prdStore.writePrd(path.join(PRD_DIR, expectedFile), prd);
5687
+ fs.unlinkSync(resultPath);
5688
+ } catch (e) {
5689
+ log('warn', `plan-to-prd ${meta.item.id}: could not import result sidecar: ${e.message}`);
5690
+ }
5691
+ }
5692
+ for (const { filename, archived, plan } of prdStore.listPrdRows()) {
5693
+ if (!archived && filename === expectedFile
5694
+ && (!meta.item.planFile || shared.prdMatchesSourcePlan(plan.source_plan, meta.item.planFile))) {
5695
+ prdFound = true;
5696
+ resolvedPrdFilename = filename;
5697
+ break;
5803
5698
  }
5804
5699
  }
5805
5700
  if (!prdFound && meta.item.planFile) {
5806
- try {
5807
- for (const f of fs.readdirSync(PRD_DIR)) {
5808
- if (!f.endsWith('.json')) continue;
5809
- try {
5810
- const prd = safeJson(path.join(PRD_DIR, f));
5811
- if (prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
5812
- prdFound = true;
5813
- resolvedPrdFilename = f;
5814
- break;
5815
- }
5816
- } catch {}
5701
+ for (const { filename, archived, plan } of prdStore.listPrdRows()) {
5702
+ if (!archived && shared.prdMatchesSourcePlan(plan.source_plan, meta.item.planFile)) {
5703
+ prdFound = true;
5704
+ resolvedPrdFilename = filename;
5705
+ break;
5817
5706
  }
5818
- } catch {}
5707
+ }
5819
5708
  }
5820
5709
  // Migrate _prdFilename when the source_plan scan found our PRD under a
5821
5710
  // different on-disk filename (e.g. agent bumped past a clobbered name, or
@@ -5854,7 +5743,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5854
5743
  // buildWorkItemDispatchVars()'s retry_context var (W-mrbq53ra000lf2e5) so the
5855
5744
  // retried agent knows the PRD file was never found instead of silently
5856
5745
  // repeating the same mistake.
5857
- const gateReason = `Validation gate (#893): no PRD JSON file was found on disk for planFile "${meta.item.planFile || w.planFile || '(unknown)'}" (expected "${expectedFile || '(unset)'}") after the previous attempt completed. Write the PRD JSON to the exact path specified in the playbook before finishing.`;
5746
+ const gateReason = `Validation gate (#893): no PRD result sidecar was imported for planFile "${meta.item.planFile || w.planFile || '(unknown)'}" (expected filename "${expectedFile || '(unset)'}") after the previous attempt completed. Write agents/<agent-id>/prd-result.json exactly as specified in the playbook before finishing.`;
5858
5747
  if (retries < ENGINE_DEFAULTS.maxRetries) {
5859
5748
  w.status = WI_STATUS.PENDING;
5860
5749
  w._retryCount = retries + 1;
@@ -5866,13 +5755,13 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5866
5755
  if (failedAgent) shared.bumpAgentRetryCount(w, failedAgent);
5867
5756
  delete w.dispatched_at;
5868
5757
  delete w.completedAt;
5869
- log('warn', `plan-to-prd ${meta.item.id} completed without PRD file — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
5758
+ log('warn', `plan-to-prd ${meta.item.id} completed without PRD result — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
5870
5759
  } else {
5871
5760
  w.status = WI_STATUS.FAILED;
5872
- w.failReason = 'PRD file not written after ' + ENGINE_DEFAULTS.maxRetries + ' attempts';
5761
+ w.failReason = 'PRD result not written after ' + ENGINE_DEFAULTS.maxRetries + ' attempts';
5873
5762
  w.failedAt = ts();
5874
5763
  w._lastRetryReason = gateReason;
5875
- log('warn', `plan-to-prd ${meta.item.id} failed — no PRD file after ${ENGINE_DEFAULTS.maxRetries} retries`);
5764
+ log('warn', `plan-to-prd ${meta.item.id} failed — no PRD result after ${ENGINE_DEFAULTS.maxRetries} retries`);
5876
5765
  }
5877
5766
  return data;
5878
5767
  }, { skipWriteIfUnchanged: true });
@@ -6472,9 +6361,7 @@ function syncPrdFromPrs(config) {
6472
6361
  // Paused, rejected, awaiting-approval, completed, and archived PRDs are
6473
6362
  // skipped via _PRD_FROZEN_STATUSES. The locked write is idempotent.
6474
6363
  function advancePrdStatusOnAllItemsDone(config) {
6475
- if (!fs.existsSync(PRD_DIR)) return;
6476
- let prdFiles;
6477
- try { prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json')); } catch { return; }
6364
+ const prdFiles = require('./prd-store').listPrdRows().filter(row => !row.archived).map(row => row.filename);
6478
6365
  if (!prdFiles.length) return;
6479
6366
 
6480
6367
  const allWorkItems = queries.getWorkItems(config);
@@ -6594,7 +6481,6 @@ function _verifyPrsNeedUpdate(existing, refMap) {
6594
6481
  function persistVerifyPrsToPrd(config) {
6595
6482
  try {
6596
6483
  config = config || queries.getConfig();
6597
- if (!fs.existsSync(PRD_DIR)) return;
6598
6484
  const projects = shared.getProjects(config);
6599
6485
  if (!projects.length) return;
6600
6486
 
@@ -6619,7 +6505,7 @@ function persistVerifyPrsToPrd(config) {
6619
6505
  for (const [planFile, refMap] of refsByPlan) {
6620
6506
  // Resolve the PRD file (active or archived). sourcePlan is a bare filename.
6621
6507
  const candidates = [path.join(PRD_DIR, planFile), path.join(PRD_DIR, 'archive', planFile)];
6622
- const fpath = candidates.find(p => { try { return fs.existsSync(p); } catch { return false; } });
6508
+ const fpath = candidates.find(p => safeJsonNoRestore(p));
6623
6509
  if (!fpath) continue;
6624
6510
 
6625
6511
  // Lock-free peek (mirrors syncPrdItemStatus): only take the write lock
@@ -7182,7 +7068,6 @@ module.exports = {
7182
7068
  enforceVerifyPrContract,
7183
7069
  markMissingPrAttachment,
7184
7070
  parseCompletionReportFile,
7185
- resolveHarnessPropagated,
7186
7071
  // #308 — exported for unit testing: the final non-clean agent note now folds
7187
7072
  // the grounded harness footprint in as a section instead of emitting a
7188
7073
  // standalone harness-usage-* inbox note.
package/engine/llm.js CHANGED
@@ -345,7 +345,7 @@ function _runtimeUnavailableResult(callOpts = {}) {
345
345
  * Capability gating (matches engine.js _buildAgentSpawnFlags from P-2a6d9c4f):
346
346
  * - effort/sessionId/maxBudget/bare/fallbackModel are dropped when the
347
347
  * runtime's matching capability is false.
348
- * - Copilot-specific opts (stream, disableBuiltinMcps, suppressAgentsMd,
348
+ * - Copilot-specific opts (stream, disableBuiltinMcps,
349
349
  * reasoningSummaries) are emitted unconditionally; the Claude adapter
350
350
  * ignores them via the "tolerate unknown opts" rule.
351
351
  */
@@ -365,7 +365,6 @@ function _buildSpawnAgentFlags(runtime, opts = {}) {
365
365
 
366
366
  if (opts.stream === 'on' || opts.stream === 'off') flags.push('--stream', opts.stream);
367
367
  if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
368
- if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
369
368
  if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
370
369
 
371
370
  return flags;
@@ -387,7 +386,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
387
386
  const {
388
387
  direct, label, runtime, model, maxTurns, allowedTools, effort, sessionId,
389
388
  maxBudget, bare, fallbackModel,
390
- stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries,
389
+ stream, disableBuiltinMcps, reasoningSummaries,
391
390
  images,
392
391
  } = callOpts;
393
392
 
@@ -403,7 +402,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
403
402
  const adapterOpts = {
404
403
  model, maxTurns, allowedTools, effort, sessionId,
405
404
  maxBudget, bare, fallbackModel,
406
- stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries,
405
+ stream, disableBuiltinMcps, reasoningSummaries,
407
406
  images,
408
407
  tmpDir: llmTmpDir,
409
408
  };
@@ -477,7 +476,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
477
476
  const adapterFlags = _buildSpawnAgentFlags(runtime, {
478
477
  model, maxTurns, allowedTools, effort, sessionId,
479
478
  maxBudget, bare, fallbackModel,
480
- stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries,
479
+ stream, disableBuiltinMcps, reasoningSummaries,
481
480
  });
482
481
  const args = [spawnScript, promptPath, sysPath, ...adapterFlags];
483
482
 
@@ -690,13 +689,12 @@ function _resolveModelForRuntime(runtime, callOpts) {
690
689
  }
691
690
 
692
691
  function _resolveRuntimeFeatureOpts({
693
- stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
692
+ stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
694
693
  } = {}) {
695
694
  const engine = engineConfig || {};
696
695
  return {
697
696
  stream: stream ?? engine.copilotStreamMode,
698
697
  disableBuiltinMcps: disableBuiltinMcps ?? engine.copilotDisableBuiltinMcps,
699
- suppressAgentsMd: suppressAgentsMd ?? engine.copilotSuppressAgentsMd,
700
698
  reasoningSummaries: reasoningSummaries ?? engine.copilotReasoningSummaries,
701
699
  };
702
700
  }
@@ -759,7 +757,7 @@ function callLLM(promptText, sysPromptText, opts = {}) {
759
757
  engineConfig,
760
758
  // Cross-runtime + Copilot opts:
761
759
  maxBudget, bare, fallbackModel,
762
- stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries,
760
+ stream, disableBuiltinMcps, reasoningSummaries,
763
761
  // P-7b2e4d01 — optional image attachments: [{ mimeType, dataBase64, filename? }]
764
762
  images,
765
763
  } = opts;
@@ -772,7 +770,7 @@ function callLLM(promptText, sysPromptText, opts = {}) {
772
770
  const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
773
771
  if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
774
772
  const runtimeFeatureOpts = _resolveRuntimeFeatureOpts({
775
- stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
773
+ stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
776
774
  });
777
775
 
778
776
  let _abort = null;
@@ -884,7 +882,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
884
882
  effort = null, direct = false,
885
883
  model: modelOverride, cli: cliOverride, engineConfig,
886
884
  maxBudget, bare, fallbackModel,
887
- stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries,
885
+ stream, disableBuiltinMcps, reasoningSummaries,
888
886
  // P-7b2e4d01 — optional image attachments: [{ mimeType, dataBase64, filename? }]
889
887
  images,
890
888
  } = opts;
@@ -897,7 +895,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
897
895
  const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
898
896
  if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
899
897
  const runtimeFeatureOpts = _resolveRuntimeFeatureOpts({
900
- stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries, engineConfig,
898
+ stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
901
899
  });
902
900
 
903
901
  let _abort = null;
@@ -969,12 +969,7 @@ function removeManagedSpec(name) {
969
969
  function listManagedSpecs(opts) {
970
970
  opts = opts || {};
971
971
  const statePath = _getStatePath();
972
- let raw;
973
- try { raw = fs.readFileSync(statePath, 'utf8'); }
974
- catch (_e) { return []; }
975
- let parsed;
976
- try { parsed = JSON.parse(raw); }
977
- catch (_e) { return []; }
972
+ const parsed = shared.safeJsonObj(statePath);
978
973
  const specs = (parsed && Array.isArray(parsed.specs)) ? parsed.specs : [];
979
974
  if (opts.project) return specs.filter(s => s && s.owner_project === opts.project);
980
975
  return specs.slice();
@@ -1,7 +1,7 @@
1
1
  // engine/memory-store.js — SQL-backed long-term memory records and retrieval.
2
2
 
3
3
  const crypto = require('crypto');
4
- const { getDb, withTransaction } = require('./db');
4
+ const { getDb, withTransaction, prepareCached } = require('./db');
5
5
  const { emitStateEvent } = require('./db-events');
6
6
 
7
7
  const MEMORY_TYPES = new Set(['semantic', 'episodic', 'procedural']);
@@ -167,7 +167,8 @@ function upsertMemoryRecord(input) {
167
167
  }
168
168
 
169
169
  function getMemoryRecord(id) {
170
- return _row(getDb().prepare('SELECT * FROM memory_records WHERE id=?').get(String(id)));
170
+ const db = getDb();
171
+ return _row(prepareCached(db, 'SELECT * FROM memory_records WHERE id=?').get(String(id)));
171
172
  }
172
173
 
173
174
  function updateMemoryRecordState(id, action) {
@@ -219,7 +220,7 @@ function listMemoryRecords(filters = {}) {
219
220
  }
220
221
  const limit = Math.max(1, Math.min(500, Number(filters.limit) || 100));
221
222
  const sql = `SELECT * FROM memory_records${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ORDER BY updated_at DESC LIMIT ?`;
222
- return getDb().prepare(sql).all(...args, limit).map(_row);
223
+ return prepareCached(getDb(), sql).all(...args, limit).map(_row);
223
224
  }
224
225
 
225
226
  function _matchQuery(query) {
@@ -268,14 +269,15 @@ function searchMemoryRecords(query, options = {}) {
268
269
  }
269
270
  const limit = Math.max(1, Math.min(200, Number(options.limit) || 50));
270
271
  args.push(limit);
271
- return getDb().prepare(`
272
+ const sql = `
272
273
  SELECT m.*, bm25(memory_records_fts, 5.0, 1.0, 2.0, 3.0) AS fts_rank
273
274
  FROM memory_records_fts
274
275
  JOIN memory_records m ON m.rowid=memory_records_fts.rowid
275
276
  WHERE ${where.join(' AND ')}
276
277
  ORDER BY fts_rank
277
278
  LIMIT ?
278
- `).all(...args).map(_row);
279
+ `;
280
+ return prepareCached(getDb(), sql).all(...args).map(_row);
279
281
  }
280
282
 
281
283
  function transitionMemoryRecord(id, status, options = {}) {
@@ -338,7 +340,7 @@ function recordRetrievalRun(run) {
338
340
  }
339
341
 
340
342
  function getRetrievalStats() {
341
- return getDb().prepare(`
343
+ return prepareCached(getDb(), `
342
344
  SELECT COUNT(*) AS runs,
343
345
  COALESCE(AVG(duration_ms), 0) AS avg_duration_ms,
344
346
  COALESCE(AVG(selected_count), 0) AS avg_selected_count,