@yemi33/minions 0.1.2369 → 0.1.2371

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/bin/minions.js +38 -11
  2. package/dashboard/js/memory-search.js +112 -0
  3. package/dashboard/js/render-kb.js +15 -0
  4. package/dashboard/js/render-other.js +2 -30
  5. package/dashboard/js/render-work-items.js +0 -34
  6. package/dashboard/js/settings.js +27 -27
  7. package/dashboard/pages/inbox.html +1 -1
  8. package/dashboard/pages/tools.html +1 -1
  9. package/dashboard.js +148 -258
  10. package/docs/README.md +2 -3
  11. package/docs/architecture.excalidraw +2 -2
  12. package/docs/auto-discovery.md +3 -3
  13. package/docs/completion-reports.md +0 -121
  14. package/docs/copilot-cli-schema.md +9 -17
  15. package/docs/deprecated.json +0 -51
  16. package/docs/design-state-storage.md +4 -4
  17. package/docs/harness-propagation.md +33 -263
  18. package/docs/human-vs-automated.md +1 -1
  19. package/docs/named-agents.md +0 -2
  20. package/docs/plan-lifecycle.md +5 -6
  21. package/docs/qa-runbook-lifecycle.md +10 -25
  22. package/docs/shared-lifecycle-module-map.md +2 -10
  23. package/docs/team-memory.md +18 -4
  24. package/engine/ado-comment.js +5 -11
  25. package/engine/ado.js +10 -5
  26. package/engine/cleanup.js +16 -36
  27. package/engine/cli.js +13 -175
  28. package/engine/comment-format.js +8 -182
  29. package/engine/consolidation.js +72 -1
  30. package/engine/db/index.js +60 -10
  31. package/engine/db/migrations/017-agent-memory.js +208 -0
  32. package/engine/db/migrations/018-sql-only-cutover.js +56 -0
  33. package/engine/dispatch-store.js +0 -114
  34. package/engine/dispatch.js +1 -6
  35. package/engine/features.js +6 -0
  36. package/engine/gh-comment.js +2 -10
  37. package/engine/github.js +8 -6
  38. package/engine/lifecycle.js +141 -173
  39. package/engine/llm.js +9 -11
  40. package/engine/managed-spawn.js +1 -6
  41. package/engine/memory-retrieval.js +165 -0
  42. package/engine/memory-store.js +367 -0
  43. package/engine/metrics-store.js +4 -128
  44. package/engine/pipeline.js +4 -7
  45. package/engine/playbook.js +64 -198
  46. package/engine/prd-store.js +115 -141
  47. package/engine/preflight.js +8 -55
  48. package/engine/projects.js +34 -41
  49. package/engine/pull-requests-store.js +9 -234
  50. package/engine/qa-runs.js +4 -15
  51. package/engine/qa-sessions.js +5 -17
  52. package/engine/queries.js +23 -103
  53. package/engine/routing.js +1 -1
  54. package/engine/runtimes/claude.js +1 -2
  55. package/engine/runtimes/codex.js +0 -2
  56. package/engine/runtimes/copilot.js +1 -4
  57. package/engine/shared-branch-pr-reconcile.js +2 -5
  58. package/engine/shared.js +179 -533
  59. package/engine/small-state-store.js +12 -647
  60. package/engine/spawn-agent.js +5 -96
  61. package/engine/state-operations.js +166 -0
  62. package/engine/supervisor.js +0 -1
  63. package/engine/watch-actions.js +2 -3
  64. package/engine/watches-store.js +2 -128
  65. package/engine/work-items-store.js +3 -254
  66. package/engine.js +102 -334
  67. package/package.json +2 -2
  68. package/playbooks/build-fix-complex.md +0 -4
  69. package/playbooks/fix.md +0 -4
  70. package/playbooks/implement-shared.md +0 -4
  71. package/playbooks/implement.md +0 -4
  72. package/playbooks/plan-to-prd.md +16 -11
  73. package/playbooks/plan.md +0 -4
  74. package/playbooks/review.md +0 -6
  75. package/playbooks/shared-rules.md +0 -65
  76. package/docs/harness-transparency.md +0 -199
  77. package/docs/project-skills.md +0 -193
  78. package/engine/discover-project-skills.js +0 -673
  79. package/engine/playbook-intents.js +0 -76
@@ -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
- }
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;
1548
1532
 
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
- }
1555
-
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
  }
@@ -5389,6 +5313,81 @@ function applyGoalInvalidation(sourceWiId, invalidates, config) {
5389
5313
  }
5390
5314
  }
5391
5315
 
