mandrel 2.30.0 → 2.32.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 (253) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -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/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  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 +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  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 +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. package/.agents/workflows/helpers/signals.md +0 -112
@@ -19,7 +19,7 @@
19
19
  * module mechanizes it.
20
20
  *
21
21
  * Contract (pure where possible — the only side effect is an injectable
22
- * `git log` read used to derive the type):
22
+ * `git log` read):
23
23
  *
24
24
  * - If `storyTitle` is **already** a parseable Conventional Commit
25
25
  * subject, it is preserved verbatim and suffixed with `(#<storyId>)`.
@@ -28,116 +28,48 @@
28
28
  * `<type>: <descriptive text> (#<storyId>)`. The `type` is derived
29
29
  * from the branch's own (already-conventional) commit subjects when
30
30
  * available, falling back to a safe configured default (`chore`).
31
+ * - Either way, a branch (or Story) that declares a breaking change gets
32
+ * the `!` marker and a `BREAKING CHANGE:` footer on the PR body.
31
33
  *
32
- * Mirrors the already-normalized Epic-finalize default in
33
- * `lib/orchestration/finalize/open-or-locate-pr.js` (`feat: Epic #<id>`),
34
- * bringing the standalone path to the same guarantee.
34
+ * The rules that decide *what* the subject says now live in
35
+ * `conventional-subject.js` type precedence, acronym-safe casing, and
36
+ * breaking-change collection are pure and unit-tested there. What is left
37
+ * here is the git read those rules consume and the assembly of the two
38
+ * strings `gh pr create` needs.
35
39
  */
36
40
 
37
41
  import { gitSpawn as defaultGitSpawn } from '../../../git-utils.js';
38
42
  import { Logger as DefaultLogger } from '../../../Logger.js';
43
+ import {
44
+ collectBreakingNotes,
45
+ isConventionalSubject,
46
+ markBreaking,
47
+ pickDominantType,
48
+ shapeDescription,
49
+ } from './conventional-subject.js';
39
50
 
40
51
  /** Safe default Conventional-Commit type when none can be derived. */
41
52
  const DEFAULT_CONVENTIONAL_TYPE = 'chore';
42
53
 
43
54
  /**
44
- * The Conventional-Commit types Mandrel accepts. Mirrors
45
- * `commitlint.config.js` `type-enum` and `release-please-config.json`
46
- * `changelog-sections`. Kept in sync by hand (single hard-cutover, no
47
- * shim) — adding a type means touching all three.
55
+ * Record separator between whole commit messages in the `git log` read. A NUL
56
+ * cannot occur inside a commit message, so splitting on it is unambiguous —
57
+ * unlike a blank-line or subject-prefix heuristic, which a commit body can
58
+ * forge.
48
59
  */
49
- const CONVENTIONAL_TYPES = Object.freeze([
50
- 'feat',
51
- 'fix',
52
- 'perf',
53
- 'refactor',
54
- 'revert',
55
- 'docs',
56
- 'style',
57
- 'chore',
58
- 'test',
59
- 'build',
60
- 'ci',
61
- ]);
62
-
63
- // Precedence used when a branch carries a mix of conventional types: pick
64
- // the most release-significant one so the squash subject communicates the
65
- // branch's headline impact (and release-please bumps appropriately).
66
- const TYPE_PRECEDENCE = Object.freeze([
67
- 'feat',
68
- 'fix',
69
- 'perf',
70
- 'refactor',
71
- 'revert',
72
- 'docs',
73
- 'style',
74
- 'test',
75
- 'build',
76
- 'ci',
77
- 'chore',
78
- ]);
79
-
80
- const TYPE_GROUP = CONVENTIONAL_TYPES.join('|');
81
- // Anchored Conventional-Commit header matcher:
82
- // <type>(<optional scope>)<optional !>: <non-empty description>
83
- // Mirrors the shape `@commitlint/config-conventional` enforces (a known
84
- // type, an optional parenthesised scope, an optional breaking `!`, a
85
- // colon-space separator, and a non-empty subject). Used for the pure
86
- // "is this already conventional?" check and to pull the type off a branch
87
- // commit subject without spawning commitlint per call.
88
- const CONVENTIONAL_HEADER_RE = new RegExp(
89
- `^(?:${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?!?: \\S.*$`,
90
- );
91
- const LEADING_TYPE_RE = new RegExp(
92
- `^(${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?!?:`,
93
- );
94
-
95
- /**
96
- * True iff `subject` is a parseable Conventional Commit subject under the
97
- * repo's type vocabulary. Pure.
98
- *
99
- * @param {string} subject
100
- * @returns {boolean}
101
- */
102
- function isConventionalSubject(subject) {
103
- if (typeof subject !== 'string') return false;
104
- return CONVENTIONAL_HEADER_RE.test(subject.trim());
105
- }
60
+ const RECORD_SEP = '\u0000';
106
61
 
