@yemi33/minions 0.1.2423 → 0.1.2425

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 (184) 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 +6 -4
  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/internal-install.md +190 -0
  55. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  56. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  57. package/docs/kb-sweep.md +2 -2
  58. package/docs/keep-processes.md +8 -1
  59. package/docs/live-checkout-mode.md +12 -7
  60. package/docs/managed-spawn.md +21 -3
  61. package/docs/named-agents.md +3 -2
  62. package/docs/plan-lifecycle.md +32 -32
  63. package/docs/pr-auto-fix-dispatch.md +3 -3
  64. package/docs/pr-review-fix-loop.md +6 -6
  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/03-plan-driven-feature.md +3 -3
  82. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  83. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  84. package/docs/tutorials/README.md +4 -3
  85. package/docs/visual-evidence-ci.md +103 -0
  86. package/docs/watches.md +35 -30
  87. package/docs/worktree-lifecycle.md +151 -13
  88. package/engine/acp-transport.js +1 -0
  89. package/engine/ado.js +80 -33
  90. package/engine/agent-api-validation.js +571 -0
  91. package/engine/agent-worker-pool.js +7 -6
  92. package/engine/api-contracts/agent-content.js +761 -0
  93. package/engine/api-contracts/cc-ops.js +219 -0
  94. package/engine/api-contracts/config-runtime.js +419 -0
  95. package/engine/api-contracts/core.js +138 -0
  96. package/engine/api-contracts/index.js +506 -0
  97. package/engine/api-contracts/orchestration.js +602 -0
  98. package/engine/api-contracts/pull-requests.js +323 -0
  99. package/engine/api-contracts/qa-process.js +764 -0
  100. package/engine/api-contracts/work-plan-prd.js +494 -0
  101. package/engine/api-validation.js +543 -0
  102. package/engine/cc-api-validation.js +586 -0
  103. package/engine/cc-worker-pool.js +9 -1
  104. package/engine/cleanup.js +393 -271
  105. package/engine/cli.js +65 -7
  106. package/engine/comment-classifier.js +70 -5
  107. package/engine/consolidation.js +123 -5
  108. package/engine/content-api-validation.js +606 -0
  109. package/engine/create-pr-worktree.js +22 -1
  110. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  111. package/engine/dispatch.js +33 -0
  112. package/engine/distribution.js +189 -0
  113. package/engine/execution-model.js +59 -0
  114. package/engine/features.js +12 -0
  115. package/engine/github.js +67 -25
  116. package/engine/harness-context.js +313 -0
  117. package/engine/inbox-store.js +1 -1
  118. package/engine/kb-sweep.js +161 -38
  119. package/engine/keep-process-sweep.js +43 -3
  120. package/engine/lifecycle.js +1066 -165
  121. package/engine/llm.js +4 -0
  122. package/engine/managed-spawn.js +463 -48
  123. package/engine/meeting.js +197 -34
  124. package/engine/memory-retrieval.js +280 -0
  125. package/engine/memory-store.js +821 -38
  126. package/engine/model-discovery.js +17 -3
  127. package/engine/pipeline.js +802 -14
  128. package/engine/plan-prd-validation.js +745 -0
  129. package/engine/playbook.js +206 -17
  130. package/engine/pooled-agent-process.js +6 -1
  131. package/engine/pr-action.js +1 -1
  132. package/engine/pr-issue-validation.js +653 -0
  133. package/engine/pr-resolve.js +75 -13
  134. package/engine/prd-store.js +39 -6
  135. package/engine/preflight.js +7 -2
  136. package/engine/process-utils.js +79 -31
  137. package/engine/projects.js +198 -12
  138. package/engine/promotion.js +371 -0
  139. package/engine/qa-from-prd.js +3 -0
  140. package/engine/qa-process-validation.js +572 -0
  141. package/engine/qa-runbooks.js +41 -37
  142. package/engine/qa-runners/maestro.js +11 -7
  143. package/engine/qa-runners/playwright.js +37 -14
  144. package/engine/qa-runners.js +9 -10
  145. package/engine/qa-runs.js +124 -11
  146. package/engine/qa-sessions.js +139 -40
  147. package/engine/quarantine-refs.js +81 -11
  148. package/engine/queries.js +163 -9
  149. package/engine/review-learning-backfill.js +482 -0
  150. package/engine/review-learning.js +1236 -0
  151. package/engine/runtimes/claude.js +60 -2
  152. package/engine/runtimes/codex.js +57 -16
  153. package/engine/runtimes/contract.js +21 -0
  154. package/engine/runtimes/copilot.js +51 -3
  155. package/engine/runtimes/index.js +1 -0
  156. package/engine/scheduler.js +201 -32
  157. package/engine/settings-validation.js +909 -0
  158. package/engine/shared.js +705 -208
  159. package/engine/spawn-agent.js +48 -21
  160. package/engine/steering-constraints.js +31 -0
  161. package/engine/steering-store.js +10 -2
  162. package/engine/steering.js +14 -2
  163. package/engine/timeout.js +404 -111
  164. package/engine/untrusted-fence.js +24 -10
  165. package/engine/watch-actions.js +7 -2
  166. package/engine/watches.js +353 -51
  167. package/engine/work-item-validation.js +561 -0
  168. package/engine/work-items-store.js +61 -1
  169. package/engine/worktree-gc.js +656 -37
  170. package/engine/worktree-preflight.js +154 -0
  171. package/engine.js +2068 -342
  172. package/package.json +1 -1
  173. package/playbooks/_pr-description-audit.md +88 -41
  174. package/playbooks/build-fix-complex.md +3 -3
  175. package/playbooks/docs.md +2 -2
  176. package/playbooks/fix.md +18 -18
  177. package/playbooks/implement.md +18 -18
  178. package/playbooks/qa-session-draft.md +10 -12
  179. package/playbooks/qa-session-execute.md +16 -12
  180. package/playbooks/qa-validate.md +8 -7
  181. package/playbooks/shared-rules.md +45 -0
  182. package/playbooks/test.md +4 -0
  183. package/prompts/cc-system.md +1 -1
  184. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
