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
@@ -0,0 +1,97 @@
1
+ /**
2
+ * footer-block.js — the Story body's `---` footer grammar.
3
+ *
4
+ * A Story body is prose an operator edits. Its **declared dependency edges**
5
+ * are not: they live in a footer block, in one exact line shape, and that
6
+ * distinction is the whole safety property. An unanchored whole-body scan
7
+ * (what `parseBlockedBy` used to be) turned any sentence that merely mentioned
8
+ * a blocker into a real dispatch gate — an example, a changelog note, an
9
+ * acceptance criterion quoting the phrase — and withheld the Story until an
10
+ * unrelated issue closed.
11
+ *
12
+ * This module is the single home for that grammar. Both readers go through it:
13
+ * `lib/story-body/story-body.js` (what a body round-trips as `depends_on`) and
14
+ * `lib/dependency-parser.js` (what gates dispatch). Sharing one implementation
15
+ * is what keeps them from drifting apart into two different answers about the
16
+ * same body.
17
+ *
18
+ * @module lib/story-body/footer-block
19
+ */
20
+
21
+ /**
22
+ * The one line shape that declares a dependency edge: `blocked by #N` alone on
23
+ * its own line inside the footer block. Anchored at both ends deliberately —
24
+ * `depends on #N`, `Blocked by: #N`, and `blocked by #N once X lands` all
25
+ * declare nothing.
26
+ */
27
+ const FOOTER_BLOCKED_BY_LINE_RE = /^blocked by\s+(#\d+)$/i;
28
+
29
+ /** A `---` rule on its own line. */
30
+ const FOOTER_RULE_RE = /^---\s*$/;
31
+
32
+ /** Footer keys that qualify a bare `---` rule as the footer separator. */
33
+ const FOOTER_KEY_RE = /^(parent:|Epic:|blocked by)/im;
34
+
35
+ /**
36
+ * True when line `index` opens the footer block: a `---` on its own line whose
37
+ * remaining lines start with a recognised footer key (`parent:`, `Epic:`,
38
+ * `blocked by`). A `---` opening a thematic break or a table mid-body is
39
+ * therefore not mistaken for the footer.
40
+ *
41
+ * @param {string} line
42
+ * @param {string[]} lines
43
+ * @param {number} index
44
+ * @returns {boolean}
45
+ */
46
+ export function isFooterSeparator(line, lines, index) {
47
+ if (!FOOTER_RULE_RE.test(line)) return false;
48
+ return FOOTER_KEY_RE.test(lines.slice(index + 1).join('\n'));
49
+ }
50
+
51
+ /**
52
+ * Return the footer block of a body — everything after the footer separator —
53
+ * or `''` when the body carries no footer.
54
+ *
55
+ * Module-private: `parseFooterBlockedByIds` is its only caller. The body
56
+ * parser splits its own sections and reaches for `parseFooterBlockedByRefs`
57
+ * with the footer it already has, so exporting this would ship a symbol with
58
+ * no consumer.
59
+ *
60
+ * @param {string} body
61
+ * @returns {string}
62
+ */
63
+ function extractFooterBlock(body) {
64
+ if (!body) return '';
65
+ const lines = String(body).split('\n');
66
+ const start = lines.findIndex((line, i) => isFooterSeparator(line, lines, i));
67
+ return start === -1 ? '' : lines.slice(start + 1).join('\n');
68
+ }
69
+
70
+ /**
71
+ * Extract the `blocked by #N` refs from an already-split footer block, as the
72
+ * `"#N"` strings a Story body's `depends_on` field round-trips.
73
+ *
74
+ * @param {string} footerBlock
75
+ * @returns {string[]}
76
+ */
77
+ export function parseFooterBlockedByRefs(footerBlock) {
78
+ if (!footerBlock) return [];
79
+ return String(footerBlock)
80
+ .split('\n')
81
+ .map((line) => line.trim().match(FOOTER_BLOCKED_BY_LINE_RE)?.[1])
82
+ .filter((ref) => typeof ref === 'string');
83
+ }
84
+
85
+ /**
86
+ * Parse a body's declared blocker issue **numbers**, deduped — the
87
+ * dispatch-edge view of the same footer the body parser reads.
88
+ *
89
+ * @param {string} body
90
+ * @returns {number[]}
91
+ */
92
+ export function parseFooterBlockedByIds(body) {
93
+ const ids = parseFooterBlockedByRefs(extractFooterBlock(body)).map((ref) =>
94
+ Number.parseInt(ref.slice(1), 10),
95
+ );
96
+ return [...new Set(ids)];
97
+ }
@@ -21,7 +21,6 @@
21
21
  * wide: { reason } | null,// declared-wide footprint (optional)
22
22
  * reason_to_exist: string | null, // one-sentence cohesion reason (optional)
23
23
  * depends_on: string[], // blocker story slugs or #ids
24
- * estimated_test_files: number | null, // absent → null (informational)
25
24
  * }
