mandrel 2.31.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 (250) 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/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 +0 -1
  14. package/.agents/schemas/agentrc.schema.json +1939 -1400
  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 +7 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/bootstrap.js +28 -26
  21. package/.agents/scripts/check-baseline-drift.js +73 -13
  22. package/.agents/scripts/check-baseline-scope.js +362 -0
  23. package/.agents/scripts/check-dead-exports.js +9 -1
  24. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  25. package/.agents/scripts/check-knip-entries.js +136 -0
  26. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  27. package/.agents/scripts/check-schema-references.js +1 -1
  28. package/.agents/scripts/diagnose-friction.js +7 -4
  29. package/.agents/scripts/generate-config-docs.js +263 -171
  30. package/.agents/scripts/install-matrix-assert.js +0 -1
  31. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  32. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  33. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  34. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  35. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  37. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  38. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  39. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  40. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  41. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  42. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  43. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  44. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  45. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  46. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  47. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  48. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  49. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  50. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  51. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  52. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  53. package/.agents/scripts/lib/child-exec.js +193 -0
  54. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  55. package/.agents/scripts/lib/cli-args.js +64 -0
  56. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  57. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  58. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  59. package/.agents/scripts/lib/config/commands.js +3 -5
  60. package/.agents/scripts/lib/config/explain.js +5 -7
  61. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  62. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  63. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  64. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  65. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  66. package/.agents/scripts/lib/config/gates/index.js +5 -2
  67. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  68. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  69. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  70. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  71. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  72. package/.agents/scripts/lib/config/github.js +0 -1
  73. package/.agents/scripts/lib/config/limits.js +3 -48
  74. package/.agents/scripts/lib/config/qa.js +105 -0
  75. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  76. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  77. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  78. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  79. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  80. package/.agents/scripts/lib/crap-utils.js +84 -520
  81. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  82. package/.agents/scripts/lib/degraded-mode.js +2 -2
  83. package/.agents/scripts/lib/doc-tiers.js +3 -3
  84. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  85. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  86. package/.agents/scripts/lib/fs-walk.js +52 -0
  87. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  88. package/.agents/scripts/lib/git-utils.js +16 -36
  89. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  90. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  91. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  92. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  93. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  94. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  95. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  96. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  97. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  98. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  99. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  100. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  101. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  102. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  105. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  106. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  107. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  108. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  109. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  110. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  111. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  112. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  122. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  123. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  124. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  125. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  126. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  127. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  128. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  129. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  130. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  131. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  132. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  133. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  134. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  135. package/.agents/scripts/lib/signals/index.js +8 -6
  136. package/.agents/scripts/lib/signals/schema.js +20 -25
  137. package/.agents/scripts/lib/signals/write.js +8 -8
  138. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  139. package/.agents/scripts/lib/temp-retention.js +1 -1
  140. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  141. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  142. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  143. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  144. package/.agents/scripts/lib/worktree-manager.js +2 -3
  145. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  146. package/.agents/scripts/pr-watch-with-update.js +7 -5
  147. package/.agents/scripts/providers/github/cache.js +2 -2
  148. package/.agents/scripts/providers/github/comments.js +6 -28
  149. package/.agents/scripts/providers/github/compose.js +0 -15
  150. package/.agents/scripts/providers/github/errors.js +10 -27
  151. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  152. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  153. package/.agents/scripts/providers/github.js +4 -7
  154. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  155. package/.agents/scripts/resolve-stories.js +0 -2
  156. package/.agents/scripts/run-lint.js +61 -61
  157. package/.agents/scripts/run-test-profile.js +6 -6
  158. package/.agents/scripts/run-verify.js +48 -30
  159. package/.agents/scripts/single-story-close.js +20 -0
  160. package/.agents/scripts/single-story-init.js +12 -35
  161. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  162. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  163. package/.agents/skills/skills.index.json +1 -11
  164. package/.agents/workflows/audit-documentation.md +5 -6
  165. package/.agents/workflows/audit-to-stories.md +2 -2
  166. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  167. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  168. package/.agents/workflows/helpers/code-review.md +3 -8
  169. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  170. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  171. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  172. package/.agents/workflows/mandrel-update.md +10 -10
  173. package/.agents/workflows/qa-assist.md +15 -20
  174. package/.agents/workflows/qa-explore.md +9 -8
  175. package/README.md +1 -1
  176. package/docs/CHANGELOG.md +42 -0
  177. package/lib/migrations/index.js +2 -0
  178. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  179. package/package.json +12 -3
  180. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  181. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  182. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  183. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  184. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  185. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  186. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  187. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  188. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  189. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  190. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  191. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  192. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  193. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  194. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  195. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  196. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  197. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  198. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  199. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  200. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  201. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  202. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  203. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  204. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  205. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  206. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  207. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  208. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  209. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  210. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  211. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  212. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  213. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  214. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  215. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  216. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  217. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  218. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  219. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  220. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  221. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  222. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  223. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  224. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  225. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  226. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  227. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  228. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  229. package/.agents/scripts/lib/planning-corpus.js +0 -37
  230. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  231. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  232. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  233. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  234. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  235. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  236. package/.agents/scripts/lib/signals/read.js +0 -268
  237. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  238. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  239. package/.agents/scripts/lib/story-plan.js +0 -379
  240. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  241. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  242. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  243. package/.agents/scripts/lint-baseline.js +0 -507
  244. package/.agents/scripts/providers/github/prs.js +0 -103
  245. package/.agents/scripts/signals-view.js +0 -309
  246. package/.agents/scripts/story-plan.js +0 -370
  247. package/.agents/scripts/sync-branch-from-base.js +0 -149
  248. package/.agents/scripts/validate-docs-freshness.js +0 -314
  249. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  250. package/.agents/workflows/helpers/signals.md +0 -112