107
62
  /**
108
- * Extract the Conventional-Commit `type` from a single commit subject, or
109
- * `null` when the subject is not conventional. Pure.
63
+ * Read the branch's own commits (those unique to the Story branch relative to
64
+ * the base branch) as whole messages — subject AND body, because the body is
65
+ * where a `BREAKING CHANGE:` footer lives.
110
66
  *
111
- * @param {string} subject
112
- * @returns {string|null}
113
- */
114
- function parseConventionalType(subject) {
115
- if (typeof subject !== 'string') return null;
116
- const match = subject.trim().match(LEADING_TYPE_RE);
117
- return match ? match[1] : null;
118
- }
119
-
120
- /**
121
- * Pick the most release-significant type from a list of conventional
122
- * types, honouring `TYPE_PRECEDENCE`. Returns `null` for an empty list.
123
- * Pure.
67
+ * Returns `[]` when the read fails, which degrades every downstream rule to
68
+ * its safe default (type `chore`, no breaking marker) rather than throwing a
69
+ * close that is otherwise healthy.
124
70
  *
125
- * @param {string[]} types
126
- * @returns {string|null}
127
- */
128
- function pickDominantType(types) {
129
- const present = new Set(types.filter(Boolean));
130
- for (const candidate of TYPE_PRECEDENCE) {
131
- if (present.has(candidate)) return candidate;
132
- }
133
- return null;
134
- }
135
-
136
- /**
137
- * Read the branch's own commit subjects (commits unique to the Story
138
- * branch relative to the base branch) and derive the dominant
139
- * Conventional-Commit type. Returns `DEFAULT_CONVENTIONAL_TYPE` when no
140
- * conventional subject is found or the git read fails.
71
+ * Oldest-first (`--reverse`) is load-bearing: `pickDominantType` breaks a tie
72
+ * on the Story's primary commit, which is the first one authored.
141
73
  *
142
74
  * @param {{
143
75
  * storyBranch: string,
@@ -146,96 +78,174 @@ function pickDominantType(types) {
146
78
  * gitSpawn?: typeof defaultGitSpawn,
147
79
  * logger?: { warn?: Function },
148
80
  * }} args
149
- * @returns {string}
81
+ * @returns {string[]} Whole commit messages, oldest first.
150
82
  */
