mandrel 2.31.0 → 2.33.0

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 (267) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +12 -9
  4. package/.agents/docs/agentrc-reference.json +63 -58
  5. package/.agents/docs/configuration.md +275 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +2 -2
  14. package/.agents/schemas/agentrc.schema.json +1955 -1410
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +16 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/audit-to-stories.js +160 -41
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +159 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +108 -124
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
  38. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
  39. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  40. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  41. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  42. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  43. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  44. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  45. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  46. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  47. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  48. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  49. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  50. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  51. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  52. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  53. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  54. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  55. package/.agents/scripts/lib/child-exec.js +193 -0
  56. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  57. package/.agents/scripts/lib/cli-args.js +64 -0
  58. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  59. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  60. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  61. package/.agents/scripts/lib/config/commands.js +3 -5
  62. package/.agents/scripts/lib/config/explain.js +5 -7
  63. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  64. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  65. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  66. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  67. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  68. package/.agents/scripts/lib/config/gates/index.js +5 -2
  69. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  70. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  71. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  72. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  73. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  74. package/.agents/scripts/lib/config/github.js +0 -1
  75. package/.agents/scripts/lib/config/limits.js +3 -48
  76. package/.agents/scripts/lib/config/qa.js +105 -0
  77. package/.agents/scripts/lib/config/runners.js +38 -16
  78. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  79. package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
  80. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  81. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  82. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  83. package/.agents/scripts/lib/crap-utils.js +84 -520
  84. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  85. package/.agents/scripts/lib/degraded-mode.js +2 -2
  86. package/.agents/scripts/lib/dependency-parser.js +20 -7
  87. package/.agents/scripts/lib/doc-tiers.js +3 -3
  88. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  89. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  90. package/.agents/scripts/lib/findings/provenance-field.js +135 -0
  91. package/.agents/scripts/lib/findings/route-finding.js +57 -8
  92. package/.agents/scripts/lib/fs-walk.js +52 -0
  93. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  94. package/.agents/scripts/lib/git-utils.js +16 -36
  95. package/.agents/scripts/lib/knip-config-resolver.js +181 -0
  96. package/.agents/scripts/lib/knip-entry-sync.js +508 -0
  97. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  98. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  99. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  100. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  101. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  102. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  103. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  104. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  105. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  106. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  107. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  108. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  109. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  110. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  111. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  112. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
  114. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
  115. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
  116. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
  117. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  118. package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
  119. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  120. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  131. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  132. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  133. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  134. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  135. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  136. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  137. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  138. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
  139. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  140. package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
  141. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  142. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  143. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  144. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  145. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  146. package/.agents/scripts/lib/signals/index.js +8 -6
  147. package/.agents/scripts/lib/signals/schema.js +20 -25
  148. package/.agents/scripts/lib/signals/write.js +8 -8
  149. package/.agents/scripts/lib/story-body/footer-block.js +97 -0
  150. package/.agents/scripts/lib/story-body/story-body.js +18 -81
  151. package/.agents/scripts/lib/temp-retention.js +1 -1
  152. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  153. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  154. package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
  156. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  157. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  158. package/.agents/scripts/lib/worktree-manager.js +2 -3
  159. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  160. package/.agents/scripts/pr-watch-with-update.js +7 -5
  161. package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
  162. package/.agents/scripts/providers/github/cache.js +2 -2
  163. package/.agents/scripts/providers/github/comments.js +6 -28
  164. package/.agents/scripts/providers/github/compose.js +0 -15
  165. package/.agents/scripts/providers/github/errors.js +10 -27
  166. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  167. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  168. package/.agents/scripts/providers/github.js +4 -7
  169. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  170. package/.agents/scripts/resolve-stories.js +21 -7
  171. package/.agents/scripts/run-lint.js +61 -61
  172. package/.agents/scripts/run-test-profile.js +6 -6
  173. package/.agents/scripts/run-verify.js +48 -30
  174. package/.agents/scripts/single-story-close.js +20 -0
  175. package/.agents/scripts/single-story-init.js +12 -35
  176. package/.agents/scripts/stories-wave-tick.js +192 -9
  177. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  178. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  179. package/.agents/skills/skills.index.json +1 -11
  180. package/.agents/workflows/audit-documentation.md +5 -6
  181. package/.agents/workflows/audit-to-stories.md +28 -2
  182. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  183. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  184. package/.agents/workflows/helpers/code-review.md +3 -8
  185. package/.agents/workflows/helpers/deliver-reference.md +30 -2
  186. package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
  187. package/.agents/workflows/helpers/plan-reference.md +76 -0
  188. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  189. package/.agents/workflows/mandrel-update.md +10 -10
  190. package/.agents/workflows/qa-assist.md +15 -20
  191. package/.agents/workflows/qa-explore.md +9 -8
  192. package/README.md +1 -1
  193. package/docs/CHANGELOG.md +58 -0
  194. package/lib/migrations/index.js +2 -0
  195. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  196. package/package.json +14 -5
  197. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  198. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  199. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  200. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  201. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  202. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  203. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  204. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  205. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  206. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  207. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  208. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  209. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  210. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  211. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  212. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  213. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  214. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  215. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  216. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  217. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  218. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  219. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  220. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  221. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  222. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  223. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  224. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  225. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  226. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  227. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  228. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  229. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  230. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  231. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  232. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  233. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  234. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  235. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  236. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  237. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  238. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  239. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  240. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  241. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  242. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  243. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  244. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  245. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  246. package/.agents/scripts/lib/planning-corpus.js +0 -37
  247. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  248. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  249. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  250. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  251. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  252. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  253. package/.agents/scripts/lib/signals/read.js +0 -268
  254. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  255. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  256. package/.agents/scripts/lib/story-plan.js +0 -379
  257. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  258. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  259. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  260. package/.agents/scripts/lint-baseline.js +0 -507
  261. package/.agents/scripts/providers/github/prs.js +0 -103
  262. package/.agents/scripts/signals-view.js +0 -309
  263. package/.agents/scripts/story-plan.js +0 -370
  264. package/.agents/scripts/sync-branch-from-base.js +0 -149
  265. package/.agents/scripts/validate-docs-freshness.js +0 -314
  266. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  267. package/.agents/workflows/helpers/signals.md +0 -112
