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
@@ -17,6 +17,10 @@
17
17
 
18
18
  import { createHash } from 'node:crypto';
19
19
  import { applyBlockedByDependencies } from '../../../providers/github/blocked-by-add.js';
20
+ import {
21
+ normalizeOwnedProvenance,
22
+ ownedProvenanceSource,
23
+ } from '../../findings/provenance-field.js';
20
24
  import { carryProvenanceFooters } from '../../findings/route-finding.js';
21
25
  import { Logger } from '../../Logger.js';
22
26
  import { AGENT_LABELS, TYPE_LABELS } from '../../label-constants.js';
@@ -273,7 +277,6 @@ function bodyObjectFromTicket(ticket) {
273
277
  wide: ticket.wide ?? null,
274
278
  reason_to_exist: ticket.reason_to_exist ?? null,
275
279
  depends_on: ticket.depends_on ?? [],
276
- estimated_test_files: ticket.estimated_test_files ?? null,
277
280
  }).body;
278
281
  }
279
282
 
@@ -320,8 +323,12 @@ function syncContractFieldFromTopLevel(ticket, bodyObject, field) {
320
323
  * bookkeeping for the `--tickets` source issues, not part of the Story's
321
324
  * executable body, so it is deliberately not serialized into the markdown.
322
325
  *
326
+ * `provenance` is likewise top-level-only (Story #5045) — it names the audit
327
+ * identities *this* Story owns, and is stamped into the body as footers rather
328
+ * than serialized as a section.
329
+ *
323
330
  * @param {object} ticket
324
- * @returns {{ slug: string, title: string, bodyObject: object, depends_on: string[], labels: string[], supersedes: Array<{ id: number, note: string|null }> }}
331
+ * @returns {{ slug: string, title: string, bodyObject: object, depends_on: string[], labels: string[], supersedes: Array<{ id: number, note: string|null }>, provenance: { fingerprints: string[], semanticKeys: string[] }|null }}
325
332
  */
326
333
  export function normalizeStoryTicket(ticket) {
327
334
  if (!ticket || typeof ticket !== 'object') {
@@ -346,8 +353,22 @@ export function normalizeStoryTicket(ticket) {
346
353
  const depends_on = normalizeDependsOn(ticket, bodyObject);
347
354
  const supersedes = normalizeSupersedes(ticket, slug);
348
355
  const labels = sanitizeAuthoredLabels(ticket.labels, slug);
356
+ let provenance;
357
+ try {
358
+ provenance = normalizeOwnedProvenance(ticket.provenance, slug);
359
+ } catch (err) {
360
+ throw new Error(`[plan-persist] ${err.message}`);
361
+ }
349
362
 
350
- return { slug, title, bodyObject, depends_on, labels, supersedes };
363
+ return {
364
+ slug,
365
+ title,
366
+ bodyObject,
367
+ depends_on,
368
+ labels,
369
+ supersedes,
370
+ provenance,
371
+ };
351
372
  }
352
373
 
353
374
  /**
@@ -389,24 +410,59 @@ export function foldSpecIntoStoryBody(bodyObject, slug, opts = {}) {
389
410
  return { bodyObject: next };
390
411
  }
391
412
 
413
+ /**
414
+ * Resolve the provenance source one Story is stamped from (Story #5045).
415
+ *
416
+ * Two channels, and the precedence between them is the whole contract:
417
+ *
418
+ * - **Attributed** — the Story authored a `provenance` field naming the audit
419
+ * identities *it* owns. Exactly those are stamped. This is what makes the
420
+ * footers answer "which Story tracks this finding?" instead of "which sweep
421
+ * planned it?": under the union every sibling carried every key, so the next
422
+ * sweep's confirmation pass could only pick an arbitrary open Story, and a
423
+ * key whose owner had since closed was masked by any open neighbour.
424
+ * - **Union fallback** — no `provenance` field, so the whole seed's footers are
425
+ * carried, exactly as before. This is **not** dead weight to be tidied away:
426
+ * hand-carried provenance is the failure Stories #4626 / #4877 measured, and
427
+ * the union is what closed it. Attribution is additive and recall-safe;
428
+ * deleting the fallback would re-open that hole for every plan that does not
429
+ * attribute.
430
+ *
431
+ * @param {{ fingerprints: string[], semanticKeys: string[] }|null} provenance
432
+ * @param {object} opts Assembly options carrying `provenanceSource`.
433
+ * @returns {string}
434
+ */
435
+ function resolveProvenanceSource(provenance, opts) {
436
+ if (provenance !== null) return ownedProvenanceSource(provenance);
437
+ return opts.provenanceSource ?? '';
438
+ }
439
+
392
440
  function assembleOnePlanStory(ticket, opts) {
393
- const { slug, title, bodyObject, depends_on, labels, supersedes } =
394
- normalizeStoryTicket(ticket);
441
+ const {
442
+ slug,
443
+ title,
444
+ bodyObject,
445
+ depends_on,
446
+ labels,
447
+ supersedes,
448
+ provenance,
449
+ } = normalizeStoryTicket(ticket);
395
450
  const { bodyObject: folded } = foldSpecIntoStoryBody(bodyObject, slug, {
396
451
  sharedSpec: opts.sharedSpec ?? null,
397
452
  });
398
453
  // Body first: the fingerprint is an identity over the *assembled* content,
399
454
  // so it cannot be computed until that content exists.
400
455
  const serialized = serializeStoryBody({ ...folded, depends_on });
401
- // Carry audit dedup provenance out of the seed this plan was authored from
402
- // (Story #4877). The audit sweep's Single-plan path stamps the
403
- // `audit-fingerprints` / `audit-semantic-keys` footers into the seed it hands
404
- // `/plan`; without this the persisted Story carries no provenance and the
405
- // next sweep re-files work it already planned. Mechanical on purpose — the
406
- // authoring agent is not asked to notice HTML comments in a one-pager. A
407
- // non-audit seed carries no footers, so this is a no-op there.
456
+ // Carry audit dedup provenance into the persisted body (Story #4877). The
457
+ // audit sweep's Single-plan path stamps the `audit-fingerprints` /
458
+ // `audit-semantic-keys` footers into the seed it hands `/plan`; without this
459
+ // the persisted Story carries no provenance and the next sweep re-files work
460
+ // it already planned. Mechanical on purpose — the authoring agent is not
461
+ // asked to notice HTML comments in a one-pager. A non-audit seed carries no
462
+ // footers, so this is a no-op there. Which identities reach *this* Story is
463
+ // `resolveProvenanceSource`'s call (Story #5045).
408
464
  const { body } = carryProvenanceFooters({
409
- from: opts.provenanceSource ?? '',
465
+ from: resolveProvenanceSource(provenance, opts),
410
466
  into: serialized,
411
467
  });
412
468
  const fingerprint = planStoryFingerprint({ slug, title, body });
@@ -65,6 +65,53 @@ function renderWaveTableLines(waveTable) {
65
65
  return ['| Order | Stories |', '| --- | --- |', ...rows];
66
66
  }
67
67
 
68
+ /**
69
+ * Render the shared-editor collisions beside the wave table (Story #5045).
70
+ *
71
+ * The wave table is a promise about parallelism — "these Stories can run
72
+ * together". `computeSharedEditorFindings` knows exactly where that promise
73
+ * breaks down: a path two same-wave Stories both write will conflict on every
74
+ * merge after the first. Until now those findings degraded to a `Logger.warn`
75
+ * on stderr and were discarded, so the comment carried the optimistic half of
76
+ * the analysis and none of the caveat. Rendering them here puts the promise
77
+ * and its known exceptions on one durable surface.
78
+ *
79
+ * Advisory by default and labelled as such — `planning.failOnSharedEditors`
80
+ * is the knob that makes a collision refuse the plan, and it stays off.
81
+ *
82
+ * @param {object[]|null} conflictFindings
83
+ * @returns {string[]} Lines to splice after the wave table, or `[]`.
84
+ */
85
+ function renderSharedEditorLines(conflictFindings) {
86
+ const shared = (
87
+ Array.isArray(conflictFindings) ? conflictFindings : []
88
+ ).filter((finding) => finding?.kind === 'shared-editor');
89
+ if (shared.length === 0) return [];
90
+ const rows = shared
91
+ .slice()
92
+ .sort((a, b) => String(a.path).localeCompare(String(b.path)))
93
+ .map(
94
+ (finding) =>
95
+ `| \`${finding.path}\` | ${(finding.storySlugs ?? [])
96
+ .map((slug) => `\`${slug}\``)
97
+ .join(', ')} |`,
98
+ );
99
+ return [
100
+ '',
101
+ `#### ⚠️ Known collisions (${shared.length} shared file(s))`,
102
+ '',
103
+ '| Path | Stories in the same order |',
104
+ '| --- | --- |',
105
+ ...rows,
106
+ '',
107
+ '_These Stories are scheduled to run together **and** write the same ' +
108
+ 'file — expect a merge conflict on every landing after the first. Add a ' +
109
+ '`depends_on` edge to serialize them, or move the shared edit into one ' +
110
+ 'Story. Advisory: `planning.failOnSharedEditors` turns this into a ' +
111
+ 'refusal and is off by default._',
112
+ ];
113
+ }
114
+
68
115
  /**
69
116
  * Build the `plan-summary` structured-comment body.
70
117
  *
@@ -81,6 +128,7 @@ export function buildPlanSummaryCommentBody({
81
128
  mode = 'stories',
82
129
  planMetricsLine = null,
83
130
  stories = null,
131
+ conflictFindings = null,
84
132
  // legacy unused knobs kept so older test call sites don't crash mid-migration
85
133
  single = null,
86
134
  amend = null,
@@ -132,6 +180,7 @@ export function buildPlanSummaryCommentBody({
132
180
  '#### Delivery order (`depends_on`)',
133
181
  '',
134
182
  ...renderWaveTableLines(waveTable),
183
+ ...renderSharedEditorLines(conflictFindings),
135
184
  '',
136
185
  `_Deliver with \`${deliverCommand}\` — \`/deliver\` resolves the dependency graph from live state, so edges may point at Stories from earlier plan runs._`,
137
186
  ].join('\n');
@@ -1,13 +1,18 @@
1
- // .agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js
1
+ // .agents/scripts/lib/orchestration/pr-watch.js
2
2
  /**
3
- * Watcherlifecycle listener that owns the required-check poll loop
4
- * for an open Epic PR. Story #2256 / Task #2261 (Epic #2172).
3
+ * pr-watch.js — the required-check poll loop for an open PR.
4
+ * Story #2256 (Epic #2172).
5
5
  *
6
- * Subscribes to:
7
- * - `pr.created` resolve the required-check names from GitHub at
8
- * runtime via `gh pr checks <pr> --required`, then poll until every
9
- * check reaches a terminal state (or a per-listener wall-clock
10
- * deadline expires), recording the outcome in `classifications`.
6
+ * **Not a listener, and no longer shelved among them.** This shipped as the
7
+ * `Watcher` lifecycle listener subscribing to `pr.created`. Story #5006
8
+ * deleted the class but left the plain primitive at
9
+ * `lifecycle/listeners/watcher.js` so its CLI consumer's import stayed
10
+ * stable. Story #5024 then retired the bus itself, which took the listener
11
+ * concept — and the `listeners/` directory — with it, so the primitive moved
12
+ * here beside [`merge-poll.js`](./merge-poll.js), the home Story #4545 chose
13
+ * for `MergeWatcher`'s surviving parts. The only production consumer,
14
+ * `pr-watch-with-update.js`, has always driven {@link watchPrToTerminal}
15
+ * directly, with no bus.
11
16
  *
12
17
  * Critical contract:
13
18
  * - Required-check **names** are resolved from `gh pr checks` at
@@ -16,29 +21,18 @@
16
21
  * branch-protection ruleset on GitHub is the source of truth at
17
22
  * watch time. This guards against config drift (a config file that
18
23
  * hasn't been updated after a protection rule changed on GitHub
19
- * would otherwise cause the watcher to either skip a required check
24
+ * would otherwise cause the watch to either skip a required check
20
25
  * or wait for a removed one indefinitely).
21
26
  *
22
- * Idempotency contract (AC-10): per-instance `Set<string>` of
23
- * `${event}:${seqId}` keys. A repeat `(event, seqId)` short-circuits
24
- * without re-polling and emits nothing. Combined with the bus-level
25
- * replay defence, this is sufficient — re-running `/deliver` after
26
- * a crash will produce a NEW seqId and the listener legitimately
27
- * re-runs the poll loop (which is itself idempotent: the outcome map
28
- * always reflects the live GitHub state).
29
- *
30
- * Side-effect firewall: the listener shells out to `gh` and records
31
- * its outcome in the in-memory `classifications` log. It does NOT
32
- * mutate ticket labels, post comments, call `notify`, or emit on the
33
- * bus — the production consumer (`pr-watch-with-update.js`) drives
34
- * `watchPrToTerminal` directly without a bus, and the retired
35
- * `epic.watch.start` / `epic.watch.end` bus events went with the
36
- * Epic-orchestration stratum.
27
+ * Side-effect firewall: the loop shells out to `gh` through injectable
28
+ * ports and returns a verdict. It does NOT mutate ticket labels, post
29
+ * comments, call `notify`, or write any ledger.
37
30
  */
38
31
 
39
32
  import { spawnSync } from 'node:child_process';
40
33
 
41
- import { parsePrNumberFromUrl } from '../../../github-url.js';
34
+ import { parsePrNumberFromUrl } from '../github-url.js';
35
+ import { applyBehindUpdate } from './behind-recovery.js';
42
36
 
43
37
  /**
44
38
  * Map `gh pr checks` `state` values to the canonical lowercase outcome
@@ -121,8 +115,8 @@ export function normalizeCheckState(raw) {
121
115
  }
122
116
 
123
117
  /**
124
- * Parse a PR number out of a PR URL. The bus contract gives us
125
- * `pr.created.prUrl`; `gh pr checks` accepts either the URL or the
118
+ * Parse a PR number out of a PR URL. Callers hand in the URL `gh pr
119
+ * create` returned; `gh pr checks` accepts either the URL or the
126
120
  * number — we pass the URL through verbatim, but the helper still
127
121
  * exists for tests asserting we never silently coerce a malformed URL.
128
122
  *
@@ -184,9 +178,9 @@ function ghPrChecks({ prUrl, cwd, repo, spawnFn = spawnSync }) {
184
178
  }
185
179
 
186
180
  /**
187
- * Default `gh pr view` spawn — probes `mergeStateStatus` so the Watcher
181
+ * Default `gh pr view` spawn — probes `mergeStateStatus` so the watch loop
188
182
  * can detect the BEHIND condition (PR head is behind its base branch)
189
- * AFTER every required check is green. Exported so tests can stub.
183
+ * AFTER every required check is green. Injectable so tests can stub.
190
184
  */
191
185
  function ghPrView({ prUrl, cwd, repo, spawnFn = spawnSync }) {
192
186
  const result = spawnFn(
@@ -377,8 +371,8 @@ function defaultSleep(ms) {
377
371
  * an empty stdout) are logged and skipped — the outer cap eventually
378
372
  * short-circuits if `gh` is unrecoverably broken.
379
373
  *
380
- * Exported so the BEHIND-recovery outer loop in `Watcher.handle()` can
381
- * call this for each CI cycle without duplicating the inner logic.
374
+ * Exported so {@link watchPrToTerminal}'s BEHIND-recovery outer loop can
375
+ * call it once per CI cycle without duplicating the inner logic.
382
376
  *
383
377
  * @param {object} opts
384
378
  * @param {string} opts.prUrl
@@ -434,9 +428,10 @@ export async function pollUntilTerminal({
434
428
  * commit after each. Plain async function with NO bus coupling: it
435
429
  * shells out to `gh` (via injectable spawns) and returns the verdict.
436
430
  *
437
- * This is the load-bearing primitive shared by the `Watcher` lifecycle
438
- * listener (`handle()`) and the `pr-watch-with-update.js` CLI, so both
439
- * paths perform identical polling and BEHIND-recovery. Story #3902.
431
+ * The load-bearing primitive the `pr-watch-with-update.js` CLI drives.
432
+ * Story #3902 introduced it so the (since-retired) `Watcher` listener and
433
+ * the CLI could not drift apart on polling or BEHIND-recovery; the CLI is
434
+ * now its only caller.
440
435
  *
441
436
  * @param {object} opts
442
437
  * @param {string} opts.prUrl PR URL or number (passed to `gh` verbatim).
@@ -463,11 +458,10 @@ export async function pollUntilTerminal({
463
458
  * real `setTimeout`-backed sleep; tests override with a no-op.
464
459
  * @param {{ info?: Function, warn?: Function, debug?: Function }} opts.logger
465
460
  * @param {{status:number,stdout:string,stderr:string}} [opts.firstProbe]
466
- * Optional already-issued `gh pr checks` result. When the caller (the
467
- * `Watcher` listener) has already probed once to resolve the required
468
- * check names, it threads that result here so the loop does not
469
- * double-spend the first `gh pr checks` call. Omit it (the CLI path)
470
- * and the loop issues the first probe itself.
461
+ * Optional already-issued `gh pr checks` result. A caller that has
462
+ * already probed once to resolve the required-check names threads it
463
+ * here so the loop does not double-spend the first `gh pr checks` call.
464
+ * Omit it (the CLI path) and the loop issues the first probe itself.
471
465
  * @returns {Promise<{
472
466
  * outcomes: object,
473
467
  * requiredChecks: string[],
@@ -502,8 +496,8 @@ export async function watchPrToTerminal({
502
496
  firstProbe,
503
497
  }) {
504
498
  // First probe: resolve the required-check name set at runtime. Reuse a
505
- // caller-supplied probe (the listener already issued one to resolve the
506
- // required check names) so we never double-spend the first `gh` call.
499
+ // caller-supplied probe (issued to resolve the required-check names) so
500
+ // we never double-spend the first `gh` call.
507
501
  const first = firstProbe ?? ghPrChecksFn({ prUrl, cwd, repo });
508
502
  // `gh` exits 8 when checks are still pending; this is expected and
509
503
  // does not indicate failure. Any other non-zero status with no
@@ -579,6 +573,9 @@ export async function watchPrToTerminal({
579
573
  // Bounded by `maxUpdates` so a racing base branch can't ping-pong
580
574
  // indefinitely.
581
575
  if (!allTerminal(outcomes) || !allGreen(outcomes)) break;
576
+ // Budget is checked before the `gh pr view` spawn so an exhausted arm
577
+ // costs no extra round-trip; the shared helper re-checks it as a
578
+ // fail-safe, and its callback therefore never fires on this path.
582
579
  if (updatesApplied >= maxUpdates) break;
583
580
  const view = ghPrViewFn({ prUrl, cwd, repo });
584
581
  if (view.status !== 0) {
@@ -587,15 +584,26 @@ export async function watchPrToTerminal({
587
584
  );
588
585
  break;
589
586
  }
590
- const mergeStateStatus = parseMergeStateStatus(view.stdout);
591
- if (mergeStateStatus !== 'BEHIND') break;
592
- const update = ghPrUpdateBranchFn({ prUrl, cwd, repo });
593
- if (update.status !== 0) {
594
- logger.warn?.(
595
- `[Watcher] gh pr update-branch failed (status=${update.status}): ${update.stderr}`,
596
- );
597
- break;
598
- }
587
+ const recovery = await applyBehindUpdate({
588
+ mergeStateStatus: parseMergeStateStatus(view.stdout),
589
+ updatesUsed: updatesApplied,
590
+ maxUpdates,
591
+ updateBranch: async () => {
592
+ const update = ghPrUpdateBranchFn({ prUrl, cwd, repo });
593
+ return update.status === 0
594
+ ? { ok: true }
595
+ : {
596
+ ok: false,
597
+ detail: `status=${update.status}: ${update.stderr}`,
598
+ };
599
+ },
600
+ onUpdateFailed: (detail) =>
601
+ logger.warn?.(`[Watcher] gh pr update-branch failed (${detail})`),
602
+ });
603
+ // Anything but a landed fast-forward ends the arm: not BEHIND means
604
+ // there is nothing to recover, and a failed update must not silently
605
+ // re-poll as though the head moved.
606
+ if (!recovery.updated) break;
599
607
  updatesApplied += 1;
600
608
  logger.info?.(
601
609
  `[Watcher] PR BEHIND base — issued gh pr update-branch (#${updatesApplied}/${maxUpdates}); re-polling required checks.`,
@@ -643,161 +651,3 @@ export async function watchPrToTerminal({
643
651
  stillRunning,
644
652
  };
645
653
  }
646
-
647
- /**
648
- * Watcher listener.
649
- */
650
- export class Watcher {
651
- /**
652
- * @param {object} opts
653
- * @param {object} opts.bus
654
- * @param {string} [opts.cwd]
655
- * @param {number} [opts.pollIntervalMs] default 10_000.
656
- * @param {number} [opts.maxPolls] safety cap on iterations; default
657
- * 180 (≈30 min @ 10s).
658
- * @param {number} [opts.maxUpdates] cap on `gh pr update-branch`
659
- * recovery calls per `pr.created` event; default 3. Mirrors the
660
- * legacy `pr-watch-with-update` cap so a racing base branch
661
- * can't induce an infinite update-branch ping-pong.
662
- * @param {number} [opts.maxResumes] Story #4358: how many times to
663
- * re-arm the poll loop after the cap fires with checks still pending
664
- * (and none failed) before declaring `still-running`; default 0.
665
- * @param {Function} [opts.ghPrChecksFn] override for tests.
666
- * @param {Function} [opts.ghPrViewFn] override for tests; resolves
667
- * `mergeStateStatus` for the BEHIND-recovery gate.
668
- * @param {Function} [opts.ghPrUpdateBranchFn] override for tests;
669
- * issues the fast-forward update on the PR.
670
- * @param {Function} [opts.sleepFn] override for tests.
671
- * @param {{ info?: Function, warn?: Function, debug?: Function }} [opts.logger]
672
- */
673
- constructor(opts = {}) {
674
- if (!opts.bus || typeof opts.bus.on !== 'function') {
675
- throw new TypeError('Watcher requires a bus with on()');
676
- }
677
- this.bus = opts.bus;
678
- this.cwd = opts.cwd ?? process.cwd();
679
- this.pollIntervalMs = Number.isInteger(opts.pollIntervalMs)
680
- ? opts.pollIntervalMs
681
- : 10_000;
682
- this.maxPolls = Number.isInteger(opts.maxPolls) ? opts.maxPolls : 180;
683
- this.maxUpdates =
684
- Number.isInteger(opts.maxUpdates) && opts.maxUpdates >= 0
685
- ? opts.maxUpdates
686
- : 3;
687
- this.maxResumes =
688
- Number.isInteger(opts.maxResumes) && opts.maxResumes >= 0
689
- ? opts.maxResumes
690
- : 0;
691
- this.ghPrChecksFn = opts.ghPrChecksFn ?? ghPrChecks;
692
- this.ghPrViewFn = opts.ghPrViewFn ?? ghPrView;
693
- this.ghPrUpdateBranchFn = opts.ghPrUpdateBranchFn ?? ghPrUpdateBranch;
694
- this.sleepFn = opts.sleepFn ?? defaultSleep;
695
- this.logger = opts.logger ?? console;
696
- /** @type {Set<string>} `${event}:${seqId}` idempotency cache. */
697
- this._seen = new Set();
698
- /**
699
- * Classification log — every `pr.created` we observe lands here
700
- * with the outcome (`watched`, `failed`, `skipped-duplicate`,
701
- * `still-running`, `timed-out`). Mirrors the Finalizer / Reconciler
702
- * "no silent skip" surface.
703
- */
704
- this.classifications = [];
705
- this.events = Object.freeze(['pr.created']);
706
- }
707
-
708
- register() {
709
- return this.events.map((event) =>
710
- this.bus.on(event, async (ctx) => this.handle(ctx)),
711
- );
712
- }
713
-
714
- async handle({ event, seqId, payload }) {
715
- const key = `${event}:${seqId}`;
716
- if (this._seen.has(key)) {
717
- this.classifications.push({
718
- event,
719
- seqId,
720
- outcome: 'skipped',
721
- reason: 'duplicate-seqId',
722
- });
723
- this.logger.debug?.(`[Watcher] skip duplicate ${key} (idempotent)`);
724
- return;
725
- }
726
- this._seen.add(key);
727
-
728
- const prUrl = payload?.prUrl;
729
- if (typeof prUrl !== 'string' || prUrl.length === 0) {
730
- this.classifications.push({
731
- event,
732
- seqId,
733
- outcome: 'failed',
734
- reason: 'no-pr-url',
735
- });
736
- return;
737
- }
738
-
739
- // First probe: resolve the required-check name set at runtime BEFORE
740
- // the (potentially long) poll loop runs. We thread this probe into
741
- // `watchPrToTerminal` (via `firstProbe`) so the shared loop reuses it
742
- // instead of double-spending the first `gh pr checks` call.
743
- const first = this.ghPrChecksFn({ prUrl, cwd: this.cwd });
744
- const firstEntries = parseGhPrChecks(first.stdout);
745
- if (firstEntries.length === 0 && first.status !== 0 && first.status !== 8) {
746
- this.classifications.push({
747
- event,
748
- seqId,
749
- outcome: 'failed',
750
- reason: `gh-checks-failed:status=${first.status}`,
751
- });
752
- this.logger.warn?.(
753
- `[Watcher] gh pr checks failed (status=${first.status}): ${first.stderr}`,
754
- );
755
- return;
756
- }
757
-
758
- const requiredChecks = firstEntries.map((e) => e.name);
759
-
760
- // Delegate the poll + BEHIND-recovery loop to the shared plain
761
- // primitive so the CLI (`pr-watch-with-update.js`) and this listener
762
- // run identical logic.
763
- const { polls, updatesApplied, resumesApplied, terminal, stillRunning } =
764
- await watchPrToTerminal({
765
- prUrl,
766
- cwd: this.cwd,
767
- maxPolls: this.maxPolls,
768
- maxUpdates: this.maxUpdates,
769
- maxResumes: this.maxResumes,
770
- pollIntervalMs: this.pollIntervalMs,
771
- ghPrChecksFn: this.ghPrChecksFn,
772
- ghPrViewFn: this.ghPrViewFn,
773
- ghPrUpdateBranchFn: this.ghPrUpdateBranchFn,
774
- sleepFn: this.sleepFn,
775
- logger: this.logger,
776
- firstProbe: first,
777
- });
778
-
779
- // `still-running` (slow CI, not red) is a distinct classification from
780
- // a genuine `timed-out` — reserved for a check that never went
781
- // terminal within the poll cap AND the resume budget while none
782
- // failed. `watched` covers every terminal arm (green or red).
783
- const outcome = terminal
784
- ? 'watched'
785
- : stillRunning
786
- ? 'still-running'
787
- : 'timed-out';
788
- this.classifications.push({
789
- event,
790
- seqId,
791
- outcome,
792
- polls,
793
- updatesApplied,
794
- resumesApplied,
795
- requiredChecks: requiredChecks.length,
796
- });
797
- }
798
-
799
- reset() {
800
- this._seen.clear();
801
- this.classifications = [];
802
- }
803
- }