151
- function deriveTypeFromBranchCommits({
83
+ function readBranchCommits({
152
84
  storyBranch,
153
85
  baseBranch,
154
86
  cwd = process.cwd(),
155
87
  gitSpawn = defaultGitSpawn,
156
88
  logger = DefaultLogger,
157
89
  }) {
90
+ if (!storyBranch || !baseBranch) return [];
91
+ const range = `${baseBranch}..${storyBranch}`;
158
92
  try {
159
- const range = `${baseBranch}..${storyBranch}`;
160
- const result = gitSpawn(cwd, 'log', '--no-merges', '--format=%s', range);
161
- if (!result || result.status !== 0) {
93
+ const result = gitSpawn(
94
+ cwd,
95
+ 'log',
96
+ '--no-merges',
97
+ '--reverse',
98
+ '--format=%B%x00',
99
+ range,
100
+ );
101
+ if (result?.status !== 0) {
162
102
  logger?.warn?.(
163
103
  `[normalize-pr-title] git log ${range} failed (status=${result?.status ?? 'n/a'}); ` +
164
- `defaulting type to "${DEFAULT_CONVENTIONAL_TYPE}".`,
104
+ `defaulting type to "${DEFAULT_CONVENTIONAL_TYPE}" and assuming no breaking change.`,
165
105
  );
166
- return DEFAULT_CONVENTIONAL_TYPE;
106
+ return [];
167
107
  }
168
- const types = String(result.stdout ?? '')
169
- .split('\n')
170
- .map((line) => parseConventionalType(line))
171
- .filter(Boolean);
172
- return pickDominantType(types) ?? DEFAULT_CONVENTIONAL_TYPE;
108
+ return String(result.stdout ?? '')
109
+ .split(RECORD_SEP)
110
+ .map((message) => message.trim())
111
+ .filter((message) => message.length > 0);
173
112
  } catch (err) {
174
113
  logger?.warn?.(
175
- `[normalize-pr-title] could not derive type from branch commits ` +
176
- `(defaulting to "${DEFAULT_CONVENTIONAL_TYPE}"): ${err?.message ?? err}`,
114
+ `[normalize-pr-title] could not read branch commits ` +
115
+ `(defaulting to "${DEFAULT_CONVENTIONAL_TYPE}", no breaking change): ${err?.message ?? err}`,
177
116
  );
178
- return DEFAULT_CONVENTIONAL_TYPE;
117
+ return [];
179
118
  }
180
119
  }
181
120
 
182
121
  /**
183
- * Produce a PR title that parses as a Conventional Commit.
122
+ * Produce the PR title and the breaking-change notes that belong with it.
184
123
  *
185
124
  * - Already-conventional `storyTitle` → preserved verbatim + `(#<id>)`.
186
- * - Otherwise → `<derivedType>: <storyTitle> (#<id>)`.
187
- * - Empty / missing `storyTitle` → `<derivedType>: Story #<id>`.
125
+ * - Otherwise → `<derivedType>: <shaped storyTitle> (#<id>)`.
126
+ * - Empty / missing `storyTitle` → `<derivedType>: story #<id>`.
127
+ * - Breaking → `!` inserted before the colon in either shape.
188
128
  *
189
- * The type derivation (`deriveTypeFromBranchCommits`) is the only side
190
- * effect, and is skipped entirely when the title is already conventional.
129
+ * `commitMessages` is the branch read (`readBranchCommits`); passing `[]`
130
+ * yields the safe default type and no breaking marker. `storyBody` is the
131
+ * Story issue's body, scanned for a declared `BREAKING CHANGE:` footer.
191
132
  *
192
133
  * @param {{
193
134
  * storyTitle: string,
194
135
  * storyId: number|string,
195
- * storyBranch?: string,
196
- * baseBranch?: string,
197
- * cwd?: string,
198
- * gitSpawn?: typeof defaultGitSpawn,
199
- * logger?: { warn?: Function },
136
+ * commitMessages?: string[],
137
+ * storyBody?: string,
200
138
  * }} args
139
+ * @returns {{ title: string, breaking: boolean, breakingNotes: string[] }}
140
+ */
141
+ function normalizePrTitle({
142
+ storyTitle,
143
+ storyId,
144
+ commitMessages = [],
145
+ storyBody = '',
146
+ }) {
147
+ const idSuffix = `(#${storyId})`;
148
+ const trimmed = typeof storyTitle === 'string' ? storyTitle.trim() : '';
149
+ const { breaking, notes } = collectBreakingNotes({
150
+ commitMessages,
151
+ storyBody,
152
+ });
153
+
154
+ // Already conventional → preserve verbatim (the maker's own casing and
155
+ // scope survive), append the id reference. Only the breaking marker may be
156
+ // added, and only when it is not already there.
157
+ const subject = isConventionalSubject(trimmed)
158
+ ? trimmed
159
+ : synthesizeSubject({ description: trimmed, storyId, commitMessages });
160
+
161
+ const marked = breaking ? markBreaking(subject) : subject;
162
+ return { title: `${marked} ${idSuffix}`, breaking, breakingNotes: notes };
163
+ }
164
+
165
+ /**
166
+ * Build a conventional subject for a Story whose title is plain prose.
167
+ *
168
+ * @param {{ description: string, storyId: number|string, commitMessages: string[] }} args
169
+ * @returns {string}
170
+ */
171
+ function synthesizeSubject({ description, storyId, commitMessages }) {
172
+ const subjects = commitMessages.map((message) => message.split('\n')[0]);
173
+ const type = pickDominantType(subjects) ?? DEFAULT_CONVENTIONAL_TYPE;
174
+ const raw = description.length > 0 ? description : `Story #${storyId}`;
175
+ return `${type}: ${shapeDescription(raw)}`;
176
+ }
177
+
178
+ /**
179
+ * Build the PR body.
180
+ *
181
+ * The `Closes #<id>` footer is what auto-closes the Story on merge. A
182
+ * `BREAKING CHANGE:` footer goes LAST, as the spec requires, so that a repo
183
+ * configured to use the PR body as the squash-commit message hands
184
+ * release-please a parseable note rather than prose. When the squash body is
185
+ * built from the constituent commit messages instead (GitHub's default, and
186
+ * this repo's setting), the note still reaches `main` through whichever
187
+ * commit carried the footer — and the `!` in the subject carries the signal
188
+ * either way.
189
+ *
190
+ * @param {{ storyId: number|string, breakingNotes?: string[] }} args
201
191
  * @returns {string}
202
192
  */
203
- export function normalizePrTitle({
193
+ function buildPrBody({ storyId, breakingNotes }) {
194
+ const lines = [`Closes #${storyId}`, '', '_Auto-opened by `/deliver`._'];
195
+ if (breakingNotes.length > 0) {
196
+ lines.push('', `BREAKING CHANGE: ${breakingNotes.join(' ')}`);
197
+ }
198
+ return lines.join('\n');
199
+ }
200
+
201
+ /**
202
+ * Derive the two strings `gh pr create` needs. One `git log` read serves both
203
+ * halves: the commit SUBJECTS decide the type, and the commit BODIES — plus
204
+ * the Story body — decide whether this is a breaking change.
205
+ *
206
+ * A declared break is announced on the progress channel, because a `!` the
207
+ * operator did not expect in the squash subject should be visible while the
208
+ * close is running rather than discovered in the release notes.
209
+ *
210
+ * @param {{ storyTitle: string, storyId: number|string, storyBody?: string,
211
+ * storyBranch: string, baseBranch: string, cwd?: string,
212
+ * gitSpawn?: typeof defaultGitSpawn,
213
+ * progress?: (tag: string, msg: string) => void }} args
214
+ * @returns {{ title: string, body: string, breaking: boolean, breakingNotes: string[] }}
215
+ */
216
+ export function buildPullRequestFields({
204
217
  storyTitle,
205
218
  storyId,
219
+ storyBody = '',
206
220
  storyBranch,
207
221
  baseBranch,
208
222
  cwd = process.cwd(),
209
223
  gitSpawn = defaultGitSpawn,
210
- logger = DefaultLogger,
224
+ progress = () => {},
211
225
  }) {
212
- const idSuffix = `(#${storyId})`;
213
- const trimmed = typeof storyTitle === 'string' ? storyTitle.trim() : '';
214
-
215
- // Already conventional → preserve verbatim, append the id reference.
216
- if (isConventionalSubject(trimmed)) {
217
- return `${trimmed} ${idSuffix}`;
226
+ const commitMessages = readBranchCommits({
227
+ storyBranch,
228
+ baseBranch,
229
+ cwd,
230
+ gitSpawn,
231
+ });
232
+ const { title, breaking, breakingNotes } = normalizePrTitle({
233
+ storyTitle,
234
+ storyId,
235
+ commitMessages,
236
+ storyBody,
237
+ });
238
+ if (breaking) {
239
+ progress(
240
+ 'PR',
241
+ '⚠️ Breaking change declared — the PR title carries `!` and the body a ' +
242
+ `BREAKING CHANGE footer: ${breakingNotes.join(' ') || '(no note text)'}`,
243
+ );
218
244
  }
219
-
220
- // Not conventional → derive a type and synthesize.
221
- const type =
222
- storyBranch && baseBranch
223
- ? deriveTypeFromBranchCommits({
224
- storyBranch,
225
- baseBranch,
226
- cwd,
227
- gitSpawn,
228
- logger,
229
- })
230
- : DEFAULT_CONVENTIONAL_TYPE;
231
-
232
- // Lowercase the leading character of a synthesized description so the
233
- // subject satisfies commitlint's `subject-case` rule (matching the
234
- // `shapeMergeSubject` behaviour). An already-conventional title is left
235
- // untouched (it was preserved verbatim above). The empty-title fallback
236
- // uses a lowercased `story #<id>` for the same reason.
237
- const rawDescription = trimmed.length > 0 ? trimmed : `Story #${storyId}`;
238
- const description =
239
- rawDescription.charAt(0).toLowerCase() + rawDescription.slice(1);
240
- return `${type}: ${description} ${idSuffix}`;
245
+ return {
246
+ title,
247
+ body: buildPrBody({ storyId, breakingNotes }),
248
+ breaking,
249
+ breakingNotes,
250
+ };
241
251
  }
@@ -9,7 +9,11 @@
9
9
  */
10
10
 
11
11
  import path from 'node:path';
12
- import { parseMergeWatchMode, parseSprintArgs } from '../../../cli-args.js';
12
+ import {
13
+ parseMergeWatchMode,
14
+ parseOverrideReviewBlock,
15
+ parseSprintArgs,
16
+ } from '../../../cli-args.js';
13
17
  import { getDeliveryRouting } from '../../../config/delivery-routing.js';
14
18
  import { PROJECT_ROOT } from '../../../project-root.js';
15
19
  import { isOperatorMergeReason } from './auto-merge.js';
@@ -91,8 +95,8 @@ export function resolveWaitForMerge({
91
95
  * (`waitForMergeExplicit` / `noWaitForMerge`) for the runner to resolve once
92
96
  * the config and the arm outcome exist.
93
97
  *
94
- * @param {{ storyIdParam, cwdParam, skipValidationParam, skipSyncParam, noAutoMergeParam, waitForMergeParam, noWaitForMergeParam, maxWaitSecondsParam, mergeWatchModeParam }} raw
95
- * @returns {{ storyId, cwd, skipValidation, skipSync, noAutoMerge, waitForMergeExplicit, noWaitForMerge, maxWaitSeconds, mergeWatchMode }}
98
+ * @param {{ storyIdParam, cwdParam, skipValidationParam, skipSyncParam, noAutoMergeParam, waitForMergeParam, noWaitForMergeParam, maxWaitSecondsParam, mergeWatchModeParam, overrideReviewBlockParam }} raw
99
+ * @returns {{ storyId, cwd, skipValidation, skipSync, noAutoMerge, waitForMergeExplicit, noWaitForMerge, maxWaitSeconds, mergeWatchMode, overrideReviewBlock }}
96
100
  */
97
101
  export function parseCloseOptions({
98
102
  storyIdParam,
@@ -104,6 +108,7 @@ export function parseCloseOptions({
104
108
  noWaitForMergeParam,
105
109
  maxWaitSecondsParam,
106
110
  mergeWatchModeParam,
111
+ overrideReviewBlockParam,
107
112
  }) {
108
113
  // An injecting caller (`storyIdParam` supplied) is not reading argv at all,
109
114
  // so there is nothing to parse and `parsed` stays empty. This used to build a
@@ -148,5 +153,12 @@ export function parseCloseOptions({
148
153
  ? waitForMergeExplicit
149
154
  : undefined,
150
155
  noWaitForMerge: !!resolveFlag(noWaitForMergeParam, parsed.noWaitForMerge),
156
+ // `undefined` when unsupplied, which is what keeps a review
157
+ // critical blocker blocking. An injecting caller passes the reason string
158
+ // directly; both doors run the same validating parser, so neither can arm
159
+ // a reasonless override.
160
+ overrideReviewBlock: parseOverrideReviewBlock(
161
+ resolveFlag(overrideReviewBlockParam, parsed.overrideReviewBlock),
162
+ ),
151
163
  };
152
164
  }
@@ -44,7 +44,7 @@
44
44
  import { gh as defaultGh } from '../../../gh-exec.js';
45
45
  import { Logger } from '../../../Logger.js';
46
46
  import { computeChangeSet as defaultComputeChangeSet } from '../../change-set.js';
47
- import { normalizePrTitle } from './normalize-pr-title.js';
47
+ import { buildPullRequestFields } from './normalize-pr-title.js';
48
48
 
49
49
  /**
50
50
  * Pick the PR this head branch should resolve to from a `gh pr list
@@ -112,6 +112,7 @@ function probeEmptyDiff({ cwd, baseBranch, storyBranch, computeChangeSet }) {
112
112
  * cwd: string,
113
113
  * storyId: number,
114
114
  * storyTitle: string,
115
+ * storyBody?: string,
115
116
  * storyBranch: string,
116
117
  * baseBranch: string,
117
118
  * gh?: ReturnType<typeof import('../../../gh-exec.js').createGh>,
@@ -124,6 +125,7 @@ export async function ensurePullRequestWith({
124
125
  cwd: _cwd,
125
126
  storyId,
126
127
  storyTitle,
128
+ storyBody = '',
127
129
  storyBranch,
128
130
  baseBranch,
129
131
  gh = defaultGh,
@@ -185,26 +187,19 @@ export async function ensurePullRequestWith({
185
187
  }
186
188
 
187
189
  progress('PR', `Opening PR for ${storyBranch} → ${baseBranch}...`);
188
- // The repo squash-merges and GitHub uses the PR title as the squash
189
- // subject on `main`. A raw human issue title is not a Conventional
190
- // Commit, so release-please silently counts it as 0 releasable commits
191
- // (Story #3969). Normalize the title to conventional form: preserve an
192
- // already-conventional `storyTitle` verbatim, otherwise synthesize a
193
- // type derived from the branch's own commit subjects (default `chore`).
194
- // `gh-exec` spawns `gh` against the current process cwd (the worktree),
195
- // so the branch-commit read uses the same cwd.
196
- const title = normalizePrTitle({
190
+ // The repo squash-merges and GitHub uses the PR title as the squash subject
191
+ // on `main`, so both fields are derived rather than typed — see
192
+ // `normalize-pr-title.js`. `gh-exec` spawns `gh` against the current process
193
+ // cwd (the worktree), so the branch read uses the same cwd.
194
+ const { title, body } = buildPullRequestFields({
197
195
  storyTitle,
198
196
  storyId,
197
+ storyBody,
199
198
  storyBranch,
200
199
  baseBranch,
201
200
  cwd: _cwd ?? process.cwd(),
201
+ progress,
202
202
  });
203
- const body = [
204
- `Closes #${storyId}`,
205
- '',
206
- `_Auto-opened by \`/deliver\`._`,
207
- ].join('\n');
208
203
  try {
209
204
  const createResult = await gh.pr.create([
210
205
  '--base',
@@ -15,6 +15,18 @@
15
15
  * added because it would re-ask a question the push exit code already
16
16
  * answered authoritatively.
17
17
  *
18
+ * Issue #4990 — the push runs in the Story worktree, not the main
19
+ * checkout. `core.hooksPath` is the relative `.husky/_`, so the
20
+ * invocation directory is what selects which tree `pre-push` resolves
21
+ * and measures. Pushing from the main checkout therefore validated a
22
+ * tree other than the one being sent — false red when that checkout sat
23
+ * on unrelated work, and worse, false green whenever it happened to be
24
+ * clean. `worktreePath` falls back to `cwd` because single-tree mode and
25
+ * a disabled `delivery.worktreeIsolation` both legitimately have no
26
+ * worktree, and there the main checkout IS the tree being pushed. The
27
+ * two sibling phases resolve their tree the same way
28
+ * (`base-sync.js`, `close-validation.js`).
29
+ *
18
30
  * `gitSync` is accepted as an injected dependency rather than statically
19
31
  * imported so the caller's (cache-busted) binding wins. The
20
32
  * `single-story-close.js` orchestrator owns the static import; test
@@ -29,6 +41,7 @@ import { gitSync as defaultGitSync } from '../../../git-utils.js';
29
41
  *
30
42
  * @param {{
31
43
  * cwd: string,
44
+ * worktreePath?: string|null,
32
45
  * storyBranch: string,
33
46
  * gitSync?: typeof defaultGitSync,
34
47
  * progress: (tag: string, msg: string) => void,
@@ -36,6 +49,7 @@ import { gitSync as defaultGitSync } from '../../../git-utils.js';
36
49
  */
37
50
  export function pushStoryBranch({
38
51
  cwd,
52
+ worktreePath = null,
39
53
  storyBranch,
40
54
  gitSync = defaultGitSync,
41
55
  progress,
@@ -46,8 +60,9 @@ export function pushStoryBranch({
46
60
  // before this point, but `--skip-validation` skips that chain, and the
47
61
  // bypass then left nothing running at all. `pre-push` is the backstop,
48
62
  // and it only became reachable once hooks were materialized into
49
- // worktrees — which is where every Story branch is built.
50
- gitSync(cwd, 'push', '-u', 'origin', storyBranch);
63
+ // worktrees — which is where every Story branch is built, and where the
64
+ // cwd below now resolves so the hook reads that tree.
65
+ gitSync(worktreePath ?? cwd, 'push', '-u', 'origin', storyBranch);
51
66
  progress('GIT', `✅ Pushed ${storyBranch}.`);
52
67
  } catch (err) {
53
68
  throw new Error(
@@ -20,6 +20,11 @@ export async function handleCriticalReviewBlock({
20
20
  '',
21
21
  `The Story-scope review reported **${criticalCount} critical blocker(s)** on ${prUrl}.`,
22
22
  'Remediate the posted findings, then re-run `/deliver`.',
23
+ '',
24
+ 'If you have reviewed a finding and judged it wrong, re-run close with',
25
+ '`--override-review-block "<reason>"` rather than merging by hand — see',
26
+ '`phases/review-override.js`. The override is recorded on the Story, on the',
27
+ 'PR, and as friction telemetry.',
23
28
  ].join('\n');
24
29
  try {
25
30
  await upsertStructuredComment(provider, storyId, 'friction', body);