@@ -35,11 +35,11 @@ import { fingerprintSeverity } from './severity.js';
35
35
  const SEP = '␟'; // unit separator — keeps fingerprint fields unambiguous
36
36
  const MARKER = 'audit-fingerprints:';
37
37
  const SEMANTIC_MARKER = 'audit-semantic-keys:';
38
- const SHA1_RE = /^[0-9a-f]{40}$/;
38
+ export const SHA1_RE = /^[0-9a-f]{40}$/;
39
39
  // A semantic key round-trips through a comma-joined footer, so it must not
40
40
  // carry a comma or a `>` (which would truncate the HTML comment). Both are
41
41
  // stripped when the key is built, so this guard is defence-in-depth.
42
- const SEMANTIC_KEY_RE = /^[^,>]+$/;
42
+ export const SEMANTIC_KEY_RE = /^[^,>]+$/;
43
43
 
44
44
  /**
45
45
  * Normalise a single scalar identity field to a stable string.
@@ -343,12 +343,59 @@ function decisionForIssue(issue) {
343
343
  return state === 'closed' ? 'regression-of-closed' : 'update-existing';
344
344
  }
345
345
 
346
+ /**
347
+ * Resolve the pool that **attributes** a finding, out of everything that
348
+ * confirmed it (Story #5045).
349
+ *
350
+ * Confirmation admits two different strengths of claim, and collapsing them
351
+ * was the source of two wrong routes:
352
+ *
353
+ * - An issue carrying the finding's exact **fingerprint** owns it. That is
354
+ * identity: this issue tracks *this* finding.
355
+ * - An issue matching only on the location-based **semantic key** is merely
356
+ * adjacent: it tracks *a* finding at the same `area␟primaryFile`.
357
+ *
358
+ * Owners win outright when any exist. Location-only matches are not discarded
359
+ * — they are the whole point of the semantic key and remain the pool when
360
+ * nothing carries the fingerprint (a reworded finding at an unchanged
361
+ * location). The pool is sorted by issue number so a genuine tie resolves to
362
+ * the earliest-filed issue rather than to whatever order the search port
363
+ * happened to return.
364
+ *
365
+ * @param {Array<{ number: number, state: string, body?: string }>} confirmed
366
+ * @param {string} sha
367
+ * @returns {Array<{ number: number, state: string }>}
368
+ */
369
+ function attributedPool(confirmed, sha) {
370
+ const owns = (issue) => issueCarriesFingerprint(issue, sha);
371
+ const owners = confirmed.filter(owns);
372
+ const pool = owners.length > 0 ? owners : confirmed.filter((i) => !owns(i));
373
+ return [...pool].sort((a, b) => (a?.number ?? 0) - (b?.number ?? 0));
374
+ }
375
+
346
376
  /**
347
377
  * Decide the final route from a confirmed-match pool (issues that both
348
- * surfaced in the candidate/search pass AND carry the finding's fingerprint
349
- * in their footer). Shared by both the semantic-first and fingerprint-only
350
- * code paths so the decision enum is identical regardless of how candidates
351
- * were gathered.
378
+ * surfaced in the candidate/search pass AND carry a confirming footer).
379
+ * Shared by both the semantic-first and fingerprint-only code paths so the
380
+ * decision enum is identical regardless of how candidates were gathered.
381
+ *
382
+ * **Attribution decides, not array order (Story #5045).** The pool used to be
383
+ * read flat, which produced two wrong answers whenever more than one issue
384
+ * confirmed:
385
+ *
386
+ * 1. Two open matches routed `duplicate` pinned to `open[0]` — whichever
387
+ * issue the search port happened to return first. With per-Story
388
+ * provenance that pick is answerable rather than arbitrary: the issue
389
+ * carrying the finding's own fingerprint owns it, and a sibling matching
390
+ * only by location does not.
391
+ * 2. Any open match at all masked a closed one, so a finding whose
392
+ * fingerprint is owned by a **closed** Story routed `update-existing`
393
+ * against an open neighbour — a genuine regression filed as a
394
+ * business-as-usual update. Attribution restores it: state is read off the
395
+ * owning issue, not off whatever else shares its location.
396
+ *
397
+ * {@link attributedPool} owns that selection; the decision below reads only
398
+ * the pool it returns.
352
399
  *
353
400
  * @param {Array<{ number: number, state: string }>} confirmed
354
401
  * @param {string} sha
@@ -359,7 +406,8 @@ function decideFromConfirmed(confirmed, sha) {
359
406
  return { decision: 'new', matchedIssue: null, fingerprint: sha };
360
407
  }
361
408
 
362
- const open = confirmed.filter((h) => normaliseField(h.state) === 'open');
409
+ const attributed = attributedPool(confirmed, sha);
410
+ const open = attributed.filter((h) => normaliseField(h.state) === 'open');
363
411
  if (open.length > 1) {
364
412
  return { decision: 'duplicate', matchedIssue: open[0], fingerprint: sha };
365
413
  }
@@ -371,7 +419,7 @@ function decideFromConfirmed(confirmed, sha) {
371
419
  };
372
420
  }
373
421
 
374
- const closed = confirmed[0];
422
+ const closed = attributed[0];
375
423
  return {
376
424
  decision: decisionForIssue(closed),
377
425
  matchedIssue: closed,
@@ -484,4 +532,5 @@ export const __testing = {
484
532
  decideFromConfirmed,
485
533
  issueCarriesSemanticKey,
486
534
  parseSemanticKeyFooter,
535
+ attributedPool,
487
536
  };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * fs-walk.js — the one recursive file walker the lint scripts share.
3
+ *
4
+ * `check-lifecycle-lint.js` and `lint-label-vocabulary.js` each carried a
5
+ * private copy of this generator, differing only in the extension they
6
+ * matched — the second one's comment said so outright ("mirrors the walker
7
+ * shape in check-lifecycle-lint.js"). Story #5024 extracted it: shrinking
8
+ * check-lifecycle-lint.js pushed its duplication percentage up on an unchanged
9
+ * clone count, which is the ratchet correctly pointing at boilerplate that
10
+ * should not have been copied twice.
11
+ *
12
+ * A missing directory yields nothing rather than throwing. That is
13
+ * load-bearing, not lenient: the lint surfaces are directories a deletion can
14
+ * legitimately remove (Story #5024 deleted `lifecycle/listeners/`), and a scan
15
+ * of an absent path is vacuously clean, not an error. Every other `readdir`
16
+ * failure — a permission error, a file where a directory was expected —
17
+ * propagates, so a genuinely broken scan cannot read as clean.
18
+ */
19
+
20
+ import { readdirSync } from 'node:fs';
21
+ import path from 'node:path';
22
+
23
+ /**
24
+ * Walk a directory tree synchronously, yielding absolute paths of every file
25
+ * whose name ends with `extension`.
26
+ *
27
+ * @param {string} dir Absolute directory to walk.
28
+ * @param {string} extension Extension to match, leading dot included (`.js`).
29
+ * @param {{ readDir?: typeof readdirSync }} [ports] Injectable for tests.
30
+ * @returns {Generator<string, void, void>}
31
+ */
32
+ export function* walkFilesByExtension(
33
+ dir,
34
+ extension,
35
+ { readDir = readdirSync } = {},
36
+ ) {
37
+ let entries;
38
+ try {
39
+ entries = readDir(dir, { withFileTypes: true });
40
+ } catch (err) {
41
+ if (err.code === 'ENOENT') return;
42
+ throw err;
43
+ }
44
+ for (const entry of entries) {
45
+ const p = path.join(dir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ yield* walkFilesByExtension(p, extension, { readDir });
48
+ } else if (entry.isFile() && entry.name.endsWith(extension)) {
49
+ yield p;
50
+ }
51
+ }
52
+ }
@@ -129,8 +129,8 @@ export function classifyBranchSeed({ localHas, remoteHas }) {
129
129
  * throws (the v2 standalone path has no concurrent creator to race).
130
130
  *
131
131
  * The asymmetric surrounding wrappers (merged-sweep, fast-forward,
132
- * donor-prime, workspace-verify, phase-timer) are deliberately NOT folded
133
- * in — they stay in their respective callers.
132
+ * donor-prime, workspace-verify) are deliberately NOT folded in — they stay
133
+ * in their respective callers.
134
134
  *
