@event4u/agent-config 16.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 (246) 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 +297 -137
  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 +74 -27
  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 +47 -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/skills/accessibility-auditor/data/manifest.json +3 -3
  40. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  41. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  42. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  43. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  44. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  45. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  46. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  47. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  48. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  49. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  50. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  51. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  52. package/dist/agent-src/templates/agent-settings.md +1 -1
  53. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  54. package/dist/agent-src/templates/roadmaps.md +188 -0
  55. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  56. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  58. package/dist/cli-delegate/check_update_banner.js +40 -2
  59. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  60. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  61. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  62. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  63. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  64. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  65. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  66. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  67. package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
  68. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  69. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  70. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  71. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  72. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  73. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  74. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  75. package/dist/cli-delegate/cmd_conformance.js +90 -56
  76. package/dist/cli-delegate/cmd_doctor.js +7 -5
  77. package/dist/cli-delegate/cmd_explain.js +2 -2
  78. package/dist/cli-delegate/cmd_export.js +9 -7
  79. package/dist/cli-delegate/cmd_fleet.js +4 -2
  80. package/dist/cli-delegate/cmd_handoff.js +4 -4
  81. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  82. package/dist/cli-delegate/cmd_migrate.js +2 -2
  83. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  84. package/dist/cli-delegate/cmd_preflight.js +4 -2
  85. package/dist/cli-delegate/cmd_prune.js +10 -8
  86. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  87. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  88. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  89. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  90. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  91. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  92. package/dist/cli-delegate/cmd_sync.js +10 -8
  93. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  94. package/dist/cli-delegate/cmd_update.js +1 -1
  95. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  96. package/dist/cli-delegate/cmd_validate.js +12 -10
  97. package/dist/cli-delegate/cmd_versions.js +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +85 -38
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +4 -4
  102. package/dist/discovery/dormancy-report.md +1 -1
  103. package/dist/discovery/orphan-report.md +1 -1
  104. package/dist/discovery/packs.json +10 -8
  105. package/dist/discovery/trust-report.md +3 -3
  106. package/dist/discovery/workspaces.json +8 -6
  107. package/dist/hooks/dispatch.js +1832 -1007
  108. package/dist/install/claudeRuleRewrite.js +14 -3
  109. package/dist/install/conflict.js +60 -10
  110. package/dist/install/conflictTracker.js +218 -0
  111. package/dist/install/fsPrimitives.js +54 -0
  112. package/dist/install/install.mjs +2007 -1481
  113. package/dist/install/manifestFiles.js +259 -0
  114. package/dist/install/preflight.js +14 -2
  115. package/dist/install/preserve.js +157 -0
  116. package/dist/install/recordedOwnership.js +132 -0
  117. package/dist/install/txlog.js +11 -2
  118. package/dist/mcp/registry-manifest.json +3 -3
  119. package/dist/mcp/server.json +2 -2
  120. package/dist/mcp/server.mjs +398 -39
  121. package/dist/router.json +1 -1
  122. package/dist/scripts/_lib/agent_settings.js +3 -2
  123. package/dist/scripts/_lib/fs_atomic.js +167 -0
  124. package/dist/scripts/_lib/installed_tools.js +460 -0
  125. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  126. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  127. package/dist/server/routes/install.js +8 -0
  128. package/dist/server/routes/wizard.js +9 -3
  129. package/dist/server/schemas/settings.js +17 -5
  130. package/dist/shared/missionExecution.js +97 -0
  131. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  132. package/dist/ui/index.html +1 -1
  133. package/docs/MIGRATION.md +103 -0
  134. package/docs/contracts/settings-classes.md +26 -7
  135. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  136. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  137. package/docs/guidelines/design-fidelity-routing.md +29 -0
  138. package/package.json +1 -1
  139. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  140. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  141. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  142. package/src/config/agent-settings.template.yml +139 -27
  143. package/src/config/auto-rule-budget.json +16 -0
  144. package/src/config/ci-local-parity.yml +18 -5
  145. package/src/config/continuity-surface.json +21 -0
  146. package/src/config/gate-coverage.yml +146 -13
  147. package/src/config/gate-violation-baselines.json +6 -6
  148. package/src/config/hook-token-budget.json +2 -0
  149. package/src/config/loop-surfaces.yaml +215 -0
  150. package/src/config/platform-anchor.json +3 -2
  151. package/src/config/release-gate-locality.yml +11 -0
  152. package/src/config/routing-coverage-seed.json +18 -4
  153. package/src/config/rule-enforcement-baseline.json +0 -1
  154. package/src/install/claudeRuleRewrite.ts +16 -2
  155. package/src/install/conflict.ts +65 -9
  156. package/src/install/conflictTracker.ts +299 -0
  157. package/src/install/fsPrimitives.ts +54 -0
  158. package/src/install/manifestFiles.ts +312 -0
  159. package/src/install/preflight.ts +16 -2
  160. package/src/install/preserve.ts +205 -0
  161. package/src/install/recordedOwnership.ts +149 -0
  162. package/src/install/txlog.ts +11 -2
  163. package/src/install/types.ts +19 -4
  164. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  165. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  166. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  167. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  168. package/src/scripts/_cli/doctor_execution.ts +90 -0
  169. package/src/scripts/_lib/activation_payload.ts +247 -0
  170. package/src/scripts/_lib/agent_settings.ts +4 -2
  171. package/src/scripts/_lib/authority_path.ts +102 -0
  172. package/src/scripts/_lib/cascade_base.ts +94 -0
  173. package/src/scripts/_lib/changelog_eras.ts +7 -2
  174. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  175. package/src/scripts/_lib/council_transport.ts +129 -0
  176. package/src/scripts/_lib/delivery_ready.ts +112 -0
  177. package/src/scripts/_lib/forge_protection.ts +267 -0
  178. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  179. package/src/scripts/_lib/json_python_parity.ts +127 -0
  180. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  181. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  182. package/src/scripts/_lib/mission_record.ts +190 -0
  183. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  184. package/src/scripts/_lib/obligations.ts +402 -0
  185. package/src/scripts/_lib/orchestration_record.ts +11 -20
  186. package/src/scripts/_lib/release_highlights.ts +199 -10
  187. package/src/scripts/_lib/release_holds.ts +493 -0
  188. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  189. package/src/scripts/_lib/rides_along.ts +102 -0
  190. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  191. package/src/scripts/_lib/test_delta.ts +166 -0
  192. package/src/scripts/_lib/test_provenance.ts +171 -0
  193. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  194. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  195. package/src/scripts/ai_council/config.ts +8 -11
  196. package/src/scripts/ai_council/fallback_config.ts +5 -3
  197. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  198. package/src/scripts/ask_block_census.ts +285 -15
  199. package/src/scripts/check_enforcement_matrix.ts +624 -0
  200. package/src/scripts/check_gate_reachability.ts +173 -10
  201. package/src/scripts/check_generator_sync.ts +954 -0
  202. package/src/scripts/check_preamble_payload_budget.ts +75 -2
  203. package/src/scripts/check_release_highlights.ts +33 -1
  204. package/src/scripts/check_release_holds.ts +567 -0
  205. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  206. package/src/scripts/check_test_delta.ts +294 -0
  207. package/src/scripts/check_test_weakening.ts +299 -0
  208. package/src/scripts/closure_scan.ts +333 -0
  209. package/src/scripts/compile_hook_manifest.ts +43 -3
  210. package/src/scripts/council_record_shape.ts +209 -0
  211. package/src/scripts/hook_manifest.json +1 -1
  212. package/src/scripts/hook_manifest.yaml +88 -9
  213. package/src/scripts/hooks/concern_registry.ts +4 -0
  214. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  215. package/src/scripts/hooks/evidence_independence.ts +96 -0
  216. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  217. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  218. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  219. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  220. package/src/scripts/hooks_doctor.ts +85 -0
  221. package/src/scripts/hooks_status.ts +19 -0
  222. package/src/scripts/install.ts +106 -206
  223. package/src/scripts/janitor.ts +7 -0
  224. package/src/scripts/lint_decision_classes.ts +464 -0
  225. package/src/scripts/lint_hook_manifest.ts +11 -0
  226. package/src/scripts/lint_major_migration_sections.ts +450 -0
  227. package/src/scripts/lint_plan_risk_register.ts +47 -3
  228. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  229. package/src/scripts/lint_roadmap_producers.ts +202 -0
  230. package/src/scripts/measure_rule_budget.ts +101 -5
  231. package/src/scripts/new_roadmap.ts +37 -0
  232. package/src/scripts/orchestration_record.ts +0 -5
  233. package/src/scripts/release.ts +14 -98
  234. package/src/scripts/release_drill.ts +267 -1
  235. package/src/scripts/release_publication.ts +51 -0
  236. package/src/scripts/report_enforcement_drift.ts +566 -0
  237. package/src/scripts/report_host_injection_effect.ts +3 -11
  238. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  239. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  240. package/src/scripts/schemas/command.schema.json +4 -0
  241. package/src/scripts/scope_growth.ts +219 -0
  242. package/src/scripts/trigger_eval_grandfather.json +0 -1
  243. package/src/scripts/ui_conformance_probe.ts +622 -0
  244. package/src/server/schemas/settings.ts +31 -7
  245. package/src/shared/missionExecution.ts +126 -0
  246. 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.