5316
+ function captureTaskMemoryEpisode(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, config) {
5317
+ if ((config?.engine?.memoryEpisodicCapture ?? ENGINE_DEFAULTS.memoryEpisodicCapture) !== true) return null;
5318
+ if (!dispatchItem || !dispatchItem.id) return null;
5319
+ if (structuredCompletion?.failure_class === FAILURE_CLASS.INJECTION_FLAGGED) return null;
5320
+
5321
+ const meta = dispatchItem.meta || {};
5322
+ const item = meta.item || {};
5323
+ const project = meta.project?.name
5324
+ || (item._source && item._source !== 'central' ? item._source : '')
5325
+ || dispatchItem.project
5326
+ || '';
5327
+ const files = structuredCompletion?.files_changed
5328
+ || structuredCompletion?.affected_files
5329
+ || [];
5330
+ const redact = typeof shared.redactSecrets === 'function' ? shared.redactSecrets : String;
5331
+ const fileList = (Array.isArray(files) ? files : String(files || '').split(','))
5332
+ .map(value => redact(String(value).trim()).slice(0, 300))
5333
+ .filter(Boolean)
5334
+ .slice(0, 20);
5335
+ const rawTests = structuredCompletion?.tests || 'N/A';
5336
+ const tests = Array.isArray(rawTests)
5337
+ ? rawTests.slice(0, 20).map(value => redact(String(value)).slice(0, 500))
5338
+ : redact(String(rawTests)).slice(0, 2000);
5339
+ const pr = redact(String(structuredCompletion?.pr || item._prUrl || item._pr || 'N/A')).slice(0, 1000);
5340
+ const failureClass = redact(String(structuredCompletion?.failure_class || 'N/A')).slice(0, 200);
5341
+ const safeSummary = redact(String(resultSummary || structuredCompletion?.summary || 'No summary reported')).slice(0, 4000);
5342
+ const title = redact(String(item.title || item.name || dispatchItem.title || item.id || dispatchItem.id)).slice(0, 500);
5343
+ const body = [
5344
+ `Outcome: ${outcome}`,
5345
+ `Work item: ${item.id || 'N/A'}`,
5346
+ `Work type: ${dispatchItem.type || item.type || 'N/A'}`,
5347
+ `Agent: ${agentId || dispatchItem.agent || 'unknown'}`,
5348
+ `Project: ${project || 'N/A'}`,
5349
+ `PR: ${pr}`,
5350
+ `Tests: ${Array.isArray(tests) ? tests.join(', ') : tests}`,
5351
+ `Failure class: ${failureClass}`,
5352
+ fileList.length ? `Files: ${fileList.join(', ')}` : 'Files: N/A',
5353
+ '',
5354
+ 'Summary:',
5355
+ safeSummary,
5356
+ ].join('\n');
5357
+
5358
+ try {
5359
+ return require('./memory-store').upsertMemoryRecord({
5360
+ memoryType: 'episodic',
5361
+ scopeType: project ? 'project' : (agentId ? 'agent' : 'global'),
5362
+ scopeKey: project || agentId || '',
5363
+ title: `${outcome}: ${title}`,
5364
+ body,
5365
+ tags: ['task-episode', dispatchItem.type || item.type || 'unknown', outcome, ...fileList.slice(0, 12)],
5366
+ sourceType: 'dispatch',
5367
+ sourceRef: String(dispatchItem.id),
5368
+ sourcePath: structuredCompletion?._path || null,
5369
+ trust: 'system',
5370
+ importance: outcome === 'failed' ? 0.85 : (outcome === 'partial' ? 0.75 : 0.6),
5371
+ confidence: structuredCompletion ? 0.8 : 0.55,
5372
+ metadata: {
5373
+ dispatchId: dispatchItem.id,
5374
+ workItemId: item.id || null,
5375
+ agent: agentId || dispatchItem.agent || null,
5376
+ project: project || null,
5377
+ outcome,
5378
+ workType: dispatchItem.type || item.type || null,
5379
+ pr,
5380
+ tests,
5381
+ failureClass,
5382
+ files: fileList,
5383
+ },
5384
+ });
5385
+ } catch (err) {
5386
+ log('warn', `Task memory episode write failed for ${dispatchItem.id}: ${err.message}`);
5387
+ return null;
5388
+ }
5389
+ }
5390
+
5392
5391
  async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config, opts) {
5393
5392
 
5394
5393
  const detectPhantom = !!(opts && opts.detectPhantom);
@@ -5484,31 +5483,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5484
5483
  structuredCompletion.status = 'failed-injection-flagged';
5485
5484
  }
5486
5485
  }
