mandrel 2.31.0 → 2.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +12 -9
  4. package/.agents/docs/agentrc-reference.json +63 -58
  5. package/.agents/docs/configuration.md +275 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +2 -2
  14. package/.agents/schemas/agentrc.schema.json +1955 -1410
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +16 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/audit-to-stories.js +160 -41
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +159 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +108 -124
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
  38. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
  39. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  40. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  41. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  42. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  43. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  44. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  45. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  46. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  47. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  48. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  49. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  50. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  51. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  52. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  53. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  54. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  55. package/.agents/scripts/lib/child-exec.js +193 -0
  56. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  57. package/.agents/scripts/lib/cli-args.js +64 -0
  58. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  59. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  60. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  61. package/.agents/scripts/lib/config/commands.js +3 -5
  62. package/.agents/scripts/lib/config/explain.js +5 -7
  63. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  64. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  65. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  66. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  67. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  68. package/.agents/scripts/lib/config/gates/index.js +5 -2
  69. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  70. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  71. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  72. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  73. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  74. package/.agents/scripts/lib/config/github.js +0 -1
  75. package/.agents/scripts/lib/config/limits.js +3 -48
  76. package/.agents/scripts/lib/config/qa.js +105 -0
  77. package/.agents/scripts/lib/config/runners.js +38 -16
  78. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  79. package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
  80. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  81. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  82. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  83. package/.agents/scripts/lib/crap-utils.js +84 -520
  84. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  85. package/.agents/scripts/lib/degraded-mode.js +2 -2
  86. package/.agents/scripts/lib/dependency-parser.js +20 -7
  87. package/.agents/scripts/lib/doc-tiers.js +3 -3
  88. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  89. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  90. package/.agents/scripts/lib/findings/provenance-field.js +135 -0
  91. package/.agents/scripts/lib/findings/route-finding.js +57 -8
  92. package/.agents/scripts/lib/fs-walk.js +52 -0
  93. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  94. package/.agents/scripts/lib/git-utils.js +16 -36
  95. package/.agents/scripts/lib/knip-config-resolver.js +181 -0
  96. package/.agents/scripts/lib/knip-entry-sync.js +508 -0
  97. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  98. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  99. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  100. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  101. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  102. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  103. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  104. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  105. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  106. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  107. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  108. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  109. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  110. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  111. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  112. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
  114. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
  115. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
  116. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
  117. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  118. package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
  119. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  120. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  131. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  132. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  133. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  134. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  135. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  136. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  137. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  138. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
  139. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  140. package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
  141. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  142. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  143. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  144. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  145. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  146. package/.agents/scripts/lib/signals/index.js +8 -6
  147. package/.agents/scripts/lib/signals/schema.js +20 -25
  148. package/.agents/scripts/lib/signals/write.js +8 -8
  149. package/.agents/scripts/lib/story-body/footer-block.js +97 -0
  150. package/.agents/scripts/lib/story-body/story-body.js +18 -81
  151. package/.agents/scripts/lib/temp-retention.js +1 -1
  152. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  153. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  154. package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
  156. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  157. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  158. package/.agents/scripts/lib/worktree-manager.js +2 -3
  159. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  160. package/.agents/scripts/pr-watch-with-update.js +7 -5
  161. package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
  162. package/.agents/scripts/providers/github/cache.js +2 -2
  163. package/.agents/scripts/providers/github/comments.js +6 -28
  164. package/.agents/scripts/providers/github/compose.js +0 -15
  165. package/.agents/scripts/providers/github/errors.js +10 -27
  166. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  167. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  168. package/.agents/scripts/providers/github.js +4 -7
  169. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  170. package/.agents/scripts/resolve-stories.js +21 -7
  171. package/.agents/scripts/run-lint.js +61 -61
  172. package/.agents/scripts/run-test-profile.js +6 -6
  173. package/.agents/scripts/run-verify.js +48 -30
  174. package/.agents/scripts/single-story-close.js +20 -0
  175. package/.agents/scripts/single-story-init.js +12 -35
  176. package/.agents/scripts/stories-wave-tick.js +192 -9
  177. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  178. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  179. package/.agents/skills/skills.index.json +1 -11
  180. package/.agents/workflows/audit-documentation.md +5 -6
  181. package/.agents/workflows/audit-to-stories.md +28 -2
  182. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  183. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  184. package/.agents/workflows/helpers/code-review.md +3 -8
  185. package/.agents/workflows/helpers/deliver-reference.md +30 -2
  186. package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
  187. package/.agents/workflows/helpers/plan-reference.md +76 -0
  188. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  189. package/.agents/workflows/mandrel-update.md +10 -10
  190. package/.agents/workflows/qa-assist.md +15 -20
  191. package/.agents/workflows/qa-explore.md +9 -8
  192. package/README.md +1 -1
  193. package/docs/CHANGELOG.md +58 -0
  194. package/lib/migrations/index.js +2 -0
  195. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  196. package/package.json +14 -5
  197. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  198. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  199. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  200. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  201. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  202. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  203. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  204. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  205. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  206. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  207. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  208. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  209. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  210. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  211. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  212. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  213. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  214. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  215. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  216. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  217. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  218. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  219. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  220. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  221. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  222. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  223. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  224. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  225. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  226. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  227. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  228. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  229. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  230. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  231. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  232. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  233. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  234. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  235. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  236. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  237. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  238. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  239. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  240. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  241. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  242. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  243. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  244. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  245. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  246. package/.agents/scripts/lib/planning-corpus.js +0 -37
  247. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  248. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  249. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  250. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  251. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  252. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  253. package/.agents/scripts/lib/signals/read.js +0 -268
  254. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  255. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  256. package/.agents/scripts/lib/story-plan.js +0 -379
  257. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  258. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  259. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  260. package/.agents/scripts/lint-baseline.js +0 -507
  261. package/.agents/scripts/providers/github/prs.js +0 -103
  262. package/.agents/scripts/signals-view.js +0 -309
  263. package/.agents/scripts/story-plan.js +0 -370
  264. package/.agents/scripts/sync-branch-from-base.js +0 -149
  265. package/.agents/scripts/validate-docs-freshness.js +0 -314
  266. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  267. package/.agents/workflows/helpers/signals.md +0 -112
@@ -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,