@yemi33/minions 0.1.2424 → 0.1.2426

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 (185) hide show
  1. package/bin/install-internal-minions.js +1209 -0
  2. package/bin/minions.js +75 -9
  3. package/dashboard/docs/typography.md +27 -12
  4. package/dashboard/js/command-center.js +13 -11
  5. package/dashboard/js/live-stream.js +1 -1
  6. package/dashboard/js/memory-search.js +388 -43
  7. package/dashboard/js/modal-qa.js +5 -5
  8. package/dashboard/js/qa.js +39 -26
  9. package/dashboard/js/refresh.js +37 -18
  10. package/dashboard/js/render-agents.js +14 -18
  11. package/dashboard/js/render-dispatch.js +14 -74
  12. package/dashboard/js/render-inbox.js +3 -3
  13. package/dashboard/js/render-meetings.js +8 -8
  14. package/dashboard/js/render-other.js +45 -20
  15. package/dashboard/js/render-pipelines.js +6 -6
  16. package/dashboard/js/render-plans.js +14 -14
  17. package/dashboard/js/render-prd.js +46 -46
  18. package/dashboard/js/render-prs.js +9 -65
  19. package/dashboard/js/render-schedules.js +5 -5
  20. package/dashboard/js/render-skills.js +6 -6
  21. package/dashboard/js/render-utils.js +4 -4
  22. package/dashboard/js/render-watches.js +4 -16
  23. package/dashboard/js/render-work-items.js +95 -107
  24. package/dashboard/js/settings.js +175 -74
  25. package/dashboard/layout.html +3 -2
  26. package/dashboard/pages/inbox.html +1 -1
  27. package/dashboard/pages/work.html +1 -1
  28. package/dashboard/shared/model-display.js +9 -0
  29. package/dashboard/shared/watches-source.js +43 -0
  30. package/dashboard/shared/welcome-popup.js +259 -0
  31. package/dashboard/slim/body.html +6 -6
  32. package/dashboard/slim/js/chat.js +4 -2
  33. package/dashboard/slim/js/history.js +10 -3
  34. package/dashboard/slim/js/modals-tiles.js +85 -16
  35. package/dashboard/slim/js/status.js +12 -16
  36. package/dashboard/slim/styles.css +24 -4
  37. package/dashboard/styles.css +62 -27
  38. package/dashboard-build.js +8 -2
  39. package/dashboard.js +3854 -2514
  40. package/docs/README.md +7 -5
  41. package/docs/api-errors.md +144 -0
  42. package/docs/auto-discovery.md +83 -61
  43. package/docs/capture-demos.js +173 -30
  44. package/docs/command-center.md +5 -1
  45. package/docs/completion-reports.md +127 -11
  46. package/docs/constants.md +15 -3
  47. package/docs/constellation-style-telemetry.md +2 -2
  48. package/docs/copilot-cli-schema.md +32 -22
  49. package/docs/cross-repo-plans.md +30 -30
  50. package/docs/demo/memory-system.html +1 -1
  51. package/docs/deprecated.json +132 -18
  52. package/docs/diagnostics-memory.md +8 -4
  53. package/docs/human-vs-automated.md +2 -2
  54. package/docs/index.html +9 -2
  55. package/docs/internal-install.md +212 -0
  56. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  57. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  58. package/docs/kb-sweep.md +2 -2
  59. package/docs/keep-processes.md +8 -1
  60. package/docs/live-checkout-mode.md +1 -1
  61. package/docs/managed-spawn.md +21 -3
  62. package/docs/named-agents.md +3 -2
  63. package/docs/onboarding.md +26 -0
  64. package/docs/plan-lifecycle.md +32 -32
  65. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  66. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  67. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  68. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  69. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  70. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  71. package/docs/qa-runbook-lifecycle.md +67 -51
  72. package/docs/qa-runbooks.md +2 -2
  73. package/docs/runtime-adapters.md +101 -50
  74. package/docs/security.md +28 -0
  75. package/docs/self-improvement.md +47 -13
  76. package/docs/skills.md +29 -0
  77. package/docs/slim-ux/concepts.md +2 -2
  78. package/docs/specs/agent-configurability.md +43 -41
  79. package/docs/specs/agent-rename.md +13 -13
  80. package/docs/team-memory.md +283 -14
  81. package/docs/tutorials/01-install-and-connect.md +23 -0
  82. package/docs/tutorials/03-plan-driven-feature.md +3 -3
  83. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  84. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  85. package/docs/tutorials/README.md +4 -3
  86. package/docs/visual-evidence-ci.md +103 -0
  87. package/docs/watches.md +35 -30
  88. package/docs/worktree-lifecycle.md +151 -13
  89. package/engine/acp-transport.js +1 -0
  90. package/engine/ado.js +80 -33
  91. package/engine/agent-api-validation.js +571 -0
  92. package/engine/agent-worker-pool.js +7 -6
  93. package/engine/api-contracts/agent-content.js +761 -0
  94. package/engine/api-contracts/cc-ops.js +219 -0
  95. package/engine/api-contracts/config-runtime.js +419 -0
  96. package/engine/api-contracts/core.js +138 -0
  97. package/engine/api-contracts/index.js +506 -0
  98. package/engine/api-contracts/orchestration.js +602 -0
  99. package/engine/api-contracts/pull-requests.js +323 -0
  100. package/engine/api-contracts/qa-process.js +764 -0
  101. package/engine/api-contracts/work-plan-prd.js +494 -0
  102. package/engine/api-validation.js +543 -0
  103. package/engine/cc-api-validation.js +586 -0
  104. package/engine/cc-worker-pool.js +9 -1
  105. package/engine/cleanup.js +393 -271
  106. package/engine/cli.js +65 -7
  107. package/engine/comment-classifier.js +70 -5
  108. package/engine/consolidation.js +123 -5
  109. package/engine/content-api-validation.js +606 -0
  110. package/engine/create-pr-worktree.js +22 -1
  111. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  112. package/engine/dispatch.js +33 -0
  113. package/engine/distribution.js +189 -0
  114. package/engine/execution-model.js +59 -0
  115. package/engine/features.js +12 -0
  116. package/engine/github.js +67 -25
  117. package/engine/harness-context.js +313 -0
  118. package/engine/inbox-store.js +1 -1
  119. package/engine/kb-sweep.js +161 -38
  120. package/engine/keep-process-sweep.js +43 -3
  121. package/engine/lifecycle.js +1066 -165
  122. package/engine/llm.js +4 -0
  123. package/engine/managed-spawn.js +463 -48
  124. package/engine/meeting.js +197 -34
  125. package/engine/memory-retrieval.js +280 -0
  126. package/engine/memory-store.js +821 -38
  127. package/engine/model-discovery.js +17 -3
  128. package/engine/pipeline.js +802 -14
  129. package/engine/plan-prd-validation.js +745 -0
  130. package/engine/playbook.js +206 -17
  131. package/engine/pooled-agent-process.js +6 -1
  132. package/engine/pr-action.js +1 -1
  133. package/engine/pr-issue-validation.js +653 -0
  134. package/engine/pr-resolve.js +75 -13
  135. package/engine/prd-store.js +39 -6
  136. package/engine/preflight.js +7 -2
  137. package/engine/process-utils.js +79 -31
  138. package/engine/projects.js +198 -12
  139. package/engine/promotion.js +371 -0
  140. package/engine/qa-from-prd.js +3 -0
  141. package/engine/qa-process-validation.js +572 -0
  142. package/engine/qa-runbooks.js +41 -37
  143. package/engine/qa-runners/maestro.js +11 -7
  144. package/engine/qa-runners/playwright.js +37 -14
  145. package/engine/qa-runners.js +9 -10
  146. package/engine/qa-runs.js +124 -11
  147. package/engine/qa-sessions.js +139 -40
  148. package/engine/quarantine-refs.js +81 -11
  149. package/engine/queries.js +163 -9
  150. package/engine/review-learning-backfill.js +482 -0
  151. package/engine/review-learning.js +1236 -0
  152. package/engine/runtimes/claude.js +60 -2
  153. package/engine/runtimes/codex.js +57 -16
  154. package/engine/runtimes/contract.js +21 -0
  155. package/engine/runtimes/copilot.js +51 -3
  156. package/engine/runtimes/index.js +1 -0
  157. package/engine/scheduler.js +201 -32
  158. package/engine/settings-validation.js +909 -0
  159. package/engine/shared.js +705 -208
  160. package/engine/spawn-agent.js +48 -21
  161. package/engine/steering-constraints.js +31 -0
  162. package/engine/steering-store.js +10 -2
  163. package/engine/steering.js +14 -2
  164. package/engine/timeout.js +404 -111
  165. package/engine/untrusted-fence.js +24 -10
  166. package/engine/watch-actions.js +7 -2
  167. package/engine/watches.js +353 -51
  168. package/engine/work-item-validation.js +561 -0
  169. package/engine/work-items-store.js +61 -1
  170. package/engine/worktree-gc.js +656 -37
  171. package/engine/worktree-preflight.js +154 -0
  172. package/engine.js +2068 -342
  173. package/package.json +1 -1
  174. package/playbooks/_pr-description-audit.md +88 -41
  175. package/playbooks/build-fix-complex.md +3 -3
  176. package/playbooks/docs.md +2 -2
  177. package/playbooks/fix.md +18 -18
  178. package/playbooks/implement.md +18 -18
  179. package/playbooks/qa-session-draft.md +10 -12
  180. package/playbooks/qa-session-execute.md +16 -12
  181. package/playbooks/qa-validate.md +8 -7
  182. package/playbooks/shared-rules.md +45 -0
  183. package/playbooks/test.md +4 -0
  184. package/prompts/cc-system.md +1 -1
  185. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
