@event4u/agent-config 9.16.0 → 9.18.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +80 -130
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/agent-handoff.md +100 -48
  6. package/dist/agent-src/commands/challenge-me/vision.md +5 -0
  7. package/dist/agent-src/commands/commit.md +1 -1
  8. package/dist/agent-src/commands/condense.md +19 -64
  9. package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
  10. package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
  11. package/dist/agent-src/commands/feature/plan.md +20 -0
  12. package/dist/agent-src/commands/feature/roadmap.md +19 -0
  13. package/dist/agent-src/commands/memory/promote.md +14 -1
  14. package/dist/agent-src/commands/mode.md +9 -5
  15. package/dist/agent-src/commands/optimize/deep.md +72 -11
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/roadmap/create.md +29 -0
  18. package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
  19. package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
  20. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  21. package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
  22. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
  23. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
  24. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
  25. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
  26. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
  27. package/dist/agent-src/profiles/agency.yml +3 -3
  28. package/dist/agent-src/profiles/content_creator.yml +2 -2
  29. package/dist/agent-src/profiles/developer.yml +2 -2
  30. package/dist/agent-src/profiles/finance.yml +1 -1
  31. package/dist/agent-src/profiles/founder.yml +2 -2
  32. package/dist/agent-src/profiles/ops.yml +2 -2
  33. package/dist/agent-src/rules/active-remediation.md +2 -0
  34. package/dist/agent-src/rules/architecture.md +4 -2
  35. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  36. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  37. package/dist/agent-src/rules/brand-consistency.md +10 -62
  38. package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
  39. package/dist/agent-src/rules/broken-access-control.md +4 -0
  40. package/dist/agent-src/rules/cli-output-handling.md +5 -0
  41. package/dist/agent-src/rules/code-comment-discipline.md +3 -0
  42. package/dist/agent-src/rules/commit-conventions.md +2 -0
  43. package/dist/agent-src/rules/context-hygiene.md +3 -2
  44. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  45. package/dist/agent-src/rules/design-fidelity.md +2 -0
  46. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  47. package/dist/agent-src/rules/docker-commands.md +4 -0
  48. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  49. package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
  50. package/dist/agent-src/rules/domain-safety-pii.md +1 -1
  51. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  52. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +4 -0
  55. package/dist/agent-src/rules/laravel-routing.md +4 -0
  56. package/dist/agent-src/rules/legal-safety-floor.md +3 -0
  57. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  58. package/dist/agent-src/rules/media-governance-routing.md +5 -0
  59. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  60. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  61. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  62. package/dist/agent-src/rules/output-discipline.md +9 -2
  63. package/dist/agent-src/rules/php-coding.md +2 -0
  64. package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
  65. package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
  66. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
  67. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  68. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  69. package/dist/agent-src/rules/scale-discipline.md +3 -0
  70. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  71. package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
  72. package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
  73. package/dist/agent-src/rules/skill-quality.md +2 -0
  74. package/dist/agent-src/rules/source-confidentiality.md +4 -0
  75. package/dist/agent-src/rules/source-discovery-gate.md +3 -0
  76. package/dist/agent-src/rules/source-of-truth.md +2 -0
  77. package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
  78. package/dist/agent-src/rules/think-before-action.md +5 -0
  79. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  80. package/dist/agent-src/rules/ui-audit-gate.md +2 -0
  81. package/dist/agent-src/rules/user-interaction.md +3 -4
  82. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  83. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
  84. package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
  85. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
  86. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  87. package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
  88. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
  89. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
  90. package/dist/agent-src/skills/design-review/SKILL.md +4 -141
  91. package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
  92. package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
  93. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
  94. package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
  95. package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
  96. package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
  97. package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
  98. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  99. package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
  100. package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
  101. package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
  102. package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
  103. package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
  104. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  105. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  106. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  107. package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
  108. package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
  109. package/dist/agent-src/templates/agent-settings.md +13 -15
  110. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
  111. package/dist/agent-src/templates/persona.md +2 -2
  112. package/dist/agent-src/templates/roadmaps.md +13 -1
  113. package/dist/agent-src/templates/rule.md +1 -1
  114. package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  115. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  116. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  117. package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  118. package/dist/agent-src/templates/skill.md +1 -1
  119. package/dist/cli/registry.js +3 -0
  120. package/dist/cli/registry.js.map +1 -1
  121. package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
  122. package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
  123. package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
  124. package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
  125. package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
  126. package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
  127. package/dist/cli-delegate/cmd_conformance.js +4 -3
  128. package/dist/cli-delegate/cmd_converge.js +3 -3
  129. package/dist/cli-delegate/cmd_doctor.js +3 -2
  130. package/dist/cli-delegate/cmd_explain.js +36 -36
  131. package/dist/cli-delegate/cmd_export.js +4 -4
  132. package/dist/cli-delegate/cmd_handoff.js +1169 -0
  133. package/dist/cli-delegate/cmd_memory_get.js +35 -3754
  134. package/dist/cli-delegate/cmd_migrate.js +2 -2
  135. package/dist/cli-delegate/cmd_prune.js +4 -4
  136. package/dist/cli-delegate/cmd_route_audit.js +249 -0
  137. package/dist/cli-delegate/cmd_route_explain.js +285 -0
  138. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  139. package/dist/cli-delegate/cmd_sync.js +4 -4
  140. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  141. package/dist/cli-delegate/cmd_update.js +3 -3
  142. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  143. package/dist/cli-delegate/cmd_validate.js +4 -4
  144. package/dist/cli-delegate/cmd_versions.js +3 -3
  145. package/dist/discovery/deprecation-report.md +1 -1
  146. package/dist/discovery/discovery-manifest.json +84 -84
  147. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  148. package/dist/discovery/discovery-manifest.summary.md +1 -1
  149. package/dist/discovery/orphan-report.md +1 -1
  150. package/dist/discovery/packs.json +2 -2
  151. package/dist/discovery/trust-report.md +1 -1
  152. package/dist/discovery/workspaces.json +2 -2
  153. package/dist/hooks/dispatch.js +602 -507
  154. package/dist/install/install.mjs +34 -0
  155. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  156. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  157. package/dist/mcp/registry-manifest.json +4 -4
  158. package/dist/mcp/server.json +2 -2
  159. package/dist/mcp/server.mjs +25 -3
  160. package/dist/router.json +1 -1
  161. package/dist/scripts/_lib/agent_src.js +39 -6
  162. package/dist/scripts/_lib/agent_src.js.map +1 -1
  163. package/dist/server/schemas/settings.js +25 -0
  164. package/dist/server/schemas/settings.js.map +1 -1
  165. package/docs/guidelines/abstraction-thresholds.md +53 -0
  166. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
  167. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
  168. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
  169. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
  170. package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
  171. package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
  172. package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
  173. package/llms.txt +1 -0
  174. package/package.json +1 -1
  175. package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  176. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  177. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  178. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  179. package/src/config/agent-settings.template.yml +68 -0
  180. package/src/config/evaluator-budgets.json +8 -11
  181. package/src/config/gate-coverage.yml +70 -0
  182. package/src/scripts/_cli/cmd_explain.ts +41 -36
  183. package/src/scripts/_cli/cmd_handoff.ts +405 -0
  184. package/src/scripts/_cli/cmd_route_audit.ts +290 -0
  185. package/src/scripts/_cli/cmd_route_explain.ts +269 -0
  186. package/src/scripts/_cli/handoff_generate.ts +453 -0
  187. package/src/scripts/_cli/handoff_sessions.ts +435 -0
  188. package/src/scripts/_dispatch.bash +32 -0
  189. package/src/scripts/_lib/agent_src.ts +36 -6
  190. package/src/scripts/_lib/git_env.ts +40 -0
  191. package/src/scripts/_lib/md_table.ts +27 -0
  192. package/src/scripts/_lib/planning_settings.ts +43 -0
  193. package/src/scripts/_lib/release_material.ts +156 -0
  194. package/src/scripts/_lib/router_match.ts +301 -0
  195. package/src/scripts/annotate_r1_outcomes.ts +258 -0
  196. package/src/scripts/check_completion_review.ts +1140 -0
  197. package/src/scripts/check_condensation.ts +2 -1
  198. package/src/scripts/check_finding_dispositions.ts +278 -0
  199. package/src/scripts/check_pack_size.ts +4 -3
  200. package/src/scripts/check_references.ts +1 -0
  201. package/src/scripts/check_release_highlights.ts +300 -0
  202. package/src/scripts/check_release_pr_shape.ts +4 -0
  203. package/src/scripts/check_release_surface_equality.ts +132 -0
  204. package/src/scripts/check_reply_consistency.ts +187 -138
  205. package/src/scripts/compile_router.ts +20 -3
  206. package/src/scripts/dispatch_r2_reviewer.ts +968 -0
  207. package/src/scripts/eval_ui_triviality.ts +156 -0
  208. package/src/scripts/handoff_context_hook.ts +148 -0
  209. package/src/scripts/hook_manifest.yaml +17 -7
  210. package/src/scripts/hooks/concern_registry.ts +2 -0
  211. package/src/scripts/lint_abstraction_thresholds.ts +327 -0
  212. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  213. package/src/scripts/lint_plan_risk_register.ts +931 -0
  214. package/src/scripts/lint_profile_personas.ts +153 -0
  215. package/src/scripts/lint_token_budget_discipline.ts +148 -0
  216. package/src/scripts/lint_trigger_collisions.ts +257 -0
  217. package/src/scripts/release.ts +156 -32
  218. package/src/scripts/release_drill.ts +491 -0
  219. package/src/scripts/router_telemetry.ts +16 -290
  220. package/src/scripts/schemas/command.schema.json +33 -0
  221. package/src/scripts/schemas/rule.schema.json +10 -0
  222. package/src/scripts/self_review_gate.ts +41 -3
  223. package/src/scripts/skill_linter.ts +3 -1
  224. package/src/scripts/smoke/router.sh +1 -1
  225. package/src/scripts/update_counts.ts +60 -12
  226. package/src/server/schemas/settings.ts +41 -0
  227. package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Release-orchestration drill — execute() against a simulated git/gh world.