5487
- // P-c6f5e8b3 — harness grounding cross-check (Stage 2). Cross-check the
5488
- // agent's self-reported `harnessUsed` against the spawn-time
5489
- // `_harnessPropagated` manifest and annotate each entry `grounded:true|false`.
5490
- // The grounded result replaces `structuredCompletion.harnessUsed` IN PLACE, so
5491
- // it rides the existing `structuredCompletion` storage onto the completed
5492
- // dispatch record (completeDispatch persists `item.structuredCompletion`) for
5493
- // the Stage-3 surfaces (PR comment / final agent note / WI modal) to read.
5494
- // Pure, non-destructive (grounded:false entries are kept + flagged, never
5495
- // dropped), and best-effort: any failure here must never block completion.
5496
- //
5497
- // #308 — the grounded footprint is no longer routed to its own
5498
- // `harness-usage-*` inbox note (that produced one duplicate-looking note chip
5499
- // per attempt). For clean completions the structured `_harnessUsed` WI-modal
5500
- // UI surfaces it; for non-clean outcomes writeNonCleanAgentReport folds the
5501
- // same buildHarnessUsedSection block into the single final agent note.
5502
- if (structuredCompletion && structuredCompletion.harnessUsed) {
5503
- try {
5504
- const propagated = resolveHarnessPropagated(dispatchItem);
5505
- const grounded = shared.groundHarnessUsed(structuredCompletion.harnessUsed, propagated);
5506
- structuredCompletion.harnessUsed = grounded; // canonical grounded shape, or null when nothing survived
5507
- } catch (err) {
5508
- log('warn', `Harness grounding cross-check failed for ${dispatchItem.id} (non-fatal): ${err.message}`);
5509
- }
5510
- }
5511
-
5512
5486
  const completionGateSummary = resultSummary || (typeof stdout === 'string' && !stdout.includes('"type":') ? stdout : '');
5513
5487
 
5514
5488
  // Save session for potential resume on next dispatch
@@ -5577,6 +5551,12 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5577
5551
  log('info', `Auto-recovery: agent failed but created ${prsCreatedCount} PR(s) — upgrading ${meta.item.id} to done`);
5578
5552
  }
5579
5553
  const effectiveSuccess = !nonceMismatch && ((isSuccess && !agentReportedFailure) || autoRecovered);
5554
+ if (!nonceMismatch) {
5555
+ const episodeOutcome = effectiveSuccess
5556
+ ? (autoRecovered || completionStatus.startsWith('partial') ? 'partial' : 'success')
5557
+ : 'failed';
5558
+ captureTaskMemoryEpisode(dispatchItem, agentId, episodeOutcome, structuredCompletion, completionGateSummary, config);
5559
+ }
5580
5560
 
5581
5561
  let nonCleanReportWritten = false;
5582
5562
  if (completionStatus.startsWith('partial') || autoRecovered || (agentReportedFailure && isSuccess)) {
@@ -5682,59 +5662,49 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5682
5662
  }
5683
5663
  }
5684
5664
 
5685
- // 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.
5686
5667
  // Must run BEFORE updateWorkItemStatus(DONE) — otherwise _retryCount is deleted and retries never advance
5687
5668
  if (effectiveSuccess && type === WORK_TYPE.PLAN_TO_PRD && meta?.item?.id) {
5688
5669
  let prdFound = false;
5689
5670
  let resolvedPrdFilename = null;
5690
5671
  const expectedFile = meta.item._prdFilename;
5691
- if (expectedFile) {
5692
- // Inline `fs.existsSync(path.join(PRD_DIR, expectedFile))` is the literal
5693
- // substring pinned by source-text regression test (test/unit.test.js#893).
5694
- // Do not refactor into a temp variable without updating that assertion.
5695
- if (fs.existsSync(path.join(PRD_DIR, expectedFile))) {
5696
- // Defense-in-depth (W-mozkn1bb001j66fd): a near-simultaneous plan-to-prd
5697
- // dispatch for a different plan may have clobbered our pinned filename.
5698
- // Verify the file's source_plan matches our planFile before accepting it.
5699
- // Without this check we'd mark the WI done against ANOTHER plan's PRD.
5700
- if (meta.item.planFile) {
5701
- try {
5702
- const prd = safeJson(path.join(PRD_DIR, expectedFile));
5703
- if (prd && shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
5704
- prdFound = true;
5705
- resolvedPrdFilename = expectedFile;
5706
- } else {
5707
- 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`);
5708
- }
5709
- } catch (e) {
5710
- // Unparseable PRD file — log and accept (legacy behavior). A real
5711
- // collision could hide behind a parse error, so the warn keeps the
5712
- // anomaly visible for follow-up rather than failing silently.
5713
- 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`);
5714
- prdFound = true;
5715
- resolvedPrdFilename = expectedFile;
5716
- }
5717
- } else {
5718
- // No planFile to verify against — fall back to legacy existence check.
5719
- prdFound = true;
5720
- 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}"`);
5721
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;
5722
5698
  }
5723
5699
  }
5724
5700
  if (!prdFound && meta.item.planFile) {
5725
- try {
5726
- for (const f of fs.readdirSync(PRD_DIR)) {
5727
- if (!f.endsWith('.json')) continue;
5728
- try {
5729
- const prd = safeJson(path.join(PRD_DIR, f));
5730
- if (prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
5731
- prdFound = true;
5732
- resolvedPrdFilename = f;
5733
- break;
5734
- }
5735
- } 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;
5736
5706
  }
5737
- } catch {}
5707
+ }
5738
5708
  }