package/engine/cli.js CHANGED
@@ -23,6 +23,17 @@ function engine() {
23
23
  let _dispatchModule = null;
24
24
  function dispatchModule() { if (!_dispatchModule) _dispatchModule = require('./dispatch'); return _dispatchModule; }
25
25
 
26
+ function spawnPendingDispatch(e, item, config) {
27
+ return e._withSpawnAttemptLease(
28
+ item.id,
29
+ e.tickGeneration,
30
+ (lease) => e.spawnAgent(item, config, lease),
31
+ ).catch((err) => {
32
+ e.log('error', `Immediate spawn failed for ${item.id}: ${err.message}`);
33
+ return null;
34
+ });
35
+ }
36
+
26
37
  function normalizeSessionBranch(branch) {
27
38
  if (!branch) return null;
28
39
  return String(branch).replace(/^refs\/heads\//, '');
@@ -635,7 +646,33 @@ const commands = {
635
646
  continue;
636
647
  }
637
648
 
638
- let agentPid = readDispatchPid(item.id);
649
+ let agentPid = null;
650
+ let hadPid = false;
651
+ if (item.runtimePid) {
652
+ let persistedRuntimePid = null;
653
+ try { persistedRuntimePid = shared.validatePid(item.runtimePid); } catch { persistedRuntimePid = null; }
654
+ if (persistedRuntimePid) {
655
+ hadPid = true;
656
+ if (isPidAlive(persistedRuntimePid)) agentPid = persistedRuntimePid;
657
+ }
658
+ }
659
+ if (!agentPid) {
660
+ const filePid = readDispatchPid(item.id);
661
+ if (filePid) {
662
+ agentPid = filePid;
663
+ hadPid = true;
664
+ }
665
+ }
666
+ if (!agentPid && item._spawnPhase === 'launching') {
667
+ e.engineRestartGraceExempt.add(item.id);
668
+ e.log('info', `Restart: ${agentId} (${item.id}) stopped during pre-launch handoff — marking for retry`);
669
+ continue;
670
+ }
671
+ if (!agentPid && hadPid) {
672
+ e.engineRestartGraceExempt.add(item.id);
673
+ e.log('info', `Restart: ${agentId} (${item.id}) had no live runtime PID — marking for retry`);
674
+ continue;
675
+ }
639
676
 
640
677
  if (!agentPid) {
641
678
  const status = getAgentStatus(agentId);
@@ -651,7 +688,6 @@ const commands = {
651
688
  }
652
689
  }
653
690
 
654
- const hadPid = agentPid && agentPid > 0; // track before liveness check
655
691
  if (agentPid && agentPid > 0 && !isPidAlive(agentPid)) agentPid = null;
656
692
  if (agentPid && !shared.isDispatchExecutionPathSafe(item, config)) {
657
693
  e.log('warn', `Reattach: refusing ${item.id} — persisted execution path is not safe under current checkout config`);
@@ -999,8 +1035,12 @@ const commands = {
999
1035
  const cfg = require('./queries').getConfig();
1000
1036
  const projects = sharedMod.getProjects(cfg);
1001
1037
  if (projects.length === 0) return;
1038
+ const worktreeProjects = projects.filter((project) => {
1039
+ try { return !sharedMod.isLiveCheckoutProject(project); }
1040
+ catch { return false; }
1041
+ });
1002
1042
  const worktreeRootRel = cfg?.engine?.worktreeRoot || sharedMod.ENGINE_DEFAULTS.worktreeRoot;
1003
- const result = worktreeGc.pruneOrphanWorktrees({
1043
+ const reconcileResult = worktreeGc.reconcileStaleWorktreeDirectories({
1004
1044
  projects,
1005
1045
  dispatchSnap,
1006
1046
  worktreeRootRel,
@@ -1008,6 +1048,19 @@ const commands = {
1008
1048
  config: cfg,
1009
1049
  writeToInbox: (a, s, c) => { try { return sharedMod.writeToInbox(a, s, c); } catch (_e) { return false; } },
1010
1050
  });
1051
+ if (reconcileResult.removed > 0 || reconcileResult.failed > 0) {
1052
+ console.log(` Worktree filesystem reconcile: removed ${reconcileResult.removed} stale director${reconcileResult.removed === 1 ? 'y' : 'ies'}, kept ${reconcileResult.kept}, failed ${reconcileResult.failed} (scanned ${reconcileResult.scanned})`);
1053
+ }
1054
+ const result = worktreeGc.pruneOrphanWorktrees({
1055
+ projects: worktreeProjects,
1056
+ dispatchSnap,
1057
+ worktreeRootRel,
1058
+ log: (lvl, msg) => e.log(lvl, msg),
1059
+ config: cfg,
1060
+ writeToInbox: (a, s, c) => { try { return sharedMod.writeToInbox(a, s, c); } catch (_e) { return false; } },
1061
+ protectedPaths: reconcileResult.protectedPaths,
1062
+ failedRoots: reconcileResult.failedRoots,
1063
+ });
1011
1064
  if (result.evicted > 0 || result.failed > 0) {
1012
1065
  console.log(` Worktree boot reconcile: evicted ${result.evicted} orphan worktree(s)${result.failed ? `, ${result.failed} failed` : ''} (scanned ${result.scanned}, kept ${result.kept})`);
1013
1066
  }
@@ -1018,7 +1071,7 @@ const commands = {
1018
1071
  // consistent. Also runs `git worktree prune --expire=now` per project
1019
1072
  // so manually-removed dirs drop out of the registry immediately.
1020
1073
  const regResult = worktreeGc.pruneOrphanWorktreesFromGitRegistry({
1021
- projects,
1074
+ projects: worktreeProjects,
1022
1075
  dispatchSnap,
1023
1076
  worktreeRootRel,
1024
1077
  log: (lvl, msg) => e.log(lvl, msg),
@@ -1400,8 +1453,13 @@ const commands = {
1400
1453
  const eng = config.engine || {};
1401
1454
  const defaultCli = shared.resolveAgentCli(null, eng);
1402
1455
  const defaultModelResolved = shared.resolveAgentModel(null, eng);
1456
+ const defaultModelDisplay = queries.resolveRuntimeModelDisplay({
1457
+ runtime: defaultCli,
1458
+ configuredModel: defaultModelResolved,
1459
+ dispatch: getDispatch(),
1460
+ });
1403
1461
  console.log(`Default CLI: ${defaultCli}${eng.defaultCli ? '' : ' (default)'}`);
1404
- console.log(`Default model: ${defaultModelResolved || '(runtime default)'}`);
1462
+ console.log(`Default model: ${defaultModelDisplay.model || '(runtime model not observed yet)'}`);
1405
1463
  const ccCliOverride = (eng.ccCli !== undefined && eng.ccCli !== null && eng.ccCli !== '') ? String(eng.ccCli) : null;
1406
1464
  const ccModelOverride = (eng.ccModel !== undefined && eng.ccModel !== null && eng.ccModel !== '') ? String(eng.ccModel) : null;
1407
1465
  if (ccCliOverride || ccModelOverride) {
@@ -1556,7 +1614,7 @@ const commands = {
1556
1614
  const dispatch = getDispatch();
1557
1615
  const item = dispatch.pending.find(d => d.id === id);
1558
1616
  if (item) {
1559
- e.spawnAgent(item, config);
1617
+ spawnPendingDispatch(e, item, config);
1560
1618
  }
1561
1619
  },
1562
1620
 
@@ -1764,7 +1822,7 @@ const commands = {
1764
1822
  const dispatch = getDispatch();
1765
1823
  const item = dispatch.pending.find(d => d.id === id);
1766
1824
  if (item) {
1767
- e.spawnAgent(item, config);
1825
+ spawnPendingDispatch(e, item, config);
1768
1826
  console.log('Agent spawned immediately.');
1769
1827
  }
1770
1828
  } else {
@@ -8,8 +8,8 @@
8
8
  * signatures + Coverage/Build/Test/Deploy/Pipeline report headings +
9
9
  * shields.io-style badge images. Ported from engine/github.js
10
10
  * `_isPreviewStatusComment` body checks. ADO has no `user.type === 'bot'`
11
- * equivalent, so the GH-side bot-author gate stays in github.js — this
12
- * module's body match is what both pollers consume.
11
+ * equivalent, so only the narrower `_isPreviewStatusComment` helper keeps
12
+ * a GH-side bot-author gate; both pollers consume this body match directly.
13
13
  *
14
14
  * hasMinionsMarker(body) — structural `<!-- minions:agent=… kind=… -->`
15
15
  * check using gh-comment.MINIONS_COMMENT_MARKER_RE. Strict: marker must
@@ -29,9 +29,14 @@
29
29
  * mid-paragraph mentions are rejected (line-start anchor + only `**`,
30
30
  * `__`, `#+` allowed before `VERDICT:`).
31
31
  *
32
- * IMPORTANT author identity is intentionally NOT consulted here. These are
33
- * body-only predicates. Callers MUST combine them with their own identity
34
- * gates before treating a comment as agent-authored:
32
+ * The reason-returning `classifyNonActionableAutomationComment()` composes
33
+ * `isPreviewStatusBody()` with exact host-specific automation envelopes.
34
+ * Unlike the body predicates, it also receives trusted host author metadata
35
+ * so a copied marker cannot suppress actionable feedback.
36
+ *
37
+ * IMPORTANT — author identity is intentionally NOT consulted by the three
38
+ * body-only predicates above. Callers MUST combine Minions marker matches with
39
+ * their own identity gates before treating a comment as agent-authored:
35
40
  *
36
41
  * GitHub: `comment.viewerDidAuthor === true`, backfilled via `gh api user`
37
42
  * by engine/github.js `_backfillViewerDidAuthor`. The combined
@@ -80,6 +85,29 @@ const _FIREBASE_HOSTING_LEAD_RE = /^Firebase\s+Hosting\b/im;
80
85
  const _DEPLOY_READY_RE =
81
86
  /\b(?:deploy|deployment|preview)\s+(?:ready|available|succeeded|complete|completed)\b/i;
82
87
 
88
+ const NON_ACTIONABLE_AUTOMATION_REASONS = Object.freeze({
89
+ PREVIEW_STATUS: 'preview-status',
90
+ GITHUB_VISUAL_EVIDENCE: 'github-visual-evidence',
91
+ ADO_PR_ASSISTANT_STATUS: 'ado-pr-assistant-status',
92
+ });
93
+
94
+ // GitHub's global Actions bot identity. All fields are API-controlled.
95
+ const _GITHUB_ACTIONS_BOT = Object.freeze({
96
+ login: 'github-actions[bot]',
97
+ type: 'bot',
98
+ id: 41898282,
99
+ });
100
+
101
+ const _VISUAL_EVIDENCE_ENVELOPE_RE =
102
+ /^<!-- minions:visual-evidence -->\r?\n<!-- visual-evidence-head:[0-9a-f]{40} -->\r?\n<!-- visual-evidence-run:[1-9]\d* -->\r?\n\r?\n### (?:Visual evidence capture failed|Visual evidence is stale|Visual evidence)(?:\r?\n|$)/;
103
+
104
+ const _ADO_GITOPS_AUTHOR = 'gitops (git lowpriv)';
105
+ const _ADO_PR_ASSISTANT_HEADER =
106
+ `<span style="font-family:'Segoe UI',SegoeUI,Segoe,'Helvetica Neue',Arial,sans-serif; font-size:14px; font-weight:600;">PR Assistant</span>`;
107
+ const _ADO_PR_ASSISTANT_STATUS = 'Pull request description has been updated with summary.';
108
+ const _ADO_CONFIG_LEVEL_PREFIX = '<small class="secondary-text">Config Level:';
109
+ const _ADO_GITOPS_USER_AGENT = '<!-- GitOpsUserAgent=GitOps.Apps.Server.pullrequestcopilot -->';
110
+
83
111
  function isPreviewStatusBody(body) {
84
112
  const text = String(body || '');
85
113
  if (!text) return false;
@@ -94,6 +122,41 @@ function isPreviewStatusBody(body) {
94
122
  return false;
95
123
  }
96
124
 
125
+ function _isGitHubVisualEvidenceStatus(body, author) {
126
+ if (String(author?.login || '').toLowerCase() !== _GITHUB_ACTIONS_BOT.login) return false;
127
+ if (String(author?.type || '').toLowerCase() !== _GITHUB_ACTIONS_BOT.type) return false;
128
+ if (Number(author?.id) !== _GITHUB_ACTIONS_BOT.id) return false;
129
+ return _VISUAL_EVIDENCE_ENVELOPE_RE.test(String(body || ''));
130
+ }
131
+
132
+ function _isAdoPrAssistantStatus(body, author) {
133
+ if (String(author?.displayName || '').trim().toLowerCase() !== _ADO_GITOPS_AUTHOR) return false;
134
+
135
+ const text = String(body || '');
136
+ if (!text.startsWith(_ADO_PR_ASSISTANT_HEADER)) return false;
137
+ if (!text.trimEnd().endsWith(_ADO_GITOPS_USER_AGENT)) return false;
138
+
139
+ const afterHeader = text.slice(_ADO_PR_ASSISTANT_HEADER.length);
140
+ const configIndex = afterHeader.indexOf(_ADO_CONFIG_LEVEL_PREFIX);
141
+ if (configIndex === -1) return false;
142
+ return afterHeader.slice(0, configIndex).trim() === _ADO_PR_ASSISTANT_STATUS;
143
+ }
144
+
145
+ function classifyNonActionableAutomationComment({ host, body, author } = {}) {
146
+ if (isPreviewStatusBody(body)) {
147
+ return NON_ACTIONABLE_AUTOMATION_REASONS.PREVIEW_STATUS;
148
+ }
149
+
150
+ const normalizedHost = String(host || '').toLowerCase();
151
+ if (normalizedHost === 'github' && _isGitHubVisualEvidenceStatus(body, author)) {
152
+ return NON_ACTIONABLE_AUTOMATION_REASONS.GITHUB_VISUAL_EVIDENCE;
153
+ }
154
+ if (normalizedHost === 'ado' && _isAdoPrAssistantStatus(body, author)) {
155
+ return NON_ACTIONABLE_AUTOMATION_REASONS.ADO_PR_ASSISTANT_STATUS;
156
+ }
157
+ return null;
158
+ }
159
+
97
160
  // ── Structural Minions marker ──────────────────────────────────────────────
98
161
  //
99
162
  // MINIONS_COMMENT_MARKER_RE uses the /m flag so its `^` anchor matches the
@@ -151,4 +214,6 @@ module.exports = {
151
214
  isPreviewStatusBody,
152
215
  hasMinionsMarker,
153
216
  hasVerdictPrefix,
217
+ classifyNonActionableAutomationComment,
218
+ NON_ACTIONABLE_AUTOMATION_REASONS,
154
219
  };
@@ -15,6 +15,8 @@ const queries = require('./queries');
15
15
  const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR,
16
16
  NOTES_PATH, KNOWLEDGE_DIR, ARCHIVE_DIR } = queries;
17
17
  const { wrapUntrusted, buildSource } = require('./untrusted-fence');
18
+ const reviewLearning = require('./review-learning');
19
+ const memoryStore = require('./memory-store');
18
20
 
19
21
  // notes.md size cap. Consolidation appends to notes.md every cycle; without a
20
22
  // cap it grows unbounded.
@@ -836,13 +838,114 @@ let _consolidationStartedAt = 0;
836
838
  let _forceResetTimeout = null; // force-reset handle; cancelled by _clearProcessingState
837
839
  const _processingFiles = new Set(); // files currently being consolidated (race guard)
838
840
 
841
+ const LEGACY_REVIEW_FEEDBACK_FILE_RE =
842
+ /^feedback-.+-from-.+-\d{4}-\d{2}-\d{2}\.md$/;
843
+
844
+ function _flatFrontmatter(content) {
845
+ const match = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
846
+ if (!match) return {};
847
+ const fields = {};
848
+ for (const line of match[1].split(/\r?\n/)) {
849
+ const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
850
+ if (field) fields[field[1].toLowerCase()] = field[2].trim();
851
+ }
852
+ return fields;
853
+ }
854
+
855
+ function _legacyReviewProject(config, projectName, prId) {
856
+ const projects = shared.getProjects(config);
857
+ const named = projects.find(project => String(project?.name || '').toLowerCase() === projectName);
858
+ if (named) return named;
859
+ const prScope = String(prId || '').replace(/#[1-9]\d*$/, '').toLowerCase();
860
+ return projects.find(project => String(shared.getProjectPrScope(project) || '').toLowerCase() === prScope) || null;
861
+ }
862
+
863
+ function _captureLegacyReviewFeedback(item, config) {
864
+ const fields = _flatFrontmatter(item.content);
865
+ const author = String(fields.author || '').toLowerCase();
866
+ const reviewer = String(fields.reviewer || '').toLowerCase();
867
+ if (fields.agent && String(fields.agent).toLowerCase() !== 'feedback') {
868
+ throw new TypeError(`legacy review feedback ${item.name} has non-feedback provenance`);
869
+ }
870
+ if (!/^[a-z][a-z0-9-]{1,63}$/.test(author) || !/^[a-z][a-z0-9-]{1,63}$/.test(reviewer)
871
+ || !item.name.startsWith(`feedback-${author}-from-${reviewer}-`)) {
872
+ throw new TypeError(`legacy review feedback ${item.name} author/reviewer does not match its filename`);
873
+ }
874
+ const prId = reviewLearning.normalizeCanonicalPrId(fields.sourcepr);
875
+ const project = _legacyReviewProject(config, String(fields.project || '').toLowerCase(), prId);
876
+ const projectName = project?.name || fields.project;
877
+ if (!projectName) throw new TypeError(`legacy review feedback ${item.name} has no project scope`);
878
+ const host = prId.slice(0, prId.indexOf(':'));
879
+ const sourceHash = reviewLearning.hashReviewCommentContent(item.content);
880
+ const sourceIdentity = crypto.createHash('sha256').update(item.name).digest('hex').slice(0, 24);
881
+ const findingKey = reviewLearning.buildFindingKey({
882
+ project: projectName,
883
+ prId,
884
+ sourceKey: `${host}:legacy-feedback:${sourceIdentity}`,
885
+ });
886
+ const candidate = reviewLearning.upsertReviewLearningCandidate({
887
+ findingKey,
888
+ project: projectName,
889
+ prId,
890
+ authorAgent: author,
891
+ reviewerAgent: reviewer,
892
+ sourceHash,
893
+ workItemId: fields.sourceitem || null,
894
+ dispatchId: fields.dispatchid || null,
895
+ metadata: {
896
+ legacyUnverified: true,
897
+ legacySourceRef: item.name,
898
+ backfilledBy: 'inbox-consolidation',
899
+ },
900
+ });
901
+ memoryStore.recordMemoryLifecycleEvent({
902
+ eventType: 'candidate-created',
903
+ memoryId: candidate.id,
904
+ findingKey,
905
+ project: projectName,
906
+ agent: author,
907
+ workItemId: fields.sourceitem || null,
908
+ dispatchId: fields.dispatchid || null,
909
+ metadata: {
910
+ legacyUnverified: true,
911
+ legacySourceRef: item.name,
912
+ sourceHash,
913
+ },
914
+ });
915
+ }
916
+
917
+ function divertLegacyReviewFeedback(items, config) {
918
+ const remaining = [];
919
+ const diverted = [];
920
+ for (const item of Array.isArray(items) ? items : []) {
921
+ if (!LEGACY_REVIEW_FEEDBACK_FILE_RE.test(String(item?.name || ''))) {
922
+ remaining.push(item);
923
+ continue;
924
+ }
925
+ try {
926
+ _captureLegacyReviewFeedback(item, config);
927
+ } catch (err) {
928
+ if (!(err instanceof TypeError)) {
929
+ log('warn', `Legacy review feedback backfill deferred for ${item.name}: ${err.message}`);
930
+ continue;
931
+ }
932
+ log('warn', `Legacy review feedback has invalid provenance and will be archived (${item.name}): ${err.message}`);
933
+ }
934
+ diverted.push(item.name);
935
+ }
936
+ if (diverted.length > 0) {
937
+ archiveInboxFiles(diverted);
938
+ log('info', `Diverted ${diverted.length} legacy review feedback file(s) from broad memory`);
939
+ }
940
+ return remaining;
941
+ }
942
+
839
943
  function consolidateInbox(config) {
840
944
 
841
945
  const { ENGINE_DEFAULTS } = shared;
842
946
  const threshold = config.engine?.inboxConsolidateThreshold || ENGINE_DEFAULTS.inboxConsolidateThreshold;
843
947
  const pinnedInboxKeys = new Set(shared.getPinnedItems().filter(k => k.startsWith('notes/inbox/')));
844
- const files = getInboxFiles().filter(f => !_processingFiles.has(f) && !pinnedInboxKeys.has('notes/inbox/' + f));
845
- if (files.length < threshold) return;
948
+ const candidateFiles = getInboxFiles().filter(f => !_processingFiles.has(f) && !pinnedInboxKeys.has('notes/inbox/' + f));
846
949
  // Auto-reset stale flag if consolidation has been running for >5 minutes (process died without cleanup)
847
950
  if (_consolidationInFlight && (Date.now() - _consolidationStartedAt) > 300000) {
848
951
  log('warn', 'Consolidation flag was stale (>5m) — resetting');
@@ -851,12 +954,15 @@ function consolidateInbox(config) {
851
954
  }
852
955
  if (_consolidationInFlight) return;
853
956
 
854
- log('info', `Consolidating ${files.length} inbox items into notes.md`);
855
-
856
- const items = files.map(f => ({
957
+ const candidateItems = candidateFiles.map(f => ({
857
958
  name: f,
858
959
  content: safeRead(path.join(INBOX_DIR, f)) || ''
859
960
  }));
961
+ const items = divertLegacyReviewFeedback(candidateItems, config);
962
+ const files = items.map(item => item.name);
963
+ if (files.length < threshold) return;
964
+
965
+ log('info', `Consolidating ${files.length} inbox items into notes.md`);
860
966
 
861
967
  const existingNotes = getNotes() || '';
862
968
  consolidateWithLLM(items, existingNotes, files, config);
@@ -942,6 +1048,11 @@ Use today's date: ${dateStamp()}`;
942
1048
 
943
1049
  function consolidateWithLLM(items, existingNotes, files, config) {
944
1050
 
1051
+ items = divertLegacyReviewFeedback(items, config);
1052
+ const allowedFiles = new Set(items.map(item => item.name));
1053
+ files = files.filter(file => allowedFiles.has(file));
1054
+ if (files.length === 0) return;
1055
+
945
1056
  // Cancel any stale force-reset from a prior run before starting fresh
946
1057
  clearTimeout(_forceResetTimeout);
947
1058
  _forceResetTimeout = null;
@@ -1103,6 +1214,11 @@ function consolidateWithLLM(items, existingNotes, files, config) {
1103
1214
 
1104
1215
  function consolidateWithRegex(items, files, config) {
1105
1216
 
1217
+ items = divertLegacyReviewFeedback(items, config);
1218
+ const allowedFiles = new Set(items.map(item => item.name));
1219
+ files = files.filter(file => allowedFiles.has(file));
1220
+ if (files.length === 0) return;
1221
+
1106
1222
  const allInsights = [];
1107
1223
  for (const item of items) {
1108
1224
  const content = item.content || '';
@@ -1327,6 +1443,7 @@ function buildCondensedKbBody(content, titleLine, archiveRelPath) {
1327
1443
 
1328
1444
  async function classifyToKnowledgeBase(items, config) {
1329
1445
 
1446
+ items = divertLegacyReviewFeedback(items, config);
1330
1447
  if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
1331
1448
 
1332
1449
  const knownAgents = config && config.agents
@@ -1571,6 +1688,7 @@ module.exports = {
1571
1688
  NOTES_MAX_BYTES,
1572
1689
  hasReusableSignal,
1573
1690
  buildCondensedKbBody,
1691
+ divertLegacyReviewFeedback, // exported for testing
1574
1692
  _alertContentHash,
1575
1693
  _alertHashExists,
1576
1694
  _writeNotesOrDefer,