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
@@ -0,0 +1,157 @@
1
+ /**
2
+ * phases/review-override.js — the sanctioned, logged override of a Story-scope
3
+ * code-review critical blocker.
4
+ *
5
+ * Split out of `phases/review-block.js`, which owns the opposite outcome: the
6
+ * blocker that *holds*. Keeping the two in one module meant one file owning
7
+ * both "park this Story" and "ship it anyway", and the override's audit trail is
8
+ * substantial enough — three write surfaces, each independently best-effort —
9
+ * to be its own reason to change.
10
+ *
11
+ * **Why an override exists at all.** A critical review finding halts
12
+ * `single-story-close.js` before auto-merge, and no flag overrode a review
13
+ * verdict: `--skip-validation` bypasses the gate chain and `--no-auto-merge`
14
+ * declines to arm, but neither touches the review. So an operator who had read a
15
+ * finding and judged it wrong could only land by merging the PR by hand, which
16
+ * bypasses the gate with no record anywhere of what was overridden or why. This
17
+ * module does not weaken the gate; it relocates that escape hatch out of an
18
+ * untraceable hand-merge and into a mandatory-reason audit trail.
19
+ *
20
+ * Live provenance: Story #5007 / PR #5022, where a FALSE critical blocker — an
21
+ * MI finding on files `delivery.quality.gates.maintainability.ignoreGlobs`
22
+ * exempts, which the `check-baselines.js` ratchet correctly ignored in the same
23
+ * run — halted a legitimate delivery with a hand-merge as the only way out.
24
+ */
25
+
26
+ import { Logger } from '../../../Logger.js';
27
+ import {
28
+ emitRuntimeFriction,
29
+ RUNTIME_FRICTION_CATEGORIES,
30
+ } from '../../../observability/runtime-friction.js';
31
+ import {
32
+ postStructuredComment,
33
+ upsertStructuredComment,
34
+ } from '../../ticketing.js';
35
+
36
+ /** Cap on the reason text copied into the friction signal's `details`. */
37
+ const REASON_SIGNAL_LIMIT = 500;
38
+
39
+ /**
40
+ * Build the audit record posted when an operator overrides a review blocker.
41
+ * Pure.
42
+ *
43
+ * The body restates the count, the reason, and the fact that auto-merge was
44
+ * armed anyway — an override whose trail says only "overridden" is no better
45
+ * than the hand-merge it replaces.
46
+ *
47
+ * Module-local: an implementation detail of
48
+ * {@link handleOverriddenReviewBlock}, whose posted body is observable through
49
+ * that public entry point. Exporting it would add a public symbol no production
50
+ * path reaches — the exact dead-export shape this repo ratchets against.
51
+ *
52
+ * @param {{ prUrl: string, criticalCount: number, reason: string }} args
53
+ * @returns {string}
54
+ */
55
+ function buildReviewOverrideBody({ prUrl, criticalCount, reason }) {
56
+ return [
57
+ '### Code-review blocker overridden by operator',
58
+ '',
59
+ `The Story-scope review reported **${criticalCount} critical blocker(s)** on ${prUrl}.`,
60
+ 'The operator reviewed and rejected the finding(s) and authorized delivery',
61
+ 'with `--override-review-block`; auto-merge was armed.',
62
+ '',
63
+ '**Recorded reason:**',
64
+ '',
65
+ `> ${reason.split('\n').join('\n> ')}`,
66
+ '',
67
+ 'The findings comment on the PR is left in place unchanged — this record',
68
+ 'sits beside it rather than resolving it.',
69
+ ].join('\n');
70
+ }
71
+
72
+ /**
73
+ * Post one audit record, swallowing the failure into a warning.
74
+ *
75
+ * Every write here is best-effort by design: an override whose audit trail
76
+ * partly failed must still land the delivery the operator authorized, because
77
+ * the close would otherwise fail for a reason the operator cannot act on. The
78
+ * friction signal is the durable record — it is what makes a rising override
79
+ * count visible to the retro.
80
+ *
81
+ * Module-local: the two call sites below are its only callers.
82
+ *
83
+ * @param {{ post: () => Promise<unknown>, surface: string }} args
84
+ * @returns {Promise<boolean>} true when the record landed.
85
+ */
86
+ async function postAuditRecord({ post, surface }) {
87
+ try {
88
+ await post();
89
+ return true;
90
+ } catch (err) {
91
+ Logger.warn(
92
+ `[single-story-close] failed to post review-override record on ${surface}: ${err?.message ?? err}`,
93
+ );
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Record an operator-authorized override of a critical code-review blocker,
100
+ * then let close continue to the auto-merge phase.
101
+ *
102
+ * Writes to three surfaces: the Story issue via `upsert` so a re-run does not
103
+ * stack duplicates; the PR via `post` so the trail is append-only on the surface
104
+ * a human reviews, and so a reviewer reading the PR need not open the Story to
105
+ * learn the blocker was overridden; and the friction stream.
106
+ *
107
+ * @param {{
108
+ * provider: object,
109
+ * storyId: number,
110
+ * prUrl: string,
111
+ * prNumber: number|null,
112
+ * criticalCount: number,
113
+ * reason: string,
114
+ * config?: object,
115
+ * emitFrictionFn?: typeof emitRuntimeFriction,
116
+ * }} args
117
+ * @returns {Promise<{ overridden: true, reason: string, criticalCount: number }>}
118
+ */
119
+ export async function handleOverriddenReviewBlock({
120
+ provider,
121
+ storyId,
122
+ prUrl,
123
+ prNumber,
124
+ criticalCount,
125
+ reason,
126
+ config,
127
+ emitFrictionFn = emitRuntimeFriction,
128
+ }) {
129
+ const body = buildReviewOverrideBody({ prUrl, criticalCount, reason });
130
+ Logger.warn(
131
+ `[single-story-close] ⚠️ Story-scope review reported ${criticalCount} critical blocker(s) on ` +
132
+ `PR ${prUrl} — OVERRIDDEN by operator: ${reason}`,
133
+ );
134
+ await postAuditRecord({
135
+ post: () => upsertStructuredComment(provider, storyId, 'friction', body),
136
+ surface: `Story #${storyId}`,
137
+ });
138
+ if (Number.isInteger(prNumber)) {
139
+ await postAuditRecord({
140
+ post: () =>
141
+ postStructuredComment(provider, prNumber, 'notification', body),
142
+ surface: `PR #${prNumber}`,
143
+ });
144
+ }
145
+ await emitFrictionFn({
146
+ storyId,
147
+ category: RUNTIME_FRICTION_CATEGORIES.REVIEW_BLOCK_OVERRIDDEN,
148
+ tool: 'single-story-close',
149
+ details: {
150
+ prUrl,
151
+ criticalCount,
152
+ reason: reason.slice(0, REASON_SIGNAL_LIMIT),
153
+ },
154
+ config,
155
+ });
156
+ return { overridden: true, reason, criticalCount };
157
+ }
@@ -16,13 +16,9 @@
16
16
  * actually reaped and every close said it did. Best-effort means the close
17
17
  * still succeeds on refusal; it does not mean the close may claim an
18
18
  * outcome it never checked.
19
- *
20
- * Also clears the trace-hook env vars so subsequent tooling falls back to
21
- * the no-op branch instead of pointing at a (now-reaped) worktree.
22
19
  */
23
20
 
24
21
  import { Logger } from '../../../Logger.js';
25
- import { clearActiveStoryEnv } from '../../../observability/active-story-env.js';
26
22
  import { WorktreeManager as DefaultWorktreeManager } from '../../../worktree-manager.js';
27
23
 
28
24
  /**
@@ -92,15 +88,5 @@ export async function reapWorktreePhase({
92
88
  }
93
89
  }
94
90
 
95
- // Clear the trace-hook env vars so subsequent tooling falls back to the
96
- // no-op branch instead of pointing at a (now-reaped) worktree.
97
- try {
98
- clearActiveStoryEnv({
99
- logger: { warn: (m) => progress('ENV', `⚠️ ${m}`) },
100
- });
101
- } catch {
102
- // Non-fatal.
103
- }
104
-
105
91
  return worktreeReaped;
106
92
  }
@@ -29,6 +29,7 @@ import { parseCloseOptions, resolveWaitForMerge } from './phases/options.js';
29
29
  import { ensurePullRequestWith } from './phases/pull-request.js';
30
30
  import { pushStoryBranch } from './phases/push.js';
31
31
  import { handleCriticalReviewBlock } from './phases/review-block.js';
32
+ import { handleOverriddenReviewBlock } from './phases/review-override.js';
32
33
  import { reapWorktreePhase } from './phases/worktree-reap.js';
33
34
  import { runWrongTreeGuardPhase } from './phases/wrong-tree-guard.js';
34
35
 
@@ -201,22 +202,31 @@ async function runPrePushPhases({
201
202
 
202
203
  async function openAndReviewPr({
203
204
  cwd,
205
+ worktreePath,
204
206
  story,
205
207
  storyId,
206
208
  storyBranch,
207
209
  baseBranch,
208
210
  provider,
211
+ config,
212
+ overrideReviewBlock,
209
213
  injectedGh,
210
214
  injectedRunCodeReview,
211
215
  setPhase = () => {},
212
216
  }) {
213
217
  setPhase('push');
214
- pushStoryBranch({ cwd, storyBranch, gitSync, progress });
218
+ // Issue #4990 push from the Story worktree so `pre-push` measures the
219
+ // tree being sent. `gh` and the review's ref-based diffs below keep the
220
+ // caller's `cwd`: they read the shared `.git`, so they resolve identically
221
+ // from either tree.
222
+ pushStoryBranch({ cwd, worktreePath, storyBranch, gitSync, progress });
215
223
  setPhase('pull-request');
216
224
  const { url: prUrl, alreadyMerged } = await ensurePullRequestWith({
217
225
  cwd,
218
226
  storyId,
219
227
  storyTitle: story.title,
228
+ // Scanned for a declared `BREAKING CHANGE:` footer — see pull-request.js.
229
+ storyBody: story.body,
220
230
  storyBranch,
221
231
  baseBranch,
222
232
  gh: injectedGh,
@@ -244,6 +254,27 @@ async function openAndReviewPr({
244
254
  });
245
255
  if (reviewOutcome.halted) {
246
256
  const criticalCount = reviewOutcome.severity?.critical ?? 0;
257
+ // The sanctioned override. Checked BEFORE the blocked
258
+ // transition so an overridden run never touches `agent::blocked`: the
259
+ // Story is proceeding, and parking it would make the label lie for the
260
+ // rest of the close.
261
+ if (overrideReviewBlock) {
262
+ const override = await handleOverriddenReviewBlock({
263
+ provider,
264
+ storyId,
265
+ prUrl,
266
+ prNumber,
267
+ criticalCount,
268
+ reason: overrideReviewBlock,
269
+ config,
270
+ });
271
+ return {
272
+ prUrl,
273
+ prNumber,
274
+ alreadyMerged: false,
275
+ reviewOverride: override,
276
+ };
277
+ }
247
278
  await handleCriticalReviewBlock({
248
279
  provider,
249
280
  storyId,
@@ -252,10 +283,12 @@ async function openAndReviewPr({
252
283
  });
253
284
  throw new Error(
254
285
  `[single-story-close] Story-scope review reported ${criticalCount} critical blocker(s) on PR ${prUrl}. ` +
255
- 'Auto-merge was not enabled. Remediate the findings posted to the PR and re-run `/deliver`.',
286
+ 'Auto-merge was not enabled. Remediate the findings posted to the PR and re-run `/deliver`. ' +
287
+ 'If you have reviewed a finding and judged it wrong, re-run with ' +
288
+ '`--override-review-block "<reason>"` rather than merging by hand.',
256
289
  );
257
290
  }
258
- return { prUrl, prNumber, alreadyMerged: false };
291
+ return { prUrl, prNumber, alreadyMerged: false, reviewOverride: null };
259
292
  }
260
293
 
261
294
  async function releaseLease({
@@ -291,11 +324,9 @@ async function releaseLease({
291
324
  * `runBaseSyncPhase`, and a critical-blocker review halt in
292
325
  * `openAndReviewPr`) throw before the clean-close lease release at the
293
326
  * tail of `runSingleStoryClose`, stranding the operator's lease
294
- * indefinitely. The standalone lease does **not** expire by TTL: it is
295
- * fail-closed by design (`lease-guard-shared.js` anchors `heartbeatAt` to
296
- * now, so `isClaimLive` is true for any foreign assignee regardless of the
297
- * configured TTL), so a stranded claim is cleared only by `--steal` or
298
- * de-assignment. That fail-closed-refuses a different operator who picks up
327
+ * indefinitely. The standalone lease has **no** TTL to expire by (Story
328
+ * #5006 deleted it): `acquireLease` refuses any foreign assignee outright,
329
+ * so a stranded claim is cleared only by `--steal` or de-assignment. That fail-closed-refuses a different operator who picks up
299
330
  * the blocked Story — exactly the hand-off case. Releasing here closes
300
331
  * that gap.
301
332
  *
@@ -378,6 +409,7 @@ export async function runSingleStoryClose({
378
409
  noWaitForMerge: noWaitForMergeParam,
379
410
  maxWaitSeconds: maxWaitSecondsParam,
380
411
  mergeWatchMode: mergeWatchModeParam,
412
+ overrideReviewBlock: overrideReviewBlockParam,
381
413
  injectedProvider,
382
414
  injectedConfig,
383
415
  injectedNotify,
@@ -397,10 +429,11 @@ export async function runSingleStoryClose({
397
429
  noWaitForMergeParam,
398
430
  maxWaitSecondsParam,
399
431
  mergeWatchModeParam,
432
+ overrideReviewBlockParam,
400
433
  });
401
434
  if (!options.storyId) {
402
435
  throw new Error(
403
- 'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--wait-merge|--no-wait-merge] [--max-wait-seconds <n>] [--merge-watch-mode <sync|async>]',
436
+ 'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--wait-merge|--no-wait-merge] [--max-wait-seconds <n>] [--merge-watch-mode <sync|async>] [--override-review-block <reason>]',
404
437
  );
405
438
  }
406
439
 
@@ -725,21 +758,25 @@ async function runClosePipeline({
725
758
  leaseArgs,
726
759
  );
727
760
 
728
- const { prUrl, prNumber, alreadyMerged } = await releaseLeaseOnBlock(
729
- () =>
730
- openAndReviewPr({
731
- cwd: options.cwd,
732
- story,
733
- storyId: options.storyId,
734
- storyBranch,
735
- baseBranch,
736
- provider,
737
- injectedGh,
738
- injectedRunCodeReview,
739
- setPhase,
740
- }),
741
- leaseArgs,
742
- );
761
+ const { prUrl, prNumber, alreadyMerged, reviewOverride } =
762
+ await releaseLeaseOnBlock(
763
+ () =>
764
+ openAndReviewPr({
765
+ cwd: options.cwd,
766
+ worktreePath,
767
+ story,
768
+ storyId: options.storyId,
769
+ storyBranch,
770
+ baseBranch,
771
+ provider,
772
+ config,
773
+ overrideReviewBlock: options.overrideReviewBlock,
774
+ injectedGh,
775
+ injectedRunCodeReview,
776
+ setPhase,
777
+ }),
778
+ leaseArgs,
779
+ );
743
780
  // Reap the per-Story worktree BEFORE the arm (Story #4681). Arming runs
744
781
  // `gh pr merge --auto --squash --delete-branch`, which — against an
745
782
  // already-mergeable PR — merges immediately and then shells out to local
@@ -837,7 +874,10 @@ async function runClosePipeline({
837
874
  gates: {
838
875
  validation: options.skipValidation ? 'skipped' : 'passed',
839
876
  baseSync: options.skipSync ? 'skipped' : 'passed',
840
- codeReview: 'passed',
877
+ // An overridden blocker reports `overridden`, never
878
+ // `passed`. The review DID fail; a human authorized shipping anyway, and
879
+ // the envelope is the machine-readable trail that says so.
880
+ codeReview: reviewOverride ? 'overridden' : 'passed',
841
881
  },
842
882
  };
843
883
 
@@ -24,18 +24,17 @@
24
24
  * operator handle from config and delegates assignee mutation to the pure
25
25
  * `ticket-lease.js` primitive.
26
26
  *
27
- * **Fail-closed liveness (audit #3513).** The standalone path has no
28
- * Epic-scoped lifecycle ledger to read a per-owner `story.heartbeat` from, so
29
- * there is no live-heartbeat source to feed the lease primitive's liveness
30
- * check. Defaulting `heartbeatAt` to `null` made *every* foreign claim look
31
- * stale (`isClaimLive(null) === false`), so the guard silently reclaimed any
32
- * foreign assignee leaving it inert as a concurrency guard. We therefore
33
- * **fail closed**: a foreign assignee is treated as a *live* claim by default
34
- * and refuses the take (naming the current owner), unless the operator passes
35
- * `--steal` to forcibly transfer it. This is the safer choice for a guard
36
- * whose whole job is to stop two operators clobbering the same Story; a
37
- * genuinely abandoned claim is cleared by hand (or `--steal`) rather than
38
- * raced into automatically. Unclaimed and self-held tickets still proceed.
27
+ * **Fail-closed (audit #3513, hard-wired by Story #5006).** The lease
28
+ * primitive used to reclaim a foreign claim whose heartbeat was older than a
29
+ * TTL and with no heartbeat source, *every* foreign claim looked stale, so
30
+ * the guard silently reclaimed any foreign assignee and was inert as a
31
+ * concurrency guard. The guard worked around that by anchoring the heartbeat
32
+ * to `now`; Story #5006 deleted the TTL outright, so the fail-closed
33
+ * behaviour is now the primitive's own: a foreign assignee refuses the take
34
+ * (naming the current owner) unless the operator passes `--steal` to forcibly
35
+ * transfer it. A genuinely abandoned claim is cleared by hand (or `--steal`)
36
+ * rather than raced into automatically. Unclaimed and self-held tickets still
37
+ * proceed.
39
38
  */
40
39
 
41
40
  import {
@@ -75,21 +74,19 @@ export function resolveOperator(config) {
75
74
  }
76
75
 
77
76
  /**
78
- * Acquire (or re-affirm / reclaim) the Story lease for the standalone path.
77
+ * Acquire (or re-affirm) the Story lease for the standalone path.
79
78
  *
80
- * **Fail-closed:** because the standalone path has no Epic ledger to source a
81
- * live heartbeat from, a foreign assignee is treated as a live claim and
82
- * refuses the take by default the guard throws naming the current owner so
83
- * the operator can coordinate. Pass `steal: true` (`--steal`) to forcibly
84
- * transfer it. Unclaimed and self-held tickets proceed without a write.
79
+ * **Fail-closed:** a foreign assignee refuses the take the guard throws
80
+ * naming the current owner so the operator can coordinate. Pass
81
+ * `steal: true` (`--steal`) to forcibly transfer it. Unclaimed and self-held
82
+ * tickets proceed without a write.
85
83
  *
86
84
  * @param {object} opts
87
85
  * @param {object} opts.provider Ticketing provider (getTicket/updateTicket).
88
86
  * @param {number} opts.storyId Story ticket to claim.
89
- * @param {object} opts.config Resolved config (operator handle + TTL default).
87
+ * @param {object} opts.config Resolved config (operator handle).
90
88
  * @param {string} [opts.operator] Override the resolved operator (tests).
91
89
  * @param {boolean} [opts.steal=false] Forcibly transfer a foreign claim.
92
- * @param {number} [opts.now] Injectable clock (epoch ms) for tests.
93
90
  * @returns {Promise<{ acquired: boolean, owner: string, previousOwner: string|null, reason: string }>}
94
91
  * @throws {Error} When a foreign claim refuses the acquire (no `steal`).
95
92
  */
@@ -99,27 +96,19 @@ export async function acquireStoryLease({
99
96
  config,
100
97
  operator,
101
98
  steal = false,
102
- now,
103
99
  }) {
104
100
  const owner = operator ?? resolveOperator(config);
105
- // Fail closed: with no live-heartbeat source on the standalone path, the
106
- // shared kernel anchors `heartbeatAt` to the same `now` the primitive
107
- // evaluates against, so `isClaimLive` returns true for any foreign owner
108
- // and `acquireLease` refuses unless `steal` is set.
109
101
  return acquireLeaseFailClosed({
110
102
  provider,
111
103
  ticketId: storyId,
112
104
  operator: owner,
113
105
  steal,
114
- config,
115
- now,
116
- anchorHeartbeatToNow: true,
117
106
  renderRefusal: (result) =>
118
107
  `single-story lease: Story #${storyId} is currently held by @${result.owner}. ` +
119
108
  'Another /deliver run owns this Story. Coordinate with that ' +
120
109
  'operator, or re-run with --steal to forcibly transfer the claim once you ' +
121
- 'have confirmed the other run is dead. (The standalone path has no Epic ' +
122
- 'heartbeat ledger, so a foreign assignee always blocks unless stolen.)',
110
+ 'have confirmed the other run is dead. (A foreign assignee always ' +
111
+ 'blocks unless stolen.)',
123
112
  });
124
113
  }
125
114
 
@@ -142,5 +131,5 @@ export async function releaseStoryLease({
142
131
  operator,
143
132
  }) {
144
133
  const owner = operator ?? resolveOperator(config);
145
- return releaseLease({ provider, ticketId: storyId, operator: owner, config });
134
+ return releaseLease({ provider, ticketId: storyId, operator: owner });
146
135
  }
@@ -6,13 +6,27 @@
6
6
  * Spec is treated as a sizing smell (the Story should be split or the Spec
7
7
  * tightened), not as a reason to write temporary product docs.
8
8
  *
9
- * The budget reuses the §2 FinOps estimator ({@link estimateTokens}, ~4
10
- * chars/token) so the threshold speaks the same units as hydration budgets.
9
+ * This module also **owns** the §2 FinOps token estimator
10
+ * ({@link estimateTokens}, ~4 chars/token) the one shared approximation every
11
+ * fixed framework ceiling speaks in (the Spec budget below, the plan-time
12
+ * sizing ceilings in `ticket-validator-sizing.js`, and the audit checklist
13
+ * threading budget). It used to live in a `context-envelope.js` SDK whose
14
+ * remaining surface had no live caller; the estimator is all that survived.
11
15
  *
12
16
  * @module lib/orchestration/spec-spill
13
17
  */
14
18
 
15
- import { estimateTokens } from './context-envelope.js';
19
+ /**
20
+ * Rough token estimate: ~4 characters per token. Deliberately cheap and
21
+ * deterministic — every ceiling that quotes "tokens" is quoting this number,
22
+ * so the approximation matters far less than every caller sharing one.
23
+ *
24
+ * @param {string} text
25
+ * @returns {number}
26
+ */
27
+ export function estimateTokens(text) {
28
+ return Math.ceil(String(text ?? '').length / 4);
29
+ }
16
30
 
17
31
  /**
18
32
  * Soft budget (estimated tokens) for an inline `## Spec`. ~1500 tokens ≈ 6KB —
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * phases/review-core.js — the shared Story-scope review spine.
3
3
  *
4
- * Extracted from `phases/code-review.js` (Story #4603). `runStoryReviewCore` is
5
- * the one implementation both close paths call `runCodeReview` through the
6
- * epic-attached phase (`code-review.js#runStoryCodeReview`) and the standalone
7
- * v2 path (`single-story-close/phases/code-review.js#runStoryScopeReview`) —
8
- * so it belongs to neither and lives here rather than inside one path's phase
9
- * file (Story #3653 established the shared-spine contract).
4
+ * Extracted from the Epic-era `phases/code-review.js` (Story #4603) so the
5
+ * spine belonged to neither close path. `runStoryReviewCore` is the one
6
+ * implementation `runCodeReview` is called through; since Story #5006 retired
7
+ * the Epic-attached phase, its sole caller is the v2 standalone path
8
+ * (`single-story-close/phases/code-review.js#runStoryScopeReview`). It stays
9
+ * here — the shared-spine contract (Story #3653) is what keeps the maker-blind
10
+ * review invocation out of a phase file.
10
11
  */
11
12
 
12
13
  import { countChangedLines } from '../../../audit-suite/index.js';
@@ -425,7 +425,8 @@ export function persistTerminalEnvelope(
425
425
  * machine-readable contract — every invocation emits exactly ONE, and a
426
426
  * headless caller parses it out of stdout to decide what happened.
427
427
  * `Logger.info` is level-gated, so under the documented
428
- * `AGENT_LOG_LEVEL=silent` (§ 1.H) the envelope silently vanished and the
428
+ * `AGENT_LOG_LEVEL=silent` (execution-reference § Log-level control) the
429
+ * envelope silently vanished and the
429
430
  * caller got a bare exit code: precisely the "no envelope at all" outcome
430
431
  * Story #4543 exists to remove. A contract payload must not be suppressible
431
432
  * by a verbosity knob.
@@ -45,7 +45,10 @@
45
45
  * `body.verify` entries must either name a testing tier in parentheses
46
46
  * drawn from `VERIFY_TIER_VALUES` (e.g. `npm run test (unit)`) or be the
47
47
  * literal `manual:<reason>` escape hatch when the Story is genuinely
48
- * unverifiable in isolation.
48
+ * unverifiable in isolation. `verify-tier-repair.js#normalizeVerifyTiers` runs
49
+ * first on the persist path (Story #5005) and **repairs** the entries whose
50
+ * tier `suggestVerifyFix` can infer, so the hard error below is reserved for
51
+ * the entries only the author can resolve.
49
52
  *
50
53
  * The errors are batched and surfaced as a single thrown Error so the
51
54
  * planner can see every offending slug in one pass instead of fixing one