4
+ *
5
+ * `task release` step machinery (checkout → bump → commit → push → PR →
6
+ * checks → merge → tag → GitHub Release → branch cleanup) historically ran
7
+ * NOWHERE except live: `--dry-run` returns before execute(), and the unit
8
+ * tests never call it. Every orchestration bug therefore fired mid-release —
9
+ * three times in one week (2026-08-03: push rejected after a remote move,
10
+ * merge dying on a BEHIND head, gh resolving the PR from the wrong HEAD).
11
+ *
12
+ * This drill closes that hole. It installs release.ts's exec seam
13
+ * (`_set_exec_override`) so every external command is answered by a scripted
14
+ * FakeWorld, then runs the REAL execute() through named scenarios — the happy
15
+ * path plus each measured failure mode. run()'s check/die/CalledProcessError
16
+ * semantics apply to the simulated results, so the drill exercises the real
17
+ * error paths, not a parallel reimplementation.
18
+ *
19
+ * Surfaces:
20
+ * - vitest: tests/scripts/release_drill.test.ts runs every scenario in CI.
21
+ * - operator: `task release-drill` (or `./scripts-run src/scripts/release_drill`)
22
+ * runs them all and prints one ✅/❌ line per scenario.
23
+ *
24
+ * The drill never touches git, gh, npm, or the filesystem outside reading
25
+ * package.json (execute()'s own resume-mode bump-skip probe).
26
+ */
27
+
28
+ import * as fs from 'node:fs';
29
+ import * as path from 'node:path';
30
+ import { fileURLToPath, pathToFileURL } from 'node:url';
31
+
32
+ import {
33
+ _MERGE_UPDATE_ROUNDS,
34
+ _set_exec_override,
35
+ execute,
36
+ Plan,
37
+ SystemExitError,
38
+ } from './release.js';
39
+
40
+ const _HERE = fileURLToPath(import.meta.url);
41
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
42
+
43
+ /** execute()'s bump-skip probe reads the real package.json — mirror it. */
44
+ function current_version(): string {
45
+ const pkg = JSON.parse(
46
+ fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8'),
47
+ ) as Record<string, unknown>;
48
+ return String(pkg['version']);
49
+ }
50
+
51
+ interface ExecResult {
52
+ status: number;
53
+ stdout: string;
54
+ stderr: string;
55
+ }
56
+
57
+ const OK: ExecResult = { status: 0, stdout: '', stderr: '' };
58
+
59
+ /** Knobs a scenario turns to inject the measured failure modes. */
60
+ interface WorldConfig {
61
+ /** `git push -u` is rejected this many times before succeeding (9.15.0). */
62
+ push_rejections?: number;
63
+ /** mergeStateStatus reports BEHIND this many probes in a row (9.16.0). */
64
+ behind_probes?: number;
65
+ /** `gh pr merge` fails once with the not-up-to-date error (the race). */
66
+ merge_races_once?: boolean;
67
+ /** `gh pr merge` fails hard with an unrelated error (must surface). */
68
+ merge_fails_hard?: boolean;
69
+ /** PR checks fail (watch exits non-zero) — the release must die. */
70
+ checks_fail?: boolean;
71
+ }
72
+
73
+ /**
74
+ * A scriptable stand-in for every git/gh/task command execute() issues.
75
+ * Stateful where the real world is: checkouts move HEAD, `git tag` makes the
76
+ * tag exist, a successful merge flips the PR to MERGED and deletes the branch.
77
+ */
78
+ class FakeWorld {
79
+ readonly calls: string[] = [];
80
+ head = 'main';
81
+ tag_local = false;
82
+ tag_remote = false;
83
+ pr_merged = false;
84
+ release_created = false;
85
+ branch_exists_remote = true;
86
+ /** Live PR body (release-truth Phase 1: refreshed from the changelog at head). */
87
+ pr_body = '';
88
+
89
+ private push_rejections: number;
90
+ private behind_probes: number;
91
+ private merge_races_once: boolean;
92
+ private merge_fails_hard: boolean;
93
+ private checks_fail: boolean;
94
+
95
+ constructor(
96
+ readonly branch: string,
97
+ readonly target: string,
98
+ cfg: WorldConfig,
99
+ ) {
100
+ this.push_rejections = cfg.push_rejections ?? 0;
101
+ this.behind_probes = cfg.behind_probes ?? 0;
102
+ this.merge_races_once = cfg.merge_races_once ?? false;
103
+ this.merge_fails_hard = cfg.merge_fails_hard ?? false;
104
+ this.checks_fail = cfg.checks_fail ?? false;
105
+ }
106
+
107
+ exec(args: readonly string[]): ExecResult {
108
+ const cmd = args.join(' ');
109
+ this.calls.push(cmd);
110
+
111
+ // ── git ──────────────────────────────────────────────────────────
112
+ if (cmd === 'git rev-parse --abbrev-ref HEAD') {
113
+ return { ...OK, stdout: `${this.head}\n` };
114
+ }
115
+ if (args[0] === 'git' && args[1] === 'checkout') {
116
+ this.head = args[2] === '-b' ? (args[3] as string) : (args[2] as string);
117
+ return OK;
118
+ }
119
+ if (cmd === `git rev-parse --verify --quiet refs/heads/${this.branch}`) {
120
+ return this.pr_merged ? { ...OK, status: 1 } : OK;
121
+ }
122
+ if (cmd === `git ls-remote --exit-code --heads origin ${this.branch}`) {
123
+ return this.branch_exists_remote ? OK : { ...OK, status: 1 };
124
+ }
125
+ if (cmd === `git ls-remote --exit-code --tags origin ${this.target}`) {
126
+ return this.tag_remote ? OK : { ...OK, status: 1 };
127
+ }
128
+ if (cmd === `git tag -l ${this.target}`) {
129
+ return { ...OK, stdout: this.tag_local ? `${this.target}\n` : '' };
130
+ }
131
+ if (cmd === `git tag ${this.target}` || (args[1] === 'tag' && args[2] === '-a' && args[3] === this.target)) {
132
+ // release-truth Phase 1: the tag is annotated, message derived
133
+ // from the merged changelog section.
134
+ this.tag_local = true;
135
+ return OK;
136
+ }
137
+ if (cmd === `git show ${this.target}:CHANGELOG.md`) {
138
+ // The drill runs in the real repo — the changelog at the tag is
139
+ // the real file (execute() extracts the target's section from it).
140
+ return { ...OK, stdout: fs.readFileSync(path.join(REPO_ROOT, 'CHANGELOG.md'), 'utf-8') };
141
+ }
142
+ if (cmd === `git push origin ${this.target}`) {
143
+ this.tag_remote = true;
144
+ return OK;
145
+ }
146
+ if (cmd === `git push -u origin ${this.branch}`) {
147
+ if (this.push_rejections > 0) {
148
+ this.push_rejections -= 1;
149
+ return {
150
+ status: 1,
151
+ stdout: '',
152
+ stderr: `! [rejected] ${this.branch} -> ${this.branch} (fetch first)`,
153
+ };
154
+ }
155
+ return OK;
156
+ }
157
+ if (cmd === 'git log -1 --format=%s') {
158
+ return { ...OK, stdout: `release: ${this.target}\n` };
159
+ }
160
+ if (cmd === 'git status --porcelain') {
161
+ return OK; // clean tree — resume mode, everything committed
162
+ }
163
+ if (cmd === 'git diff --cached --name-only') {
164
+ return OK;
165
+ }
166
+ if (args[0] === 'git' && ['fetch', 'merge', 'pull', 'add', 'branch', 'push'].includes(args[1] as string)) {
167
+ return OK;
168
+ }
169
+
170
+ // ── task ─────────────────────────────────────────────────────────
171
+ if (args[0] === 'task') {
172
+ return OK; // release-prepare — regeneration is out of drill scope
173
+ }
174
+
175
+ // ── gh ───────────────────────────────────────────────────────────
176
+ if (cmd.startsWith(`gh pr list --head ${this.branch}`)) {
177
+ const state = this.pr_merged ? 'MERGED' : 'OPEN';
178
+ return {
179
+ ...OK,
180
+ stdout: JSON.stringify([{ number: 999, state, url: 'https://example.invalid/pr/999' }]),
181
+ };
182
+ }
183
+ if (cmd === `gh pr checks ${this.branch} --watch`) {
184
+ return this.checks_fail
185
+ ? { status: 1, stdout: 'some-required-check\tfail\t10s\n', stderr: '' }
186
+ : { ...OK, stdout: 'all checks pass\n' };
187
+ }
188
+ if (cmd === `gh pr checks ${this.branch} --json name,bucket`) {
189
+ return {
190
+ ...OK,
191
+ stdout: this.checks_fail
192
+ ? JSON.stringify([{ name: 'some-required-check', bucket: 'fail' }])
193
+ : '[]',
194
+ };
195
+ }
196
+ if (cmd === `gh pr view ${this.branch} --json mergeStateStatus --jq .mergeStateStatus`) {
197
+ if (this.behind_probes > 0) {
198
+ this.behind_probes -= 1;
199
+ return { ...OK, stdout: 'BEHIND\n' };
200
+ }
201
+ return { ...OK, stdout: 'CLEAN\n' };
202
+ }
203
+ if (cmd === `gh pr merge ${this.branch} --merge --delete-branch`) {
204
+ if (this.merge_fails_hard) {
205
+ return { status: 1, stdout: '', stderr: 'GraphQL: Base branch was modified (unexpected)' };
206
+ }
207
+ if (this.merge_races_once) {
208
+ this.merge_races_once = false;
209
+ // The race: main moved between the CLEAN probe and the merge.
210
+ this.behind_probes += 1;
211
+ return {
212
+ status: 1,
213
+ stdout: '',
214
+ stderr: 'X Pull request #999 is not mergeable: the head branch is not up to date with the base branch.',
215
+ };
216
+ }
217
+ this.pr_merged = true;
218
+ this.branch_exists_remote = false;
219
+ this.head = 'main'; // gh checks out the default branch after deleting
220
+ return OK;
221
+ }
222
+ if (cmd === `gh pr view ${this.branch} --json body -q .body`) {
223
+ // release-truth Phase 1: _refresh_pr_body_from_head probes the
224
+ // live body before deciding whether to edit.
225
+ return { ...OK, stdout: this.pr_body };
226
+ }
227
+ if (args[0] === 'gh' && args[1] === 'pr' && args[2] === 'edit' && args[3] === this.branch) {
228
+ const bodyIdx = args.indexOf('--body');
229
+ this.pr_body = bodyIdx >= 0 ? String(args[bodyIdx + 1] ?? '') : this.pr_body;
230
+ return OK;
231
+ }
232
+ if (cmd === `gh release view ${this.target}`) {
233
+ return this.release_created ? OK : { ...OK, status: 1 };
234
+ }
235
+ if (cmd.startsWith(`gh release create ${this.target}`)) {
236
+ this.release_created = true;
237
+ return OK;
238
+ }
239
+
240
+ throw new Error(`FakeWorld: unscripted command: ${cmd}`);
241
+ }
242
+ }
243
+
244
+ interface ScenarioOutcome {
245
+ ok: boolean;
246
+ failures: string[];
247
+ world: FakeWorld;
248
+ error: string | null;
249
+ }
250
+
251
+ interface Scenario {
252
+ /** What the scenario proves — printed by the CLI. */
253
+ summary: string;
254
+ config: WorldConfig;
255
+ /** Whether execute() is expected to complete (vs die/throw). */
256
+ expect_success: boolean;
257
+ /** Extra assertions over the finished world; return failure strings. */
258
+ verify: (world: FakeWorld, error: string | null) => string[];
259
+ }
260
+
261
+ function _expect(cond: boolean, msg: string, failures: string[]): void {
262
+ if (!cond) {
263
+ failures.push(msg);
264
+ }
265
+ }
266
+
267
+ function _count(world: FakeWorld, needle: string): number {
268
+ return world.calls.filter((c) => c === needle).length;
269
+ }
270
+
271
+ const SCENARIOS: Record<string, Scenario> = {
272
+ 'happy-resume': {
273
+ summary: 'resume with everything green runs checkout → checks → merge → tag → release',
274
+ config: {},
275
+ expect_success: true,
276
+ verify: (w, _err) => {
277
+ const f: string[] = [];
278
+ _expect(w.pr_merged, 'PR was never merged', f);
279
+ _expect(w.tag_remote, 'tag never pushed', f);
280
+ _expect(w.release_created, 'GitHub Release never created', f);
281
+ const merge = w.calls.indexOf(`gh pr merge ${w.branch} --merge --delete-branch`);
282
+ const watch = w.calls.indexOf(`gh pr checks ${w.branch} --watch`);
283
+ _expect(watch >= 0 && merge > watch, 'merge did not wait for checks', f);
284
+ return f;
285
+ },
286
+ },
287
+ 'push-rejected-then-recover': {
288
+ summary: 'step 4: remote moved under the run — integrate + retry instead of dying (9.15.0)',
289
+ config: { push_rejections: 1 },
290
+ expect_success: true,
291
+ verify: (w, _err) => {
292
+ const f: string[] = [];
293
+ _expect(
294
+ _count(w, `git push -u origin ${w.branch}`) === 2,
295
+ 'push was not retried exactly once',
296
+ f,
297
+ );
298
+ _expect(
299
+ w.calls.includes(`git fetch origin ${w.branch}`) &&
300
+ w.calls.includes(`git merge --no-edit origin/${w.branch}`),
301
+ 'rejected push did not integrate the moved remote',
302
+ f,
303
+ );
304
+ _expect(w.pr_merged && w.release_created, 'release did not complete after recovery', f);
305
+ return f;
306
+ },
307
+ },
308
+ 'behind-then-merge': {
309
+ summary: 'step 7: head BEHIND main — update branch, re-run checks, merge (9.16.0)',
310
+ config: { behind_probes: 1 },
311
+ expect_success: true,
312
+ verify: (w, _err) => {
313
+ const f: string[] = [];
314
+ _expect(
315
+ w.calls.includes('git merge --no-edit origin/main'),
316
+ 'BEHIND head was never updated with main',
317
+ f,
318
+ );
319
+ _expect(
320
+ _count(w, `gh pr checks ${w.branch} --watch`) >= 2,
321
+ 'checks were not re-run after the update',
322
+ f,
323
+ );
324
+ _expect(w.pr_merged && w.release_created, 'release did not complete after the update', f);
325
+ return f;
326
+ },
327
+ },
328
+ 'merge-race-recovers': {
329
+ summary: 'step 7: main moves between the CLEAN probe and the merge — retry, not crash',
330
+ config: { merge_races_once: true },
331
+ expect_success: true,
332
+ verify: (w, _err) => {
333
+ const f: string[] = [];
334
+ _expect(
335
+ _count(w, `gh pr merge ${w.branch} --merge --delete-branch`) === 2,
336
+ 'raced merge was not retried exactly once',
337
+ f,
338
+ );
339
+ _expect(w.pr_merged && w.release_created, 'release did not complete after the race', f);
340
+ return f;
341
+ },
342
+ },
343
+ 'behind-forever-dies': {
344
+ summary: `step 7: BEHIND persists past ${_MERGE_UPDATE_ROUNDS} update rounds — die with the resume command`,
345
+ config: { behind_probes: 99 },
346
+ expect_success: false,
347
+ verify: (w, err) => {
348
+ const f: string[] = [];
349
+ _expect(err !== null && err.includes('task release -- --resume --yes'),
350
+ 'death message does not name the resume command', f);
351
+ _expect(
352
+ _count(w, 'git merge --no-edit origin/main') === _MERGE_UPDATE_ROUNDS,
353
+ `expected exactly ${_MERGE_UPDATE_ROUNDS} update rounds`,
354
+ f,
355
+ );
356
+ _expect(!w.pr_merged, 'PR must not be merged in this scenario', f);
357
+ return f;
358
+ },
359
+ },
360
+ 'merge-fails-hard-surfaces': {
361
+ summary: 'step 7: a non-BEHIND merge failure surfaces unchanged instead of looping',
362
+ config: { merge_fails_hard: true },
363
+ expect_success: false,
364
+ verify: (w, err) => {
365
+ const f: string[] = [];
366
+ _expect(err !== null && err.includes('gh pr merge'), 'error does not name the merge command', f);
367
+ _expect(
368
+ _count(w, `gh pr merge ${w.branch} --merge --delete-branch`) === 1,
369
+ 'hard merge failure must not be retried',
370
+ f,
371
+ );
372
+ return f;
373
+ },
374
+ },
375
+ 'checks-fail-dies': {
376
+ summary: 'step 6: failing PR checks kill the run before any merge',
377
+ config: { checks_fail: true },
378
+ expect_success: false,
379
+ verify: (w, err) => {
380
+ const f: string[] = [];
381
+ _expect(err !== null && err.includes('PR checks failed'), 'death does not name failing checks', f);
382
+ _expect(
383
+ !w.calls.some((c) => c.startsWith(`gh pr merge`)),
384
+ 'merge must never run after failing checks',
385
+ f,
386
+ );
387
+ return f;
388
+ },
389
+ },
390
+ };
391
+
392
+ /** Run one scenario through the real execute(); never touches the world. */
393
+ function run_scenario(name: string): ScenarioOutcome {
394
+ const scenario = SCENARIOS[name];
395
+ if (scenario === undefined) {
396
+ throw new Error(`unknown scenario: ${name}`);
397
+ }
398
+ const target = current_version();
399
+ const branch = `release/${target}`;
400
+ const world = new FakeWorld(branch, target, scenario.config);
401
+ const plan = new Plan(target, target, 'patch', [], null, `Release ${target}.`, `## ${target}\n`, null);
402
+
403
+ let error: string | null = null;
404
+ // Capture the run's own output: die() writes its message to stderr before
405
+ // throwing a bare SystemExitError, and the step lines on stdout are noise
406
+ // when seven scenarios run back to back.
407
+ const captured: string[] = [];
408
+ const orig_out = process.stdout.write.bind(process.stdout);
409
+ const orig_err = process.stderr.write.bind(process.stderr);
410
+ (process.stdout as { write: unknown }).write = (s: string): boolean => {
411
+ captured.push(String(s));
412
+ return true;
413
+ };
414
+ (process.stderr as { write: unknown }).write = (s: string): boolean => {
415
+ captured.push(String(s));
416
+ return true;
417
+ };
418
+ _set_exec_override((args) => world.exec(args));
419
+ try {
420
+ execute(plan, { wait_for_checks: true, dry_run: false, resume: true });
421
+ } catch (err) {
422
+ if (err instanceof SystemExitError) {
423
+ error = `SystemExit(${err.code}): ${captured.join('')}`;
424
+ } else {
425
+ error = err instanceof Error ? err.message : String(err);
426
+ }
427
+ } finally {
428
+ _set_exec_override(null);
429
+ process.stdout.write = orig_out;
430
+ process.stderr.write = orig_err;
431
+ }
432
+
433
+ const failures: string[] = [];
434
+ if (scenario.expect_success && error !== null) {
435
+ failures.push(`expected success, got: ${error}`);
436
+ }
437
+ if (!scenario.expect_success && error === null) {
438
+ failures.push('expected the run to die, but it completed');
439
+ }
440
+ failures.push(...scenario.verify(world, error));
441
+ return { ok: failures.length === 0, failures, world, error };
442
+ }
443
+
444
+ function main(): number {
445
+ let failed = 0;
446
+ const names = Object.keys(SCENARIOS);
447
+ process.stdout.write(`release drill — ${names.length} scenario(s), simulated git/gh world\n\n`);
448
+ for (const name of names) {
449
+ const outcome = run_scenario(name);
450
+ if (outcome.ok) {
451
+ process.stdout.write(`✅ ${name} — ${SCENARIOS[name]!.summary}\n`);
452
+ } else {
453
+ failed += 1;
454
+ process.stdout.write(`❌ ${name} — ${SCENARIOS[name]!.summary}\n`);
455
+ for (const f of outcome.failures) {
456
+ process.stdout.write(` ${f}\n`);
457
+ }
458
+ }
459
+ }
460
+ process.stdout.write(`\nscanned: ${names.length}\n`);
461
+ process.stdout.write(
462
+ failed === 0
463
+ ? '✅ release drill green — the step machinery survives every measured failure mode.\n'
464
+ : `❌ release drill: ${failed} scenario(s) failed.\n`,
465
+ );
466
+ return failed === 0 ? 0 : 1;
467
+ }
468
+
469
+ function _isCliEntry(): boolean {
470
+ if (process.argv[1] === undefined) {
471
+ return false;
472
+ }
473
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
474
+ if (import.meta.url === argvUrl) {
475
+ return true;
476
+ }
477
+ try {
478
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
479
+ const argvPath = fs.realpathSync(path.resolve(process.argv[1]));
480
+ return here === argvPath;
481
+ } catch {
482
+ return false;
483
+ }
484
+ }
485
+
486
+ if (_isCliEntry() || process.argv[1] === _HERE) {
487
+ process.exit(main());
488
+ }
489
+
490
+ export { FakeWorld, SCENARIOS, run_scenario };
491
+ export type { Scenario, WorldConfig };