@event4u/agent-config 15.0.0 → 16.1.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 (283) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +301 -347
  5. package/CONTRIBUTING.md +9 -0
  6. package/README.md +2 -2
  7. package/dist/agent-src/commands/analyze/inbox.md +26 -0
  8. package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
  9. package/dist/agent-src/commands/challenge-me/closure.md +178 -0
  10. package/dist/agent-src/commands/challenge-me.md +9 -2
  11. package/dist/agent-src/commands/council/default.md +25 -5
  12. package/dist/agent-src/commands/feature/plan.md +28 -1
  13. package/dist/agent-src/commands/feature/roadmap.md +28 -1
  14. package/dist/agent-src/commands/implement-ticket.md +27 -0
  15. package/dist/agent-src/commands/jira-ticket.md +27 -0
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/pr/merge.md +60 -9
  18. package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
  19. package/dist/agent-src/commands/roadmap/create.md +77 -29
  20. package/dist/agent-src/commands/roadmap/materialize.md +27 -0
  21. package/dist/agent-src/commands/roadmap/process-full.md +123 -18
  22. package/dist/agent-src/commands/team.md +22 -0
  23. package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
  24. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
  25. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
  26. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
  27. package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
  28. package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
  29. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
  30. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
  31. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  32. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  33. package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
  34. package/dist/agent-src/rules/autonomous-execution.md +15 -8
  35. package/dist/agent-src/rules/delegation-policy.md +23 -45
  36. package/dist/agent-src/rules/design-fidelity.md +32 -37
  37. package/dist/agent-src/rules/evaluator-independence.md +43 -49
  38. package/dist/agent-src/rules/test-first.md +41 -0
  39. package/dist/agent-src/rules/user-interaction.md +31 -30
  40. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  41. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  42. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  43. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  44. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  45. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  46. package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
  47. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  48. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  49. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  51. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  52. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  53. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  54. package/dist/agent-src/templates/agent-settings.md +1 -1
  55. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  56. package/dist/agent-src/templates/roadmaps.md +188 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  58. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  59. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  60. package/dist/cli-delegate/check_update_banner.js +40 -2
  61. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  62. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  63. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  64. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  65. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  66. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  67. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  68. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  69. package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
  70. package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
  71. package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
  72. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  73. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  74. package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
  75. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  76. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  77. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  78. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  79. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  80. package/dist/cli-delegate/cmd_conformance.js +918 -325
  81. package/dist/cli-delegate/cmd_doctor.js +7 -5
  82. package/dist/cli-delegate/cmd_explain.js +5 -3
  83. package/dist/cli-delegate/cmd_export.js +9 -7
  84. package/dist/cli-delegate/cmd_fleet.js +4 -2
  85. package/dist/cli-delegate/cmd_handoff.js +6 -6
  86. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +3 -2
  89. package/dist/cli-delegate/cmd_preflight.js +4 -2
  90. package/dist/cli-delegate/cmd_prune.js +10 -8
  91. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  92. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  93. package/dist/cli-delegate/cmd_session_recycle.js +4 -4
  94. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  95. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  96. package/dist/cli-delegate/cmd_settings_set.js +14 -1
  97. package/dist/cli-delegate/cmd_sync.js +10 -8
  98. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  99. package/dist/cli-delegate/cmd_update.js +1 -1
  100. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  101. package/dist/cli-delegate/cmd_validate.js +12 -10
  102. package/dist/cli-delegate/cmd_versions.js +1 -1
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +87 -40
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +4 -4
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +10 -8
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +8 -6
  112. package/dist/hooks/dispatch.js +2601 -1073
  113. package/dist/install/claudeRuleRewrite.js +14 -3
  114. package/dist/install/conflict.js +60 -10
  115. package/dist/install/conflictTracker.js +218 -0
  116. package/dist/install/fsPrimitives.js +54 -0
  117. package/dist/install/install.mjs +2008 -1481
  118. package/dist/install/manifestFiles.js +259 -0
  119. package/dist/install/preflight.js +14 -2
  120. package/dist/install/preserve.js +157 -0
  121. package/dist/install/recordedOwnership.js +132 -0
  122. package/dist/install/txlog.js +11 -2
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/mcp/server.json +2 -2
  125. package/dist/mcp/server.mjs +1612 -667
  126. package/dist/router.json +1 -1
  127. package/dist/scripts/_lib/agent_settings.js +3 -2
  128. package/dist/scripts/_lib/fs_atomic.js +167 -0
  129. package/dist/scripts/_lib/installed_lock.js +1 -1
  130. package/dist/scripts/_lib/installed_tools.js +460 -0
  131. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  132. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  133. package/dist/server/io/yamlIO.js +50 -0
  134. package/dist/server/routes/install.js +8 -0
  135. package/dist/server/routes/wizard.js +9 -3
  136. package/dist/server/schemas/settings.js +17 -5
  137. package/dist/shared/missionExecution.js +97 -0
  138. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  139. package/dist/ui/index.html +1 -1
  140. package/docs/MIGRATION.md +103 -0
  141. package/docs/contracts/settings-classes.md +26 -7
  142. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  143. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  144. package/docs/guidelines/design-fidelity-routing.md +29 -0
  145. package/package.json +1 -1
  146. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  147. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  148. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  149. package/src/config/agent-settings.template.yml +139 -27
  150. package/src/config/auto-rule-budget.json +16 -0
  151. package/src/config/ci-local-parity.yml +18 -5
  152. package/src/config/continuity-surface.json +28 -0
  153. package/src/config/gate-coverage.yml +146 -13
  154. package/src/config/gate-violation-baselines.json +6 -6
  155. package/src/config/hook-token-budget.json +2 -0
  156. package/src/config/host-injection-effect.json +1 -1
  157. package/src/config/loop-surfaces.yaml +215 -0
  158. package/src/config/platform-anchor.json +3 -2
  159. package/src/config/preamble-payload-budget.json +12 -7
  160. package/src/config/preamble-payload-exceptions.json +28 -0
  161. package/src/config/release-gate-locality.yml +11 -0
  162. package/src/config/routing-coverage-seed.json +18 -4
  163. package/src/config/rule-enforcement-baseline.json +0 -1
  164. package/src/install/claudeRuleRewrite.ts +16 -2
  165. package/src/install/conflict.ts +65 -9
  166. package/src/install/conflictTracker.ts +299 -0
  167. package/src/install/fsPrimitives.ts +54 -0
  168. package/src/install/manifestFiles.ts +312 -0
  169. package/src/install/preflight.ts +16 -2
  170. package/src/install/preserve.ts +205 -0
  171. package/src/install/recordedOwnership.ts +149 -0
  172. package/src/install/txlog.ts +11 -2
  173. package/src/install/types.ts +19 -4
  174. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  175. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  176. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  177. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  178. package/src/scripts/_cli/doctor_execution.ts +90 -0
  179. package/src/scripts/_dispatch.bash +3 -1
  180. package/src/scripts/_lib/activation_payload.ts +247 -0
  181. package/src/scripts/_lib/agent_settings.ts +4 -2
  182. package/src/scripts/_lib/authority_path.ts +102 -0
  183. package/src/scripts/_lib/base_ref_payload.ts +315 -0
  184. package/src/scripts/_lib/cascade_base.ts +94 -0
  185. package/src/scripts/_lib/changelog_eras.ts +7 -2
  186. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  187. package/src/scripts/_lib/council_transport.ts +129 -0
  188. package/src/scripts/_lib/delivery_ready.ts +112 -0
  189. package/src/scripts/_lib/dropped_decision.ts +142 -0
  190. package/src/scripts/_lib/forge_protection.ts +267 -0
  191. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  192. package/src/scripts/_lib/installed_lock.ts +1 -1
  193. package/src/scripts/_lib/json_python_parity.ts +127 -0
  194. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  195. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  196. package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
  197. package/src/scripts/_lib/mission_record.ts +190 -0
  198. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  199. package/src/scripts/_lib/obligations.ts +402 -0
  200. package/src/scripts/_lib/orchestration_record.ts +11 -20
  201. package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
  202. package/src/scripts/_lib/release_findings_ingest.ts +324 -0
  203. package/src/scripts/_lib/release_highlights.ts +199 -10
  204. package/src/scripts/_lib/release_holds.ts +493 -0
  205. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  206. package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
  207. package/src/scripts/_lib/review_baseline.ts +254 -0
  208. package/src/scripts/_lib/review_skipped_record.ts +61 -1
  209. package/src/scripts/_lib/rides_along.ts +102 -0
  210. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  211. package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
  212. package/src/scripts/_lib/test_delta.ts +166 -0
  213. package/src/scripts/_lib/test_provenance.ts +171 -0
  214. package/src/scripts/_lib/turn_end_refusals.ts +51 -4
  215. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  216. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  217. package/src/scripts/ai_council/config.ts +8 -11
  218. package/src/scripts/ai_council/fallback_config.ts +5 -3
  219. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  220. package/src/scripts/ask_block_census.ts +285 -15
  221. package/src/scripts/check_beta_review_markers.ts +35 -4
  222. package/src/scripts/check_enforcement_matrix.ts +624 -0
  223. package/src/scripts/check_finding_dispositions.ts +167 -16
  224. package/src/scripts/check_gate_reachability.ts +173 -10
  225. package/src/scripts/check_generator_sync.ts +954 -0
  226. package/src/scripts/check_held_object_arrivals.ts +381 -0
  227. package/src/scripts/check_preamble_payload_budget.ts +474 -124
  228. package/src/scripts/check_release_highlights.ts +33 -1
  229. package/src/scripts/check_release_holds.ts +567 -0
  230. package/src/scripts/check_release_published.ts +1 -1
  231. package/src/scripts/check_reply_consistency.ts +86 -17
  232. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  233. package/src/scripts/check_test_delta.ts +294 -0
  234. package/src/scripts/check_test_weakening.ts +299 -0
  235. package/src/scripts/closure_scan.ts +333 -0
  236. package/src/scripts/compile_hook_manifest.ts +43 -3
  237. package/src/scripts/council_record_shape.ts +209 -0
  238. package/src/scripts/hook_manifest.json +1 -1
  239. package/src/scripts/hook_manifest.yaml +127 -12
  240. package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
  241. package/src/scripts/hooks/concern_registry.ts +6 -0
  242. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  243. package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
  244. package/src/scripts/hooks/evidence_independence.ts +96 -0
  245. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  246. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  247. package/src/scripts/hooks/review_baseline_hook.ts +130 -0
  248. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  249. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  250. package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
  251. package/src/scripts/hooks_doctor.ts +89 -1
  252. package/src/scripts/hooks_status.ts +19 -0
  253. package/src/scripts/install.ts +107 -207
  254. package/src/scripts/interruption_report.ts +8 -2
  255. package/src/scripts/janitor.ts +15 -1
  256. package/src/scripts/lint_decision_classes.ts +464 -0
  257. package/src/scripts/lint_hook_manifest.ts +11 -0
  258. package/src/scripts/lint_major_migration_sections.ts +450 -0
  259. package/src/scripts/lint_plan_risk_register.ts +47 -3
  260. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  261. package/src/scripts/lint_roadmap_producers.ts +202 -0
  262. package/src/scripts/measure_rule_budget.ts +101 -5
  263. package/src/scripts/measure_turn_end_gate.ts +302 -5
  264. package/src/scripts/new_roadmap.ts +37 -0
  265. package/src/scripts/orchestration_record.ts +0 -5
  266. package/src/scripts/release.ts +66 -162
  267. package/src/scripts/release_drill.ts +460 -15
  268. package/src/scripts/release_env.ts +1 -1
  269. package/src/scripts/release_publication.ts +52 -1
  270. package/src/scripts/report_enforcement_drift.ts +566 -0
  271. package/src/scripts/report_held_object_arrivals.ts +456 -0
  272. package/src/scripts/report_host_injection_effect.ts +3 -11
  273. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  274. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  275. package/src/scripts/schemas/command.schema.json +4 -0
  276. package/src/scripts/scope_growth.ts +219 -0
  277. package/src/scripts/sync_agent_settings.ts +140 -18
  278. package/src/scripts/trigger_eval_grandfather.json +0 -1
  279. package/src/scripts/ui_conformance_probe.ts +622 -0
  280. package/src/server/io/yamlIO.ts +48 -0
  281. package/src/server/schemas/settings.ts +31 -7
  282. package/src/shared/missionExecution.ts +126 -0
  283. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import * as fs from 'node:fs';
