@1aboveio/skills 0.10.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 (397) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +90 -0
  3. package/bin/1aboveio-skills.mjs +18 -0
  4. package/package.json +28 -0
  5. package/runtime/skills/distribution/generated/recipes.json +1189 -0
  6. package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
  7. package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  8. package/skills/backend/airflow-dag-develop/LICENSE +3 -0
  9. package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
  10. package/skills/backend/app-debug/LICENSE +3 -0
  11. package/skills/backend/app-debug/SKILL.md +109 -0
  12. package/skills/backend/app-debug/references/common-errors.md +128 -0
  13. package/skills/backend/python-backend/LICENSE +3 -0
  14. package/skills/backend/python-backend/SKILL.md +326 -0
  15. package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
  16. package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
  17. package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
  18. package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
  19. package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
  20. package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
  21. package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
  22. package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
  23. package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
  24. package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
  25. package/skills/cicd-pipeline/docker/LICENSE +3 -0
  26. package/skills/cicd-pipeline/docker/SKILL.md +126 -0
  27. package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
  28. package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
  29. package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
  30. package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
  31. package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
  32. package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
  33. package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
  34. package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
  35. package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
  36. package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
  37. package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
  38. package/skills/cicd-pipeline/mergify/LICENSE +3 -0
  39. package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
  40. package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
  41. package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
  42. package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
  43. package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
  44. package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
  45. package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
  46. package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
  47. package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
  48. package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
  49. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
  50. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
  51. package/skills/cicd-pipeline/podman/LICENSE +3 -0
  52. package/skills/cicd-pipeline/podman/SKILL.md +70 -0
  53. package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
  54. package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
  55. package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
  56. package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
  57. package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
  58. package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
  59. package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
  60. package/skills/engineering/e2e-test/LICENSE +3 -0
  61. package/skills/engineering/e2e-test/SKILL.md +156 -0
  62. package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
  63. package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
  64. package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
  65. package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
  66. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
  67. package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
  68. package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
  69. package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
  70. package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
  71. package/skills/engineering/e2e-test/references/debugging.md +36 -0
  72. package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
  73. package/skills/engineering/e2e-test/references/reviewing.md +39 -0
  74. package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
  75. package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
  76. package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
  77. package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
  78. package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
  79. package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
  80. package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
  81. package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
  82. package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
  83. package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
  84. package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
  85. package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
  86. package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
  87. package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
  88. package/skills/engineering/engineering-runtime/LICENSE +3 -0
  89. package/skills/engineering/engineering-runtime/SKILL.md +10 -0
  90. package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
  91. package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
  92. package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
  93. package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
  94. package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  95. package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
  96. package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
  97. package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
  98. package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
  99. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
  100. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
  101. package/skills/engineering/ensure-coverage/LICENSE +3 -0
  102. package/skills/engineering/ensure-coverage/SKILL.md +136 -0
  103. package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
  104. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
  105. package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
  106. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
  107. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
  108. package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
  109. package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
  110. package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
  111. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
  112. package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
  113. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
  114. package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
  115. package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
  116. package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
  117. package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
  118. package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
  119. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
  120. package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
  121. package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
  122. package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
  123. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
  124. package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
  125. package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
  126. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
  127. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
  128. package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
  129. package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
  130. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
  131. package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
  132. package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
  133. package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
  134. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
  135. package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
  136. package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
  137. package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
  138. package/skills/engineering/harness-runtime/LICENSE +3 -0
  139. package/skills/engineering/harness-runtime/SKILL.md +18 -0
  140. package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
  141. package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
  142. package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
  143. package/skills/engineering/harness-runtime/contracts.md +15 -0
  144. package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
  145. package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
  146. package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
  147. package/skills/engineering/harness-runtime/index.mjs +767 -0
  148. package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
  149. package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
  150. package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
  151. package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
  152. package/skills/engineering/implement-and-pr/LICENSE +3 -0
  153. package/skills/engineering/implement-and-pr/SKILL.md +176 -0
  154. package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
  155. package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
  156. package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
  157. package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
  158. package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
  159. package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
  160. package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
  161. package/skills/engineering/resolve-issues/LICENSE +3 -0
  162. package/skills/engineering/resolve-issues/SKILL.md +167 -0
  163. package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
  164. package/skills/engineering/resolve-issues/references/breaker.md +82 -0
  165. package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
  166. package/skills/engineering/resolve-issues/references/delivery.md +108 -0
  167. package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
  168. package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
  169. package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
  170. package/skills/engineering/resolve-issues/references/finalization.md +68 -0
  171. package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
  172. package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
  173. package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
  174. package/skills/engineering/resolve-issues/references/intake.md +86 -0
  175. package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
  176. package/skills/engineering/resolve-issues/references/interference.md +87 -0
  177. package/skills/engineering/resolve-issues/references/loop.md +134 -0
  178. package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
  179. package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
  180. package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
  181. package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
  182. package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
  183. package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
  184. package/skills/engineering/resolve-issues/references/run-state.md +288 -0
  185. package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
  186. package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
  187. package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
  188. package/skills/engineering/resolve-issues/references/why.md +653 -0
  189. package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
  190. package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
  191. package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
  192. package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
  193. package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
  194. package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
  195. package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
  196. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
  197. package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
  198. package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
  199. package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
  200. package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
  201. package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
  202. package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
  203. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
  204. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
  205. package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
  206. package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
  207. package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
  208. package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
  209. package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
  210. package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
  211. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
  212. package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
  213. package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
  214. package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
  215. package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
  216. package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
  217. package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
  218. package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
  219. package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
  220. package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
  221. package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
  222. package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
  223. package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
  224. package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
  225. package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
  226. package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
  227. package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
  228. package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
  229. package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
  230. package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
  231. package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
  232. package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
  233. package/skills/engineering/resolve-release/LICENSE +3 -0
  234. package/skills/engineering/resolve-release/SKILL.md +112 -0
  235. package/skills/engineering/resolve-release/references/assembly.md +137 -0
  236. package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
  237. package/skills/engineering/resolve-release/references/candidate.md +167 -0
  238. package/skills/engineering/resolve-release/references/exposure.md +178 -0
  239. package/skills/engineering/resolve-release/references/handoff.md +24 -0
  240. package/skills/engineering/resolve-release/references/postmortem.md +230 -0
  241. package/skills/engineering/resolve-release/references/preflight.md +207 -0
  242. package/skills/engineering/resolve-release/references/principles.md +94 -0
  243. package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
  244. package/skills/engineering/resolve-release/references/related-skills.md +12 -0
  245. package/skills/engineering/resolve-release/references/routing.md +149 -0
  246. package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
  247. package/skills/engineering/resolve-release/references/versioning.md +202 -0
  248. package/skills/engineering/resolve-release/references/why.md +53 -0
  249. package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
  250. package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
  251. package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
  252. package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
  253. package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
  254. package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
  255. package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
  256. package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
  257. package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
  258. package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
  259. package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
  260. package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
  261. package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
  262. package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
  263. package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
  264. package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
  265. package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
  266. package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
  267. package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
  268. package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
  269. package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
  270. package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
  271. package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
  272. package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
  273. package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
  274. package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
  275. package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
  276. package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
  277. package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
  278. package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
  279. package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
  280. package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
  281. package/skills/engineering/resolve-release/vendor/README.md +60 -0
  282. package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
  283. package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
  284. package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
  285. package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
  286. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
  287. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
  288. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
  289. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
  290. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
  291. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
  292. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
  293. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
  294. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
  295. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
  296. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
  297. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
  298. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
  299. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
  300. package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
  301. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
  302. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
  303. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
  304. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
  305. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
  306. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
  307. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
  308. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
  309. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
  310. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
  311. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
  312. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
  313. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
  314. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
  315. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
  316. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  317. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
  318. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
  319. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
  320. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
  321. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
  322. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
  323. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
  324. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
  325. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
  326. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
  327. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
  328. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
  329. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
  330. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
  331. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
  332. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
  333. package/skills/engineering/review-pr/LICENSE +3 -0
  334. package/skills/engineering/review-pr/SKILL.md +123 -0
  335. package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
  336. package/skills/engineering/review-pr/references/code-correctness.md +5 -0
  337. package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
  338. package/skills/engineering/review-pr/references/coordination.md +18 -0
  339. package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
  340. package/skills/engineering/review-pr/references/finding-themes.md +7 -0
  341. package/skills/engineering/review-pr/references/github-posting.md +98 -0
  342. package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
  343. package/skills/engineering/review-pr/references/incremental-output.md +16 -0
  344. package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
  345. package/skills/engineering/review-pr/references/output-format.md +99 -0
  346. package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
  347. package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
  348. package/skills/engineering/review-pr/references/re-review.md +18 -0
  349. package/skills/engineering/review-pr/references/review-method.md +199 -0
  350. package/skills/engineering/review-pr/references/review-mode.md +30 -0
  351. package/skills/engineering/review-pr/references/review-posture.md +53 -0
  352. package/skills/engineering/review-pr/references/round1-depth.md +62 -0
  353. package/skills/engineering/review-pr/references/scripts.md +17 -0
  354. package/skills/engineering/review-pr/references/workflow.md +16 -0
  355. package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
  356. package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
  357. package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
  358. package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
  359. package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
  360. package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
  361. package/skills/engineering/smoke/LICENSE +3 -0
  362. package/skills/engineering/smoke/SKILL.md +131 -0
  363. package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
  364. package/skills/engineering/smoke/references/manifest.md +192 -0
  365. package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
  366. package/skills/fullstack/better-auth/LICENSE +3 -0
  367. package/skills/fullstack/better-auth/SKILL.md +601 -0
  368. package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
  369. package/skills/fullstack/monorepo/LICENSE +3 -0
  370. package/skills/fullstack/monorepo/SKILL.md +465 -0
  371. package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
  372. package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
  373. package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
  374. package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
  375. package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
  376. package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
  377. package/skills/fullstack/prisma-setup/LICENSE +3 -0
  378. package/skills/fullstack/prisma-setup/SKILL.md +180 -0
  379. package/skills/fullstack/prisma-setup/nextjs.md +258 -0
  380. package/skills/fullstack/prisma-setup/turborepo.md +301 -0
  381. package/skills/fullstack/shadcn/LICENSE +3 -0
  382. package/skills/fullstack/shadcn/SKILL.md +119 -0
  383. package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
  384. package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
  385. package/skills/fullstack/shadcn/cli.md +411 -0
  386. package/skills/fullstack/shadcn/customization.md +224 -0
  387. package/skills/fullstack/shadcn/evals/evals.json +90 -0
  388. package/skills/fullstack/shadcn/mcp.md +101 -0
  389. package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
  390. package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
  391. package/skills/fullstack/shadcn/rules/composition.md +195 -0
  392. package/skills/fullstack/shadcn/rules/data-table.md +201 -0
  393. package/skills/fullstack/shadcn/rules/forms.md +255 -0
  394. package/skills/fullstack/shadcn/rules/icons.md +103 -0
  395. package/skills/fullstack/shadcn/rules/styling.md +167 -0
  396. package/skills/fullstack/zod-v4/LICENSE +3 -0
  397. package/skills/fullstack/zod-v4/SKILL.md +287 -0