@@ -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
 
@@ -207,6 +208,8 @@ async function openAndReviewPr({
207
208
  storyBranch,
208
209
  baseBranch,
209
210
  provider,
211
+ config,
212
+ overrideReviewBlock,
210
213
  injectedGh,
211
214
  injectedRunCodeReview,
212
215
  setPhase = () => {},
@@ -222,6 +225,8 @@ async function openAndReviewPr({
222
225
  cwd,
223
226
  storyId,
224
227
  storyTitle: story.title,
228
+ // Scanned for a declared `BREAKING CHANGE:` footer — see pull-request.js.
229
+ storyBody: story.body,
225
230
  storyBranch,
226
231
  baseBranch,
227
232
  gh: injectedGh,
@@ -249,6 +254,27 @@ async function openAndReviewPr({
249
254
  });
250
255
  if (reviewOutcome.halted) {
251
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
+ }
252
278
  await handleCriticalReviewBlock({
253
279
  provider,
254
280
  storyId,
@@ -257,10 +283,12 @@ async function openAndReviewPr({
257
283
  });
258
284
  throw new Error(
259
285
  `[single-story-close] Story-scope review reported ${criticalCount} critical blocker(s) on PR ${prUrl}. ` +
260
- '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.',
261
289
  );
262
290
  }
263
- return { prUrl, prNumber, alreadyMerged: false };
291
+ return { prUrl, prNumber, alreadyMerged: false, reviewOverride: null };
264
292
  }
265
293
 
266
294
  async function releaseLease({
@@ -296,11 +324,9 @@ async function releaseLease({
296
324
  * `runBaseSyncPhase`, and a critical-blocker review halt in
297
325
  * `openAndReviewPr`) throw before the clean-close lease release at the
298
326
  * tail of `runSingleStoryClose`, stranding the operator's lease
299
- * indefinitely. The standalone lease does **not** expire by TTL: it is
300
- * fail-closed by design (`lease-guard-shared.js` anchors `heartbeatAt` to
301
- * now, so `isClaimLive` is true for any foreign assignee regardless of the
302
- * configured TTL), so a stranded claim is cleared only by `--steal` or
303
- * 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
304
330
  * the blocked Story — exactly the hand-off case. Releasing here closes
305
331
  * that gap.
306
332
  *
@@ -383,6 +409,7 @@ export async function runSingleStoryClose({
383
409
  noWaitForMerge: noWaitForMergeParam,
384
410
  maxWaitSeconds: maxWaitSecondsParam,
385
411
  mergeWatchMode: mergeWatchModeParam,
412
+ overrideReviewBlock: overrideReviewBlockParam,
386
413
  injectedProvider,
387
414
  injectedConfig,
388
415
  injectedNotify,
@@ -402,10 +429,11 @@ export async function runSingleStoryClose({
402
429
  noWaitForMergeParam,
403
430
  maxWaitSecondsParam,
404
431
  mergeWatchModeParam,
432
+ overrideReviewBlockParam,
405
433
  });
406
434
  if (!options.storyId) {
407
435
  throw new Error(
408
- '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>]',
409
437
  );
410
438
  }
411
439
 
@@ -730,22 +758,25 @@ async function runClosePipeline({
730
758
  leaseArgs,
731
759
  );
732
760
 
733
- const { prUrl, prNumber, alreadyMerged } = await releaseLeaseOnBlock(
734
- () =>
735
- openAndReviewPr({
736
- cwd: options.cwd,
737
- worktreePath,
738
- story,
739
- storyId: options.storyId,
740
- storyBranch,
741
- baseBranch,
742
- provider,
743
- injectedGh,
744
- injectedRunCodeReview,
745
- setPhase,
746
- }),
747
- leaseArgs,
748
- );
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
+ );
749
780
  // Reap the per-Story worktree BEFORE the arm (Story #4681). Arming runs
750
781
  // `gh pr merge --auto --squash --delete-branch`, which — against an
751
782
  // already-mergeable PR — merges immediately and then shells out to local
@@ -843,7 +874,10 @@ async function runClosePipeline({
843
874
  gates: {
844
875
  validation: options.skipValidation ? 'skipped' : 'passed',
845
876
  baseSync: options.skipSync ? 'skipped' : 'passed',
846
- 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',
847
881
  },
848
882
  };
849
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
@@ -1,57 +1,38 @@
1
1
  /**
2
2
  * ticket-lease.js — assignee-as-lease primitive (Story #3480, Epic #3457).
3
3
  *
4
- * The workflow-guards Feature (#3478) needs a way for one operator to take
5
- * an exclusive, time-bounded claim on a ticket so two concurrent runs do not
6
- * both drive the same Story. Rather than invent a new state column, the lease
7
- * rides the ticket's existing **assignees** surface: the single assignee *is*
8
- * the lease owner. Liveness is decided by the owner's last-heartbeat epoch-ms
9
- * compared against a configured TTL.
4
+ * The workflow-guards Feature (#3478) needs a way for one operator to take an
5
+ * exclusive claim on a ticket so two concurrent runs do not both drive the
6
+ * same Story. Rather than invent a new state column, the lease rides the
7
+ * ticket's existing **assignees** surface: the single assignee *is* the lease
8
+ * owner.
10
9
  *
11
- * **There is no live heartbeat source.** The `story.heartbeat` emitter this
12
- * module was designed against was structurally inert (it demanded an
13
- * `epicId >= 1` that v2, which has no Epics, never supplies) and has been
14
- * deleted. Every caller now reaches `acquireLease` through
15
- * `lease-guard-shared.acquireLeaseFailClosed` with `anchorHeartbeatToNow`,
16
- * which pins `heartbeatAt` to `now`: the lease **fails closed**, so any
17
- * foreign claim reads as live and refuses. A stranded claim is cleared with
18
- * `--steal`, never by TTL expiry. The TTL and the stale-reclaim branch below
19
- * are therefore reachable only via an explicit caller-supplied `heartbeatAt`
20
- * the seam is kept, the automatic expiry is not real.
10
+ * **A foreign claim is a refusal, full stop (Story #5006).** The lease shipped
11
+ * with a TTL: a claim whose owner's last heartbeat was older than
12
+ * `delivery.lease.ttlMs` counted as stale and was silently reclaimed. The
13
+ * `story.heartbeat` emitter that would have fed it was structurally inert (it
14
+ * demanded an `epicId >= 1` that v2, which has no Epics, never supplies) and
15
+ * was deleted, after which every guard pinned `heartbeatAt` to `now` so the
16
+ * liveness test always answered "live" the TTL, the heartbeat parameter and
17
+ * the reclaim branch were an elaborate way of writing `true`. They are gone.
18
+ * A stranded claim is cleared with `--steal`, which is now the only way past
19
+ * a foreign owner.
21
20
  *
22
- * The three exported operations are deliberately thin and provider-agnostic:
21
+ * The two exported operations are deliberately thin and provider-agnostic:
23
22
  *
24
23
  * - `acquireLease` — claim an unassigned ticket, re-affirm a self-held
25
- * claim, reclaim a ticket whose foreign claim has gone
26
- * stale (heartbeat older than TTL), or — with
27
- * `steal: true` — forcibly transfer a *live* foreign
28
- * claim.
24
+ * claim, or with `steal: true` forcibly transfer a
25
+ * foreign claim.
29
26
  * - `releaseLease` — clear the assignment, but only when the operator
30
27
  * still holds it (a no-op once the ticket was
31
28
  * reassigned elsewhere, so a late release never steals
32
29
  * a claim back from whoever took over).
33
- * - `describeLease` — a read-only snapshot of the current claim and its
34
- * liveness, used by callers (and tests) to reason about
35
- * a ticket without mutating it.
36
30
  *
37
31
  * Provider contract (a subset of `ITicketingProvider`):
38
32
  * - `getTicket(id)` → `{ assignees: string[], ... }`
39
33
  * - `updateTicket(id, { assignees })` writes the assignee list.
40
- *
41
- * Liveness seam: callers supply the owner's last-heartbeat epoch-ms via the
42
- * `heartbeatAt` option (a number, or `null`/`undefined` when no heartbeat has
43
- * ever been recorded for the current owner). Threading the timestamp in keeps
44
- * this module pure and trivially unit-testable — it does not read any ledger
45
- * itself. A claim with no heartbeat is treated as stale (reclaimable) by
46
- * `isClaimLive`; note the live guards never take that branch, per the
47
- * fail-closed anchoring described above.
48
- *
49
- * `now` is injectable (epoch ms) for deterministic tests; it defaults to
50
- * `Date.now()`.
51
34
  */
52
35
 
53
- import { resolveLeaseTtlMs } from '../config/limits.js';
54
-
55
36
  /**
56
37
  * The shipped, non-personal operator-identity placeholder (and its bare,
57
38
  * post-normalise form). The committed `.agentrc.json` and the distributed
@@ -94,35 +75,6 @@ export function normalizeOperatorHandle(raw) {
94
75
  return trimmed;
95
76
  }
96
77
 
97
- /**
98
- * Decide whether a foreign claim is still "live" given the owner's last
99
- * heartbeat and the configured TTL. A claim is live when a heartbeat exists
100
- * and is no older than `ttlMs`. A missing heartbeat (`null`/`undefined`) or a
101
- * heartbeat older than the TTL is stale and therefore reclaimable.
102
- *
103
- * The `heartbeatAt` seam is retained, but there is no longer any in-repo
104
- * heartbeat *source*: the `story.heartbeat` emitter was structurally inert
105
- * (it required an `epicId >= 1` that v2 never sets) and was deleted along
106
- * with the ledger reader that scanned for it. Every live caller reaches this
107
- * through `lease-guard-shared.acquireLeaseFailClosed` with
108
- * `anchorHeartbeatToNow`, which pins `heartbeatAt` to `now` so ANY foreign
109
- * claim reads live and the guard fails closed — a stranded claim is cleared
110
- * with `--steal`, not by TTL expiry. The parameter stays because that
111
- * anchoring is expressed through it.
112
- *
113
- * @param {object} args
114
- * @param {number|null|undefined} args.heartbeatAt Owner's last heartbeat (epoch ms).
115
- * @param {number} args.ttlMs Lease TTL in milliseconds.
116
- * @param {number} args.now Current time (epoch ms).
117
- * @returns {boolean}
118
- */
119
- export function isClaimLive({ heartbeatAt, ttlMs, now }) {
120
- if (typeof heartbeatAt !== 'number' || !Number.isFinite(heartbeatAt)) {
121
- return false;
122
- }
123
- return now - heartbeatAt <= ttlMs;
124
- }
125
-
126
78
  /**
127
79
  * Normalise the assignee list into a single current owner. The lease model is
128
80
  * single-holder: the first assignee is authoritative. Returns `null` for an
@@ -141,10 +93,10 @@ export function currentOwner(assignees) {
141
93
  *
142
94
  * @param {string} op
143
95
  * @param {object} opts
144
- * @returns {{ provider: object, ticketId: number, operator: string, ttlMs: number, now: number }}
96
+ * @returns {{ provider: object, ticketId: number, operator: string }}
145
97
  */
146
98
  function normaliseOpts(op, opts) {
147
- const { provider, ticketId, operator, config, now } = opts ?? {};
99
+ const { provider, ticketId, operator } = opts ?? {};
148
100
 
149
101
  if (!provider || typeof provider.getTicket !== 'function') {
150
102
  throw new Error(`${op}: provider with getTicket/updateTicket is required`);
@@ -156,45 +108,7 @@ function normaliseOpts(op, opts) {
156
108
  throw new Error(`${op}: operator must be a non-empty string`);
157
109
  }
158
110
 
159
- const ttlMs = resolveLeaseTtlMs(config, opts.ttlMs);
160
- const resolvedNow =
161
- typeof now === 'number' && Number.isFinite(now) ? now : Date.now();
162
-
163
- return { provider, ticketId, operator, ttlMs, now: resolvedNow };
164
- }
165
-
166
- /**
167
- * Read-only snapshot of the current lease on a ticket. Never mutates.
168
- *
169
- * @param {object} opts
170
- * @param {object} opts.provider Ticketing provider (getTicket).
171
- * @param {number} opts.ticketId Ticket to inspect.
172
- * @param {string} opts.operator The operator asking.
173
- * @param {number|null} [opts.heartbeatAt] Owner's last heartbeat (epoch ms).
174
- * @param {object} [opts.config] Resolved config (for TTL default).
175
- * @param {number} [opts.ttlMs] Explicit TTL override (epoch ms).
176
- * @param {number} [opts.now] Injectable clock (epoch ms).
177
- * @returns {Promise<{
178
- * ticketId: number,
179
- * owner: string|null,
180
- * heldByOperator: boolean,
181
- * live: boolean,
182
- * ttlMs: number,
183
- * }>}
184
- */
185
- export async function describeLease(opts) {
186
- const { provider, ticketId, operator, ttlMs, now } = normaliseOpts(
187
- 'describeLease',
188
- opts,
189
- );
190
- const ticket = await provider.getTicket(ticketId);
191
- const owner = currentOwner(ticket?.assignees);
192
- const heldByOperator = owner === operator;
193
- const live =
194
- owner === null
195
- ? false
196
- : isClaimLive({ heartbeatAt: opts.heartbeatAt, ttlMs, now });
197
- return { ticketId, owner, heldByOperator, live, ttlMs };
111
+ return { provider, ticketId, operator };
198
112
  }
199
113
 
200
114
  /**
@@ -205,10 +119,8 @@ export async function describeLease(opts) {
205
119
  * `reason: 'unclaimed'`.
206
120
  * - Operator already holds it → no write, `acquired: true`,
207
121
  * `reason: 'already-held'`.
208
- * - Live foreign claim, no steal → no write, `acquired: false`,
122
+ * - Foreign claim, no steal → no write, `acquired: false`,
209
123
  * `owner: <foreign>`, `reason: 'held'`.
210
- * - Stale foreign claim → reassign operator, `acquired: true`,
211
- * `reason: 'reclaimed'`.
212
124
  * - Foreign claim + `steal:true` → reassign operator, `acquired: true`,
213
125
  * `reason: 'stolen'`.
214
126
  * - Lost a write race → a foreign login co-assigned between our
@@ -226,23 +138,16 @@ export async function describeLease(opts) {
226
138
  * @param {object} opts.provider Ticketing provider.
227
139
  * @param {number} opts.ticketId Ticket to claim.
228
140
  * @param {string} opts.operator Operator acquiring the lease.
229
- * @param {number|null} [opts.heartbeatAt] Current owner's last heartbeat (epoch ms).
230
- * @param {boolean} [opts.steal=false] Transfer a live foreign claim.
231
- * @param {object} [opts.config] Resolved config (TTL default).
232
- * @param {number} [opts.ttlMs] Explicit TTL override.
233
- * @param {number} [opts.now] Injectable clock.
141
+ * @param {boolean} [opts.steal=false] Transfer a foreign claim.
234
142
  * @returns {Promise<{
235
143
  * acquired: boolean,
236
144
  * owner: string,
237
145
  * previousOwner: string|null,
238
- * reason: 'unclaimed'|'already-held'|'reclaimed'|'stolen'|'held'|'lost-race',
146
+ * reason: 'unclaimed'|'already-held'|'stolen'|'held'|'lost-race',
239
147
  * }>}
240
148
  */
241
149
  export async function acquireLease(opts) {
242
- const { provider, ticketId, operator, ttlMs, now } = normaliseOpts(
243
- 'acquireLease',
244
- opts,
245
- );
150
+ const { provider, ticketId, operator } = normaliseOpts('acquireLease', opts);
246
151
  const steal = opts.steal === true;
247
152
 
248
153
  const ticket = await provider.getTicket(ticketId);
@@ -269,9 +174,8 @@ export async function acquireLease(opts) {
269
174
  };
270
175
  }
271
176
 
272
- // Foreign claim — decide on liveness / steal.
273
- const live = isClaimLive({ heartbeatAt: opts.heartbeatAt, ttlMs, now });
274
- if (live && !steal) {
177
+ // Foreign claim — refuse unless the operator explicitly steals it.
178
+ if (!steal) {
275
179
  return {
276
180
  acquired: false,
277
181
  owner,
@@ -285,7 +189,7 @@ export async function acquireLease(opts) {
285
189
  ticketId,
286
190
  operator,
287
191
  previousOwner: owner,
288
- reason: steal && live ? 'stolen' : 'reclaimed',
192
+ reason: 'stolen',
289
193
  });
290
194
  }
291
195
 
@@ -359,9 +263,6 @@ async function claimAndVerify({
359
263
  * @param {object} opts.provider Ticketing provider.
360
264
  * @param {number} opts.ticketId Ticket to release.
361
265
  * @param {string} opts.operator Operator releasing the lease.
362
- * @param {object} [opts.config] Resolved config (TTL default).
363
- * @param {number} [opts.ttlMs] Explicit TTL override.
364
- * @param {number} [opts.now] Injectable clock.
365
266
  * @returns {Promise<{
366
267
  * released: boolean,
367
268
  * owner: string|null,
@@ -29,7 +29,7 @@
29
29
  */
30
30
 
31
31
  import { parse as parseStoryBody } from '../story-body/story-body.js';
32
- import { estimateTokens } from './context-envelope.js';
32
+ import { estimateTokens } from './spec-spill.js';
33
33
 
34
34
  /**
35
35
  * Normalize a Story's `body` to the structured object the sizing layers
@@ -146,14 +146,14 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
146
146
  // `--steal`, not by TTL expiry. One entry per ticket; re-acquires upsert
147
147
  // in place.
148
148
  'plan-lease',
149
- // Story #4415 (Epic #4406) — the feedback-loop graduators
150
- // (`audit-results-graduator.js` / `retro-proposals-graduator.js`) upsert a
151
- // `cross-repo-deferred` comment on the Epic listing findings that route
149
+ // Story #4415 (Epic #4406) — the feedback-loop graduator
150
+ // (`retro-proposals-graduator.js`) upserts a
151
+ // `cross-repo-deferred` comment listing findings that route
152
152
  // to a different repository and were therefore not filed here. Replaces
153
153
  // the prior log-line-only trace so the deferral survives the finalize
154
154
  // run as a durable, operator-visible record. Discriminated by a
155
- // `graduator="audit-results|code-review"` attr so the two graduators
156
- // upsert independent comments; re-runs upsert in place.
155
+ // `graduator="<name>"` attr so independent graduators do not clobber each
156
+ // other's comment; re-runs upsert in place.
157
157
  'cross-repo-deferred',
158
158
  // Epic #4474 (PR3) / v2 Stage 3 — `plan-persist.js` upserts a single
159
159
  // `plan-summary` comment on the primary Story at terminal persist