29
+ import * as os from 'node:os';
29
30
  import * as path from 'node:path';
30
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
32
 
@@ -37,6 +38,8 @@ import {
37
38
  Plan,
38
39
  SystemExitError,
39
40
  } from './release.js';
41
+ import { releaseHoldPreflight } from './_lib/release_holds.js';
42
+ import { die } from './release_publication.js';
40
43
  import { DERIVED_MARKER } from './_lib/release_highlights.js';
41
44
  import {
42
45
  AUGMENT_MARKETPLACE_JSON,
@@ -126,6 +129,33 @@ interface WorldConfig {
126
129
  * existed.
127
130
  */
128
131
  branch_behind_main?: number;
132
+ /**
133
+ * Commits `origin/<release-branch>` carries that this checkout does not.
134
+ *
135
+ * Sibling of `branch_behind_main`, one ref over: that one fakes `git
136
+ * rev-list --count HEAD..origin/main`, this one the distance to the
137
+ * release branch's OWN remote — the divergence a GitHub *Update branch*
138
+ * press or a concurrent resume run produces.
139
+ */
140
+ branch_behind_remote?: number;
141
+ /**
142
+ * Commits this checkout carries that `origin/<release-branch>` does not.
143
+ *
144
+ * Defaults to 1 so every pre-existing scenario keeps the local-ahead shape
145
+ * it was written against. Set to 0 together with `branch_behind_remote: 0`
146
+ * to reach the state where the remote is already AT head and the push must
147
+ * therefore not be issued at all.
148
+ */
149
+ branch_ahead_remote?: number;
150
+ /**
151
+ * Whether `origin/<release-branch>` exists at the start of the run.
152
+ *
153
+ * Internal state until 2026-09-12 — a scenario could only reach `false`
154
+ * indirectly, by having the run delete the branch. The remote-position
155
+ * probe added with `_remote_branch_state` has a never-pushed arm that no
156
+ * scenario could otherwise enter, so the knob is now declared.
157
+ */
158
+ branch_exists_remote?: boolean;
129
159
  /**
130
160
  * Whether `execute()` is invoked with `--resume`. Defaults to TRUE so every
131
161
  * pre-existing scenario keeps the shape it was written against.
@@ -143,6 +173,27 @@ interface WorldConfig {
143
173
  merge_fails_hard?: boolean;
144
174
  /** PR checks fail (watch exits non-zero) — the release must die. */
145
175
  checks_fail?: boolean;
176
+ /**
177
+ * Step 7's world — two knobs, because the step asks two questions.
178
+ *
179
+ * It VERIFIES: the ledger is produced by the self-review workflow, so the
180
+ * only things a scenario can vary are whether the file is on the remote
181
+ * branch and what the disposition gate answers. An earlier version of this
182
+ * step ingested, and carried four more knobs for run discovery and artifact
183
+ * download; those went with it when the owner moved the ingest into CI.
184
+ */
185
+ ledger_on_branch?: boolean;
186
+ /**
187
+ * Whether the ledger is on the TRUNK — the ref the merged path asks.
188
+ *
189
+ * Separate from `ledger_on_branch` because the two states worth testing are
190
+ * exactly the ones a single boolean cannot express: present on the branch
191
+ * and not on the trunk, and the reverse. Defaults to `ledger_on_branch`, so
192
+ * every scenario written before this knob keeps its answer.
193
+ */
194
+ ledger_on_trunk?: boolean;
195
+ /** Exit code + streams the disposition gate answers with. */
196
+ disposition_verdict?: { status: number; stdout: string; stderr: string };
146
197
  /**
147
198
  * Content `git show <target>:CHANGELOG.md` returns.
148
199
  *
@@ -169,7 +220,7 @@ interface WorldConfig {
169
220
  changelog?: string;
170
221
  /**
171
222
  * Content the WORKING-TREE read returns — `read_changelog_text()`, used by
172
- * step 5 (PR body) and step 8 (annotated-tag message).
223
+ * step 5 (PR body) and step 9 (annotated-tag message).
173
224
  *
174
225
  * Added 2026-09-01 with the publication guard (roadmap § 3.1). `changelog`
175
226
  * above only covers `git show <tag>:CHANGELOG.md`; the working tree was
@@ -189,19 +240,20 @@ interface WorldConfig {
189
240
  */
190
241
  tag_created_unpushed?: boolean;
191
242
  /**
192
- * The tag is already on the remote, so step 8 skips entirely and step 9 —
243
+ * The tag is already on the remote, so step 9 skips entirely and step 10 —
193
244
  * the GitHub Release body — is the FIRST irreversible transition the run
194
245
  * reaches. The only state in which the Release guard can be exercised on
195
- * its own, because a marker otherwise stops the run at step 8.
246
+ * its own, because a marker otherwise stops the run at step 9.
196
247
  */
197
248
  tag_already_remote?: boolean;
198
249
  /**
199
250
  * The release PR is ALREADY merged when the run starts — the real
200
- * resume-after-merge state, in which steps 1 through 7 skip and step 8 is
201
- * the first step that does work.
251
+ * resume-after-merge state, in which steps 1 through 6 skip, step 7 still
252
+ * verifies the ledger (on the trunk, since the branch is gone) and step 8
253
+ * is the first step that does work.
202
254
  *
203
255
  * Added 2026-09-02 with `guard_release_branch_push`. Before that guard, a
204
- * marker scenario could start from scratch and still reach step 8, because
256
+ * marker scenario could start from scratch and still reach step 9, because
205
257
  * nothing between the changelog write and the tag looked at the section.
206
258
  * The push guard now stops such a run at step 4 — correctly, and that is
207
259
  * the point of it — which makes this knob the only way left to exercise
@@ -297,9 +349,14 @@ class FakeWorld {
297
349
  private push_rejections: number;
298
350
  private behind_probes: number;
299
351
  private branch_behind_main: number;
352
+ private branch_behind_remote: number;
353
+ private readonly branch_ahead_remote: number;
300
354
  private merge_races_once: boolean;
301
355
  private merge_fails_hard: boolean;
302
356
  private checks_fail: boolean;
357
+ private readonly ledger_on_branch: boolean;
358
+ private readonly ledger_on_trunk: boolean | undefined;
359
+ private readonly disposition_verdict: { status: number; stdout: string; stderr: string };
303
360
  private readonly changelog: string;
304
361
  /** Working-tree content; see `WorldConfig.changelog_file`. */
305
362
  readonly changelog_file: string;
@@ -316,11 +373,20 @@ class FakeWorld {
316
373
  this.tag_remote = cfg.tag_already_remote ?? false;
317
374
  this.behind_probes = cfg.behind_probes ?? 0;
318
375
  this.branch_behind_main = cfg.branch_behind_main ?? 0;
376
+ this.branch_exists_remote = cfg.branch_exists_remote ?? true;
377
+ this.branch_behind_remote = cfg.branch_behind_remote ?? 0;
378
+ this.branch_ahead_remote = cfg.branch_ahead_remote ?? 1;
319
379
  this.merge_races_once = cfg.merge_races_once ?? false;
320
380
  this.merge_fails_hard = cfg.merge_fails_hard ?? false;
321
381
  this.checks_fail = cfg.checks_fail ?? false;
322
382
  this.pr_merged = cfg.pr_already_merged ?? false;
323
383
  this.release_verify_fails = cfg.release_verify_fails ?? false;
384
+ // Defaults keep every pre-2026-09-11 scenario green: the ledger is
385
+ // already on the branch and the gate is happy, so step 7 is a two-call
386
+ // no-op and the sequencing scenarios stay about sequencing.
387
+ this.ledger_on_branch = cfg.ledger_on_branch ?? true;
388
+ this.ledger_on_trunk = cfg.ledger_on_trunk;
389
+ this.disposition_verdict = cfg.disposition_verdict ?? { status: 0, stdout: '', stderr: '' };
324
390
  }
325
391
 
326
392
  exec(args: readonly string[]): ExecResult {
@@ -344,6 +410,18 @@ class FakeWorld {
344
410
  this.branch_behind_main = 0;
345
411
  return OK;
346
412
  }
413
+ if (cmd === `git rev-list --left-right --count HEAD...origin/${this.branch}`) {
414
+ return {
415
+ ...OK,
416
+ stdout: `${String(this.branch_ahead_remote)}\t${String(this.branch_behind_remote)}\n`,
417
+ };
418
+ }
419
+ if (cmd === `git merge --no-edit origin/${this.branch}`) {
420
+ // A real merge closes the gap, so the re-read sees 0 — the same
421
+ // reason the origin/main handler above zeroes its own counter.
422
+ this.branch_behind_remote = 0;
423
+ return OK;
424
+ }
347
425
  if (cmd === `git rev-parse --verify --quiet refs/heads/${this.branch}`) {
348
426
  return this.pr_merged ? { ...OK, status: 1 } : OK;
349
427
  }
@@ -483,6 +561,21 @@ class FakeWorld {
483
561
  return OK;
484
562
  }
485
563
 
564
+ // Step 7 — the findings ledger.
565
+ if (cmd.startsWith('git cat-file -e ')) {
566
+ // REF-AWARE, because one boolean for every ref made the two states
567
+ // that distinguish right from wrong inexpressible: on the branch
568
+ // but not the trunk, and the reverse. A fourth review named the
569
+ // merged-path scenario as asserting a string in the argv rather
570
+ // than the ref that was read.
571
+ const onTrunk = this.ledger_on_trunk ?? this.ledger_on_branch;
572
+ const present = cmd.includes('/main:') ? onTrunk : this.ledger_on_branch;
573
+ return present ? OK : { ...OK, status: 1 };
574
+ }
575
+ if (cmd.includes('check_finding_dispositions --release')) {
576
+ return { ...this.disposition_verdict };
577
+ }
578
+
486
579
  throw new Error(`FakeWorld: unscripted command: ${cmd}`);
487
580
  }
488
581
  }
@@ -498,6 +591,16 @@ interface Scenario {
498
591
  /** What the scenario proves — printed by the CLI. */
499
592
  summary: string;
500
593
  config: WorldConfig;
594
+ /**
595
+ * Run THIS instead of `execute()`.
596
+ *
597
+ * Added for the release-holds scenarios, whose boundary is `preflight()` —
598
+ * it runs before step 1 and therefore before `execute()`, so a scenario
599
+ * driving `execute()` could never reach it. Every pre-existing scenario
600
+ * omits the field and keeps `execute()` byte-identically; the override is
601
+ * additive and changes nothing it does not set.
602
+ */
603
+ run?: () => void;
501
604
  /** Whether execute() is expected to complete (vs die/throw). */
502
605
  expect_success: boolean;
503
606
  /** Extra assertions over the finished world; return failure strings. */
@@ -514,7 +617,270 @@ function _count(world: FakeWorld, needle: string): number {
514
617
  return world.calls.filter((c) => c === needle).length;
515
618
  }
516
619
 
620
+
621
+ /* ------------------------------------------------------------------ *
622
+ * Release-holds fixtures (template rule 28, roadmap step 4.3).
623
+ *
624
+ * A throwaway tree per scenario, because the assertion is about what the
625
+ * evaluator READS — a mocked return would assert the mock.
626
+ * ------------------------------------------------------------------ */
627
+
628
+ function _holdFixtureRoot(name: string, body: string): string {
629
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), `release-holds-${name}-`));
630
+ const dir = path.join(root, 'agents', 'roadmaps');
631
+ fs.mkdirSync(dir, { recursive: true });
632
+ fs.writeFileSync(path.join(dir, 'fixture.md'), body, 'utf8');
633
+ return root;
634
+ }
635
+
636
+ function _openHoldBody(channel: string): string {
637
+ return [
638
+ '## Phase 1',
639
+ '- [x] **1.1 opener** <!-- opens-hold: half-wired -->',
640
+ ' verify: the opener\'s own check',
641
+ '- [ ] **1.2 clearer** <!-- clears-hold: half-wired -->',
642
+ ' verify: ./scripts-run src/scripts/check_surface_whole',
643
+ '',
644
+ '## Release holds',
645
+ '',
646
+ '### hold: half-wired',
647
+ `- **Channel:** ${channel}`,
648
+ '- **Opened by:** 1.1',
649
+ '- **Cleared by:** 1.2',
650
+ '- **State:** the surface is half wired while this is open.',
651
+ '- **Why not a guard:** the entry point is reachable and cannot be made inert.',
652
+ '',
653
+ ].join('\n');
654
+ }
655
+
517
656
  const SCENARIOS: Record<string, Scenario> = {
657
+ 'release-hold-open-all-refuses-the-cut': {
658
+ summary:
659
+ 'pre-flight: an open hold with `Channel: all` refuses the cut, naming the roadmap, the hold, its opener, its closer and the closer\'s verify command',
660
+ config: {},
661
+ expect_success: false,
662
+ run: () => {
663
+ releaseHoldPreflight(_holdFixtureRoot('all', _openHoldBody('all')), die);
664
+ },
665
+ verify: (_w, error) => {
666
+ const f: string[] = [];
667
+ const msg = error ?? '';
668
+ // All five fields, asserted individually. A refusal missing any one
669
+ // of them tells the operator they are stuck rather than what to do,
670
+ // which is the failure step 4.1 names.
671
+ for (const field of [
672
+ 'fixture.md',
673
+ 'half-wired',
674
+ 'opened by: 1.1',
675
+ 'cleared by: 1.2',
676
+ 'check_surface_whole',
677
+ ]) {
678
+ _expect(msg.includes(field), `the refusal never named \`${field}\`: ${msg}`, f);
679
+ }
680
+ for (const way of ['finish the clearing step', '-next.N', 'release line']) {
681
+ _expect(msg.includes(way), `the refusal never offered \`${way}\`: ${msg}`, f);
682
+ }
683
+ return f;
684
+ },
685
+ },
686
+ 'release-hold-open-latest-refuses-the-stable-cut': {
687
+ summary:
688
+ 'pre-flight: an open hold with `Channel: latest` still refuses, because release.ts cuts a bare X.Y.Z and never silently redirects it to a prerelease',
689
+ config: {},
690
+ expect_success: false,
691
+ run: () => {
692
+ releaseHoldPreflight(_holdFixtureRoot('latest', _openHoldBody('latest')), die);
693
+ },
694
+ verify: (_w, error) => {
695
+ const f: string[] = [];
696
+ const msg = error ?? '';
697
+ _expect(msg.includes('half-wired'), `the refusal never named the hold: ${msg}`, f);
698
+ _expect(
699
+ msg.includes('channel latest'),
700
+ `the refusal never showed the hold's own channel: ${msg}`,
701
+ f,
702
+ );
703
+ // The silent-redirect prohibition, asserted as a behaviour rather
704
+ // than as a grep: the run DIED. A release.ts that quietly converted
705
+ // this into `-next.N` would have completed.
706
+ _expect(
707
+ msg.startsWith('SystemExit('),
708
+ `the stable cut was not refused — a silent channel redirect: ${msg}`,
709
+ f,
710
+ );
711
+ return f;
712
+ },
713
+ },
714
+ 'release-hold-not-evaluable-refuses-the-cut': {
715
+ summary:
716
+ 'pre-flight: a malformed declaration the evaluator cannot read refuses the cut — there is no path on which "could not evaluate" reads as safe',
717
+ config: {},
718
+ expect_success: false,
719
+ run: () => {
720
+ // Missing `Why not a guard:`, which rule 28 makes mandatory. The
721
+ // evaluator can parse the file but not trust the declaration, which
722
+ // is exactly the not-evaluable row.
723
+ const body = _openHoldBody('all').replace(
724
+ /- \*\*Why not a guard:\*\*.*\n/,
725
+ '',
726
+ );
727
+ releaseHoldPreflight(_holdFixtureRoot('bad', body), die);
728
+ },
729
+ verify: (_w, error) => {
730
+ const f: string[] = [];
731
+ const msg = error ?? '';
732
+ _expect(
733
+ msg.startsWith('SystemExit('),
734
+ `an unreadable declaration did not refuse — it failed OPEN: ${msg}`,
735
+ f,
736
+ );
737
+ _expect(
738
+ msg.includes('not evaluable'),
739
+ `the refusal did not say the declaration was unreadable: ${msg}`,
740
+ f,
741
+ );
742
+ _expect(
743
+ msg.includes('Why not a guard'),
744
+ `the refusal did not name what was wrong with it: ${msg}`,
745
+ f,
746
+ );
747
+ return f;
748
+ },
749
+ },
750
+
751
+ 'findings-ledger-verified-on-the-remote-branch': {
752
+ summary:
753
+ 'step 7: the ledger is on the remote branch and the gate passes — the release continues without producing anything',
754
+ // Step 7 VERIFIES. The ingest lives in the self-review workflow, which
755
+ // commits the ledger to the release branch; two review rounds showed no
756
+ // placement inside this script can produce it, because the gate it
757
+ // anticipates already reds the check step 6 waits on.
758
+ // `resume: false` is load-bearing, not tidiness. `resume` defaults to
759
+ // TRUE here, and on a resumed run the PR number is already in hand — so
760
+ // a scenario that kept the default could not see the `--pr` flag being
761
+ // dropped, which is exactly the hole this scenario asserts against.
762
+ config: { ledger_on_branch: true, resume: false },
763
+ expect_success: true,
764
+ verify: (w) => {
765
+ const f: string[] = [];
766
+ const probe = w.calls.find((c) => c.startsWith('git cat-file -e '));
767
+ _expect(probe !== undefined, 'the ledger presence was never probed', f);
768
+ _expect(
769
+ (probe ?? '').includes('origin/'),
770
+ `the probe read a local ref, which a failed push makes true: ${probe ?? '(none)'}`,
771
+ f,
772
+ );
773
+ _expect(
774
+ !w.calls.some((c) => c.includes('--ingest')),
775
+ 'step 7 ingested — that belongs to the workflow now, not the release',
776
+ f,
777
+ );
778
+ _expect(
779
+ !w.calls.some((c) => c.startsWith('gh run download')),
780
+ 'step 7 downloaded an artifact — the release no longer produces the ledger',
781
+ f,
782
+ );
783
+ // `--pr` is what makes the gate compare the review's own report
784
+ // against the committed ledger. The PR number used to come only
785
+ // from the resume-time probe, so on a FIRST run — this scenario —
786
+ // the flag was silently dropped and the release passed a strictly
787
+ // weaker check than the one it was trying to pre-satisfy.
788
+ _expect(
789
+ w.calls.some(
790
+ (c) => c.includes('check_finding_dispositions --release') && c.includes('--pr '),
791
+ ),
792
+ 'the disposition gate ran without --pr, so an un-ingested finding would pass here',
793
+ f,
794
+ );
795
+ _expect(
796
+ w.calls.includes(`gh pr merge ${w.branch} --merge --delete-branch`),
797
+ 'the release never reached the merge',
798
+ f,
799
+ );
800
+ return f;
801
+ },
802
+ },
803
+ 'findings-ledger-absent-stops-the-release': {
804
+ summary:
805
+ 'step 7: no ledger on the remote branch — the release STOPS rather than merging and tagging without one',
806
+ config: { ledger_on_branch: false },
807
+ expect_success: false,
808
+ verify: (w, error) => {
809
+ const f: string[] = [];
810
+ _expect(
811
+ (error ?? '').includes('is not on origin/'),
812
+ `the stop did not name the ref it checked: ${error ?? '(no error)'}`,
813
+ f,
814
+ );
815
+ _expect(
816
+ !w.calls.includes(`gh pr merge ${w.branch} --merge --delete-branch`),
817
+ 'merged with no ledger — the failure this step exists to end',
818
+ f,
819
+ );
820
+ _expect(!w.tag_remote, 'tagged with no ledger', f);
821
+ return f;
822
+ },
823
+ },
824
+ 'findings-ledger-absent-after-a-hand-merge-still-stops': {
825
+ summary:
826
+ 'step 7: the PR is already merged and no ledger reached the trunk — the release STOPS instead of tagging a version whose ledger does not exist',
827
+ // The reachable path to a shipped version with no ledger, and the one
828
+ // the old code asserted away: the ingest produces nothing, step 7
829
+ // stops, a human merges the PR, and the resumed run used to print "the
830
+ // ledger rode in with it" without reading anything.
831
+ // The two knobs differ on purpose: the ledger IS on the release branch
832
+ // and is NOT on the trunk. A run that read the branch ref would pass
833
+ // here, which is the wrong answer — the branch is deleted and the merge
834
+ // is what decides whether the ledger shipped.
835
+ config: { pr_already_merged: true, ledger_on_branch: true, ledger_on_trunk: false },
836
+ expect_success: false,
837
+ verify: (w, error) => {
838
+ const f: string[] = [];
839
+ _expect(
840
+ (error ?? '').includes('already merged'),
841
+ `the stop did not name the merged state: ${error ?? '(no error)'}`,
842
+ f,
843
+ );
844
+ _expect(
845
+ w.calls.some((c) => c.startsWith('git cat-file -e ') && c.includes('/main:')),
846
+ 'the merged path never asked the trunk whether the ledger is there',
847
+ f,
848
+ );
849
+ _expect(!w.tag_remote, 'tagged a version whose findings ledger does not exist', f);
850
+ return f;
851
+ },
852
+ },
853
+ 'findings-ledger-undispositioned-stops-and-shows-why': {
854
+ summary:
855
+ 'step 7: the disposition gate refuses — the release stops and the operator is shown the gate report, not just its scan line',
856
+ config: {
857
+ ledger_on_branch: true,
858
+ disposition_verdict: {
859
+ status: 1,
860
+ stdout: 'scanned: 1',
861
+ stderr: '- `a1` (critical security: x): no disposition status',
862
+ },
863
+ },
864
+ expect_success: false,
865
+ verify: (w, error) => {
866
+ const f: string[] = [];
867
+ // The defect an independent review reproduced: the stop message
868
+ // preferred stdout, so the operator saw `scanned: 1` and the entire
869
+ // per-finding diagnosis — which the gate writes to stderr — was
870
+ // discarded on the one path where the message has to carry content.
871
+ _expect(
872
+ (error ?? '').includes('no disposition status'),
873
+ `the gate's own diagnosis was swallowed: ${error ?? '(no error)'}`,
874
+ f,
875
+ );
876
+ _expect(
877
+ !w.calls.includes(`gh pr merge ${w.branch} --merge --delete-branch`),
878
+ 'merged over an undispositioned ledger',
879
+ f,
880
+ );
881
+ return f;
882
+ },
883
+ },
518
884
  'plain-run-reuses-an-existing-branch': {
519
885
  summary:
520
886
  'step 1: a PLAIN run (no --resume) over an existing release branch checks it out instead of dying at `git checkout -b`',
@@ -573,6 +939,81 @@ const SCENARIOS: Record<string, Scenario> = {
573
939
  return f;
574
940
  },
575
941
  },
942
+ 'remote-branch-moved-merges-before-push': {
943
+ summary:
944
+ 'step 4: a release branch that diverged on the remote is merged in BEFORE the push — not after a rejection the local preflight never lets git produce',
945
+ // Measured 2026-09-12 on 16.0.0. `origin/release/16.0.0` carried five
946
+ // commits this checkout did not have. `push_release_branch` has a
947
+ // merge-and-retry for exactly that, keyed on git's rejection wording —
948
+ // but the local pre-push preflight refused first, with
949
+ // `check_pr_ci_current`'s own message naming the divergence. git never
950
+ // printed `[rejected]`, `_is_non_fast_forward` correctly answered
951
+ // "not the fetch-and-retry case" on the text it was given, and the
952
+ // recovery was unreachable from the state it was written for.
953
+ config: { branch_behind_remote: 5 },
954
+ expect_success: true,
955
+ verify: (w) => {
956
+ const f: string[] = [];
957
+ const merge = w.calls.indexOf(`git merge --no-edit origin/${w.branch}`);
958
+ const push = w.calls.indexOf(`git push -u origin ${w.branch}`);
959
+ _expect(merge >= 0, 'the diverged remote branch was never merged in', f);
960
+ _expect(push >= 0, 'the branch was never pushed', f);
961
+ // Ordering is the whole point: a merge only AFTER the push means
962
+ // the reactive path ran, and that path is what the gate pre-empts.
963
+ _expect(
964
+ merge >= 0 && push >= 0 && merge < push,
965
+ 'the remote branch was merged after the push, not before it',
966
+ f,
967
+ );
968
+ _expect(
969
+ _count(w, `git merge --no-edit origin/${w.branch}`) === 1,
970
+ 'merged the remote branch more than once — the probe is not re-read after the merge',
971
+ f,
972
+ );
973
+ return f;
974
+ },
975
+ },
976
+ 'remote-already-at-head-skips-the-push': {
977
+ summary:
978
+ 'step 4: a remote already carrying this commit is not pushed to again — the no-op push still runs the pre-push hook, and the hook refuses',
979
+ // The second half of the same 16.0.0 failure, and the one no rejection
980
+ // text can reach. The push had succeeded; a resumed run re-entered
981
+ // step 4 and pushed again. `git push -u` is idempotent on the network
982
+ // side, but git runs the pre-push hook BEFORE discovering there is
983
+ // nothing to send — so the preflight was handed the head this run had
984
+ // just pushed, found its CI still pending, and killed the release.
985
+ config: { branch_ahead_remote: 0, branch_behind_remote: 0 },
986
+ expect_success: true,
987
+ verify: (w) => {
988
+ const f: string[] = [];
989
+ _expect(
990
+ !w.calls.includes(`git push -u origin ${w.branch}`),
991
+ 'pushed a branch the remote already carries — that push runs the pre-push hook for nothing',
992
+ f,
993
+ );
994
+ return f;
995
+ },
996
+ },
997
+ 'unpushed-branch-still-pushes': {
998
+ summary:
999
+ 'step 4: a branch that was never pushed is pushed, and no distance is probed against a remote ref that does not exist',
1000
+ // The near-miss for the direction the new probe opens: `git rev-list
1001
+ // HEAD...origin/<branch>` against a missing ref is the 9.26.0 shape all
1002
+ // over again — an exit from the recovery masking the real state.
1003
+ // `--exit-code` on the ls-remote probe is what keeps it closed.
1004
+ config: { branch_exists_remote: false },
1005
+ expect_success: true,
1006
+ verify: (w) => {
1007
+ const f: string[] = [];
1008
+ _expect(w.calls.includes(`git push -u origin ${w.branch}`), 'the branch was never pushed', f);
1009
+ _expect(
1010
+ !w.calls.includes(`git rev-list --left-right --count HEAD...origin/${w.branch}`),
1011
+ 'probed the distance to a remote ref that does not exist',
1012
+ f,
1013
+ );
1014
+ return f;
1015
+ },
1016
+ },
576
1017
  'local-gates-refuse-before-push': {
577
1018
  summary:
578
1019
  'step 4: a red local release gate stops the run BEFORE the branch is pushed — no branch on origin, no PR, no CI',
@@ -664,7 +1105,7 @@ const SCENARIOS: Record<string, Scenario> = {
664
1105
  },
665
1106
  'marker-refuses-before-tag': {
666
1107
  summary:
667
- 'step 8: a draft marker in the merged section refuses BEFORE the annotated tag is created',
1108
+ 'step 9: a draft marker in the merged section refuses BEFORE the annotated tag is created',
668
1109
  config: {
669
1110
  pr_already_merged: true,
670
1111
  changelog_file: markedChangelogFixture(current_version()),
@@ -691,7 +1132,7 @@ const SCENARIOS: Record<string, Scenario> = {
691
1132
  },
692
1133
  'marker-refuses-resumed-tag-push': {
693
1134
  summary:
694
- 'step 8 (§ 3.2): a tag created but never pushed is NOT pushed when its section carries the marker',
1135
+ 'step 9 (§ 3.2): a tag created but never pushed is NOT pushed when its section carries the marker',
695
1136
  config: {
696
1137
  pr_already_merged: true,
697
1138
  tag_created_unpushed: true,
@@ -720,7 +1161,7 @@ const SCENARIOS: Record<string, Scenario> = {
720
1161
  },
721
1162
  'marker-refuses-github-release': {
722
1163
  summary:
723
- 'step 9: with the tag already pushed, a marker in the TAGGED section refuses before the Release is created',
1164
+ 'step 10: with the tag already pushed, a marker in the TAGGED section refuses before the Release is created',
724
1165
  config: {
725
1166
  pr_already_merged: true,
726
1167
  tag_already_remote: true,
@@ -793,7 +1234,7 @@ const SCENARIOS: Record<string, Scenario> = {
793
1234
  },
794
1235
  },
795
1236
  'behind-then-merge': {
796
- summary: 'step 7: head BEHIND main — update branch, re-run checks, merge (9.16.0)',
1237
+ summary: 'step 8: head BEHIND main — update branch, re-run checks, merge (9.16.0)',
797
1238
  config: { behind_probes: 1 },
798
1239
  expect_success: true,
799
1240
  verify: (w, _err) => {
@@ -813,7 +1254,7 @@ const SCENARIOS: Record<string, Scenario> = {
813
1254
  },
814
1255
  },
815
1256
  'merge-race-recovers': {
816
- summary: 'step 7: main moves between the CLEAN probe and the merge — retry, not crash',
1257
+ summary: 'step 8: main moves between the CLEAN probe and the merge — retry, not crash',
817
1258
  config: { merge_races_once: true },
818
1259
  expect_success: true,
819
1260
  verify: (w, _err) => {
@@ -828,7 +1269,7 @@ const SCENARIOS: Record<string, Scenario> = {
828
1269
  },
829
1270
  },
830
1271
  'behind-forever-dies': {
831
- summary: `step 7: BEHIND persists past ${_MERGE_UPDATE_ROUNDS} update rounds — die with the resume command`,
1272
+ summary: `step 8: BEHIND persists past ${_MERGE_UPDATE_ROUNDS} update rounds — die with the resume command`,
832
1273
  config: { behind_probes: 99 },
833
1274
  expect_success: false,
834
1275
  verify: (w, err) => {
@@ -845,7 +1286,7 @@ const SCENARIOS: Record<string, Scenario> = {
845
1286
  },
846
1287
  },
847
1288
  'merge-fails-hard-surfaces': {
848
- summary: 'step 7: a non-BEHIND merge failure surfaces unchanged instead of looping',
1289
+ summary: 'step 8: a non-BEHIND merge failure surfaces unchanged instead of looping',
849
1290
  config: { merge_fails_hard: true },
850
1291
  expect_success: false,
851
1292
  verify: (w, err) => {
@@ -905,10 +1346,14 @@ function run_scenario(name: string): ScenarioOutcome {
905
1346
  const tree_snapshot = _snapshot_step_2_files();
906
1347
  _set_exec_override((args) => world.exec(args));
907
1348
  // The working-tree read, faked for the same reason the command layer is.
908
- // Without this the drill's step 8 reads the repository's real CHANGELOG.md.
1349
+ // Without this the drill's step 9 reads the repository's real CHANGELOG.md.
909
1350
  _set_changelog_reader(() => world.changelog_file);
910
1351
  try {
911
- execute(plan, { wait_for_checks: true, dry_run: false, resume: scenario.config.resume ?? true });
1352
+ if (scenario.run !== undefined) {
1353
+ scenario.run();
1354
+ } else {
1355
+ execute(plan, { wait_for_checks: true, dry_run: false, resume: scenario.config.resume ?? true });
1356
+ }
912
1357
  } catch (err) {
913
1358
  if (err instanceof SystemExitError) {
914
1359
  error = `SystemExit(${err.code}): ${captured.join('')}`;
@@ -98,7 +98,7 @@ const CHANGELOG = path.join(REPO_ROOT, 'CHANGELOG.md');
98
98
  * Mirrors `_set_exec_override` in `release_publication.ts`, and exists for the
99
99
  * same reason: `execute()` reads `CHANGELOG.md` off disk at two points (the PR
100
100
  * body and the annotated-tag message), and the release drill fakes commands
101
- * but not the filesystem. Before this seam the drill's step 8 read the
101
+ * but not the filesystem. Before this seam the drill's step 9 read the
102
102
  * repository's REAL changelog — whose current section carries four
103
103
  * `_auto-derived, rewrite before merge:_` lines at `14.13.0` — so a
104
104
  * publication guard added at that call site refused every drill scenario for a