@@ -319,6 +349,8 @@ class FakeWorld {
319
349
  private push_rejections: number;
320
350
  private behind_probes: number;
321
351
  private branch_behind_main: number;
352
+ private branch_behind_remote: number;
353
+ private readonly branch_ahead_remote: number;
322
354
  private merge_races_once: boolean;
323
355
  private merge_fails_hard: boolean;
324
356
  private checks_fail: boolean;
@@ -341,6 +373,9 @@ class FakeWorld {
341
373
  this.tag_remote = cfg.tag_already_remote ?? false;
342
374
  this.behind_probes = cfg.behind_probes ?? 0;
343
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;
344
379
  this.merge_races_once = cfg.merge_races_once ?? false;
345
380
  this.merge_fails_hard = cfg.merge_fails_hard ?? false;
346
381
  this.checks_fail = cfg.checks_fail ?? false;
@@ -375,6 +410,18 @@ class FakeWorld {
375
410
  this.branch_behind_main = 0;
376
411
  return OK;
377
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
+ }
378
425
  if (cmd === `git rev-parse --verify --quiet refs/heads/${this.branch}`) {
379
426
  return this.pr_merged ? { ...OK, status: 1 } : OK;
380
427
  }
@@ -544,6 +591,16 @@ interface Scenario {
544
591
  /** What the scenario proves — printed by the CLI. */
545
592
  summary: string;
546
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;
547
604
  /** Whether execute() is expected to complete (vs die/throw). */
548
605
  expect_success: boolean;
549
606
  /** Extra assertions over the finished world; return failure strings. */
@@ -560,7 +617,137 @@ function _count(world: FakeWorld, needle: string): number {
560
617
  return world.calls.filter((c) => c === needle).length;
561
618
  }
562
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
+
563
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
+
564
751
  'findings-ledger-verified-on-the-remote-branch': {
565
752
  summary:
566
753
  'step 7: the ledger is on the remote branch and the gate passes — the release continues without producing anything',
@@ -752,6 +939,81 @@ const SCENARIOS: Record<string, Scenario> = {
752
939
  return f;
753
940
  },
754
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
+ },
755
1017
  'local-gates-refuse-before-push': {
756
1018
  summary:
757
1019
  'step 4: a red local release gate stops the run BEFORE the branch is pushed — no branch on origin, no PR, no CI',
@@ -1087,7 +1349,11 @@ function run_scenario(name: string): ScenarioOutcome {
1087
1349
  // Without this the drill's step 9 reads the repository's real CHANGELOG.md.
1088
1350
  _set_changelog_reader(() => world.changelog_file);
1089
1351
  try {
1090
- 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
+ }
1091
1357
  } catch (err) {
1092
1358
  if (err instanceof SystemExitError) {
1093
1359
  error = `SystemExit(${err.code}): ${captured.join('')}`;
@@ -745,8 +745,59 @@ export function guard_release_curation(version: string, prMerged = false): void
745
745
  );
746
746
  }
747
747
 
748
+ /**
749
+ * What `REMOTE/<branch>` holds relative to HEAD, before anything is pushed.
750
+ *
751
+ * `push_release_branch` used to issue the push and learn the remote's position
752
+ * only from the rejection that came back. That reading is unavailable in two
753
+ * measured states, both on 16.0.0 (2026-09-12): the local pre-push gate refuses
754
+ * a diverged branch before git can print `[rejected]`, and a remote already AT
755
+ * head produces no rejection at all — git runs the pre-push hook before it
756
+ * discovers there is nothing to send, so the hook refuses over CI still pending
757
+ * on the head this run had already pushed. Asking first is the only place both
758
+ * are visible.
759
+ */
760
+ type RemoteBranchState = 'absent' | 'in-sync' | 'remote-ahead' | 'local-ahead';
761
+
762
+ function _remote_branch_state(branch: string): RemoteBranchState {
763
+ // `--exit-code` so a never-pushed branch reads as a clean non-zero rather
764
+ // than an empty success the count below would then run against a missing
765
+ // ref — the exit-128 masking this file already carries a test for.
766
+ const exists = run(['git', 'ls-remote', '--exit-code', '--heads', REMOTE, branch], {
767
+ check: false,
768
+ capture: true,
769
+ });
770
+ if (exists.returncode !== 0) return 'absent';
771
+ run(['git', 'fetch', REMOTE, branch], { check: false, capture: true });
772
+ const counts = run(['git', 'rev-list', '--left-right', '--count', `HEAD...${REMOTE}/${branch}`], {
773
+ check: false,
774
+ capture: true,
775
+ });
776
+ // Unreadable → push and let git judge, which is the pre-2026-09-12 behaviour.
777
+ if (counts.returncode !== 0) return 'local-ahead';
778
+ const [ahead = '0', behind = '0'] = counts.stdout.trim().split(/\s+/);
779
+ if (Number(behind) > 0) return 'remote-ahead';
780
+ return Number(ahead) > 0 ? 'local-ahead' : 'in-sync';
781
+ }
782
+
748
783
  function push_release_branch(branch: string): void {
784
+ let state = _remote_branch_state(branch);
785
+ if (state === 'remote-ahead') {
786
+ process.stdout.write(`↻ ${REMOTE}/${branch} moved under us — merging it in before the push\n`);
787
+ run(['git', 'merge', '--no-edit', `${REMOTE}/${branch}`]);
788
+ // Re-read: a fast-forward merge lands HEAD exactly on the remote, and
789
+ // the push that followed would then be the no-op case below.
790
+ state = _remote_branch_state(branch);
791
+ }
792
+
749
793
  guard_release_branch_push(branch);
794
+
795
+ // Ahead of the skip on purpose: the guard validates the release content,
796
+ // which has to be right whether or not this run is the one that sends it.
797
+ if (state === 'in-sync') {
798
+ process.stdout.write(`✅ ${REMOTE}/${branch} already carries this commit — nothing to push\n`);
799
+ return;
800
+ }
750
801
  const first = run(['git', 'push', '-u', REMOTE, branch], { check: false, capture: true });
751
802
  if (first.returncode === 0) {
752
803
  process.stdout.write(first.stdout);