package/engine/github.js CHANGED
@@ -7,8 +7,14 @@
7
7
  const shared = require('./shared');
8
8
  const { exec, execAsync, getProjects, readWorkItems, readPullRequests, safeJson, safeJsonArr, safeWrite, mutateMetrics, mutatePullRequests, MINIONS_DIR, getPrLinks, backfillPrPrdItems, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, ENGINE_DEFAULTS, createThrottleTracker, getProjectOrg } = shared;
9
9
  const { getPrs } = require('./queries');
10
- const { isPreviewStatusBody, hasMinionsMarker, hasVerdictPrefix } = require('./comment-classifier');
10
+ const {
11
+ isPreviewStatusBody,
12
+ hasMinionsMarker,
13
+ hasVerdictPrefix,
14
+ classifyNonActionableAutomationComment,
15
+ } = require('./comment-classifier');
11
16
  const { wrapUntrusted, buildSource } = require('./untrusted-fence');
17
+ const reviewLearning = require('./review-learning');
12
18
  const ghToken = require('./gh-token');
13
19
  const path = require('path');
14
20
 
@@ -176,19 +182,16 @@ function _isNonActionableComment(c, config = {}) {
176
182
  const ignoredAuthors = new Set((config.engine?.ignoredCommentAuthors || []).map(a => String(a).toLowerCase()));
177
183
  const login = String(c?.user?.login || '').toLowerCase();
178
184
  if (ignoredAuthors.has(login)) return true;
179
- // W-mqr41te300026723: removed blanket _isGitHubBotComment author-type drop.
180
- // Bot comments are now filtered by CONTENT (isPreviewStatusBody below), not
181
- // author type symmetric with ADO's pollPrHumanComments which has no
182
- // user.type==='bot' equivalent and relies solely on body-content + marker gates.
185
+ // W-mqr41te300026723: no blanket bot-author drop. Known automation noise
186
+ // requires a content signature; identity is consulted only for exact
187
+ // producer envelopes such as the visual-evidence GitHub Actions status.
183
188
  if (_isAgentComment(c)) return true;
184
- // P-f23classifier (F2): body-only preview/CI-report check is now host-
185
- // agnostic. The legacy `_isCiReportCommentBody` body match (Coverage /
186
- // Build / Test / Deploy / Pipeline headings + shields.io badges) is the
187
- // strict subset of isPreviewStatusBody, so a single call covers both
188
- // pre-existing branches. Bot-author gated preview matches still go
189
- // through _isPreviewStatusComment for callers that want the narrower
190
- // bot-only signal exported for testing.
191
- if (isPreviewStatusBody(c?.body)) return true;
189
+ const automationReason = classifyNonActionableAutomationComment({
190
+ host: 'github',
191
+ body: c?.body,
192
+ author: c?.user,
193
+ });
194
+ if (automationReason) return true;
192
195
  if (_isMinionsAuthoredComment(c)) return true;
193
196
  return false;
194
197
  }