26
25
  * ```
27
26
  *
@@ -49,6 +48,7 @@ import {
49
48
  } from '../framework-version.js';
50
49
  import { FILE_ASSUMPTION_VALUES } from '../orchestration/file-assumption-enum.js';
51
50
  import { suggestPathEntryFix } from './body-format-lints.js';
51
+ import { isFooterSeparator, parseFooterBlockedByRefs } from './footer-block.js';
52
52
 
53
53
  // ---------------------------------------------------------------------------
54
54
  // Public types (JSDoc only — no runtime schema file)
@@ -79,7 +79,6 @@ import { suggestPathEntryFix } from './body-format-lints.js';
79
79
  * @property {{ reason: string }|null} wide - Declared-wide footprint (reason), or null.
80
80
  * @property {string|null} reason_to_exist - One-sentence cohesion reason ("why this Story exists"), or null.
81
81
  * @property {string[]} depends_on - Blocking story slugs / issue refs.
82
- * @property {number|null} estimated_test_files - Test surface count or null.
83
82
  * @property {string|null} mandrel_version - Framework version stamped at authoring, or null.
84
83
  * @property {string|null} authored_at - Authoring date (YYYY-MM-DD) stamped at authoring, or null.
85
84
  */
@@ -315,16 +314,15 @@ function pathEntryFixIt(raw) {
315
314
  * Extract the `blocked by #N` lines from the footer block (text after
316
315
  * the last `---` separator). Returns an array of "#N" strings.
317
316
  *