135
135
  * Caller-specific log lines and error text are passed in as the `messages`
136
136
  * data bag so behaviour stays byte-identical to the pre-extraction switches.
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import { execFileSync, spawnSync } from 'node:child_process';
31
+ import { execFileCapture, spawnCapture } from './child-exec.js';
31
32
 
32
33
  /**
33
34
  * Result of a `gitSpawn` invocation. Field semantics match `child_process.spawnSync`
@@ -40,23 +41,6 @@ import { execFileSync, spawnSync } from 'node:child_process';
40
41
  * @property {string} stderr - Trimmed stderr.
41
42
  */
42
43
 
43
- /**
44
- * Explicit stdout ceiling for every git invocation in this module.
45
- *
46
- * Both child-process runners default `maxBuffer` to 1 MB, at which point the
47
- * child is killed and the call fails with `ENOBUFS` for a reason unrelated to
48
- * git. The push path is the sharp edge: `git push` relays the whole `pre-push`
49
- * hook output, which is unbounded by design — this repo's hook emits a full
50
- * `check-baselines` envelope, measured at 2,166,643 bytes, so every Story
51
- * close failed at `phase: push` once hooks became reachable inside worktrees.
52
- *
53
- * 64 MB is the bound Story #4914 already set on `baselines/git-base.js` for
54
- * the identical failure, matching `run-test-profile.js`,
55
- * `audit-baselines/trend.js` and `audit-baselines/weights.js`. This module was
56
- * missed by that sweep.
57
- */
58
- const MAX_BUFFER_BYTES = 64 * 1024 * 1024;
59
-
60
44
  let _execFileSync = execFileSync;
