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
@@ -29,18 +29,17 @@
29
29
  * against {@link STORY_SHAPE_CEILINGS}. A `lite` claim whose work exceeds
30
30
  * them **fails closed to `full`** (`run-plan-persist.js`). Artifact
31
31
  * cardinality is deliberately not an axis (Story #4764).
32
- * 4. **Deliver re-derives.** `/deliver` computes the route from the fetched
33
- * Story body via the **same** shape function at dispatch
34
- * ({@link resolveStoryDispatchMode}) and **reports** it, while the
35
- * dispatch *mode* answers a different question: may the engine run in the
36
- * router's own session? Only a **single-Story run** may (Story #4736)
37
- * sub-agent isolation buys nothing when there is no concurrent sibling to
38
- * isolate from. Shape cannot grant that session (Story #4829): a lite body
39
- * makes work cheap, it does not conjure a second session for a sibling to
40
- * run in. The `route::lite` label is a **human-visible hint only**, never
41
- * the control signal: a lost label or an unread marker can no longer
42
- * misroute delivery. Either way every `single-story-close.js` gate runs
43
- * unchanged.
32
+ * 4. **Deliver dispatches on topology alone.** The dispatch *mode*
33
+ * ({@link resolveStoryDispatchMode}) answers a different question from
34
+ * the route: may the engine run in the router's own session? Only a
35
+ * **single-Story run** may (Story #4736) sub-agent isolation buys
36
+ * nothing when there is no concurrent sibling to isolate from. Shape
37
+ * cannot grant that session (Story #4829): a lite body makes work cheap,
38
+ * it does not conjure a second session for a sibling to run in. Story
39
+ * #5006 removed the shape derivation that survived there for reporting,
40
+ * since no consumer read it. The `route::lite` label is a
41
+ * **human-visible hint only**, never the control signal. Either way every
42
+ * `single-story-close.js` gate runs unchanged.
44
43
  *
45
44
  * The shape taxonomy is deliberately the one `review-depth.js` already
46
45
  * applies to the landed diff at close (`deriveChangeLevel` over the
@@ -77,10 +76,7 @@
77
76
 
78
77
  import { existsSync } from 'node:fs';
79
78
  import path from 'node:path';
80
- import {
81
- extractChangePaths,
82
- parse as parseStoryBody,
83
- } from '../story-body/story-body.js';
79
+ import { extractChangePaths } from '../story-body/story-body.js';
84
80
  import { deriveChangeLevel } from './review-depth.js';
85
81
 
86
82
  /**
@@ -97,11 +93,10 @@ const DEFAULT_COMPLEXITY_GATE = Object.freeze({
97
93
  * The persisted route marker for a lite-routed Story.
98
94
  *
99
95
  * **A human-visible hint only (Story #4722)** — never the control signal.
100
- * Persist still applies it so a lite cohort is filterable in the GitHub UI,
101
- * but `/deliver` derives the route from the Story body's own shape
102
- * ({@link resolveStoryDispatchMode}); a Story with the label whose shape
103
- * derives `full` dispatches as a sub-agent, and a lite-shaped Story with the
104
- * label absent (or its write failed) still executes inline.
96
+ * Persist applies it so a lite cohort is filterable in the GitHub UI, and
97
+ * `deliver-light` reads the Story's own shape ({@link deriveStoryShape}) when
98
+ * it needs one. Nothing routes on the label: a lost label or an unread marker
99
+ * cannot misroute delivery.
105
100
  */
106
101
  export const LITE_ROUTE_LABEL = 'route::lite';
107
102
 
@@ -279,8 +274,8 @@ function spansMigrationAndConsumers(paths) {
279
274
  * against ground truth by the diff backstop.
280
275
  * - **Absolute rules** — `migration-span`, `sensitive-path`. Risk, not size.
281
276
  * - **Unknown-footprint rejections** — `no-changes`, `unreadable-changes`,
282
- * `glob-footprint`, `no-acceptance`, `classification-unavailable`,
283
- * `unparseable-body`. Nothing was judged, so there is nothing to waive.
277
+ * `glob-footprint`, `no-acceptance`, `classification-unavailable`.
278
+ * Nothing was judged, so there is nothing to waive.
284
279
  *
285
280
  * A `lite` route carries `code: null`.
286
281
  */
@@ -296,7 +291,6 @@ export const SHAPE_CODES = Object.freeze({
296
291
  GLOB_FOOTPRINT: 'glob-footprint',
297
292
  NO_ACCEPTANCE: 'no-acceptance',
298
293
  CLASSIFICATION_UNAVAILABLE: 'classification-unavailable',
299
- UNPARSEABLE_BODY: 'unparseable-body',
300
294
  });
301
295
 
302
296
  /**
@@ -409,9 +403,9 @@ function normalizeCeiling(value, fallback) {
409
403
  *
410
404
  * Exported for persist (`run-plan-persist.js#resolveEffectiveRoute`), which
411
405
  * consults `enabled` to refuse a planner lite claim when the gate is off —
412
- * the schema's documented contract, and the same switch dispatch reads in
413
- * {@link resolveStoryDispatchMode}, so the two read points cannot disagree
414
- * about whether lite routing is live.
406
+ * the schema's documented contract. It is the only read point: Story #5006
407
+ * removed the second one in {@link resolveStoryDispatchMode}, where the switch
408
+ * gated a shape derivation whose result no consumer read.
415
409
  *
416
410
  * @param {object | null | undefined} config
417
411
  * @returns {{ enabled: boolean, maxArtifacts: number }}
@@ -810,60 +804,7 @@ export function deriveStoryShape({
810
804
  }
811
805
 
812
806
  /**
813
- * Derive the complexity route from a Story's **serialized body markdown**
814
- * the deliver-side entry to {@link deriveStoryShape} (`/deliver` already
815
- * fetches the body; the route is computed from it, never from a label). An
816
- * unparseable body degrades to `full`: unknown shape is not trivial shape.
817
- *
818
- * Module-private, reachable end to end through
819
- * {@link resolveStoryDispatchMode} (which returns the derived route) — so
820
- * there is no test-only export to leave production-dead.
821
- *
822
- * @param {string} body Serialized Story-body markdown.
823
- * @param {{ injectedRules?: object, selectSensitivePathClassesFn?: Function }} [opts]
824
- * @returns {ReturnType<typeof deriveStoryShape>}
825
- */
826
- function deriveStoryRouteFromBody(body, opts = {}) {
827
- let parsed;
828
- try {
829
- parsed = parseStoryBody(String(body ?? '')).body;
830
- } catch (err) {
831
- return {
832
- route: 'full',
833
- reasons: [
834
- `Story body is unparseable (${err?.message ?? err}) — shape unknown; conservative full route`,
835
- ],
836
- code: SHAPE_CODES.UNPARSEABLE_BODY,
837
- shape: null,
838
- ceilings: STORY_SHAPE_CEILINGS,
839
- preserves: LITE_PATH_INVARIANTS,
840
- };
841
- }
842
- return deriveStoryShape({
843
- changes: parsed?.changes,
844
- acceptance: parsed?.acceptance,
845
- injectedRules: opts.injectedRules,
846
- selectSensitivePathClassesFn: opts.selectSensitivePathClassesFn,
847
- });
848
- }
849
-
850
- /**
851
- * Best-effort route derivation for reporting, when the *mode* is already
852
- * pinned by run topology and only `route` remains to be filled in. A body
853
- * that will not parse yields `null` rather than throwing — the caller is not
854
- * asking the shape to decide anything.
855
- *
856
- * @param {unknown} body
857
- * @param {{ injectedRules?: object, selectSensitivePathClassesFn?: Function }} opts
858
- * @returns {ReturnType<typeof deriveStoryShape>|null}
859
- */
860
- function routeForReporting(body, opts) {
861
- if (typeof body !== 'string' || body.trim() === '') return null;
862
- return deriveStoryRouteFromBody(body, opts);
863
- }
864
-
865
- /**
866
- * Decide how `/deliver` executes a Story.
807
+ * Decide how `/deliver` executes a Story: **run topology, and nothing else.**
867
808
  *
868
809
  * **`inline` names one indivisible resource: the router's own session.** Two
869
810
  * Stories cannot both own it, so exactly one premise can grant it —
@@ -873,9 +814,7 @@ function routeForReporting(body, opts) {
873
814
  * branch refs) and a one-Story run has no sibling to race. It therefore pays
874
815
  * the spawn premium (a boot is a cache WRITE at full rate, where an inline
875
816
  * continuation is a cache read at ~10%; ~$1.43/M vs ~$1.07/M on comparable
876
- * bench work) for nothing. That is a fact about the run, not about the work, so
877
- * the shape gate's `enabled` switch — which governs *shape derivation* — does
878
- * not reach it.
817
+ * bench work) for nothing.
879
818
  *
880
819
  * **Shape cannot grant it (Story #4829).** The shape read used to return
881
820
  * `inline` for any lite-shaped body in a multi-Story run, inheriting no
@@ -883,16 +822,15 @@ function routeForReporting(body, opts) {
883
822
  * run came back `inline` for *every* Story while `stories-wave-tick.js`
884
823
  * reported the whole set ready under a concurrency cap of five — a router
885
824
  * following both signals literally runs several engines over one session and
886
- * one checkout, the precise hazard the sub-agent path exists to prevent. Both
887
- * runs were completed only by an operator overriding the verdict by hand, which
888
- * is an invariant held by judgment rather than by code. So this function has
889
- * exactly **one** `inline` exit, guarded by the topology premise; every path
890
- * below it returns `subagent`, and the derived shape is carried on `route` for
891
- * reporting only. A lite shape makes the work cheap — it does not conjure a
892
- * second session for a sibling to run in.
825
+ * one checkout, the precise hazard the sub-agent path exists to prevent.
893
826
  *
894
- * The label is read only to report hint consistency in `reasons`; it never
895
- * routes on either premise (Story #4722 AC-4/AC-5).
827
+ * **So this function reads only `storyCount` (Story #5006).** #4829 left the
828
+ * body parse, the shape derivation, the `route::lite` hint note and the
829
+ * `planning.complexityGate.enabled` branch in place to populate a `route`
830
+ * field for reporting — but the sole consumer, `resolve-stories.js`, reads
831
+ * `.mode` and discards the rest, so every one of those inputs was a parse
832
+ * whose result nothing could act on. A caller that wants the shape calls
833
+ * {@link deriveStoryShape} directly, as the light path and plan-persist do.
896
834
  *
897
835
  * Inline execution removes model-side fan-out only — it changes **where** the
898
836
  * engine runs, never **what** runs. Every deterministic
@@ -900,35 +838,13 @@ function routeForReporting(body, opts) {
900
838
  * `story-deliver-terminal` envelope are identical in both modes; see the
901
839
  * module header's non-negotiables.
902
840
  *
903
- * @param {{
904
- * body?: unknown,
905
- * labels?: unknown,
906
- * config?: object,
907
- * storyCount?: unknown,
908
- * injectedRules?: object,
909
- * selectSensitivePathClassesFn?: Function,
910
- * }} [args] `storyCount` is the number of Stories the invoking `/deliver` run
911
- * resolved. Omitted (or not a positive integer) means "unknown run size",
912
- * which cannot be shown sibling-free and therefore dispatches as a sub-agent
913
- * — never an assumed 1.
914
- * @returns {{ mode: 'inline'|'subagent', reasons: string[], route: ReturnType<typeof deriveStoryShape>|null }}
841
+ * @param {{ storyCount?: unknown }} [args] `storyCount` is the number of
842
+ * Stories the invoking `/deliver` run resolved. Omitted (or not exactly 1)
843
+ * means the run cannot be shown sibling-free and therefore dispatches as a
844
+ * sub-agent — never an assumed 1.
845
+ * @returns {{ mode: 'inline'|'subagent', reasons: string[] }}
915
846
  */
916
- export function resolveStoryDispatchMode({
917
- body,
918
- labels,
919
- config,
920
- storyCount,
921
- injectedRules,
922
- selectSensitivePathClassesFn,
923
- } = {}) {
924
- const labelList = Array.isArray(labels)
925
- ? labels.filter((l) => typeof l === 'string')
926
- : [];
927
- const hasHint = labelList.includes(LITE_ROUTE_LABEL);
928
- const hintNote = hasHint
929
- ? `the ${LITE_ROUTE_LABEL} label is present (hint only — the derived shape is the control signal)`
930
- : `the ${LITE_ROUTE_LABEL} label is absent (hint only — the derived shape is the control signal)`;
931
-
847
+ export function resolveStoryDispatchMode({ storyCount } = {}) {
932
848
  // The ONLY `inline` exit in this function, and the guard is the whole
933
849
  // contract: an inline verdict must mean the engine can actually run inline.
934
850
  if (storyCount === 1) {
@@ -936,48 +852,14 @@ export function resolveStoryDispatchMode({
936
852
  mode: 'inline',
937
853
  reasons: [
938
854
  'single-Story run — execute deliver-story inline; sub-agent isolation is load-bearing only for concurrent dispatch, and a one-Story run has no sibling to race (close gates, PR, and terminal envelope unchanged)',
939
- hintNote,
940
- ],
941
- route: routeForReporting(body, {
942
- injectedRules,
943
- selectSensitivePathClassesFn,
944
- }),
945
- };
946
- }
947
-
948
- const gate = resolveComplexityGate(config);
949
- if (!gate.enabled) {
950
- return {
951
- mode: 'subagent',
952
- reasons: [
953
- 'complexity routing disabled (planning.complexityGate.enabled=false) — standard sub-agent dispatch',
954
855
  ],
955
- route: null,
956
856
  };
957
857
  }
958
858
 
959
- if (typeof body !== 'string' || body.trim() === '') {
960
- return {
961
- mode: 'subagent',
962
- reasons: [
963
- 'no Story body to derive shape from — conservative sub-agent dispatch',
964
- hintNote,
965
- ],
966
- route: null,
967
- };
968
- }
969
-
970
- // Past the topology guard a sibling may be dispatched concurrently, so the
971
- // router's session is not available to anyone. The shape is still derived and
972
- // returned on `route` — ceremony and reporting read it — but it decides
973
- // nothing here: both shapes dispatch as a sub-agent.
974
- const route = deriveStoryRouteFromBody(body, {
975
- injectedRules,
976
- selectSensitivePathClassesFn,
977
- });
978
- const shapeNote =
979
- route.route === 'lite'
980
- ? `lite-shaped Story in a multi-Story run — the shape is inline-eligible but the router's session is not: a concurrent sibling would have to share it, racing worktrees and branch refs; sub-agent dispatch (${route.reasons[0]})`
981
- : `full-shaped Story — ${route.reasons[0]}`;
982
- return { mode: 'subagent', reasons: [shapeNote, hintNote], route };
859
+ return {
860
+ mode: 'subagent',
861
+ reasons: [
862
+ "multi-Story (or unknown-size) run — a concurrent sibling would have to share the router's session, racing worktrees and branch refs; sub-agent dispatch",
863
+ ],
864
+ };
983
865
  }
@@ -18,9 +18,9 @@
18
18
  *
19
19
  * So the axis is **changed lines over implementation files**, and the companion
20
20
  * classes the framework itself mandates are exempt from the count: obeying
21
- * `rules/testing-standards.md` (test-first), the `delivery.docsFreshness` gate,
22
- * and the baseline ratchets must not inflate the number that then rejects the
23
- * change. Re-counting those same 33 merges on implementation files alone moves
21
+ * `rules/testing-standards.md` (test-first), the documentation a change of
22
+ * consequence is expected to touch, and the baseline ratchets must not inflate
23
+ * the number that then rejects the change. Re-counting those same 33 merges on implementation files alone moves
24
24
  * the four-file pass rate from 21% to 58%.
25
25
  *
26
26
  * ## Three contracts worth not rediscovering
@@ -88,7 +88,7 @@ const COMPANION_PATH_GLOBS = Object.freeze([
88
88
  '**/*.test.tsx',
89
89
  'tests/**',
90
90
  'features/**',
91
- // Documentation — mandated by the delivery.docsFreshness gate.
91
+ // Documentation — the prose a change of consequence is expected to refresh.
92
92
  'docs/**',
93
93
  '**/*.md',
94
94
  // Generated baseline data — written by the ratchets, not hand-authored.
@@ -26,8 +26,9 @@ export function toExecuting(provider, ticketId, opts) {
26
26
  /**
27
27
  * Transition an array of tickets to `agent::done`, in order. Each call
28
28
  * triggers its own cascade (via `transitionTicketState`). Failures for
29
- * individual tickets propagate — callers that need per-ticket tolerance
30
- * should use `batchTransitionTickets` instead.
29
+ * individual tickets propagate — the loop aborts on the first error, so
30
+ * callers that need per-ticket tolerance must catch around each id
31
+ * themselves.
31
32
  *
32
33
  * @param {object} provider
33
34
  * @param {number[]} ticketIds
@@ -6,9 +6,9 @@
6
6
  * historically also the retired Epic-tier deliver/plan guards —
7
7
  * each carried their own copy of
8
8
  * the operator-handle resolution and the fail-closed acquire wrapper around
9
- * `ticket-lease.acquireLease` (anchor `heartbeatAt` to `now` so a foreign
10
- * assignee always reads as a live claim, then throw an operator-facing
11
- * refusal naming the current owner unless `--steal`). The three copies had
9
+ * `ticket-lease.acquireLease` (refuse any foreign assignee, throwing an
10
+ * operator-facing message naming the current owner unless `--steal`). The
11
+ * three copies had
12
12
  * already diverged — different `resolveOperator` signatures and different
13
13
  * missing-handle behaviour (`null` vs `throw`) — and were synchronised only
14
14
  * by docstring promise ("This mirrors the sibling lease guards…").
@@ -30,16 +30,15 @@
30
30
  * handle must degrade to a `no-operator` no-op rather than throw.
31
31
  * - **Refusal wording** — each surface renders its own operator-facing
32
32
  * message via `renderRefusal(result, ticketId)`.
33
- * - **Liveness anchoring** — the plan/standalone paths have no heartbeat
34
- * ledger, so they anchor `heartbeatAt` to `now` (fail-closed: every
35
- * foreign claim reads live). `/deliver` threads a real
36
- * `heartbeatAt` through from the lifecycle ledger, so it opts out of
37
- * anchoring.
33
+ *
34
+ * A third axis, **liveness anchoring**, is gone: Story #5006 deleted the
35
+ * lease TTL, so every foreign claim refuses unconditionally and there is no
36
+ * heartbeat to anchor.
38
37
  *
39
38
  * The unclaimed / already-held / foreign-claim decision table itself (and
40
39
  * the steal transfer) lives in `ticket-lease.acquireLease`; this kernel owns
41
- * the fail-closed parameterisation and the refuse-by-throw boundary that the
42
- * three guards previously each re-implemented.
40
+ * the refuse-by-throw boundary that the three guards previously each
41
+ * re-implemented.
43
42
  *
44
43
  * Per `.agents/rules/orchestration-error-handling.md`, failures surface via
45
44
  * `throw new Error(...)`, never `Logger.fatal`.
@@ -85,27 +84,16 @@ export function resolveOperatorFromCandidates({
85
84
 
86
85
  /**
87
86
  * Acquire a ticket lease, failing closed by throwing the surface's refusal
88
- * message when the claim is refused (live foreign owner, no `steal`).
87
+ * message when the claim is refused (foreign owner, no `steal`).
89
88
  *
90
- * When `anchorHeartbeatToNow` is set (the plan/standalone paths, which have
91
- * no heartbeat ledger), `heartbeatAt` and `now` are both anchored to the
92
- * same resolved clock value so `isClaimLive` returns true for ANY foreign
93
- * owner — `acquireLease` then refuses a foreign assignee unless `steal` is
94
- * set, while unclaimed and self-held tickets still proceed without a write.
95
- * When unset (`/deliver`), the caller-supplied `heartbeatAt` / `now`
96
- * pass through untouched.
89
+ * Unclaimed and self-held tickets proceed — the self-held case without a
90
+ * write. A foreign assignee always refuses unless `steal` is set.
97
91
  *
98
92
  * @param {object} opts
99
93
  * @param {object} opts.provider Ticketing provider.
100
94
  * @param {number} opts.ticketId Ticket to claim.
101
95
  * @param {string} opts.operator Resolved operator handle.
102
- * @param {number|null} [opts.heartbeatAt=null] Current owner's last
103
- * heartbeat (epoch ms). Ignored when `anchorHeartbeatToNow` is set.
104
96
  * @param {boolean} [opts.steal=false] Forcibly transfer a foreign claim.
105
- * @param {object} [opts.config] Resolved config (TTL default).
106
- * @param {number} [opts.now] Injectable clock (epoch ms; tests).
107
- * @param {boolean} [opts.anchorHeartbeatToNow=false] Fail-closed liveness
108
- * anchoring for surfaces with no heartbeat source.
109
97
  * @param {(result: object, ticketId: number) => string} opts.renderRefusal
110
98
  * Renders the operator-facing refusal message for a refused claim.
111
99
  * @returns {Promise<{ acquired: boolean, owner: string, previousOwner: string|null, reason: string }>}
@@ -115,28 +103,14 @@ export async function acquireLeaseFailClosed({
115
103
  provider,
116
104
  ticketId,
117
105
  operator,
118
- heartbeatAt = null,
119
106
  steal = false,
120
- config,
121
- now,
122
- anchorHeartbeatToNow = false,
123
107
  renderRefusal,
124
108
  }) {
125
- let resolvedHeartbeatAt = heartbeatAt;
126
- let resolvedNow = now;
127
- if (anchorHeartbeatToNow) {
128
- resolvedNow =
129
- typeof now === 'number' && Number.isFinite(now) ? now : Date.now();
130
- resolvedHeartbeatAt = resolvedNow;
131
- }
132
109
  const result = await acquireLease({
133
110
  provider,
134
111
  ticketId,
135
112
  operator,
136
- heartbeatAt: resolvedHeartbeatAt,
137
113
  steal,
138
- config,
139
- now: resolvedNow,
140
114
  });
141
115
  if (!result.acquired) {
142
116
  throw new Error(renderRefusal(result, ticketId));
@@ -3,12 +3,11 @@
3
3
  *
4
4
  * Programmatic helper that appends a single `merge.unlanded` NDJSON
5
5
  * record to the lifecycle ledger whenever a headless delivery run
6
- * finishes its work without a confirmed merge. Pattern mirrors
7
- * `emit-story-heartbeat.js`: direct schema validation via Ajv followed by
8
- * a synchronous `appendFileSync` this event is NOT routed through the
9
- * bus (unlike `emit-loop-tick.js`) because it fires from the standalone
10
- * `single-story-close` flow, which has no bus at all. A bare append keeps
11
- * the call site simple and dependency-free.
6
+ * finishes its work without a confirmed merge. Direct schema validation via
7
+ * Ajv followed by a synchronous `appendFileSync`: it fires from the standalone
8
+ * `single-story-close` flow, and a bare append keeps the call site simple and
9
+ * dependency-free. This was the shape that outlived the lifecycle bus — Story
10
+ * #5024 retired the bus, leaving this path the only lifecycle-ledger writer.
12
11
  *
13
12
  * Ledger destination (Story #4426 AC4): `storyLedgerPath(null, ticketId)`
14
13
  * — the standalone story-scope destination
@@ -6,9 +6,8 @@
6
6
  * the current 12-phase baseline is captured on disk. Each record is one
7
7
  * newline-terminated JSON line appended to
8
8
  * `temp/run-<id>/plan-metrics.json` (per-Epic plan CLIs) or
9
- * `temp/standalone/plan-metrics.json` (the standalone `story-plan.js` path
10
- * and Epic-less healthcheck runs — same standalone routing the friction
11
- * ledger uses).
9
+ * `temp/standalone/plan-metrics.json` (Epic-less plan and healthcheck runs —
10
+ * same standalone routing the friction ledger uses).
12
11
  *
13
12
  * Record shape (v1):
14
13
  *
@@ -15,11 +15,11 @@
15
15
  */
16
16
 
17
17
  import posix from 'node:path/posix';
18
- import { resolveListValue } from '../../config/shared.js';
19
18
  import { gitSpawn } from '../../git-utils.js';
20
19
  import { validateTaskBodies } from '../task-body-validator.js';
21
20
  import { validateAndNormalizeTickets } from '../ticket-validator.js';
22
- import { DEFAULT_REGISTRY_PATTERNS } from '../ticket-validator-conflicts.js';
21
+ import { resolveConflictPolicy } from '../ticket-validator-conflicts.js';
22
+ import { normalizeVerifyTiers } from '../verify-tier-repair.js';
23
23
 
24
24
  /**
25
25
  * Extensions an import specifier may elide. Probed longest-path-first when
@@ -175,30 +175,6 @@ export function makeDefaultFanOutCounter({ baseBranchRef, cwd, git } = {}) {
175
175
  };
176
176
  }
177
177
 
178
- /**
179
- * Resolve the cross-Story conflict-finding policy from `_config.planning`.
180
- */
181
- function resolveConflictPolicy(cfg) {
182
- const planning = cfg?.planning;
183
- const policy = {
184
- failOnSharedEditors: planning?.failOnSharedEditors === true,
185
- requireExplicitCrossStoryDeps:
186
- planning?.requireExplicitCrossStoryDeps === true,
187
- failOnRegistryConflicts: planning?.failOnRegistryConflicts === true,
188
- failOnLargeFanOut: planning?.failOnLargeFanOut === true,
189
- };
190
- if (Number.isFinite(planning?.largeFanOutThreshold)) {
191
- policy.largeFanOutThreshold = planning.largeFanOutThreshold;
192
- }
193
- if (planning?.crossCuttingRegistries !== undefined) {
194
- policy.registries = resolveListValue(
195
- DEFAULT_REGISTRY_PATTERNS,
196
- planning.crossCuttingRegistries,
197
- );
198
- }
199
- return policy;
200
- }
201
-
202
178
  /**
203
179
  * Resolve the ref the persist gates probe against.
204
180
  *
@@ -240,6 +216,11 @@ export function validateTickets(tickets, config, opts = {}) {
240
216
  // process.cwd(), which is only the repo root by coincidence.
241
217
  cwd: opts.cwd,
242
218
  });
219
+ // Repair before judging (Story #5005): a `verify[]` entry whose tier the
220
+ // validator can already infer is auto-corrected rather than rejected, so a
221
+ // mechanically-fixable formality no longer costs a re-authoring round. An
222
+ // uninferable entry survives untouched and still hard-errors below.
223
+ normalizeVerifyTiers(validated);
243
224
  validateTaskBodies(validated);
244
225
  return validated;
245
226
  }
@@ -62,6 +62,11 @@ import {
62
62
  evaluateDraftReachability,
63
63
  renderReachabilityOrphans,
64
64
  } from '../plan-reachability.js';
65
+ import {
66
+ computeAssembledConflictFindings,
67
+ conflictFindingKey,
68
+ renderHardConflictError,
69
+ } from '../ticket-validator-conflicts.js';
65
70
  import { upsertStructuredComment } from '../ticketing.js';
66
71
  import {
67
72
  enforceFanOutGate,
@@ -376,6 +381,50 @@ function resolveEffectiveRoute({
376
381
  };
377
382
  }
378
383
 
384
+ /**
385
+ * Re-run the cross-Story conflict passes over the assembled bodies and route
386
+ * the result (Story #5045).
387
+ *
388
+ * `validateTickets` runs before assembly, over the raw payload, so until now
389
+ * plan-time conflict analysis judged an artifact that is not the one persist
390
+ * writes — and the passes that scan `body.acceptance` / `body.verify` were
391
+ * inert on the canonical top-level authoring shape as a result.
392
+ *
393
+ * Three outcomes, in order:
394
+ *
395
+ * 1. **Hard findings throw.** Policy upgrades (`planning.failOnSharedEditors`,
396
+ * `planning.requireExplicitCrossStoryDeps`) are off by default; when an
397
+ * operator turns one on it must bite on the persisted artifact too, and it
398
+ * must bite **before** the first `createIssue`.
399
+ * 2. **Soft findings the raw pass already reported are dropped**, so the same
400
+ * collision is not announced twice per run.
401
+ * 3. **Everything else is returned** for the plan-summary comment, which is
402
+ * where these findings stop being a stderr line nobody keeps.
403
+ *
404
+ * @param {{ stories: object[], config: object, rawFindings: object[] }} args
405
+ * @returns {object[]} The assembled-pass findings, for the summary comment.
406
+ */
407
+ function analyzeAssembledStories({ stories, config, rawFindings }) {
408
+ const findings = computeAssembledConflictFindings({ stories, config });
409
+ const hard = findings.filter((finding) => finding.severity === 'hard');
410
+ if (hard.length > 0) {
411
+ throw new Error(
412
+ `[plan-persist] ${hard.length} cross-Story conflict(s) in the assembled ` +
413
+ `Story bodies:\n${hard.map((f) => ` - ${renderHardConflictError(f)}`).join('\n')}`,
414
+ );
415
+ }
416
+ const alreadyReported = new Set(
417
+ (rawFindings ?? []).map((finding) => conflictFindingKey(finding)),
418
+ );
419
+ surfaceSoftConflictFindings(
420
+ findings.filter(
421
+ (finding) => !alreadyReported.has(conflictFindingKey(finding)),
422
+ ),
423
+ 'plan-persist/assembled',
424
+ );
425
+ return findings;
426
+ }
427
+
379
428
  /**
380
429
  * Reap abandoned `plan-*` directories under the temp root (Story #4541).
381
430
  *
@@ -702,14 +751,25 @@ export async function runPlanPersist({
702
751
  sharedSpec: techSpecContent,
703
752
  planAcceptance: planAcceptance ?? undefined,
704
753
  sourceTicketIds,
705
- // The seed this plan was authored from is the provenance source: an audit
706
- // sweep's Single-plan seed carries the `audit-fingerprints` /
707
- // `audit-semantic-keys` footers, which assembly copies into every persisted
708
- // Story body so the next sweep recognises what it already planned
709
- // (Story #4877). Empty for a `--tickets` run, which is a no-op.
754
+ // The seed this plan was authored from: an audit sweep's Single-plan seed
755
+ // carries the `audit-fingerprints` / `audit-semantic-keys` footers, and
756
+ // assembly copies them into the persisted Story bodies so the next sweep
757
+ // recognises what it already planned (Story #4877). Since Story #5045 this
758
+ // is the **fallback** it is carried onto every Story that did not
759
+ // attribute its own `provenance`, which keeps an un-attributed plan exactly
760
+ // as recall-safe as it was. Empty for a `--tickets` run, a no-op there.
710
761
  provenanceSource: planContextEnvelope?.seed?.content ?? '',
711
762
  });
712
763
 
764
+ // Story #5045: the cross-Story conflict passes re-run over the assembled,
765
+ // footer-stamped bodies — the artifact persist actually writes — before any
766
+ // GitHub call, so a policy upgrade still refuses the plan pre-creation.
767
+ const assembledConflicts = analyzeAssembledStories({
768
+ stories,
769
+ config,
770
+ rawFindings: validated.findings,
771
+ });
772
+
713
773
  // Effective complexity route (Story #4722): the planner's authored lite
714
774
  // verdict (recorded reason), validated against every assembled Story's own
715
775
  // shape — a claim exceeding the shape ceilings fails closed to full. Lite
@@ -764,6 +824,10 @@ export async function runPlanPersist({
764
824
  mode: 'stories',
765
825
  planMetricsLine,
766
826
  stories: created,
827
+ // Story #5045: the wave table promises what can run in parallel; the
828
+ // collisions the conflict passes found belong on the same surface, or the
829
+ // promise is the only half anyone reads.
830
+ conflictFindings: assembledConflicts,
767
831
  });
768
832
 
769
833
  if (!dryRun) {