317
+ * Delegates to `./footer-block.js`, which owns the footer-block grammar
318
+ * (Story #5046) so the body parser and the dispatch-edge parser cannot
319
+ * disagree about what declares an edge.
320
+ *
318
321
  * @param {string} footerBlock
319
322
  * @returns {string[]}
320
323
  */
321
324
  function extractBlockedBy(footerBlock) {
322
- const deps = [];
323
- for (const line of footerBlock.split('\n')) {
324
- const m = line.trim().match(/^blocked by\s+(#\d+)$/i);
325
- if (m) deps.push(m[1]);
326
- }
327
- return deps;
325
+ return parseFooterBlockedByRefs(footerBlock);
328
326
  }
329
327
 
330
328
  // Matches any trailing `<!-- meta: … -->` block. Object payloads are the
@@ -334,7 +332,7 @@ const META_BLOCK_RE = /<!--\s*meta:\s*([\s\S]*?)\s*-->/;
334
332
  const META_OBJECT_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
335
333
 
336
334
  /**
337
- * Extract the `wide` / `estimated_test_files` fields from the trailing
335
+ * Extract the `wide` / `reason_to_exist` fields from the trailing
338
336
  * `<!-- meta: {...} -->` comment block written by {@link serialize}. Returns
339
337
  * canonical-shaped values (null when absent or malformed) so the parser
340
338
  * round-trips the meta block faithfully.
@@ -350,13 +348,12 @@ const META_OBJECT_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
350
348
  * rather than dropping or re-deriving it.
351
349
  *
352
350
  * @param {string} markdown
353
- * @returns {{ wide: { reason: string }|null, reason_to_exist: string|null, estimated_test_files: number|null, mandrel_version: string|null, authored_at: string|null }}
351
+ * @returns {{ wide: { reason: string }|null, reason_to_exist: string|null, mandrel_version: string|null, authored_at: string|null }}
354
352
  */
355
353
  function extractMeta(markdown) {
356
354
  const result = {
357
355
  wide: null,
358
356
  reason_to_exist: null,
359
- estimated_test_files: null,
360
357
  mandrel_version: null,
361
358
  authored_at: null,
362
359
  };
@@ -377,9 +374,6 @@ function extractMeta(markdown) {
377
374
 
378
375
  result.wide = normalizeWide(parsed.wide);
379
376
  result.reason_to_exist = normalizeReasonToExist(parsed.reason_to_exist);
380
- if (typeof parsed.estimated_test_files === 'number') {
381
- result.estimated_test_files = parsed.estimated_test_files;
382
- }
383
377
  if (
384
378
  typeof parsed.mandrel_version === 'string' &&
385
379
  parsed.mandrel_version.trim()
@@ -497,22 +491,6 @@ function splitSections(markdown) {
497
491
  return { sections, footer, preamble };
498
492
  }
499
493
 
500
- /**
501
- * True when line `index` opens the footer block: a `---` on its own line
502
- * whose remaining lines start with a recognised footer key (`parent:`,
503
- * `Epic:`, `blocked by`).
504
- *
505
- * @param {string} line
506
- * @param {string[]} lines
507
- * @param {number} index
508
- * @returns {boolean}
509
- */
510
- function isFooterSeparator(line, lines, index) {
511
- if (!/^---\s*$/.test(line)) return false;
512
- const remaining = lines.slice(index + 1).join('\n');
513
- return /^(parent:|Epic:|blocked by)/im.test(remaining);
514
- }
515
-
516
494
  /**
517
495
  * True for a non-canonical markdown heading that TERMINATES the current
518
496
  * structured section. Trailing extended content a producer appends after the
@@ -579,7 +557,6 @@ function isMachineMarkerLine(line) {
579
557
  function parseUnstructuredBody(input, preamble, footer) {
580
558
  const warnings = [
581
559
  'unstructured-body: no structured sections found; returning minimal body from preamble text.',
582
- 'test-surface-unestimated: estimated_test_files not present.',
583
560
  ];
584
561
  const body = {
585
562
  goal: preamble || input.trim(),
@@ -593,7 +570,6 @@ function parseUnstructuredBody(input, preamble, footer) {
593
570
  wide: null,
594
571
  reason_to_exist: null,
595
572
  depends_on: extractBlockedBy(footer),
596
- estimated_test_files: null,
597
573
  mandrel_version: null,
598
574
  authored_at: null,
599
575
  };
@@ -688,11 +664,6 @@ function parseTextListSection(lines) {
688
664
  * `result.warnings` to detect legacy path entries that should be
689
665
  * migrated.
690
666
  *
691
- * Informational finding emitted on `result.warnings`:
692
- * - `test-surface-unestimated` — when `estimated_test_files` is absent
693
- * from both a structured body and the markdown. Callers that care about
694
- * test-surface coverage SHOULD surface this to the operator.
695
- *
696
667
  * @param {string|object} input - Markdown string or already-structured body object.
697
668
  * @returns {ParseResult}
698
669
  * @throws {StoryBodyParseError} When the body is structurally unrecoverable.
@@ -765,20 +736,16 @@ export function parse(input) {
765
736
  const non_goals = parseTextListSection(sections.get('non_goals') ?? []);
766
737
  const dependsOn = extractBlockedBy(footer);
767
738
 
768
- // --- Recover wide / estimated_test_files from the meta block ---
739
+ // --- Recover wide / reason_to_exist / provenance from the meta block ---
769
740
  // serialize() writes these into a trailing `<!-- meta: {...} -->` comment
770
741
  // so round-trips preserve them. Absent meta block → canonical null defaults.
742
+ // Unknown keys are ignored, so a body carrying a retired meta field parses
743
+ // clean and simply drops it.
771
744
  const meta = extractMeta(input);
772
- const estimated_test_files = meta.estimated_test_files;
773
745
  const wide = meta.wide;
774
746
  const reason_to_exist = meta.reason_to_exist;
775
747
  const mandrel_version = meta.mandrel_version;
776
748
  const authored_at = meta.authored_at;
777
- if (estimated_test_files === null) {
778
- warnings.push(
779
- 'test-surface-unestimated: estimated_test_files not present.',
780
- );
781
- }
782
749
 
783
750
  const body = {
784
751
  goal,
@@ -792,7 +759,6 @@ export function parse(input) {
792
759
  wide,
793
760
  reason_to_exist,
794
761
  depends_on: dependsOn,
795
- estimated_test_files,
796
762
  mandrel_version,
797
763
  authored_at,
798
764
  };
@@ -834,13 +800,6 @@ function parseStructuredObject(obj) {
834
800
  body[name] = STRUCTURED_FIELD_NORMALIZERS[kind](obj[name], warnings);
835
801
  }
836
802
 
837
- // estimated_test_files is the one warning-coupled scalar: absent (== null)
838
- // warns; a non-number, non-null value stays null silently.
839
- body.estimated_test_files = normalizeEstimatedTestFiles(
840
- obj.estimated_test_files,
841
- warnings,
842
- );
843
-
844
803
  // Provenance stamp (preserved verbatim; never re-derived here).
845
804
  body.mandrel_version = normalizeProvenanceString(obj.mandrel_version);
846
805
  body.authored_at = normalizeProvenanceString(obj.authored_at);
@@ -917,25 +876,6 @@ const STRUCTURED_FIELD_NORMALIZERS = {
917
876
  reasonToExist: (raw) => normalizeReasonToExist(raw),
918
877
  };
919
878
 
920
- /**
921
- * Normalize `estimated_test_files`: a number passes through; an absent
922
- * (`null`/`undefined`) value warns `test-surface-unestimated`; any other
923
- * value stays `null` silently.
924
- *
925
- * @param {unknown} raw
926
- * @param {string[]} warnings
927
- * @returns {number|null}
928
- */
929
- function normalizeEstimatedTestFiles(raw, warnings) {
930
- if (typeof raw === 'number') return raw;
931
- if (raw == null) {
932
- warnings.push(
933
- 'test-surface-unestimated: estimated_test_files not present.',
934
- );
935
- }
936
- return null;
937
- }
938
-
939
879
  /**
940
880
  * Normalize a provenance stamp field (`mandrel_version` / `authored_at`) to
941
881
  * a non-empty trimmed string or `null`.
@@ -1066,11 +1006,11 @@ const SERIALIZE_SECTIONS = [
1066
1006
 
1067
1007
  /**
1068
1008
  * Build the trailing `<!-- meta: {...} -->` block carrying the fields that
1069
- * have no human-readable section (`wide`, `reason_to_exist`,
1070
- * `estimated_test_files`). Returns the empty string when no meta field is
1071
- * present so {@link serialize} appends nothing.
1009
+ * have no human-readable section (`wide`, `reason_to_exist`). Returns the
1010
+ * empty string when no meta field is present so {@link serialize} appends
1011
+ * nothing.
1072
1012
  *
1073
- * Key insertion order (`wide` → `reason_to_exist` → `estimated_test_files` →
1013
+ * Key insertion order (`wide` → `reason_to_exist` →
1074
1014
  * `mandrel_version` → `authored_at`) is load-bearing: it fixes the serialized
1075
1015
  * JSON byte sequence the parser's meta round-trip and the unit suite assert
1076
1016
  * against. The provenance stamp keys are appended **last** so every
@@ -1089,9 +1029,6 @@ function serializeMetaBlock(body) {
1089
1029
  if (reasonToExist !== null) {
1090
1030
  metaFields.reason_to_exist = reasonToExist;
1091
1031
  }
1092
- if (typeof body.estimated_test_files === 'number') {
1093
- metaFields.estimated_test_files = body.estimated_test_files;
1094
- }
1095
1032
  if (typeof body.mandrel_version === 'string' && body.mandrel_version.trim()) {
1096
1033
  metaFields.mandrel_version = body.mandrel_version.trim();
1097
1034
  }
@@ -1153,9 +1090,9 @@ function serializeFooter(body, opts) {
1153
1090
  * `## Goal`, `## Slicing`, `## Spec`, `## Changes`, `## Acceptance`,
1154
1091
  * `## Verify`, `## References`, `## Non-Goals` (each omitted when empty).
1155
1092
  *
1156
- * `wide`, `reason_to_exist`, and `estimated_test_files` are emitted as a
1157
- * fenced `<!-- meta -->` comment block so round-trips preserve them without
1158
- * polluting the human-readable body.
1093
+ * `wide` and `reason_to_exist` are emitted as a fenced `<!-- meta -->`
1094
+ * comment block so round-trips preserve them without polluting the
1095
+ * human-readable body.
1159
1096
  *
1160
1097
  * @param {StoryBody} body
1161
1098
  * @param {SerializeOptions} [opts]
@@ -31,7 +31,7 @@
31
31
  * ## Keep-class
32
32
  *
33
33
  * `signals.ndjson` is the artifact whose value *starts* when the run ends —
34
- * `signals-view`, `acceptance-eval`, and the loop-health check all read it
34
+ * `acceptance-eval` and the loop-health check both read it
35
35
  * long after the Story merged. It is excluded twice over: it is not in the
36
36
  * evidence basename allowlist, and {@link KEEP_BASENAMES} is re-checked at
37
37
  * the deletion site. Defence in depth is warranted for the one file whose
@@ -109,7 +109,7 @@ You MUST respond ONLY with a valid JSON array of objects. No prose, no markdown
109
109
  **Slug format**: \`^[a-z0-9][a-z0-9-]*$\` — hyphen-case only. Underscores are rejected by the validator.
110
110
 
111
111
  ### STORY BODY SCHEMA (REQUIRED FOR EVERY STORY):
112
- \`body\` is either the serialized markdown **string** (the section format below) or a **structured object** carrying the same fields (\`goal\`, optional \`slicing\` / \`spec\`, \`changes\`, optional \`non_goals\` / \`wide\` / \`reason_to_exist\` / \`estimated_test_files\`) — persist parses either shape and serializes the canonical markdown itself, so you never need to read \`story-body.js\` or hand-assemble the markdown (the \`stories.template.json\` file emitted next to the plan-context envelope is a ready-to-fill structured-object skeleton). Stories are consumed by non-interactive sub-agents that must self-verify from the Story ticket alone — so the ticket must carry everything an agent needs to execute and self-verify.
112
+ \`body\` is either the serialized markdown **string** (the section format below) or a **structured object** carrying the same fields (\`goal\`, optional \`slicing\` / \`spec\`, \`changes\`, optional \`non_goals\` / \`wide\` / \`reason_to_exist\`) — persist parses either shape and serializes the canonical markdown itself, so you never need to read \`story-body.js\` or hand-assemble the markdown (the \`stories.template.json\` file emitted next to the plan-context envelope is a ready-to-fill structured-object skeleton). Stories are consumed by non-interactive sub-agents that must self-verify from the Story ticket alone — so the ticket must carry everything an agent needs to execute and self-verify.
113
113
 
114
114
  The \`acceptance[]\` and \`verify[]\` arrays live at the **top level** of the Story ticket object — that is the machine contract the validator reads. Author each list **once, at top level**, and **omit** the \`## Acceptance\` / \`## Verify\` sections from the authored \`body\` string: persist syncs the top-level arrays into those sections so the GitHub issue stays a complete executable document. The validator resolves both fields from the top level, so an omitted section is the expected shape, not a violation.
115
115
 
@@ -151,7 +151,6 @@ The **persisted** \`body\` renders these markdown sections (in order) — you au
151
151
  - **acceptance** (top-level array on the ticket object): Items MUST be observable from outside the agent. Acceptable shapes: a specific command exits 0, a file exists at a given path, a snapshot test matches, a \`data-testid\` resolves under a given selector, a row count in a fixture matches. UNACCEPTABLE: "verify by reading the diff", "looks good", "matches the spec" — push these down into a \`verify\` command instead.
152
152
  - **verify** (top-level array on the ticket object): Each entry MUST name a testing tier in parentheses, drawn from \`unit\` / \`contract\` / \`e2e\` / \`validate\`. Example: \`npm run test -- src/x.test.ts (unit)\`, \`npm run validate (validate)\`. Stories with zero verify entries SHOULD fail validation; if a story is genuinely unverifiable in isolation (e.g., a copy edit auditor will eyeball), the literal entry \`manual:<reason>\` is allowed so the absence is intentional, not lazy. Manual entries without a reason are rejected.
153
153
  - **reason to exist** (REQUIRED, encoded as the \`reason_to_exist\` field of the \`<!-- meta: {...} -->\` comment appended to the serialized body string — NOT a top-level ticket field): One sentence stating the single coherent reason this Story exists, distinct from its broader \`## Goal\` prose. Every Story MUST carry a non-empty \`reason_to_exist\`; it is the machine-checkable form of the cohesion rule (**one Story = one coherent change with one reason to exist**) and the \`epic-plan-consolidate\` critic flags any Story whose body carries no non-empty reason to exist. Encode it as \`<!-- meta: {"reason_to_exist": "..."} -->\`.
154
- - **estimated_test_files** (optional, encoded in the \`<!-- meta: {...} -->\` comment appended to the serialized body string — NOT a top-level ticket field): Integer estimate of how many test files this Story creates or modifies. Omit when the number is not estimable. Informational only — it does not gate the decompose.
155
154
  - **Observed-behavior claims open with \`Current state (verified <date>)\`.** Any Spec claim about how the codebase behaves today MUST open with that preamble (e.g. \`Current state (verified 2026-07-17): …\`) so a reader can tell a verified observation from an assumption, and can tell when the observation went stale.
156
155
  - **Intent-then-proxy acceptance shape.** When an acceptance item verifies through a proxy check (a grep, a file-exists probe, an exit-code test), state the intent clause before the proxy check — what outcome the check stands in for — so the proxy never becomes the goal (e.g. "the workflow names hygiene findings as re-author input: \`grep -n "textHygiene" …\` exits 0").
157
156
  - **Slicing checkpoints are one line each.** Each \`## Slicing\` checkpoint is a single line naming the checkpoint; implementation detail lives in \`## Spec\`, never duplicated into Slicing. A Slicing section outweighing its Spec is a defect the text-hygiene lint flags.
@@ -171,7 +170,7 @@ The Story is executed by a frontier-model deliverer that reads the codebase itse
171
170
 
172
171
  #### DETERMINISTIC BODY-FORMAT LINTS — author lint-clean by construction:
173
172
 
174
- Persist enforces the deterministic body-format rules below and **rejects** an authored body that violates any of them. Author every Story to satisfy all of them on the FIRST draft — each rule is stated example-first so there is nothing to discover by trial-and-error. The two auto-fixable rules (\`changes-path-entry-shape\`, \`verify-tier-suffix\`) also emit the corrected form in the dry-run failure output, but authoring them right up front avoids the round-trip entirely.
173
+ Persist enforces the deterministic body-format rules below and **rejects** an authored body that violates any of them. Author every Story to satisfy all of them on the FIRST draft — each rule is stated example-first so there is nothing to discover by trial-and-error. \`verify-tier-suffix\` is the one rule persist repairs for you: when the tier is unambiguously inferable from the command, persist appends it and proceeds; when it is not, the entry is still rejected and you must choose the tier. \`changes-path-entry-shape\` emits the corrected form in the dry-run failure output but is never applied for you.
175
174
 
176
175
  ${bodyFormatLintChecklist}
177
176
 
@@ -231,35 +230,38 @@ Declaring \`wide\` with a non-empty reason **lifts the hard session-mass rejecti
231
230
 
232
231
  #### UI / TESTID INVARIANCE (per CLAUDE.md safety rule):
233
232
 
234
- - Stories that touch UI (\`*.tsx\`, \`*.astro\`, \`*.svelte\`, \`*.vue\`, components folders) MUST end \`changes\` with one of:
235
- - \`data-testid invariance: <list of testids that MUST be preserved>\`, or
236
- - \`data-testid changes: <old> -> <new>\` paired with a corresponding \`tests/e2e/*.spec.ts\` edit in the same story or a depends_on Story.
233
+ Every \`changes[]\` entry is a \`{ path, assumption }\` object a prose bullet there is rejected by the parser, so the testid contract is carried where prose belongs:
234
+
235
+ - Stories that touch UI (\`*.tsx\`, \`*.astro\`, \`*.svelte\`, \`*.vue\`, components folders) MUST carry the testid contract as a top-level \`acceptance[]\` item, one of:
236
+ - \`"data-testid invariance: <list of testids that MUST be preserved>"\`, or
237
+ - \`"data-testid changes: <old> -> <new>, with the matching tests/e2e/*.spec.ts selector updated"\` — paired with that \`tests/e2e/*.spec.ts\` file in \`changes[]\`, in the same Story or a depends_on Story.
238
+ - State the preserved-testid set in \`## Non-Goals\` prose as well when the Story deliberately renames nothing.
237
239
  - Renaming a testid without the matching e2e edit is FORBIDDEN.
238
240
 
239
241
  #### BRAND / COPY / STYLE WORK:
240
242
 
241
- - Stories that touch user-visible copy, brand assets, or visual style MUST cite the relevant section of \`docs/style-guide.md\` in \`acceptance\` (e.g. \`"acceptance": ["Hero copy matches docs/style-guide.md §3 (voice & tone)"]\`). If \`docs/style-guide.md\` does not exist or has no relevant section, state that explicitly: \`"acceptance": ["docs/style-guide.md absent — copy reviewed against the inline brand brief in the Epic body"]\`. Silence on style sourcing is a smell.
243
+ - Stories that touch user-visible copy, brand assets, or visual style MUST cite the relevant section of \`docs/style-guide.md\` in \`acceptance\` (e.g. \`"acceptance": ["Hero copy matches docs/style-guide.md §3 (voice & tone)"]\`). If \`docs/style-guide.md\` does not exist or has no relevant section, state that explicitly: \`"acceptance": ["docs/style-guide.md absent — copy reviewed against the inline brand brief in the plan seed"]\`. Silence on style sourcing is a smell.
242
244
 
243
- ### WAVE-0 BDD SCAFFOLD STORY (features-first; emit when the Acceptance Spec has \`new\`-disposition rows):
244
- The Acceptance Spec's AC table (columns \`AC ID | Outcome | Feature File | Scenario | Disposition\`) tags each row's \`Disposition\` with one of \`new | updated | unchanged\`. A \`new\` row names a \`.feature\` file + scenario that does NOT yet exist on \`main\`. The framework is features-first: implementing Stories reference those \`.feature\` paths in their \`verify[]\` lines, so the files MUST already exist when those Stories run — otherwise verification fails mid-delivery on a missing file. (These Gherkin \`.feature\` files are BDD artifacts, unrelated to any ticket tier.)
245
+ ### WAVE-0 BDD SCAFFOLD STORY (features-first; emit when your plan verifies against a scenario that does not exist yet):
246
+ The plan-context envelope's \`bddScenarios\` field is the index of Gherkin scenarios that **already exist on \`main\`** one row per scenario, carrying its \`.feature\` file path, line, scenario title and tags. It is the live signal for this rule: a \`.feature\` path + scenario your plan needs but that appears in no \`bddScenarios\` row does not exist yet. The framework is features-first: implementing Stories reference those \`.feature\` paths in their \`verify[]\` lines, so the files MUST already exist when those Stories run — otherwise verification fails mid-delivery on a missing file.
245
247
 
246
- When the Acceptance Spec contains **one or more \`Disposition: new\` rows**, you MUST emit **exactly one** dedicated wave-0 scaffold Story whose sole job is to create the \`.feature\` files with \`@skip\`-tagged scenarios BEFORE any implementation Story runs:
248
+ When **one or more** \`.feature\` scenarios your plan verifies against are absent from \`bddScenarios\`, you MUST emit **exactly one** dedicated wave-0 scaffold Story whose sole job is to create those \`.feature\` files with \`@skip\`-tagged scenarios BEFORE any implementation Story runs:
247
249
 
248
250
  - **goal**: contains the literal token \`bdd-scaffold\` (e.g. "bdd-scaffold: create the @skip-tagged feature files the implementation Stories verify against").
249
251
  - **depends_on**: EMPTY (\`[]\`) — it runs first, in wave 0.
250
- - **changes**: one entry per distinct \`.feature\` file named in a \`new\` row, each \`{ "path": "<feature file path>", "assumption": "creates" }\`.
252
+ - **changes**: one entry per distinct absent \`.feature\` file, each \`{ "path": "<feature file path>", "assumption": "creates" }\`.
251
253
  - **acceptance**: MUST assert (a) every new \`.feature\` file exists AND (b) every new scenario within them carries an \`@skip\` tag. Keep these observable (a grep/validate command exits 0, a file exists at a path).
252
254
  - **verify**: a grep/validate command (tier \`validate\`), NOT an e2e runner — verifying that a file exists with the required tags needs no browser/playwright run. Example: \`grep -rL '@skip' tests/features/<area>/*.feature (validate)\` paired with an existence check.
253
255
  - Each implementation Story whose \`verify[]\` references one of these scaffolded \`.feature\` paths MUST \`depends_on\` the scaffold Story (so the scaffold lands in an earlier wave). Omitting the link trips the soft \`missing-bdd-scaffold\` validator finding.
254
256
 
255
- When the Acceptance Spec contains **zero \`new\`-disposition rows** (every row is \`updated\` or \`unchanged\`), do NOT emit a scaffold Story — there is nothing to create.
257
+ When every scenario your plan verifies against is already present in \`bddScenarios\`, do NOT emit a scaffold Story — there is nothing to create.
256
258
 
257
259
  ### SCOPE-OVERLAP FLAGGING (docs/runbook downstream of config work):
258
- When a "docs update" / "runbook" / "README" Story appears downstream of an earlier Story in the same Epic whose AC already covers updating the same document (e.g. a "config + runbook" Story followed by a "docs" Story touching the same runbook), the downstream Story's deliverable may be fully absorbed by the earlier Story. Flag the risk directly in the Story's top-level \`acceptance\` array by appending an item of the form:
260
+ When a "docs update" / "runbook" / "README" Story appears downstream of an earlier Story in the same plan whose AC already covers updating the same document (e.g. a "config + runbook" Story followed by a "docs" Story touching the same runbook), the downstream Story's deliverable may be fully absorbed by the earlier Story. Flag the risk directly in the Story's top-level \`acceptance\` array by appending an item of the form:
259
261
  "Scope verification note: this story's deliverable may already be satisfied by Story #<slug-or-id>'s AC — before implementing, \`git diff main -- <path>\` against the upstream Story branch and confirm whether a substantive edit is still required, or whether only a cross-reference remains."
260
262
  This prevents the executing agent from redoing work the upstream Story already merged.
261
263
 
262
- CRITICAL: Dependencies should follow execution blockers. Stories attach directly to the Epic — never emit a 'parent_slug' field.
264
+ CRITICAL: Dependencies should follow execution blockers. There is no parent ticket — never emit a 'parent_slug' field.
263
265
  IMPORTANT DEPENDENCY RULE: Story-to-Story dependencies are expressed via \`depends_on\` (one Story depends_on another Story's slug). Use this to express execution ordering across the plan.
264
266
 
265
267
  ### REVIEWABILITY BUDGET (Story #2798):
@@ -53,11 +53,10 @@ export const TICKET_BODY_SECTIONS = Object.freeze({
53
53
  export const ACCEPTANCE_TABLE_HEADING = '## Acceptance Table';
54
54
 
55
55
  /**
56
- * Regex matching the Tech Spec's required opening heading (same variants
57
- * `spec-section-validator.js` accepts). Exported so it is the single
58
- * source of truth for this pattern — `spec-section-validator.js`,
59
- * `consolidation-precondition.js` all import
60
- * it rather than each carrying their own copy.
56
+ * Regex matching the Tech Spec's `## Delivery Slicing` heading, tolerating
57
+ * the `Delivery ` qualifier and casing variants. Exported so it is the single
58
+ * source of truth for this pattern — `consolidation-precondition.js` imports
59
+ * it rather than carrying its own copy.
61
60
  */
62
61
  export const DELIVERY_SLICING_RE = /^##\s+(?:Delivery\s+)?Slicing\s*$/im;
63
62