@@ -1259,7 +1262,18 @@ async function pollPrHumanComments(config) {
1259
1262
  const allComments = [
1260
1263
  ...(comments || []).map(c => ({ ...c, _type: 'issue' })),
1261
1264
  ...(Array.isArray(reviewComments) ? reviewComments : []).map(c => ({ ...c, _type: 'review' }))
1262
- ];
1265
+ ].filter(c => c?.id != null);
1266
+ for (const c of allComments) {
1267
+ c._commentSource = reviewLearning.buildReviewCommentSource({
1268
+ host: 'github',
1269
+ commentType: c._type,
1270
+ commentId: c.id,
1271
+ author: c.user?.login || 'Human',
1272
+ createdAt: c.created_at,
1273
+ updatedAt: c.updated_at || c.created_at,
1274
+ content: c.body || '',
1275
+ });
1276
+ }
1263
1277
 
1264
1278
  // Backfill viewerDidAuthor on each comment so _isMinionsAuthoredComment
1265
1279
  // can actually fire on production traffic. No-op when GraphQL or a test
@@ -1274,17 +1288,23 @@ async function pollPrHumanComments(config) {
1274
1288
  // timestamp differs from what we already saw are queued as new feedback so
1275
1289
  // a follow-up fix re-dispatches. First-seen comments and re-polls of
1276
1290
  // already-seen edits do NOT route through this path.
1277
- const prevEditsSeen = (pr.humanFeedback && pr.humanFeedback.editsSeen) || {};
1278
1291
  const editsInput = allComments.map(c => ({
1279
- id: c.id,
1292
+ id: c._commentSource.key,
1280
1293
  createdRaw: c.created_at,
1281
1294
  updatedRaw: c.updated_at,
1282
1295
  }));
1296
+ const persistedEditsSeen = pr.humanFeedback?.editsSeen || {};
1297
+ const prevEditsSeen = reviewLearning.migrateLegacyReviewCommentEdits(
1298
+ persistedEditsSeen,
1299
+ editsInput.map(entry => entry.id),
1300
+ );
1301
+ const editsMigrationChanged = JSON.stringify(prevEditsSeen) !== JSON.stringify(persistedEditsSeen);
1283
1302
  // Sort chronologically by updated timestamp so FIFO cap eviction keeps the
1284
1303
  // most-recently-edited entries (the ones most likely to be re-edited).
1285
1304
  editsInput.sort((a, b) => String(a.updatedRaw || '').localeCompare(String(b.updatedRaw || '')));
1286
- const { editsSeen, newlyEditedIds, changed: editsSeenChanged } =
1305
+ const { editsSeen, newlyEditedIds, changed } =
1287
1306
  shared.buildEditsSeen(editsInput, prevEditsSeen);
1307
+ const editsSeenChanged = changed || editsMigrationChanged;
1288
1308
 
1289
1309
  // Collect comments that should advance the cutoff separately from comments
1290
1310
  // that should dispatch a fix. Informational bot/status comments and
@@ -1292,7 +1312,6 @@ async function pollPrHumanComments(config) {
1292
1312
  const allCommentDates = [];
1293
1313
  const allCommentEntries = [];
1294
1314
  const newComments = [];
1295
- const seenInNewComments = new Set();
1296
1315
  const nonActionableCommentKeys = new Set();
1297
1316
  const nonActionableCommentIds = new Set();
1298
1317
 
@@ -1317,6 +1336,8 @@ async function pollPrHumanComments(config) {
1317
1336
  author: c.user?.login || 'Human',
1318
1337
  content: c.body || '',
1319
1338
  date,
1339
+ createdAt: c.created_at || date,
1340
+ updatedAt: c.updated_at || c.created_at || date,
1320
1341
  _isAgent: false,
1321
1342
  // Issue #2994: namespace GH comment ids by API source ("issue" from
1322
1343
  // `/issues/N/comments` vs "review" from `/pulls/N/comments`). Each
@@ -1327,17 +1348,20 @@ async function pollPrHumanComments(config) {
1327
1348
  _type: c._type,
1328
1349
  };
1329
1350
  entry.commentKey = _entryCommentKey(entry);
1351
+ entry.commentSource = c._commentSource;
1330
1352
  allCommentEntries.push(entry);
1331
1353
 
1332
1354
  if (dateMs && dateMs > cutoffMs) {
1333
1355
  newComments.push(entry);
1334
- seenInNewComments.add(String(c.id));
1335
- } else if (c.id != null && newlyEditedIds.has(String(c.id))) {
1356
+ } else if (newlyEditedIds.has(entry.commentSource.key)) {
1336
1357
  // F6: pre-cutoff comment that was newly edited — route into newComments
1337
1358
  // so a fresh fix dispatches, but tag with the edit timestamp so the
1338
1359
  // fix-agent prompt context flags it as new.
1339
- newComments.push({ ...entry, date: editsSeen[String(c.id)] || date, _isEditedRedispatch: true });
1340
- seenInNewComments.add(String(c.id));
1360
+ entry._isEditedRedispatch = true;
1361
+ newComments.push({
1362
+ ...entry,
1363
+ date: editsSeen[entry.commentSource.key] || entry.commentSource.updatedAt || date,
1364
+ });
1341
1365
  }
1342
1366
  }
1343
1367
 
@@ -1348,6 +1372,15 @@ async function pollPrHumanComments(config) {
1348
1372
  newComments.sort((a, b) => a.date.localeCompare(b.date));
1349
1373
  const latestActionableComment = allCommentEntries[allCommentEntries.length - 1] || null;
1350
1374
  const currentFeedback = pr.humanFeedback || {};
1375
+ const observedCommentSources = reviewLearning.normalizeReviewCommentSources(
1376
+ allCommentEntries.map(entry => entry.commentSource),
1377
+ );
1378
+ const commentSources = reviewLearning.selectReviewCommentSourceSnapshot(
1379
+ currentFeedback,
1380
+ observedCommentSources,
1381
+ );
1382
+ const commentSourcesChanged = JSON.stringify(commentSources)
1383
+ !== JSON.stringify(reviewLearning.normalizeReviewCommentSources(currentFeedback.commentSources));
1351
1384
  const currentCommentKey = String(currentFeedback.lastProcessedCommentKey || '');
1352
1385
  const currentCommentId = String(currentFeedback.lastProcessedCommentId || '');
1353
1386
  const currentCauseIsNonActionable = !!(
@@ -1366,6 +1399,7 @@ async function pollPrHumanComments(config) {
1366
1399
  delete nextFeedback.lastProcessedCommentId;
1367
1400
  delete nextFeedback.lastProcessedCommentKey;
1368
1401
  delete nextFeedback.feedbackContent;
1402
+ delete nextFeedback.commentSources;
1369
1403
  }
1370
1404
  return true;
1371
1405
  };
@@ -1376,12 +1410,15 @@ async function pollPrHumanComments(config) {
1376
1410
  pr.humanFeedback = { ...currentFeedback, lastProcessedCommentDate: allNewDates.sort().pop() };
1377
1411
  repairNonActionableCause(pr.humanFeedback);
1378
1412
  if (editsSeenChanged) pr.humanFeedback.editsSeen = editsSeen;
1413
+ if (commentSources.length > 0) pr.humanFeedback.commentSources = commentSources;
1379
1414
  return true; // non-actionable comments only — persist cutoff without triggering fix
1380
1415
  }
1381
1416
  if (newComments.length === 0) {
1382
- if (currentCauseIsNonActionable) {
1417
+ if (currentCauseIsNonActionable || commentSourcesChanged) {
1383
1418
  pr.humanFeedback = { ...currentFeedback };
1384
1419
  repairNonActionableCause(pr.humanFeedback);
1420
+ if (commentSources.length > 0) pr.humanFeedback.commentSources = commentSources;
1421
+ else delete pr.humanFeedback.commentSources;
1385
1422
  if (editsSeenChanged) pr.humanFeedback.editsSeen = editsSeen;
1386
1423
  return true;
1387
1424
  }
@@ -1403,14 +1440,18 @@ async function pollPrHumanComments(config) {
1403
1440
  // attacker-controlled part) lands inside.
1404
1441
  const feedbackContent = allCommentEntries
1405
1442
  .map(c => {
1406
- const isNew = (new Date(c.date).getTime() || 0) > cutoffMs;
1443
+ const isNew = c._isEditedRedispatch || (new Date(c.date).getTime() || 0) > cutoffMs;
1444
+ const displayDate = c._isEditedRedispatch
1445
+ ? (c.commentSource.updatedAt || c.updatedAt || c.date)
1446
+ : c.date;
1407
1447
  const cleanedBody = String(c.content || '').replace(/@minions\s*/gi, '').trim();
1408
1448
  const source = buildSource('pr-comment', {
1409
1449
  host: 'gh', slug, number: prNum, author: c.author || 'unknown',
1450
+ key: c.commentSource.key,
1410
1451
  });
1411
1452
  const fenced = wrapUntrusted(cleanedBody, source);
1412
1453
  const bodyForPrompt = fenced || cleanedBody;
1413
- return `${isNew ? '**[NEW]** ' : ''}**${c.author}** (${c.date}):\n${bodyForPrompt}`;
1454
+ return `${isNew ? '**[NEW]** ' : ''}**${c.author}** (${displayDate}):\n${bodyForPrompt}`;
1414
1455
  })
1415
1456
  .join('\n\n---\n\n');
1416
1457
 
@@ -1426,6 +1467,7 @@ async function pollPrHumanComments(config) {
1426
1467
  lastProcessedCommentKey: newComments[newComments.length - 1].commentKey || _entryCommentKey(newComments[newComments.length - 1]),
1427
1468
  pendingFix: true,
1428
1469
  feedbackContent,
1470
+ commentSources: observedCommentSources,
1429
1471
  editsSeen,
1430
1472
  };
1431
1473
 
@@ -0,0 +1,313 @@
1
+ /**
2
+ * engine/harness-context.js — "Repo harnesses used" PR-body section.
3
+ *
4
+ * W-mrtdmtwq000kf391. When Minions opens/updates a PR for a dispatch, we surface
5
+ * the repo HARNESS assets that were actually USED by THAT dispatch's runtime +
6
+ * project, so the PR reviewer can see which CLAUDE.md / AGENTS.md / MCP / skill
7
+ * context the agent actually ran with. The report is the intersection of
8
+ * "configured" and "actually applied by the resolved runtime" — categories that
9
+ * produced nothing are omitted entirely (no "none" filler), and the rendered
10
+ * block is kept compact (a short bullet list, no preamble).
11
+ *
12
+ * This module is pure REPORTING. It reuses the same read-only harness-diagnostics
13
+ * source that backs `GET /api/harness/diagnostics` and `minions doctor --harness`
14
+ * (engine/preflight.js `_runtimeHarnessRows`) plus the CLAUDE.md propagation
15
+ * discovery (engine/claude-md-context.js). It never propagates, injects,
16
+ * suppresses, or attests any repository harness asset — the runtime-adapter
17
+ * harness methods are diagnostic-only (see docs/harness-propagation.md).
18
+ *
19
+ * engine/playbook.js renders the section into a dispatch prompt appendix that
20
+ * instructs the agent to append the fenced block verbatim to its PR body. The
21
+ * block is delimited by stable HTML-comment markers so a PR-description refresh
22
+ * (the description audit) can replace it in place instead of duplicating it.
23
+ *
24
+ * No engine state, no locking, no writes.
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const os = require('os');
29
+ const path = require('path');
30
+
31
+ const HARNESS_SECTION_HEADING = '## Repo harnesses used';
32
+ const HARNESS_BLOCK_START = '<!-- minions:harnesses:start -->';
33
+ const HARNESS_BLOCK_END = '<!-- minions:harnesses:end -->';
34
+
35
+ // Defensive caps so a pathological skills dir or MCP config can never balloon
36
+ // the PR body / prompt appendix.
37
+ const MAX_NAMES = 25;
38
+
39
+ function _existing(rows) {
40
+ return (Array.isArray(rows) ? rows : []).filter(r => r && r.exists && r.path);
41
+ }
42
+
43
+ // Enumerate skill NAMES: each immediate subdirectory of an existing skill root
44
+ // is one skill (matches how Claude/Copilot/Codex discover skills from a root).
45
+ function _listSkillNames(rows, existsFn) {
46
+ const names = new Set();
47
+ for (const r of _existing(rows)) {
48
+ if (typeof existsFn === 'function' && !existsFn(r.path)) continue;
49
+ let entries;
50
+ try { entries = fs.readdirSync(r.path, { withFileTypes: true }); }
51
+ catch { continue; }
52
+ for (const ent of entries) {
53
+ try { if (ent.isDirectory()) names.add(ent.name); } catch { /* ignore */ }
54
+ if (names.size >= MAX_NAMES) break;
55
+ }
56
+ if (names.size >= MAX_NAMES) break;
57
+ }
58
+ return [...names].sort().slice(0, MAX_NAMES);
59
+ }
60
+
61
+ // Does a native-discovery runtime actually SEE an AGENTS.md this dispatch?
62
+ // Copilot/Codex walk up from their cwd (the dispatch worktree) to find the
63
+ // nearest AGENTS.md. We report the category only when one actually applies —
64
+ // checking the worktree root plus any dispatch path hints — so the block never
65
+ // claims discovery that produced nothing.
66
+ function _agentsMdApplied({ scanRoot, pathHints = [], existsFn = fs.existsSync }) {
67
+ if (!scanRoot) return false;
68
+ const dirs = new Set([scanRoot]);
69
+ for (const h of (Array.isArray(pathHints) ? pathHints : [])) {
70
+ if (h && typeof h === 'string') {
71
+ try { dirs.add(path.resolve(scanRoot, h)); } catch { /* ignore bad hint */ }
72
+ }
73
+ }
74
+ for (const d of dirs) {
75
+ try { if (existsFn(path.join(d, 'AGENTS.md'))) return true; } catch { /* ignore */ }
76
+ }
77
+ return false;
78
+ }
79
+
80
+ // Enumerate MCP server NAMES by reading the keys of each existing MCP config
81
+ // file (the cross-runtime `mcpServers` / `servers` / `mcp` object).
82
+ function _listMcpServerNames(rows) {
83
+ const names = new Set();
84
+ for (const r of _existing(rows)) {
85
+ let json;
86
+ try { json = JSON.parse(fs.readFileSync(r.path, 'utf8')); }
87
+ catch { continue; }
88
+ const servers = (json && (json.mcpServers || json.servers || json.mcp)) || {};
89
+ if (servers && typeof servers === 'object' && !Array.isArray(servers)) {
90
+ for (const k of Object.keys(servers)) {
91
+ names.add(k);
92
+ if (names.size >= MAX_NAMES) break;
93
+ }
94
+ }
95
+ if (names.size >= MAX_NAMES) break;
96
+ }
97
+ return [...names].sort().slice(0, MAX_NAMES);
98
+ }
99
+
100
+ /**
101
+ * Compute the harness inventory for a dispatch's runtime + project.
102
+ *
103
+ * The inventory reflects the RESOLVED dispatch — the runtime's actual
104
+ * invocation options and the actual dispatched checkout — not static
105
+ * discovery. Callers thread the values the engine already resolved so the
106
+ * report never over-claims context the runtime never saw:
107
+ * - `bareMode` — a `--bare` invocation strips the runtime's native
108
+ * repo-context discovery (CLAUDE.md/AGENTS.md).
109
+ * - `propagateClaudeMd` — the engine only injects CLAUDE.md into non-native
110
+ * runtimes when this resolves true.
111
+ * - `claudeMdInjectedFiles`— the authoritative list of files the engine
112
+ * actually injected this dispatch (preferred over
113
+ * re-discovery, which can diverge).
114
+ * - `worktreePath` — the actual dispatch checkout; project-scoped MCP /
115
+ * skill discovery scans THIS, not the operator
116
+ * checkout (`project.localPath`) the runtime never ran in.
117
+ *
118
+ * @param {object} opts
119
+ * @param {string} opts.runtimeCli Resolved runtime name (copilot|claude|codex).
120
+ * @param {object} [opts.project] Project object with `localPath` / `name`.
121
+ * @param {string} [opts.worktreePath] Dispatch checkout root for project-scoped scan.
122
+ * @param {boolean} [opts.bareMode] Resolved `--bare` invocation option.
123
+ * @param {boolean} [opts.nativeClaudeMd] Runtime's CLAUDE.md native-discovery
124
+ * capability (derived from the adapter when omitted).
125
+ * @param {string[]} [opts.claudeMdInjectedFiles] Authoritative injected-file result.
126
+ * @param {boolean} [opts.propagateClaudeMd] Resolved propagation flag (default true).
127
+ * @param {string[]} [opts.pathHints] CLAUDE.md discovery hints (fallback only).
128
+ * @param {string} [opts.homeDir] Override for tests.
129
+ * @param {function} [opts.existsFn] Override for tests.
130
+ * The returned inventory reports only harnesses ACTUALLY applied by the resolved
131
+ * runtime for this dispatch — the intersection of "configured" and "in effect".
132
+ * Categories that produced nothing are left empty and the renderer omits them.
133
+ *
134
+ * Skills are the exception: the engine cannot know at prompt-render time WHICH
135
+ * of the available project skills the agent will actually invoke, and listing
136
+ * every discovered/available skill is exactly the "discovered not used" bug the
137
+ * PR-body block must avoid. So `availableSkills` is a REFERENCE list (project
138
+ * scope only, BLOCKING-3 no user-scope leak) that the playbook hands to the
139
+ * agent; the agent reports the subset it actually used. It is intentionally NOT
140
+ * rendered into the block by `buildHarnessSection`.
141
+ *
142
+ * @returns {{runtime:string, claudeMd:{native:boolean, files:string[]},
143
+ * agentsMd:boolean, mcp:string[], availableSkills:string[]}}
144
+ */
145
+ function computeDispatchHarnessInventory({
146
+ runtimeCli,
147
+ project = null,
148
+ worktreePath = '',
149
+ bareMode = false,
150
+ nativeClaudeMd = undefined,
151
+ claudeMdInjectedFiles = undefined,
152
+ propagateClaudeMd = true,
153
+ pathHints = [],
154
+ homeDir = os.homedir(),
155
+ existsFn = fs.existsSync,
156
+ } = {}) {
157
+ const runtime = String(runtimeCli || '').trim() || 'copilot';
158
+ const inv = {
159
+ runtime,
160
+ claudeMd: { native: false, files: [] },
161
+ agentsMd: false,
162
+ mcp: [],
163
+ availableSkills: [],
164
+ };
165
+
166
+ let claudeMdCtx = null;
167
+ let preflight = null;
168
+ let registry = null;
169
+ try { claudeMdCtx = require('./claude-md-context'); } catch { /* optional */ }
170
+ try { preflight = require('./preflight'); } catch { /* optional */ }
171
+ try { registry = require('./runtimes'); } catch { /* optional */ }
172
+
173
+ // CLAUDE.md discovery is a runtime CAPABILITY (Claude reads CLAUDE.md itself;
174
+ // Copilot/Codex do not). Prefer the caller-resolved value; fall back to the
175
+ // adapter capability for callers that don't thread it.
176
+ const nativeCapable = (typeof nativeClaudeMd === 'boolean')
177
+ ? nativeClaudeMd
178
+ : (claudeMdCtx ? !!claudeMdCtx.runtimeAutoDiscoversClaudeMd(runtime) : false);
179
+
180
+ // A `--bare` invocation strips the runtime's native repo-context discovery, so
181
+ // native CLAUDE.md discovery is only in effect when the runtime is capable AND
182
+ // not running bare.
183
+ inv.claudeMd.native = nativeCapable && !bareMode;
184
+
185
+ if (!nativeCapable) {
186
+ // Copilot/Codex: the engine's bounded propagation injects CLAUDE.md only
187
+ // when the propagation flag is on. Prefer the authoritative injected-file
188
+ // result the engine actually produced this dispatch; fall back to discovery
189
+ // (under the same propagate gate) for callers that don't thread it. Scan the
190
+ // dispatch worktree, not the operator checkout the runtime never ran in.
191
+ if (propagateClaudeMd) {
192
+ if (Array.isArray(claudeMdInjectedFiles)) {
193
+ inv.claudeMd.files = claudeMdInjectedFiles.slice(0, MAX_NAMES);
194
+ } else if (claudeMdCtx) {
195
+ const scanRoot = worktreePath || (project && project.localPath) || '';
196
+ if (scanRoot) {
197
+ try {
198
+ const files = claudeMdCtx.discoverClaudeMdFiles({
199
+ projectRoot: scanRoot,
200
+ pathHints: Array.isArray(pathHints) ? pathHints : [],
201
+ });
202
+ inv.claudeMd.files = files.map(f => f.relPath);
203
+ } catch { /* degrade to none */ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ // AGENTS.md: the runtimes that do NOT read CLAUDE.md natively (Copilot/Codex)
210
+ // DO discover AGENTS.md natively; Claude reads CLAUDE.md instead. Report the
211
+ // category only when the resolved runtime performs the discovery AND an
212
+ // AGENTS.md actually applies in the dispatch checkout the runtime ran in —
213
+ // never as a bare capability claim that produced nothing.
214
+ inv.agentsMd = nativeCapable
215
+ ? false
216
+ : _agentsMdApplied({
217
+ scanRoot: worktreePath || (project && project.localPath) || '',
218
+ pathHints: Array.isArray(pathHints) ? pathHints : [],
219
+ existsFn,
220
+ });
221
+
222
+ // MCP + skills: reuse the diagnostic harness-rows source, scoped to this
223
+ // single project's ACTUAL checkout (the worktree), and enumerate the concrete
224
+ // names present on disk. Only PROJECT-scoped rows are reported — user-scoped
225
+ // (personal) MCP/skill directories are NOT disclosed in a public PR body, as
226
+ // they can leak private integration/client identifiers (e.g. a personal skill
227
+ // dir named `private-client-alpha`).
228
+ //
229
+ // MCP servers configured in the project are loaded/available to the runtime,
230
+ // so they are reported directly. Skills, by contrast, are only AVAILABLE — the
231
+ // engine can't know which the agent invokes — so they populate `availableSkills`
232
+ // (a reference the playbook hands to the agent) and are NOT rendered into the
233
+ // block; the agent reports the subset it actually used.
234
+ let adapter = null;
235
+ if (registry) { try { adapter = registry.resolveRuntime(runtime); } catch { adapter = null; } }
236
+ if (preflight && adapter && typeof preflight._runtimeHarnessRows === 'function') {
237
+ const scanProject = project
238
+ ? (worktreePath ? { ...project, localPath: worktreePath } : project)
239
+ : null;
240
+ try {
241
+ const rows = preflight._runtimeHarnessRows(adapter, {
242
+ homeDir,
243
+ projects: scanProject ? [scanProject] : [],
244
+ existsFn,
245
+ });
246
+ inv.mcp = _listMcpServerNames(rows.mcpConfigProject || []);
247
+ inv.availableSkills = _listSkillNames(rows.skillRootsProject || [], existsFn);
248
+ } catch { /* degrade to empty */ }
249
+ }
250
+
251
+ return inv;
252
+ }
253
+
254
+ /**
255
+ * Render the fenced "Repo harnesses used" markdown block from an inventory.
256
+ * Compact and used-only: the Runtime line always anchors the block, and every
257
+ * other engine-determinable category is emitted ONLY when it actually applied —
258
+ * no "none"/"n/a" filler rows and no explanatory preamble.
259
+ *
260
+ * Skills are deliberately NOT rendered here. The engine cannot know which of the
261
+ * available project skills the agent actually invoked, and enumerating all of
262
+ * them is the "discovered not used" bug this block must avoid. The playbook
263
+ * instructs the agent to add a `- **Skills used:** …` line for the skills it
264
+ * actually used (from `inventory.availableSkills`), or omit it entirely.
265
+ *
266
+ * The HTML-comment markers make the block idempotently replaceable when a PR
267
+ * body is refreshed.
268
+ */
269
+ function buildHarnessSection(inventory) {
270
+ const inv = inventory || {};
271
+ const claudeMd = inv.claudeMd || { native: false, files: [] };
272
+
273
+ const lines = [
274
+ HARNESS_SECTION_HEADING,
275
+ '',
276
+ HARNESS_BLOCK_START,
277
+ `- **Runtime:** ${inv.runtime || 'unknown'}`,
278
+ ];
279
+
280
+ if (claudeMd.native) {
281
+ lines.push('- **CLAUDE.md:** native (claude)');
282
+ } else if (Array.isArray(claudeMd.files) && claudeMd.files.length) {
283
+ lines.push(`- **CLAUDE.md:** ${claudeMd.files.join(', ')}`);
284
+ }
285
+ if (inv.agentsMd) {
286
+ lines.push('- **AGENTS.md:** discovered');
287
+ }
288
+ if (Array.isArray(inv.mcp) && inv.mcp.length) {
289
+ lines.push(`- **MCP:** ${inv.mcp.join(', ')}`);
290
+ }
291
+
292
+ lines.push(HARNESS_BLOCK_END);
293
+ return lines.join('\n');
294
+ }
295
+
296
+ /**
297
+ * Convenience: compute the inventory for a dispatch and render the section.
298
+ * Returns `{ inventory, section }`. `section` is always a non-empty string.
299
+ */
300
+ function renderHarnessSectionForDispatch(opts = {}) {
301
+ const inventory = computeDispatchHarnessInventory(opts);
302
+ return { inventory, section: buildHarnessSection(inventory) };
303
+ }
304
+
305
+ module.exports = {
306
+ HARNESS_SECTION_HEADING,
307
+ HARNESS_BLOCK_START,
308
+ HARNESS_BLOCK_END,
309
+ MAX_NAMES,
310
+ computeDispatchHarnessInventory,
311
+ buildHarnessSection,
312
+ renderHarnessSectionForDispatch,
313
+ };
@@ -239,7 +239,7 @@ function deleteInboxEntriesOlderThan(timestampMs, { unreadOnly = false } = {}) {
239
239
  query += ' AND unread = 1';
240
240
  }
241
241
  const result = db.prepare(query).run(...params);
242
- if (result.changes > 0) _emitInboxEvent({ timestampMs, unreadOnly, action: 'delete_older_than' });
242
+ if (result.changes > 0) _emitInboxEvent({ timestampMs, unreadOnly, action: 'delete_older_than', deletedCount: result.changes });
243
243
  return { wrote: result.changes > 0, deletedCount: result.changes };
244
244
  });
245
245
  }