61
45
  let _spawnSync = spawnSync;
62
46
 
@@ -129,26 +113,30 @@ export function __setGitRunners(exec, spawn) {
129
113
  * the interface returned by {@link createGitInterface} route through this —
130
114
  * they differ only in which `execFileSync` they hand it.
131
115
  *
116
+ * Buffer, shell and encoding policy belong to
117
+ * [`child-exec.js`](child-exec.js); this function contributes only the git
118
+ * specifics (argv prefix, cwd, scrubbed env).
119
+ *
132
120
  * @param {typeof execFileSync} exec
133
121
  * @param {string} cwd
134
122
  * @param {string[]} args
135
123
  * @returns {string} Trimmed stdout text.
136
124
  */
137
125
  function runGitSync(exec, cwd, args) {
138
- return exec('git', args, {
139
- cwd,
140
- encoding: 'utf8',
141
- stdio: ['pipe', 'pipe', 'pipe'],
142
- shell: false,
143
- env: cleanGitEnv(),
144
- maxBuffer: MAX_BUFFER_BYTES,
145
- }).trim();
126
+ return String(
127
+ execFileCapture('git', args, {
128
+ run: exec,
129
+ cwd,
130
+ stdio: ['pipe', 'pipe', 'pipe'],
131
+ env: cleanGitEnv(),
132
+ }),
133
+ ).trim();
146
134
  }
147
135
 
148
136
  /**
149
137
  * The **single** non-throwing git runner — the `spawnSync` counterpart of
150
138
  * {@link runGitSync}, normalising `status`/stdout/stderr into a
151
- * {@link GitResult}.
139
+ * {@link GitResult} via the shared exec surface.
152
140
  *
153
141
  * @param {typeof spawnSync} spawn
154
142
  * @param {string} cwd
@@ -156,19 +144,11 @@ function runGitSync(exec, cwd, args) {
156
144
  * @returns {GitResult}
157
145
  */
158
146
  function runGitSpawn(spawn, cwd, args) {
159
- const result = spawn('git', args, {
147
+ return spawnCapture('git', args, {
148
+ run: spawn,
160
149
  cwd,
161
- stdio: 'pipe',
162
- encoding: 'utf-8',
163
- shell: false,
164
150
  env: cleanGitEnv(),
165
- maxBuffer: MAX_BUFFER_BYTES,
166
151
  });
167
- return {
168
- status: result.status ?? 1,
169
- stdout: (result.stdout ?? '').trim(),
170
- stderr: (result.stderr ?? '').trim(),
171
- };
172
152
  }
173
153
 
174
154
  /**
@@ -0,0 +1,181 @@
1
+ /**
2
+ * knip-config-resolver.js — resolve a repository's knip configuration the way
3
+ * knip itself would, and hand back its declared entry patterns.
4
+ *
5
+ * Split out of `knip-entry-sync.js` (Story #5039) because these are two
6
+ * concerns that change for different reasons: *what knip's config says* tracks
7
+ * knip's own releases, while *which CLIs something invokes* tracks this
8
+ * repository's callers. Keeping them in one file also pushed that module's
9
+ * maintainability index below its floor.
10
+ *
11
+ * Why not read the file ourselves: #5026 hardcoded
12
+ * `JSON.parse(<root>/knip.json)`, so all seven of knip's other config
13
+ * locations plus `package.json#knip` resolved to ENOENT and failed the gate
14
+ * closed. That made it unusable in exactly the repositories that adopt the
15
+ * shared `knip.base.json` — knip 6 has no root-level `extends`, so inheriting
16
+ * the base means spreading it inside a TypeScript module, which a static
17
+ * `knip.json` structurally cannot express.
18
+ *
19
+ * Going through `createOptions` also drops the assumption that `entry` is
20
+ * *statically declared*. A config may build the array programmatically, so
21
+ * even a purpose-built `knip.config.ts` parser would read the wrong thing;
22
+ * knip evaluates the module and returns the computed value.
23
+ */
24
+
25
+ /**
26
+ * Load knip's own config resolver.
27
+ *
28
+ * Deliberately a dynamic import behind an injectable seam. `knip` is declared
29
+ * in `runtime-deps.json` as an **optional** dependency, mirroring `typescript`:
30
+ * the `.agents/` payload is materialized into consumers that may not run knip
31
+ * at all, so an unresolvable `knip` must degrade to the skip path rather than
32
+ * crash the gate at module load — and must never be preflight-blocked.
33
+ *
34
+ * @returns {Promise<{ createOptions?: Function }>}
35
+ */
36
+ function importKnipSession() {
37
+ return import('knip/session');
38
+ }
39
+
40
+ /**
41
+ * Collect every declared entry pattern from a resolved knip configuration.
42
+ *
43
+ * Entries live in two places and #5026 read only the first. A pnpm-workspace
44
+ * config — the shape that made this gate unusable in `Beestera/swarm-os` —
45
+ * declares them per workspace, so a config with no top-level `entry` at all is
46
+ * still fully enumerated. A named workspace's patterns are workspace-relative
47
+ * and are joined to the workspace name; the root workspace (`.`) is already
48
+ * repo-relative. A leading `!` is knip's *negation* marker (as opposed to the
49
+ * trailing production marker) and stays at the front of the pattern rather
50
+ * than getting buried behind the workspace prefix.
51
+ *
52
+ * `sawEntryArray` separates "declared nothing under `.agents/scripts/`" — a
53
+ * legitimate result, reported as `missing` divergences — from "this config
54
+ * enumerates no entry points anywhere", which leaves the gate nothing to
55
+ * compare against.
56
+ *
57
+ * @param {object} parsedConfig knip's schema-parsed configuration
58
+ * @returns {{ patterns: string[], sawEntryArray: boolean }}
59
+ */
60
+ function collectEntryPatterns(parsedConfig) {
61
+ const patterns = [];
62
+ let sawEntryArray = false;
63
+
64
+ const push = (value, prefix) => {
65
+ if (typeof value !== 'string') return;
66
+ if (!prefix) patterns.push(value);
67
+ else if (value.startsWith('!'))
68
+ patterns.push(`!${prefix}${value.slice(1)}`);
69
+ else patterns.push(`${prefix}${value}`);
70
+ };
71
+
72
+ const addAll = (entry, prefix) => {
73
+ if (!Array.isArray(entry)) return;
74
+ sawEntryArray = true;
75
+ for (const value of entry) push(value, prefix);
76
+ };
77
+
78
+ addAll(parsedConfig?.entry, '');
79
+
80
+ const workspaces = parsedConfig?.workspaces;
81
+ if (workspaces && typeof workspaces === 'object') {
82
+ for (const [name, workspace] of Object.entries(workspaces)) {
83
+ const trimmed = name.replace(/^\.\/+/, '').replace(/\/+$/, '');
84
+ const prefix = trimmed === '' || trimmed === '.' ? '' : `${trimmed}/`;
85
+ addAll(workspace?.entry, prefix);
86
+ }
87
+ }
88
+
89
+ return { patterns, sawEntryArray };
90
+ }
91
+
92
+ /**
93
+ * Resolve the entry patterns knip would see for `repoRoot`.
94
+ *
95
+ * Three outcomes, deliberately distinct — collapsing the first two is how a
96
+ * broken configuration would come to look like an absent one:
97
+ *
98
+ * skipped — nothing to check (no config file, no `package.json#knip`, or no
99
+ * resolvable `knip`). The gate exits 0. This is what makes it safe to wire
100
+ * into every consumer, matching `qa.gherkinLint`'s opt-in posture.
101
+ * error — a configuration exists but could not be resolved, or enumerates no
102
+ * entry points at all. The gate exits 2.
103
+ * resolved — `patterns` carries every declared entry, top-level and
104
+ * per-workspace, with knip's trailing `!` production markers intact.
105
+ *
106
+ * @param {{ repoRoot: string, loadKnipSession?: () => Promise<object> }} opts
107
+ * @returns {Promise<{
108
+ * patterns: string[],
109
+ * configFilePath: string | null,
110
+ * skipped: string | null,
111
+ * error: string | null,
112
+ * }>}
113
+ */
114
+ export async function resolveKnipEntryPatterns({
115
+ repoRoot,
116
+ loadKnipSession = importKnipSession,
117
+ }) {
118
+ const nothing = { patterns: [], configFilePath: null };
119
+
120
+ let createOptions;
121
+ try {
122
+ ({ createOptions } = await loadKnipSession());
123
+ } catch (error) {
124
+ return {
125
+ ...nothing,
126
+ skipped: `the "knip" package is not resolvable from ${repoRoot} (${error.message})`,
127
+ error: null,
128
+ };
129
+ }
130
+ if (typeof createOptions !== 'function') {
131
+ return {
132
+ ...nothing,
133
+ skipped: null,
134
+ error:
135
+ 'the installed "knip" does not export createOptions from "knip/session" — this gate needs knip 6 or newer',
136
+ };
137
+ }
138
+
139
+ let options;
140
+ try {
141
+ options = await createOptions({ cwd: repoRoot, args: {} });
142
+ } catch (error) {
143
+ return {
144
+ ...nothing,
145
+ skipped: null,
146
+ error: `cannot resolve the knip configuration: ${error.message}`,
147
+ };
148
+ }
149
+
150
+ const configFilePath = options?.configFilePath ?? null;
151
+ if (!configFilePath) {
152
+ return {
153
+ ...nothing,
154
+ skipped: `no knip configuration found under ${repoRoot} (no config file, no package.json#knip)`,
155
+ error: null,
156
+ };
157
+ }
158
+
159
+ const { patterns, sawEntryArray } = collectEntryPatterns(
160
+ options?.parsedConfig,
161
+ );
162
+ if (!sawEntryArray) {
163
+ return {
164
+ patterns: [],
165
+ configFilePath,
166
+ skipped: null,
167
+ error: `${configFilePath} declares no "entry" array, at the top level or in any workspace`,
168
+ };
169
+ }
170
+
171
+ return { patterns, configFilePath, skipped: null, error: null };
172
+ }
173
+
174
+ /**
175
+ * Test-only seam. Not API — `collectEntryPatterns` is exercised directly so the
176
+ * workspace-prefix and negation rules can be pinned without a fixture tree.
177
+ */
178
+ export const __testing = Object.freeze({
179
+ collectEntryPatterns,
180
+ importKnipSession,
181
+ });