@@ -0,0 +1,1199 @@
1
+ // version — the mechanized versioning / tagging / CHANGELOG helper for the release lane.
2
+ //
3
+ // Versioning is a load-bearing release step, and a prose-described load-bearing step
4
+ // degrades to a lookalike: an agent that "computes the next version" from memory picks
5
+ // a minor for a breaking change, re-points a tag to a new SHA to "fix" it, or reuses the
6
+ // version of a rolled-back release. Each of those is unrecoverable once pushed — a tag
7
+ // is a public promise about an immutable SHA. So the invariants live here, mechanically:
8
+ //
9
+ // 1. PRODUCTION ONLY. Versions, tags and CHANGELOG cuts exist only on the production
10
+ // branch. A dev/staging release is unversioned and identified by its SHA. `plan`,
11
+ // `changelog`, `rc` and `tag` refuse (exit 3, a NORMAL outcome) when --target-branch is
12
+ // not the production branch — the release loop calls them unconditionally and branches on
13
+ // 3. The gate FAILS CLOSED: both branch flags are required (exit 2 without them), or
14
+ // an agent could skip the guard by simply omitting a flag.
15
+ // 2. TAGS ARE IMMUTABLE. `tag` never moves, deletes, or force-writes a tag. Re-tagging
16
+ // the same SHA is a resume-safe no-op; re-tagging a DIFFERENT SHA is exit 1. There is
17
+ // deliberately no --force: a version that reached a remote is burned, and the next
18
+ // attempt takes the next patch (references/versioning.md, "the invariant, the trigger,
19
+ // and the burn").
20
+ // The rc tags `rc` mints obey the same rule — a given rc number names one SHA forever.
21
+ // They are cheap because they are NUMBERED, not because they move: the next attempt
22
+ // takes the next rc, and the VERSION survives, because the final `v<version>` tag is
23
+ // minted only after production verification. That is the point of rc tags — a burn is
24
+ // then possible only on the legacy shape where the final tag itself triggers the
25
+ // deploy (references/versioning.md, "the invariant, the trigger, and the burn").
26
+ // 3. THE CHANGELOG IS APPEND-ONLY. A rolled-back release's section is never deleted —
27
+ // `withdraw` annotates it. Deleting it would erase the only public record that the
28
+ // version existed and was withdrawn.
29
+ //
30
+ // For the authoritative subcommand / flag / exit-code list, run `node version.mjs --help`
31
+ // (or read the USAGE const below, right beside the dispatch it describes). That help text is
32
+ // the SINGLE source of truth: restating the case list here would give it a second copy to
33
+ // drift against, which is the exact footgun this pointer removes — one list can't contradict
34
+ // itself. What stays here is the doctrine `--help` does not encode: the three invariants
35
+ // above, plus this one rule about the codes it prints — each exit code carries exactly ONE
36
+ // meaning, because an agent branches on them, so an overloaded code (3 meaning both "skip
37
+ // versioning" and "already cut") would make a resuming run silently ship production untagged.
38
+ // The policy behind all of it: references/versioning.md.
39
+
40
+ import fs from 'node:fs'
41
+ import { spawnSync } from 'node:child_process'
42
+ import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
43
+
44
+ // ---- semver ----
45
+
46
+ const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
47
+
48
+ export function parseSemver(input) {
49
+ const m = SEMVER_RE.exec(String(input).trim())
50
+ if (!m) return null
51
+ return {
52
+ major: Number(m[1]),
53
+ minor: Number(m[2]),
54
+ patch: Number(m[3]),
55
+ prerelease: m[4] || null,
56
+ }
57
+ }
58
+
59
+ export function formatVersion(v) {
60
+ return `${v.major}.${v.minor}.${v.patch}`
61
+ }
62
+
63
+ export function compareVersions(a, b) {
64
+ for (const k of ['major', 'minor', 'patch']) {
65
+ if (a[k] !== b[k]) return a[k] < b[k] ? -1 : 1
66
+ }
67
+ // A prerelease sorts BEFORE its release (semver §11.3). The lane mints these itself now
68
+ // (`rc`), so this ordering is load-bearing rather than defensive.
69
+ if (a.prerelease && !b.prerelease) return -1
70
+ if (!a.prerelease && b.prerelease) return 1
71
+ return 0
72
+ }
73
+
74
+ // The latest RELEASE tag: a prerelease is never a release baseline. A `v1.5.0-rc.2` means a
75
+ // candidate for 1.5.0 that has NOT been verified in production — 1.5.0 is still unreleased,
76
+ // so baselining on it would propose 1.5.1 and skip a version that never shipped.
77
+ export function latestReleaseTag(tags) {
78
+ const parsed = tags
79
+ .map((t) => ({ tag: t, v: parseSemver(t) }))
80
+ .filter((x) => x.v && !x.v.prerelease)
81
+ if (parsed.length === 0) return null
82
+ parsed.sort((a, b) => compareVersions(a.v, b.v))
83
+ return parsed[parsed.length - 1]
84
+ }
85
+
86
+ // ---- change classification (conventional commits, then labels) ----
87
+
88
+ const TITLE_RE = /^\s*([a-zA-Z]+)(?:\(([^)]*)\))?(!)?:\s*(.+)$/
89
+ const BREAKING_LABELS = new Set(['breaking', 'breaking-change', 'semver-major'])
90
+
91
+ // Conventional type → CHANGELOG section. Types absent here are internal: excluded from
92
+ // the CHANGELOG unless --include-internal, because a changelog records notable changes,
93
+ // not every commit.
94
+ export const SECTION_BY_TYPE = {
95
+ feat: 'Added',
96
+ fix: 'Fixed',
97
+ perf: 'Changed',
98
+ refactor: 'Changed',
99
+ revert: 'Removed',
100
+ security: 'Security',
101
+ }
102
+ export const INTERNAL_TYPES = ['build', 'chore', 'ci', 'docs', 'style', 'test']
103
+ export const SECTION_ORDER = ['Breaking', 'Added', 'Changed', 'Removed', 'Fixed', 'Security', 'Internal']
104
+
105
+ export function classifyChange(change) {
106
+ const title = String(change.title || '')
107
+ const labels = (change.labels || []).map((l) => String(l).toLowerCase())
108
+ const m = TITLE_RE.exec(title)
109
+
110
+ let type = m ? m[1].toLowerCase() : null
111
+ if (!type) type = labels.find((l) => l in SECTION_BY_TYPE || INTERNAL_TYPES.includes(l)) || 'other'
112
+
113
+ const breaking =
114
+ Boolean(m && m[3]) ||
115
+ /(^|\n)BREAKING[ -]CHANGE:/.test(String(change.body || '')) ||
116
+ labels.some((l) => BREAKING_LABELS.has(l))
117
+
118
+ const internal = INTERNAL_TYPES.includes(type)
119
+ const section = breaking ? 'Breaking' : internal ? 'Internal' : SECTION_BY_TYPE[type] || 'Changed'
120
+
121
+ return {
122
+ ...change,
123
+ type,
124
+ breaking,
125
+ internal,
126
+ section,
127
+ scope: m ? m[2] || null : null,
128
+ summary: m ? m[4] : title,
129
+ }
130
+ }
131
+
132
+ // The bump the release CONTENT demands. Under 0.x (`--strict` disables this), breaking
133
+ // changes land as a minor because the public contract is still unstable. Non-breaking
134
+ // features remain minor releases so version numbers communicate new capability.
135
+ export function bumpFor(changes, current, { strict = false } = {}) {
136
+ const classified = changes.map(classifyChange)
137
+ const zeroVer = !strict && current.major === 0
138
+
139
+ if (classified.some((c) => c.breaking)) {
140
+ return { bump: zeroVer ? 'minor' : 'major', reason: 'breaking change in the release content' }
141
+ }
142
+ if (classified.some((c) => c.type === 'feat')) {
143
+ return { bump: 'minor', reason: 'new feature in the release content' }
144
+ }
145
+ return { bump: 'patch', reason: 'fixes and internal changes only' }
146
+ }
147
+
148
+ export function nextVersion(current, bump) {
149
+ if (bump === 'major') return { major: current.major + 1, minor: 0, patch: 0, prerelease: null }
150
+ if (bump === 'minor') return { major: current.major, minor: current.minor + 1, patch: 0, prerelease: null }
151
+ return { major: current.major, minor: current.minor, patch: current.patch + 1, prerelease: null }
152
+ }
153
+
154
+ export function tagName(version, prefix = 'v') {
155
+ return `${prefix}${typeof version === 'string' ? version : formatVersion(version)}`
156
+ }
157
+
158
+ // The rc tags belonging to ONE exact version tag, ascending by number. Matching is exact,
159
+ // not prefix-wise: `v1.5.0` must not collect `v1.5.0-rc.1-hotfix` or anything under a
160
+ // neighbouring version, or the next rc number would be computed from a foreign series.
161
+ export function rcTags(tags, base) {
162
+ const re = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-rc\\.(\\d+)$`)
163
+ return tags
164
+ .map((t) => ({ tag: t, n: Number((re.exec(t) || [])[1]) }))
165
+ .filter((x) => Number.isInteger(x.n))
166
+ .sort((a, b) => a.n - b.n)
167
+ }
168
+
169
+ // The rc tags a REMOTE holds for one version, each with the commit it names.
170
+ //
171
+ // Numbering an rc from local tags alone is wrong whenever the two can disagree, and they
172
+ // disagree in the most ordinary case there is: a fresh clone has no local tags, so the next
173
+ // number computes as rc.1 while the remote is already on rc.3. What makes that worse than a
174
+ // bad number is the ORDER it fails in — `git tag` succeeds locally, then `git push` is
175
+ // rejected, leaving a stale local tag pointing at the wrong commit and a non-zero exit that
176
+ // looks like a crash. Since it is the remote that fires the production pipeline, the remote
177
+ // is the authority on which numbers are spent. Same deref subtlety as remoteTagSha: an
178
+ // annotated tag's commit appears only on the `^{}` line, so prefer it when both are present.
179
+ export function remoteRcTags(remote, base, run = git) {
180
+ const out = run(['ls-remote', '--tags', remote, `refs/tags/${base}-rc.*`], { allowFail: true }).stdout
181
+ if (!out) return []
182
+ const shaByTag = new Map()
183
+ for (const line of out.split('\n').filter(Boolean)) {
184
+ const [sha, ref] = line.split(/\s+/)
185
+ if (!ref) continue
186
+ const isDeref = ref.endsWith('^{}')
187
+ const name = ref.replace(/^refs\/tags\//, '').replace(/\^\{\}$/, '')
188
+ if (isDeref || !shaByTag.has(name)) shaByTag.set(name, sha)
189
+ }
190
+ return rcTags([...shaByTag.keys()], base).map((x) => ({ ...x, sha: shaByTag.get(x.tag) }))
191
+ }
192
+
193
+ // ---- CHANGELOG rendering ----
194
+
195
+ const SCAFFOLD = `# Changelog
196
+
197
+ All notable changes to this project are documented in this file. The format is based on
198
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
199
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
200
+
201
+ ## [Unreleased]
202
+ `
203
+
204
+ function prLink(change, repo) {
205
+ if (!change.pr) return ''
206
+ const n = String(change.pr).replace(/^#/, '')
207
+ return repo ? ` ([#${n}](https://github.com/${repo}/pull/${n}))` : ` (#${n})`
208
+ }
209
+
210
+ function issueRefs(change) {
211
+ const issues = change.issues || []
212
+ if (issues.length === 0) return ''
213
+ return ` — closes ${issues.map((i) => `#${String(i).replace(/^#/, '')}`).join(', ')}`
214
+ }
215
+
216
+ // A release section (and the [Unreleased] body) is just `### Section` groups of bullets.
217
+ // Parsing back to that shape is what lets the cut MERGE hand-staged notes with derived
218
+ // ones instead of one clobbering the other.
219
+ export function parseGroups(body) {
220
+ const groups = new Map()
221
+ let current = null
222
+ for (const line of String(body).split('\n')) {
223
+ const heading = /^###\s+(.+?)\s*$/.exec(line)
224
+ if (heading) {
225
+ current = heading[1]
226
+ if (!groups.has(current)) groups.set(current, [])
227
+ continue
228
+ }
229
+ if (/^\s*[-*]\s+/.test(line)) {
230
+ const key = current || 'Changed' // a bare bullet under no heading still counts
231
+ if (!groups.has(key)) groups.set(key, [])
232
+ groups.get(key).push(line.trim().replace(/^\*\s+/, '- '))
233
+ }
234
+ }
235
+ return groups
236
+ }
237
+
238
+ // Staged notes first (a human wrote them), then derived entries; exact duplicates collapse.
239
+ export function mergeGroups(staged, derived) {
240
+ const out = new Map()
241
+ const keys = [...SECTION_ORDER, ...[...staged.keys(), ...derived.keys()].filter((k) => !SECTION_ORDER.includes(k))]
242
+ for (const key of keys) {
243
+ if (out.has(key)) continue
244
+ const items = [...(staged.get(key) || []), ...(derived.get(key) || [])]
245
+ const seen = new Set()
246
+ const deduped = items.filter((i) => (seen.has(i) ? false : seen.add(i)))
247
+ if (deduped.length > 0) out.set(key, deduped)
248
+ }
249
+ return out
250
+ }
251
+
252
+ export function renderGroups(version, date, groups) {
253
+ const lines = [`## [${version}] — ${date}`, '']
254
+ if (groups.size === 0) return `${lines.join('\n')}_No user-facing changes._\n`
255
+ for (const [section, items] of groups) {
256
+ lines.push(`### ${section}`, '', ...items, '')
257
+ }
258
+ return lines.join('\n')
259
+ }
260
+
261
+ export function groupsFromChanges(changes, { repo = null, includeInternal = false } = {}) {
262
+ const groups = new Map()
263
+ for (const c of changes.map(classifyChange)) {
264
+ if (!includeInternal && c.internal) continue
265
+ if (!groups.has(c.section)) groups.set(c.section, [])
266
+ const scope = c.scope ? `**${c.scope}**: ` : ''
267
+ groups.get(c.section).push(`- ${scope}${c.summary}${prLink(c, repo)}${issueRefs(c)}`)
268
+ }
269
+ // Emit in SECTION_ORDER so Breaking leads.
270
+ const ordered = new Map()
271
+ for (const s of SECTION_ORDER) if (groups.has(s)) ordered.set(s, groups.get(s))
272
+ return ordered
273
+ }
274
+
275
+ export function renderChangelogSection({ version, date, changes, repo = null, includeInternal = false }) {
276
+ return renderGroups(version, date, groupsFromChanges(changes, { repo, includeInternal }))
277
+ }
278
+
279
+ const headingRe = (version) => new RegExp(`^## \\[${version.replace(/\./g, '\\.')}\\]`)
280
+
281
+ function bodyAfter(lines, headingIdx) {
282
+ let end = headingIdx + 1
283
+ while (end < lines.length && !/^## /.test(lines[end])) end++
284
+ return { body: lines.slice(headingIdx + 1, end).join('\n'), end }
285
+ }
286
+
287
+ // Cut the release: the [Unreleased] body — the unversioned zone where changes accumulate
288
+ // as they merge to dev — is CARRIED into a new versioned heading, merged with the entries
289
+ // derived from the release's PRs, and [Unreleased] is left empty for the next cycle. This
290
+ // is the whole reason dev needs no version: `Unreleased` IS the absence of one, and the
291
+ // version is assigned once, here, to the set of changes being promoted.
292
+ //
293
+ // Carrying (never discarding) is the invariant: a hand-written note that a PR title did
294
+ // not capture must survive the cut. Idempotent — re-cutting a version already present is
295
+ // a resume, not a double-cut.
296
+ export function insertSection(md, section, version) {
297
+ const text = md && md.trim() ? md : SCAFFOLD
298
+ const lines = text.split('\n')
299
+
300
+ if (lines.some((l) => headingRe(version).test(l))) return { text, noop: true }
301
+
302
+ const sectionLines = section.trimEnd().split('\n')
303
+ const heading = sectionLines[0]
304
+ const derived = parseGroups(sectionLines.slice(1).join('\n'))
305
+ const unreleasedAt = lines.findIndex((l) => /^## \[Unreleased\]/i.test(l))
306
+
307
+ const finish = (out) => ({ text: `${out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`, noop: false })
308
+
309
+ if (unreleasedAt === -1) {
310
+ // No Unreleased section: nothing to carry. Insert above the first release heading.
311
+ const firstRelease = lines.findIndex((l) => /^## /.test(l))
312
+ const at = firstRelease === -1 ? lines.length : firstRelease
313
+ lines.splice(at, 0, ...renderGroups(version, heading.split('—').pop().trim(), derived).split('\n'))
314
+ return finish(lines)
315
+ }
316
+
317
+ const { body, end } = bodyAfter(lines, unreleasedAt)
318
+ const merged = mergeGroups(parseGroups(body), derived)
319
+ const cut = [heading, '', ...renderGroups(version, 'x', merged).split('\n').slice(2)]
320
+
321
+ return finish([...lines.slice(0, unreleasedAt + 1), '', ...cut, '', ...lines.slice(end)])
322
+ }
323
+
324
+ export function extractSection(md, version) {
325
+ const lines = String(md).split('\n')
326
+ const start = lines.findIndex((l) => headingRe(version).test(l))
327
+ if (start === -1) return null
328
+ let end = start + 1
329
+ while (end < lines.length && !/^## /.test(lines[end])) end++
330
+ return lines.slice(start, end).join('\n').trim()
331
+ }
332
+
333
+ // A rolled-back version is annotated, never deleted (invariant 3). The note is what tells
334
+ // a reader the tag exists but the version never served production.
335
+ export function markWithdrawn(md, version, incident) {
336
+ const lines = String(md).split('\n')
337
+ const at = lines.findIndex((l) => headingRe(version).test(l))
338
+ if (at === -1) return { text: md, noop: true, reason: `no [${version}] section` }
339
+ if (lines.slice(at, at + 4).some((l) => /^> \*\*Withdrawn\*\*/.test(l))) {
340
+ return { text: md, noop: true, reason: 'already withdrawn' }
341
+ }
342
+ const ref = incident ? ` See ${String(incident).startsWith('#') ? incident : `#${incident}`}.` : ''
343
+ // Wording covers BOTH doors to a burn: a failed build (never deployed) and a failed
344
+ // verification (deployed, rolled back). "Never served production traffic" is the claim
345
+ // that holds either way; "was rolled back" would be false for the build-failure case.
346
+ const note = `> **Withdrawn** — this release never served production traffic.${ref}`
347
+ lines.splice(at + 1, 0, '', note)
348
+ return { text: `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`, noop: false }
349
+ }
350
+
351
+ // ---- git ----
352
+
353
+ export function git(args, { allowFail = false, cwd } = {}) {
354
+ const r = spawnSync('git', args, { encoding: 'utf8', cwd })
355
+ if (r.status !== 0 && !allowFail) {
356
+ throw new Error(`git ${args.join(' ')} failed: ${(r.stderr || '').trim()}`)
357
+ }
358
+ return { status: r.status, stdout: (r.stdout || '').trim(), stderr: (r.stderr || '').trim() }
359
+ }
360
+
361
+ export function tagExists(tag, run = git) {
362
+ return run(['rev-parse', '-q', '--verify', `refs/tags/${tag}`], { allowFail: true }).status === 0
363
+ }
364
+
365
+ export function shaForTag(tag, run = git) {
366
+ return run(['rev-list', '-n', '1', tag]).stdout
367
+ }
368
+
369
+ // Resolve a caller-supplied commit-ish, or null. Never throws.
370
+ //
371
+ // Every `--sha` here arrives from a human or a half-finished script in the middle of a
372
+ // release: a typo, a branch this clone never fetched, a SHA copied from another checkout.
373
+ // Letting git throw turns all of those into an identical stack trace, which at that moment
374
+ // reads as "the release tool is broken" rather than "that ref isn't in this repo" — and the
375
+ // difference decides whether the operator retries or starts debugging the wrong thing.
376
+ export function resolveSha(ref, run = git) {
377
+ const r = run(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { allowFail: true })
378
+ return r.status === 0 && r.stdout ? r.stdout : null
379
+ }
380
+
381
+ // The COMMIT a remote tag names, or null if the remote has no such tag.
382
+ //
383
+ // An ANNOTATED tag is an object in its own right, so `ls-remote refs/tags/v1.5.0` returns
384
+ // the tag object's SHA, not the commit's. The commit only appears on the `^{}` deref line,
385
+ // and an exact-ref pattern does not match it — so both patterns must be requested. Compare
386
+ // the wrong one and every resume looks like a SHA conflict ("that version is burned") on a
387
+ // tag that is in fact correct. A lightweight tag has no deref line; its row is the commit.
388
+ export function remoteTagSha(remote, tag, run = git) {
389
+ const out = run(['ls-remote', '--tags', remote, `refs/tags/${tag}`, `refs/tags/${tag}^{}`], { allowFail: true }).stdout
390
+ if (!out) return null
391
+ const rows = out.split('\n').filter(Boolean).map((l) => l.split(/\s+/))
392
+ const deref = rows.find(([, ref]) => ref === `refs/tags/${tag}^{}`)
393
+ const exact = rows.find(([, ref]) => ref === `refs/tags/${tag}`)
394
+ const row = deref || exact
395
+ return row ? row[0] : null
396
+ }
397
+
398
+ // Push ONE annotated tag ref, hook-safe.
399
+ //
400
+ // Tags are not stack commits. A Mergify stacks pre-push hook that rewrites `git push` into
401
+ // `mergify stack push` is correct for branch heads and wrong for `refs/tags/*` — the tag is
402
+ // an immutable release artifact, not a stack-managed commit, and identity-v0.4.1's production
403
+ // cut stalled until the operator rediscovered `git push --no-verify origin refs/tags/…`.
404
+ // Always push the fully-qualified tag ref with `--no-verify` so a tag mint never depends on
405
+ // that rediscovery. Branch pushes are outside this helper on purpose.
406
+ export function pushTag(remote, tag, run = git) {
407
+ return run(['push', '--no-verify', remote, `refs/tags/${tag}`], { allowFail: true })
408
+ }
409
+
410
+ // Resolve the production branch against a FRESH remote tip before ancestry checks.
411
+ //
412
+ // `rc`/`tag` ask "is this the ship SHA?" by testing ancestry of the production branch. A
413
+ // local `main` that has not been fetched since the promotion landed is the wrong answer to
414
+ // that question — identity-v0.4.1 refused a SHA that was already on `origin/main` because the
415
+ // local tip was stale. When a remote is configured: fetch `<remote> <branch>` first and
416
+ // resolve against `origin/<branch>` (or the already-qualified remote-tracking form the
417
+ // operator passed). Never use a potentially stale local branch tip for the ship-SHA check
418
+ // in a clone that has a remote. A failed fetch fails closed — a pre-existing remote-tracking
419
+ // ref is not a fresh tip, so returning it with `fetched: false` is the same class of wrong
420
+ // answer. A remote-less throwaway clone (unit tests) falls back to the local ref only
421
+ // because there is no remote tip to prefer.
422
+ export function resolveProductionAncestryRef(production, remote = 'origin', run = git) {
423
+ if (typeof production !== 'string' || production.length === 0) {
424
+ return { error: 'production branch is required for the ancestry check' }
425
+ }
426
+ const remotePrefix = `${remote}/`
427
+ const branch = production.startsWith(remotePrefix)
428
+ ? production.slice(remotePrefix.length)
429
+ : production.startsWith('refs/remotes/' + remotePrefix)
430
+ ? production.slice(('refs/remotes/' + remotePrefix).length)
431
+ : production
432
+ if (!branch || branch.includes('..')) {
433
+ return { error: `production branch "${production}" is not a usable ref name` }
434
+ }
435
+ const remotes = (run(['remote'], { allowFail: true }).stdout || '')
436
+ .split('\n')
437
+ .map((s) => s.trim())
438
+ .filter(Boolean)
439
+ const hasRemote = remotes.includes(remote)
440
+ const remoteRef = `${remote}/${branch}`
441
+
442
+ if (hasRemote) {
443
+ // `git fetch origin main` only writes FETCH_HEAD — it does NOT update origin/main. The
444
+ // explicit refspec is what makes the remote-tracking tip the one we then ancestry-check.
445
+ // `--no-tags` is load-bearing: without it, fetch auto-follows remote tags that happen to
446
+ // point at commits already in the local object store (e.g. a burned vX.Y.Z on another
447
+ // SHA), which would re-create a local tag the burn path just deleted and then mint past.
448
+ const fetch = run(
449
+ ['fetch', '--quiet', '--no-tags', remote, `refs/heads/${branch}:refs/remotes/${remote}/${branch}`],
450
+ { allowFail: true },
451
+ )
452
+ // Fail closed on a failed fetch. A pre-existing origin/<branch> tip may be stale, and
453
+ // shipping against it is exactly the class of wrong answer this helper exists to stop
454
+ // (identity-v0.4.1). Never return { ref: remoteRef, fetched: false }.
455
+ if (fetch.status !== 0) {
456
+ return {
457
+ error:
458
+ `could not fetch ${remote} ${branch}: ${fetch.stderr || 'fetch failed'}. ` +
459
+ `Refusing to mint against a potentially stale remote-tracking "${remoteRef}" — ` +
460
+ `fetch ${remote} ${branch} (or pass --production-branch ${remoteRef}) and re-run.`,
461
+ }
462
+ }
463
+ const remoteOk = run(['rev-parse', '--verify', '--quiet', `${remoteRef}^{commit}`], { allowFail: true })
464
+ if (remoteOk.status === 0 && remoteOk.stdout) {
465
+ return { ref: remoteRef, branch, fetched: true }
466
+ }
467
+ // Fetch claimed success but the remote-tracking tip is still unusable — refuse rather
468
+ // than fall through to a local tip that is exactly the stale state we stop trusting.
469
+ return {
470
+ error:
471
+ `remote-tracking ref "${remoteRef}" does not resolve after fetch. ` +
472
+ `Refusing to mint against a potentially stale local "${production}" — ` +
473
+ `fetch ${remote} ${branch} (or pass --production-branch ${remoteRef}) and re-run.`,
474
+ }
475
+ }
476
+
477
+ // No remote configured: only the local ref exists. Unit-test clones hit this path.
478
+ const localOk = run(['rev-parse', '--verify', '--quiet', `${production}^{commit}`], { allowFail: true })
479
+ if (localOk.status === 0 && localOk.stdout) {
480
+ return { ref: production, branch, fetched: false, localOnly: true }
481
+ }
482
+ return {
483
+ error:
484
+ `production branch "${production}" does not resolve, and no remote "${remote}" is configured ` +
485
+ `to fetch from. Pass a resolvable ref (e.g. ${remoteRef}) after fetching.`,
486
+ }
487
+ }
488
+
489
+ // Push the tag unless the remote already agrees. Returns {pushed, error}.
490
+ //
491
+ // The local tag existing is NOT evidence the remote has it: `git tag` then `git push` is
492
+ // two steps, and a run interrupted between them leaves a local-only tag. A resume that
493
+ // short-circuits on "local tag exists" would never push — and a later `gh release create`
494
+ // would then mint the remote tag at the default branch's HEAD, a DIFFERENT commit. So the
495
+ // resume path reconciles the remote rather than assuming it.
496
+ export function ensureRemoteTag(remote, tag, sha, run = git) {
497
+ const remoteSha = remoteTagSha(remote, tag, run)
498
+ if (remoteSha === sha) return { pushed: false }
499
+ if (remoteSha != null) {
500
+ return { error: `${tag} exists on ${remote} at ${remoteSha.slice(0, 8)}, not ${sha.slice(0, 8)} — that version is burned; take the next patch.` }
501
+ }
502
+ const pushed = pushTag(remote, tag, run)
503
+ if (pushed.status !== 0) {
504
+ return { error: `could not push ${tag} to ${remote}: ${pushed.stderr}` }
505
+ }
506
+ return { pushed: true }
507
+ }
508
+
509
+ // ---- CLI ----
510
+
511
+ function parseArgs(argv) {
512
+ const opts = {}
513
+ const positional = []
514
+ for (let i = 0; i < argv.length; i++) {
515
+ const a = argv[i]
516
+ if (a.startsWith('--')) {
517
+ const key = a.slice(2)
518
+ const next = argv[i + 1]
519
+ if (next === undefined || next.startsWith('--')) opts[key] = true
520
+ else {
521
+ opts[key] = next
522
+ i++
523
+ }
524
+ } else positional.push(a)
525
+ }
526
+ return { opts, positional }
527
+ }
528
+
529
+ function emitTagResult(opts, { tag, sha, pushed = false, resumed = false }) {
530
+ const result = { tag, sha, pushed, resumed }
531
+ console.log(opts.json ? JSON.stringify(result, null, 2) : tag)
532
+ }
533
+
534
+ function emitChangelogPlan(opts, { version, file, noop, markdown }) {
535
+ if (opts.format === 'json') {
536
+ console.log(JSON.stringify({ version, file, noop, markdown }, null, 2))
537
+ } else {
538
+ process.stdout.write(markdown)
539
+ }
540
+ }
541
+
542
+ // Every parse here is a place an operator hands us text, so every one is a place they can hand us
543
+ // the wrong text. Bare `JSON.parse` reported that as a V8 stack trace: pi session 019faea6 hit
544
+ // `SyntaxError: Unexpected end of JSON input at readChanges (version.mjs:439)`, which names the
545
+ // line that threw and nothing anyone can act on. It does not say WHICH input was bad (file,
546
+ // literal, stdin), what its path was, or that "empty" is the likely cause. Mid-release that reads
547
+ // as "the release tooling is broken" rather than "your --changes file is empty", and the
548
+ // difference decides whether the next move is a one-line fix or an escalation.
549
+ //
550
+ // So each source names itself in the refusal — the same reason `preflight-probes.mjs` quotes a
551
+ // tool's own refusal id rather than paraphrasing it: the diagnosis has to survive to the person
552
+ // reading the error.
553
+ export function parseChangesJson(text, source) {
554
+ const trimmed = String(text ?? '').trim()
555
+ if (trimmed.length === 0) {
556
+ throw new Error(`version: ${source} is empty — expected a JSON array of changes ('[]' means none, which is not the same as an empty file)`)
557
+ }
558
+ let parsed
559
+ try {
560
+ parsed = JSON.parse(trimmed)
561
+ } catch (error) {
562
+ throw new Error(`version: ${source} is not valid JSON (${error.message}); expected a JSON array of changes`)
563
+ }
564
+ if (!Array.isArray(parsed)) {
565
+ throw new Error(`version: ${source} parsed as ${parsed === null ? 'null' : typeof parsed}, not an array — --changes takes a JSON array of changes`)
566
+ }
567
+ return parsed
568
+ }
569
+
570
+ function readChanges(opts) {
571
+ const raw = opts.changes
572
+ if (!raw || raw === true) return []
573
+ if (raw === '-' || opts.stdin) return parseChangesJson(fs.readFileSync(0, 'utf8'), 'stdin (--changes -)')
574
+ if (raw.trim().startsWith('[')) return parseChangesJson(raw, 'the --changes literal')
575
+ let text
576
+ try {
577
+ text = fs.readFileSync(raw, 'utf8')
578
+ } catch (error) {
579
+ // A missing or unreadable path is its own diagnosis; reporting it as bad JSON sends the
580
+ // operator to inspect a file that is not there.
581
+ throw new Error(`version: --changes file ${JSON.stringify(raw)} could not be read (${error.code || error.message})`)
582
+ }
583
+ return parseChangesJson(text, `--changes file ${JSON.stringify(raw)}`)
584
+ }
585
+
586
+ // An RC tag invokes an external production trigger. The preflight artifact is deliberately
587
+ // checked again at the mutation boundary: a release transcript that mentions a probe is not a
588
+ // guard if `rc` can still run without its successful record.
589
+ export function validateRcTopologyProbe(file) {
590
+ if (typeof file !== 'string' || file.length === 0) {
591
+ return { error: 'missing --topology-probe <preflight-probes JSON>; run the tag-trigger topology probe before minting an RC' }
592
+ }
593
+ let report
594
+ try {
595
+ report = JSON.parse(fs.readFileSync(file, 'utf8'))
596
+ } catch (error) {
597
+ return { error: `could not read topology probe ${JSON.stringify(file)} (${error.code || error.message})` }
598
+ }
599
+ const topology = Array.isArray(report && report.probes)
600
+ ? report.probes.find((p) => p && p.id === 'rc-tag-topology')
601
+ : null
602
+ if (!report || report.ready !== true || !topology || topology.state !== 'ok') {
603
+ return { error: 'topology probe does not contain a ready rc-tag-topology result' }
604
+ }
605
+ for (const [field, expected] of Object.entries({
606
+ prodBuildTrigger: 'tag-trigger',
607
+ versionSource: 'TAG_NAME',
608
+ candidateTagSource: 'TAG_NAME',
609
+ })) {
610
+ if (topology[field] !== expected) {
611
+ return { error: `topology probe ${field} must be ${expected}, got ${JSON.stringify(topology[field])}` }
612
+ }
613
+ }
614
+ const substitutions = topology.substitutions
615
+ const tagMatch = substitutions && /^v?(\d+\.\d+\.\d+)-rc\.[1-9]\d*$/.exec(String(substitutions.TAG_NAME || ''))
616
+ if (!tagMatch || substitutions._DEPLOYED_VERSION !== tagMatch[1] || substitutions._CANDIDATE_TAG !== substitutions.TAG_NAME) {
617
+ return { error: 'topology probe substitutions do not prove TAG_NAME -> _DEPLOYED_VERSION (final SemVer) and _CANDIDATE_TAG (RC tag)' }
618
+ }
619
+ return { topology }
620
+ }
621
+
622
+ // Invariant 1, mechanized. Exit 3 is NORMAL — a dev release legitimately has no version.
623
+ //
624
+ // This FAILS CLOSED: both flags are required. A guard that is skipped by simply not
625
+ // passing a flag is not a guard — an agent that forgets them would otherwise tag a
626
+ // dev-only SHA, minting a version that names a commit production will never run.
627
+ function requireProductionTarget(opts, command) {
628
+ const target = opts['target-branch']
629
+ const production = opts['production-branch']
630
+ if (typeof production !== 'string' || typeof target !== 'string') {
631
+ console.error(
632
+ `version: ${command} requires --target-branch <b> --production-branch <b>. ` +
633
+ 'They decide whether this release may be versioned at all (only a production ' +
634
+ 'promotion may), so they are never optional.',
635
+ )
636
+ return 2
637
+ }
638
+ if (target !== production) {
639
+ console.log(
640
+ JSON.stringify({
641
+ applicable: false,
642
+ target,
643
+ production,
644
+ reason:
645
+ `"${target}" is not the production branch ("${production}") — dev/staging releases are ` +
646
+ 'unversioned and untagged, identified by SHA. No version, no tag, no CHANGELOG cut.',
647
+ }, null, 2),
648
+ )
649
+ return 3
650
+ }
651
+ return null
652
+ }
653
+
654
+ // The synopsis `--help` prints and references/versioning.md points at — the SINGLE
655
+ // source of truth for subcommands, flags, and exit codes. The file-header contract
656
+ // above points HERE rather than restating them, so there is no second list to drift
657
+ // against; this text is the one that must stay accurate against the commands `main`
658
+ // dispatches below (the doc's "run --help" pointer is only true if this exists and
659
+ // is correct).
660
+ const USAGE = `version — mechanized versioning / tagging / CHANGELOG helper for the release lane.
661
+
662
+ Usage:
663
+ version plan <branch-flags> [--changes <json>] [--current <v>] [--strict]
664
+ version changelog <branch-flags> --version <v> --date <iso> [--changes <json>] [--repo owner/name] [--include-internal] [--file CHANGELOG.md] [--dry-run --format markdown|json]
665
+ version rc <branch-flags> --version <v> --sha <sha> [--push] [--remote origin] [--message <msg>] [--dry-run] [--json]
666
+ version tag <branch-flags> --version <v> --sha <sha> [--push] [--remote origin] [--message <msg>] [--dry-run] [--json]
667
+ version verify --version <v> --sha <sha> [--live-sha <sha>]
668
+ version notes --version <v> [--file CHANGELOG.md]
669
+ version withdraw --version <v> [--incident <ref>] [--file CHANGELOG.md] [--dry-run]
670
+
671
+ <branch-flags> --target-branch <b> --production-branch <b> (both REQUIRED on plan|changelog|rc|tag)
672
+ --changes <json> JSON array of { pr, title, labels?, body?, issues? }; "-" reads stdin.
673
+ Optional: absent means an empty change set (plan then bumps a patch; changelog
674
+ cuts a section from only the accumulated Unreleased body).
675
+ --include-internal keep internal-typed entries (chore/docs/…) in the cut, normally excluded.
676
+ --message <msg> annotation for the tag object minted by rc|tag (default "Release[ candidate] <tag>").
677
+ --retry-from <rc-tag> mint the next immutable RC after this exact failed RC, even when it
678
+ names the same SHA. A repeated invocation resumes the resulting retry tag.
679
+ --topology-probe <file> required on rc: JSON output from preflight-probes check, proving the
680
+ production tag trigger derives final SemVer and candidate tag from TAG_NAME.
681
+ --tag-prefix <p> default "v" --strict disable the 0.x bump convention
682
+ --json rc|tag: emit {tag, sha, pushed, resumed}; default stdout is only the tag.
683
+ --dry-run supported on changelog|rc|tag|withdraw: print the declared result, touch nothing.
684
+ --format <mode> changelog --dry-run output: markdown emits the complete planned file; json emits
685
+ {version, file, noop, markdown}. Payloads use stdout; diagnostics use stderr.
686
+ A changelog dry-run requires an explicit format; --file - reads stdin and is
687
+ rejected outside dry-run mode.
688
+
689
+ Exit codes (each has exactly one meaning — an agent branches on them):
690
+ 0 desired state reached, including idempotent no-ops (re-cut, same-SHA re-tag, already-withdrawn)
691
+ 1 violation or error — tag collision, immutable-tag conflict, a SHA not an ancestor of production,
692
+ unresolvable production ref, an invalid baseline (--current is not a semver tag), a verify against
693
+ a tag that does not exist, tag/live-SHA mismatch, notes/withdraw with no CHANGELOG file,
694
+ notes requested for a version never cut, or a rejected rc push (lost race; the local tag is rolled back)
695
+ 2 usage — a required flag is missing (both branch flags fail closed), an unknown command,
696
+ or a --sha that resolves to no commit in this repo
697
+ 3 not a production promotion — no version, tag, or CHANGELOG cut to make (normal on a dev release)
698
+
699
+ Doctrine (bump rules, the CHANGELOG's two zones, the burn case): references/versioning.md.`
700
+
701
+ export function main(argv) {
702
+ const { opts, positional } = parseArgs(argv)
703
+ const cmd = positional[0]
704
+ const prefix = typeof opts['tag-prefix'] === 'string' ? opts['tag-prefix'] : 'v'
705
+ const file = typeof opts.file === 'string' ? opts.file : 'CHANGELOG.md'
706
+
707
+ if (cmd === 'help' || cmd === '-h' || opts.help || opts.h) {
708
+ console.log(USAGE)
709
+ return 0
710
+ }
711
+ if (opts.json && !['rc', 'tag'].includes(cmd)) {
712
+ console.error('version: --json is only valid for rc|tag.')
713
+ return 2
714
+ }
715
+ if (opts.format && (cmd !== 'changelog' || !opts['dry-run'])) {
716
+ console.error('version: --format is only valid with changelog --dry-run.')
717
+ return 2
718
+ }
719
+
720
+ switch (cmd) {
721
+ case 'plan': {
722
+ const gate = requireProductionTarget(opts, 'plan')
723
+ if (gate !== null) return gate
724
+
725
+ const changes = readChanges(opts)
726
+ let currentTag = typeof opts.current === 'string' ? opts.current : null
727
+ if (!currentTag) {
728
+ // The baseline is the highest release tag reachable from the PRODUCTION branch —
729
+ // never from HEAD. `plan` runs at the pre-flight gate, where HEAD is whatever the
730
+ // human had checked out (often a feature branch forked before the last release).
731
+ // Reading HEAD there silently misses the released tags and proposes a version
732
+ // BELOW one that already exists — and the collision check cannot catch it, because
733
+ // the too-low version is genuinely free. Version numbers would go backwards.
734
+ const branch = opts['production-branch']
735
+ if (git(['rev-parse', '--verify', '--quiet', `${branch}^{commit}`], { allowFail: true }).status !== 0) {
736
+ // Silence here is the same regression by another route: an unresolvable ref makes
737
+ // `--merged` find no tags, and a 0.0.0 baseline looks exactly like a first release.
738
+ console.error(`version: production branch "${branch}" does not resolve. Fetch it, or pass the ref that exists (e.g. origin/main).`)
739
+ return 1
740
+ }
741
+ const tags = git(['tag', '--list', `${prefix}*`, '--merged', branch], { allowFail: true }).stdout
742
+ const latest = latestReleaseTag(tags ? tags.split('\n').filter(Boolean) : [])
743
+ currentTag = latest ? latest.tag : `${prefix}0.0.0`
744
+ }
745
+ const current = parseSemver(currentTag)
746
+ if (!current) {
747
+ console.error(`version: "${currentTag}" is not a semver release tag.`)
748
+ return 1
749
+ }
750
+
751
+ const { bump, reason } = bumpFor(changes, current, { strict: Boolean(opts.strict) })
752
+ const next = nextVersion(current, bump)
753
+ const tag = tagName(next, prefix)
754
+ // Deliberately an EXACT-ref check, so `v1.5.0-rc.3` does not block `v1.5.0`. The rcs
755
+ // are this release's own deploy triggers — the candidates for exactly this version.
756
+ // Treating them as a collision would make every retried release skip a version.
757
+ const collision = tagExists(tag)
758
+
759
+ console.log(
760
+ JSON.stringify({
761
+ applicable: true,
762
+ current: formatVersion(current),
763
+ bump,
764
+ reason,
765
+ next: formatVersion(next),
766
+ tag,
767
+ collision,
768
+ changes: changes.map(classifyChange).map((c) => ({
769
+ pr: c.pr, type: c.type, breaking: c.breaking, section: c.section, summary: c.summary,
770
+ })),
771
+ }, null, 2),
772
+ )
773
+ // A collision means this version already reached the repo — burned, or already cut.
774
+ // Never silently bump past it: the human decides (resume vs. next patch).
775
+ return collision ? 1 : 0
776
+ }
777
+
778
+ case 'changelog': {
779
+ const gate = requireProductionTarget(opts, 'changelog')
780
+ if (gate !== null) return gate
781
+ if (typeof opts.version !== 'string' || typeof opts.date !== 'string') {
782
+ console.error('version: changelog requires --version <v> --date <YYYY-MM-DD>')
783
+ return 2
784
+ }
785
+ if (opts['dry-run'] && !['markdown', 'json'].includes(opts.format)) {
786
+ console.error('version: changelog --dry-run requires --format markdown|json.')
787
+ return 2
788
+ }
789
+ const section = renderChangelogSection({
790
+ version: opts.version,
791
+ date: opts.date,
792
+ changes: readChanges(opts),
793
+ repo: typeof opts.repo === 'string' ? opts.repo : null,
794
+ includeInternal: Boolean(opts['include-internal']),
795
+ })
796
+ if (file === '-' && !opts['dry-run']) {
797
+ console.error('version: changelog --file - requires --dry-run.')
798
+ return 2
799
+ }
800
+ const existing = file === '-'
801
+ ? fs.readFileSync(0, 'utf8')
802
+ : (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '')
803
+ const { text, noop } = insertSection(existing, section, opts.version)
804
+ if (noop) {
805
+ if (opts['dry-run']) emitChangelogPlan(opts, { version: opts.version, file, noop, markdown: text })
806
+ // Exit 0, not 3: the desired state is already reached, exactly like a same-SHA
807
+ // re-tag. Exit 3 means ONLY "not a production promotion" — overloading it here
808
+ // would make a resuming agent read "already cut" as "skip versioning entirely".
809
+ console.error(`version: [${opts.version}] already in ${file} — nothing to cut (resume).`)
810
+ return 0
811
+ }
812
+ if (opts['dry-run']) emitChangelogPlan(opts, { version: opts.version, file, noop, markdown: text })
813
+ else fs.writeFileSync(file, text)
814
+ return 0
815
+ }
816
+
817
+ case 'notes': {
818
+ if (typeof opts.version !== 'string') {
819
+ console.error('version: notes requires --version <v>')
820
+ return 2
821
+ }
822
+ if (!fs.existsSync(file)) {
823
+ console.error(`version: no ${file}`)
824
+ return 1
825
+ }
826
+ const section = extractSection(fs.readFileSync(file, 'utf8'), opts.version)
827
+ if (!section) {
828
+ // An error, not "nothing to do": the caller is about to publish release notes and
829
+ // there are none. Better to fail than to create an empty GitHub Release.
830
+ console.error(`version: no [${opts.version}] section in ${file}`)
831
+ return 1
832
+ }
833
+ console.log(section)
834
+ return 0
835
+ }
836
+
837
+ case 'withdraw': {
838
+ if (typeof opts.version !== 'string') {
839
+ console.error('version: withdraw requires --version <v> [--incident <ref>]')
840
+ return 2
841
+ }
842
+ if (!fs.existsSync(file)) {
843
+ console.error(`version: no ${file}`)
844
+ return 1
845
+ }
846
+ const { text, noop, reason } = markWithdrawn(fs.readFileSync(file, 'utf8'), opts.version, opts.incident)
847
+ if (noop) {
848
+ // Already withdrawn → desired state, exit 0. No such section → nothing was ever
849
+ // cut for that version (the tag-after-verify path), which is also correct: exit 0.
850
+ console.error(`version: not withdrawn (${reason}).`)
851
+ return 0
852
+ }
853
+ if (opts['dry-run']) console.log(text)
854
+ else fs.writeFileSync(file, text)
855
+ return 0
856
+ }
857
+
858
+ case 'rc': {
859
+ const gate = requireProductionTarget(opts, 'rc')
860
+ if (gate !== null) return gate
861
+ if (typeof opts.version !== 'string' || typeof opts.sha !== 'string') {
862
+ console.error('version: rc requires --version <v> --sha <sha>')
863
+ return 2
864
+ }
865
+ const base = tagName(opts.version, prefix)
866
+ const sha = resolveSha(opts.sha)
867
+ if (sha == null) {
868
+ console.error(`version: --sha "${opts.sha}" does not resolve to a commit in this repo.`)
869
+ return 2
870
+ }
871
+ const topologyProbe = validateRcTopologyProbe(opts['topology-probe'])
872
+ if (topologyProbe.error) {
873
+ console.error(`version: refusing to mint an RC: ${topologyProbe.error}`)
874
+ return 1
875
+ }
876
+ const remote = typeof opts.remote === 'string' ? opts.remote : 'origin'
877
+ const listed = git(['tag', '--list', `${base}-rc.*`], { allowFail: true }).stdout
878
+ const existing = rcTags(listed ? listed.split('\n').filter(Boolean) : [], base)
879
+ const retryFrom = typeof opts['retry-from'] === 'string' ? opts['retry-from'] : null
880
+ const retryFromShape = retryFrom == null ? null : rcTags([retryFrom], base)[0] || null
881
+ if (retryFrom != null && retryFromShape == null) {
882
+ console.error(`version: --retry-from ${JSON.stringify(retryFrom)} is not an RC tag for ${base}.`)
883
+ return 2
884
+ }
885
+
886
+ // Resume safety. Pushing an rc FIRES the production pipeline, so "have I already
887
+ // minted one?" is not a cosmetic question: a resumed session that minted rc.4, rc.5,
888
+ // rc.6 for one unchanged SHA would deploy the same artifact three more times. An rc
889
+ // already naming this exact SHA is the desired state — say so and stop. The exception
890
+ // is an explicit retry of a terminal failed attempt: it receives a fresh immutable RC.
891
+ const at = existing.filter((x) => shaForTag(x.tag) === sha)
892
+ const localRetryFrom = retryFrom == null ? null : existing.find((x) => x.tag === retryFrom) || null
893
+ if (localRetryFrom != null && shaForTag(localRetryFrom.tag) !== sha) {
894
+ console.error(`version: --retry-from ${retryFrom} does not name ${sha.slice(0, 8)} — refusing to retry a different candidate.`)
895
+ return 1
896
+ }
897
+ const localRetry = localRetryFrom == null
898
+ ? null
899
+ : at.filter((x) => x.n > localRetryFrom.n).sort((a, b) => a.n - b.n)[0] || null
900
+ if (localRetry != null) {
901
+ const tag = localRetry.tag
902
+ let pushed = false
903
+ // Same convergence as `tag`: a local rc that was never pushed never deployed.
904
+ if (opts.push && !opts['dry-run']) {
905
+ const remoteSha = remoteTagSha(remote, tag)
906
+ if (remoteSha != null && remoteSha !== sha) {
907
+ console.error(
908
+ `version: ${tag} exists on ${remote} at ${remoteSha.slice(0, 8)}, not ${sha.slice(0, 8)}. ` +
909
+ 'rc tags are immutable too — that number is spent; give this SHA the next one.',
910
+ )
911
+ return 1
912
+ }
913
+ if (remoteSha == null) {
914
+ const push = pushTag(remote, tag)
915
+ if (push.status !== 0) {
916
+ console.error(`version: could not push ${tag} to ${remote}: ${push.stderr}`)
917
+ return 1
918
+ }
919
+ pushed = true
920
+ }
921
+ }
922
+ emitTagResult(opts, { tag, sha, pushed, resumed: true })
923
+ console.error(`version: ${tag} already names ${sha.slice(0, 8)} — no-op (retry resume).`)
924
+ return 0
925
+ }
926
+ if (at.length > 0 && retryFrom == null) {
927
+ const tag = at[at.length - 1].tag
928
+ let pushed = false
929
+ // Same convergence as `tag`: a local rc that was never pushed never deployed.
930
+ if (opts.push && !opts['dry-run']) {
931
+ const remoteSha = remoteTagSha(remote, tag)
932
+ if (remoteSha != null && remoteSha !== sha) {
933
+ console.error(
934
+ `version: ${tag} exists on ${remote} at ${remoteSha.slice(0, 8)}, not ${sha.slice(0, 8)}. ` +
935
+ 'rc tags are immutable too — that number is spent; give this SHA the next one.',
936
+ )
937
+ return 1
938
+ }
939
+ if (remoteSha == null) {
940
+ const push = pushTag(remote, tag)
941
+ if (push.status !== 0) {
942
+ console.error(`version: could not push ${tag} to ${remote}: ${push.stderr}`)
943
+ return 1
944
+ }
945
+ pushed = true
946
+ }
947
+ }
948
+ emitTagResult(opts, { tag, sha, pushed, resumed: true })
949
+ console.error(`version: ${tag} already names ${sha.slice(0, 8)} — no-op (resume).`)
950
+ return 0
951
+ }
952
+
953
+ // An rc must name a SHA that is already on the production branch — the same guard `tag`
954
+ // applies, and for a sharper reason. Both topologies require the merge to happen first
955
+ // (GitFlow merges the promotion, GitHub Flow merges the rebased release branch), so an
956
+ // unmerged SHA here means the order was broken. Without this, minting on an unmerged
957
+ // branch fires a real production build and deploys a candidate that `tag` will later
958
+ // refuse — the contradiction surfacing only at step 5, with traffic already shifted,
959
+ // live on an artifact that cannot be tagged. Fail now, while it is still a no-op.
960
+ // Resolve against a freshly-fetched remote tip — a stale local main is not "the ship SHA".
961
+ {
962
+ const production = opts['production-branch']
963
+ if (typeof production === 'string') {
964
+ const resolved = resolveProductionAncestryRef(production, remote)
965
+ if (resolved.error) {
966
+ console.error(`version: ${resolved.error}`)
967
+ return 1
968
+ }
969
+ const ok = git(['merge-base', '--is-ancestor', sha, resolved.ref], { allowFail: true }).status === 0
970
+ if (!ok) {
971
+ console.error(
972
+ `version: ${sha.slice(0, 8)} is not an ancestor of ${resolved.ref} — refusing to mint an rc. ` +
973
+ 'Merge the promotion first; the rc names the SHA that will ship, not one that might.',
974
+ )
975
+ return 1
976
+ }
977
+ }
978
+ }
979
+
980
+ // Everything below only matters when we intend to push, because only a pushed rc
981
+ // fires a deploy. Without --push this stays a purely local operation.
982
+ const willPush = Boolean(opts.push) && !opts['dry-run']
983
+ const remoteRcs = willPush ? remoteRcTags(remote, base) : []
984
+
985
+ // The same resume the local branch above handles, but across clones: a session that
986
+ // died after pushing rc.2 resumes on a machine whose checkout never fetched it. The
987
+ // artifact for this SHA is already built and already deployed — minting rc.3 would
988
+ // deploy it a second time, which is exactly what the resume guard exists to prevent.
989
+ const remoteAt = remoteRcs.filter((x) => x.sha === sha)
990
+ const remoteRetryFrom = retryFrom == null ? null : remoteRcs.find((x) => x.tag === retryFrom) || null
991
+ if (retryFrom != null && localRetryFrom == null && remoteRetryFrom == null) {
992
+ console.error(`version: --retry-from ${retryFrom} does not exist locally or on ${remote}.`)
993
+ return 1
994
+ }
995
+ if (remoteRetryFrom != null && remoteRetryFrom.sha !== sha) {
996
+ console.error(`version: --retry-from ${retryFrom} on ${remote} does not name ${sha.slice(0, 8)} — refusing to retry a different candidate.`)
997
+ return 1
998
+ }
999
+ const remoteRetry = remoteRetryFrom == null
1000
+ ? null
1001
+ : remoteAt.filter((x) => x.n > remoteRetryFrom.n).sort((a, b) => a.n - b.n)[0] || null
1002
+ if (remoteRetry != null) {
1003
+ emitTagResult(opts, { tag: remoteRetry.tag, sha, resumed: true })
1004
+ console.error(`version: ${remoteRetry.tag} already names ${sha.slice(0, 8)} on ${remote} — no-op (retry resume from another clone).`)
1005
+ return 0
1006
+ }
1007
+ if (remoteAt.length > 0 && retryFrom == null) {
1008
+ const tag = remoteAt[remoteAt.length - 1].tag
1009
+ emitTagResult(opts, { tag, sha, resumed: true })
1010
+ console.error(`version: ${tag} already names ${sha.slice(0, 8)} on ${remote} — no-op (resume from another clone).`)
1011
+ return 0
1012
+ }
1013
+
1014
+ const highest = Math.max(0, ...existing.map((x) => x.n), ...remoteRcs.map((x) => x.n))
1015
+ const tag = `${base}-rc.${highest + 1}`
1016
+ const message = typeof opts.message === 'string' ? opts.message : `Release candidate ${tag}`
1017
+ if (opts['dry-run']) {
1018
+ emitTagResult(opts, { tag, sha })
1019
+ console.error(`version: would tag ${sha.slice(0, 8)} as ${tag}${opts.push ? ` and push to ${remote}` : ''}.`)
1020
+ return 0
1021
+ }
1022
+
1023
+ // Belt and braces over the union numbering above: check this exact tag before writing
1024
+ // anything local. A rejected push after a successful `git tag` is the failure mode that
1025
+ // strands a wrong-SHA tag in the working clone, and it surfaces as a stack trace rather
1026
+ // than a diagnosis. Fail here instead, with a sentence that says what to do.
1027
+ if (willPush) {
1028
+ const remoteSha = remoteTagSha(remote, tag)
1029
+ if (remoteSha != null && remoteSha !== sha) {
1030
+ console.error(
1031
+ `version: ${tag} exists on ${remote} at ${remoteSha.slice(0, 8)}, not ${sha.slice(0, 8)}. ` +
1032
+ 'rc tags are immutable too — that number is spent; re-run to take the next one.',
1033
+ )
1034
+ return 1
1035
+ }
1036
+ }
1037
+
1038
+ git(['tag', '-a', tag, sha, '-m', message])
1039
+ if (opts.push) {
1040
+ const pushed = pushTag(remote, tag)
1041
+ if (pushed.status !== 0) {
1042
+ // Lost a race, or the remote moved between the check and the push. Undo the local
1043
+ // tag so the clone does not keep a candidate the remote never accepted — a stale
1044
+ // local rc silently poisons the NEXT run's numbering.
1045
+ git(['tag', '-d', tag], { allowFail: true })
1046
+ console.error(`version: could not push ${tag} to ${remote}: ${pushed.stderr}`)
1047
+ return 1
1048
+ }
1049
+ }
1050
+ emitTagResult(opts, { tag, sha, pushed: Boolean(opts.push) })
1051
+ return 0
1052
+ }
1053
+
1054
+ case 'tag': {
1055
+ const gate = requireProductionTarget(opts, 'tag')
1056
+ if (gate !== null) return gate
1057
+ if (typeof opts.version !== 'string' || typeof opts.sha !== 'string') {
1058
+ console.error('version: tag requires --version <v> --sha <sha>')
1059
+ return 2
1060
+ }
1061
+ const tag = tagName(opts.version, prefix)
1062
+ const sha = resolveSha(opts.sha)
1063
+ if (sha == null) {
1064
+ console.error(`version: --sha "${opts.sha}" does not resolve to a commit in this repo.`)
1065
+ return 2
1066
+ }
1067
+ const remote = typeof opts.remote === 'string' ? opts.remote : 'origin'
1068
+
1069
+ // Invariant 2: a tag is immutable. Same SHA → resume-safe no-op. Different SHA → stop.
1070
+ if (tagExists(tag)) {
1071
+ const at = shaForTag(tag)
1072
+ if (at !== sha) {
1073
+ console.error(
1074
+ `version: ${tag} already exists at ${at.slice(0, 8)}, not ${sha.slice(0, 8)}. ` +
1075
+ 'Tags are immutable and are never moved or deleted. If that release was rolled back, ' +
1076
+ 'the version is burned — take the next patch (references/versioning.md).',
1077
+ )
1078
+ return 1
1079
+ }
1080
+ // Resume, but CONVERGE: a local tag is no evidence the remote has it.
1081
+ if (opts.push && !opts['dry-run']) {
1082
+ const { pushed, error } = ensureRemoteTag(remote, tag, sha)
1083
+ if (error) {
1084
+ console.error(`version: ${error}`)
1085
+ return 1
1086
+ }
1087
+ emitTagResult(opts, { tag, sha, pushed, resumed: true })
1088
+ return 0
1089
+ }
1090
+ emitTagResult(opts, { tag, sha, resumed: true })
1091
+ console.error(`version: ${tag} already points at ${sha.slice(0, 8)} — no-op (resume).`)
1092
+ return 0
1093
+ }
1094
+
1095
+ // A tag must name a SHA that is actually on the production branch — against a freshly
1096
+ // fetched remote tip, never a potentially stale local branch (identity-v0.4.1).
1097
+ const production = opts['production-branch']
1098
+ if (typeof production === 'string') {
1099
+ const resolved = resolveProductionAncestryRef(production, remote)
1100
+ if (resolved.error) {
1101
+ console.error(`version: ${resolved.error}`)
1102
+ return 1
1103
+ }
1104
+ const ok = git(['merge-base', '--is-ancestor', sha, resolved.ref], { allowFail: true }).status === 0
1105
+ if (!ok) {
1106
+ console.error(`version: ${sha.slice(0, 8)} is not an ancestor of ${resolved.ref} — refusing to tag.`)
1107
+ return 1
1108
+ }
1109
+ }
1110
+
1111
+ const message = typeof opts.message === 'string' ? opts.message : `Release ${tag}`
1112
+ if (opts['dry-run']) {
1113
+ emitTagResult(opts, { tag, sha })
1114
+ console.error(`version: would tag ${sha.slice(0, 8)} as ${tag}${opts.push ? ` and push to ${remote}` : ''}.`)
1115
+ return 0
1116
+ }
1117
+
1118
+ // The remote is the source of truth for "was this version published?" — check before
1119
+ // minting a local tag that could never be pushed anyway.
1120
+ if (opts.push) {
1121
+ const remoteSha = remoteTagSha(remote, tag)
1122
+ if (remoteSha != null && remoteSha !== sha) {
1123
+ console.error(`version: ${tag} exists on ${remote} at ${remoteSha.slice(0, 8)} — that version is burned; take the next patch.`)
1124
+ return 1
1125
+ }
1126
+ }
1127
+
1128
+ git(['tag', '-a', tag, sha, '-m', message])
1129
+ if (opts.push) {
1130
+ const pushed = pushTag(remote, tag)
1131
+ if (pushed.status !== 0) {
1132
+ git(['tag', '-d', tag], { allowFail: true })
1133
+ console.error(`version: could not push ${tag} to ${remote}: ${pushed.stderr}`)
1134
+ return 1
1135
+ }
1136
+ }
1137
+ emitTagResult(opts, { tag, sha, pushed: Boolean(opts.push) })
1138
+ return 0
1139
+ }
1140
+
1141
+ case 'verify': {
1142
+ if (typeof opts.version !== 'string' || typeof opts.sha !== 'string') {
1143
+ console.error('version: verify requires --version <v> --sha <shipped-sha>')
1144
+ return 2
1145
+ }
1146
+ const tag = tagName(opts.version, prefix)
1147
+ if (!tagExists(tag)) {
1148
+ console.error(`version: ${tag} does not exist.`)
1149
+ return 1
1150
+ }
1151
+ const at = shaForTag(tag)
1152
+ const sha = resolveSha(opts.sha)
1153
+ if (sha == null) {
1154
+ console.error(`version: --sha "${opts.sha}" does not resolve to a commit in this repo.`)
1155
+ return 2
1156
+ }
1157
+ const live = typeof opts['live-sha'] === 'string' ? git(['rev-parse', opts['live-sha']], { allowFail: true }).stdout || opts['live-sha'] : null
1158
+
1159
+ const tagMatches = at === sha
1160
+ const liveMatches = live == null ? null : live === sha
1161
+ console.log(JSON.stringify({ tag, tagSha: at, shippedSha: sha, liveSha: live, tagMatches, liveMatches }, null, 2))
1162
+ if (!tagMatches) {
1163
+ console.error(`version: ${tag} names ${at.slice(0, 8)}, but the shipped SHA is ${sha.slice(0, 8)}.`)
1164
+ return 1
1165
+ }
1166
+ if (liveMatches === false) {
1167
+ console.error(`version: the live revision (${live.slice(0, 8)}) is not the promoted SHA (${sha.slice(0, 8)}).`)
1168
+ return 1
1169
+ }
1170
+ return 0
1171
+ }
1172
+
1173
+ default:
1174
+ console.error('version: unknown command. Use plan|changelog|notes|withdraw|rc|tag|verify (see --help).')
1175
+ return 2
1176
+ }
1177
+ }
1178
+
1179
+ // Only run the CLI when invoked directly (so tests can import the pure functions).
1180
+ //
1181
+ // The catch is not decoration. A refusal thrown out of `main` reaches the operator as a V8 stack
1182
+ // trace with the message buried in the middle of it, which is what pi session 019faea6 saw and
1183
+ // read as "the release tooling is broken". A refusal is a designed outcome of this tool, so it
1184
+ // exits like one: the message on stderr, exit 2, no stack. Genuine bugs are the other case and
1185
+ // still owe their stack, so an error carrying no message — or one raised before the operator's
1186
+ // input was even read — prints in full rather than being flattened into a tidy one-liner that
1187
+ // hides where it came from.
1188
+ if (isMainModule(import.meta.url)) {
1189
+ try {
1190
+ process.exit(main(process.argv.slice(2)))
1191
+ } catch (error) {
1192
+ const message = error && typeof error.message === 'string' ? error.message : ''
1193
+ if (message.startsWith('version: ')) {
1194
+ console.error(message)
1195
+ process.exit(2)
1196
+ }
1197
+ throw error
1198
+ }
1199
+ }