5739
5709
  // Migrate _prdFilename when the source_plan scan found our PRD under a
5740
5710
  // different on-disk filename (e.g. agent bumped past a clobbered name, or
@@ -5773,7 +5743,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5773
5743
  // buildWorkItemDispatchVars()'s retry_context var (W-mrbq53ra000lf2e5) so the
5774
5744
  // retried agent knows the PRD file was never found instead of silently
5775
5745
  // repeating the same mistake.
5776
- 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.`;
5777
5747
  if (retries < ENGINE_DEFAULTS.maxRetries) {
5778
5748
  w.status = WI_STATUS.PENDING;
5779
5749
  w._retryCount = retries + 1;
@@ -5785,13 +5755,13 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5785
5755
  if (failedAgent) shared.bumpAgentRetryCount(w, failedAgent);
5786
5756
  delete w.dispatched_at;
5787
5757
  delete w.completedAt;
5788
- 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}`);
5789
5759
  } else {
5790
5760
  w.status = WI_STATUS.FAILED;
5791
- w.failReason = 'PRD file not written after ' + ENGINE_DEFAULTS.maxRetries + ' attempts';
5761
+ w.failReason = 'PRD result not written after ' + ENGINE_DEFAULTS.maxRetries + ' attempts';
5792
5762
  w.failedAt = ts();
5793
5763
  w._lastRetryReason = gateReason;
5794
- 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`);
5795
5765
  }
5796
5766
  return data;
5797
5767
  }, { skipWriteIfUnchanged: true });
@@ -6391,9 +6361,7 @@ function syncPrdFromPrs(config) {
6391
6361
  // Paused, rejected, awaiting-approval, completed, and archived PRDs are
6392
6362
  // skipped via _PRD_FROZEN_STATUSES. The locked write is idempotent.
6393
6363
  function advancePrdStatusOnAllItemsDone(config) {
6394
- if (!fs.existsSync(PRD_DIR)) return;
6395
- let prdFiles;
6396
- 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);
6397
6365
  if (!prdFiles.length) return;
6398
6366
 
6399
6367
  const allWorkItems = queries.getWorkItems(config);
@@ -6513,7 +6481,6 @@ function _verifyPrsNeedUpdate(existing, refMap) {
6513
6481
  function persistVerifyPrsToPrd(config) {
6514
6482
  try {
6515
6483
  config = config || queries.getConfig();
6516
- if (!fs.existsSync(PRD_DIR)) return;
6517
6484
  const projects = shared.getProjects(config);
6518
6485
  if (!projects.length) return;
6519
6486
 
@@ -6538,7 +6505,7 @@ function persistVerifyPrsToPrd(config) {
6538
6505
  for (const [planFile, refMap] of refsByPlan) {
6539
6506
  // Resolve the PRD file (active or archived). sourcePlan is a bare filename.
6540
6507
  const candidates = [path.join(PRD_DIR, planFile), path.join(PRD_DIR, 'archive', planFile)];
6541
- const fpath = candidates.find(p => { try { return fs.existsSync(p); } catch { return false; } });
6508
+ const fpath = candidates.find(p => safeJsonNoRestore(p));
6542
6509
  if (!fpath) continue;
6543
6510
 
6544
6511
  // Lock-free peek (mirrors syncPrdItemStatus): only take the write lock
@@ -7101,11 +7068,12 @@ module.exports = {
7101
7068
  enforceVerifyPrContract,
7102
7069
  markMissingPrAttachment,
7103
7070
  parseCompletionReportFile,
7104
- resolveHarnessPropagated,
7105
7071
  // #308 — exported for unit testing: the final non-clean agent note now folds
7106
7072
  // the grounded harness footprint in as a section instead of emitting a
7107
7073
  // standalone harness-usage-* inbox note.
7108
7074
  writeNonCleanAgentReport,
7075
+ // exported for testing
7076
+ captureTaskMemoryEpisode,
7109
7077
  normalizeCompletionArtifacts,
7110
7078
  completionArtifactToNoteEntry,
7111
7079
  mergeArtifactNotes,
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();