@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,1538 @@
1
+ #!/usr/bin/env node
2
+ // metadata-sync — post-release metadata adapter for the verified-SHA GitHub Flow mode (#800).
3
+ //
4
+ // verified-sha-github-flow.md already requires: after the final tag, open a PR against current
5
+ // main that cuts the CHANGELOG section and synchronizes package version metadata, preserving
6
+ // newer unreleased work. Nothing ran it. identity v0.4.1 shipped with package.json still at
7
+ // 0.2.0 and no CHANGELOG [0.4.1] until a hand-rolled PR (identity#678).
8
+ //
9
+ // This script is that adapter:
10
+ // metadata-sync.mjs plan — compute the file edits (CHANGELOG + package versions)
11
+ // metadata-sync.mjs open — isolate and reconcile the exact branch, commit, and PR
12
+ // metadata-sync.mjs record-delivery — prove the merge, then record canonical merged state
13
+ // metadata-sync.mjs na — record an explicit n/a with reason (required for completeness)
14
+ //
15
+ // Pure helpers are exported so the unit suite can pin the planning/recording surface without
16
+ // a network. The CLI injects git/gh runners for the open path; tests pass stubs.
17
+ //
18
+ // FAIL CLOSED: open without --version/--sha is usage (exit 2). n/a without a reason is exit 2.
19
+ // An opened record without a PR number/url is incomplete (validateMetadataPr). A released
20
+ // attempt without metadataPr is incomplete in report.mjs — that is the handoff half.
21
+ //
22
+ // Doctrine: references/verified-sha-github-flow.md § Final claim and metadata ordering.
23
+ // Regression: identity-v0.4.1 · identity#677 (CF Insights) · identity#678 (metadata PR).
24
+
25
+ import fs from 'node:fs'
26
+ import path from 'node:path'
27
+ import os from 'node:os'
28
+ import { spawnSync } from 'node:child_process'
29
+ import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
30
+ import { createHash } from 'node:crypto'
31
+ import { fileURLToPath } from 'node:url'
32
+ import {
33
+ insertSection,
34
+ renderChangelogSection,
35
+ } from './version.mjs'
36
+ import { METADATA_SYNC_SOURCE } from './metadata-pr-status.mjs'
37
+ import { RELEASE_STATE_CONTRACT } from '../../resolve-issues/scripts/release-state-contract.mjs'
38
+
39
+ const VERSION_SCRIPT = fileURLToPath(new URL('./version.mjs', import.meta.url))
40
+
41
+ export const USAGE = `metadata-sync — post-release metadata adapter (verified-sha-github-flow).
42
+
43
+ After the final release claim, open a PR against current main that cuts the CHANGELOG
44
+ section and bumps declared package version files, then record release.metadataPr on the
45
+ manifest. A released attempt without that record is incomplete unless status is n/a with
46
+ a reason (report.mjs).
47
+ plan --version <v> --date <YYYY-MM-DD> [--changes <json>] [--package-file <path>]...
48
+ [--cwd <dir>] [--changelog <path>] [--dry-run]
49
+ Compute the file edits. Prints JSON {version, branch, changelog, packages}.
50
+
51
+ open --manifest <run.json> --version <v> --sha <shipped> --date <YYYY-MM-DD>
52
+ [--changes <json-or-safe-relative-file>] [--package-file <path>]... [--cwd <dir>] [--changelog <path>]
53
+ [--repo <owner/name>] [--base <branch=main>] [--branch <name>]
54
+ [--title <t>] [--body <b>]
55
+ [--lock-file <path> --lock-command <repository-command>]
56
+ [--dry-run] [--record-plan]
57
+ Build from fresh origin/<base> in a disposable worktree. Reconcile exact repo,
58
+ base, branch, version, shipped SHA, expected content, commit, and PR identities;
59
+ refuse divergence without force. Metadata paths must be normalized, single-link
60
+ regular repository files (symlinks/non-regular files are rejected); a missing,
61
+ unparseable, or unsupported package fails closed, while a missing CHANGELOG is
62
+ created in the commit. Planned bytes go directly to a disposable Git index and
63
+ private execution tree, never caller metadata pathnames. Requested package/lock
64
+ versions are verified before and after ordinary commit hooks. Push is ordinary;
65
+ only a known Mergify pre-push rejection receives one --no-verify retry. The
66
+ manifest must live outside the caller repository.
67
+ --dry-run skips git/gh; with --record-plan also writes status:"planned".
68
+
69
+
70
+ record-delivery --manifest <run.json> --watch-result <watcher.json> [--cwd <dir>]
71
+ Consume terminal JSON from watch-pr-delivery.mjs, then independently re-read the
72
+ exact PR and prove commit->merge->fresh-base ancestry plus expected file hashes
73
+ before recording release.metadataPr.status="merged" through the canonical contract.
74
+
75
+ na --manifest <run.json> --reason <text> [--version <v>]
76
+ Record release.metadataPr = {status:"n/a", reason} (reason REQUIRED).
77
+
78
+ migrate --manifest <run.json>
79
+ Convert a legacy hand-written release.metadataSync record into a canonical
80
+ release.metadataPr with source metadata-sync.mjs. Reject-only by default; this
81
+ is the opt-in migration path.
82
+
83
+ Exit codes:
84
+ 0 ok
85
+ 1 open/plan failed (git/gh non-zero, incomplete PR record)
86
+ 2 usage / unreadable input
87
+
88
+ Why: identity v0.4.1 shipped without the adapter; metadata landed as identity#678.
89
+ See references/verified-sha-github-flow.md and issue #800.`
90
+
91
+ // ---- pure planning / recording --------------------------------------------------------------
92
+
93
+ export function defaultBranchName(version) {
94
+ return `chore/release-metadata-${String(version).replace(/^v/, '')}`
95
+ }
96
+
97
+ // ---- package-file parsers -------------------------------------------------------------------
98
+ //
99
+ // Supported version metadata: JSON (`{"version": ...}`) and TOML with a `version` key in
100
+ // the `[project]` or `[tool.poetry]` table (PEP 621 / Poetry). Anything else fails closed
101
+ // with an unsupported-format error — the adapter never guesses at a format it cannot verify.
102
+
103
+ const TOML_VERSION_TABLES = new Set(['project', 'tool.poetry'])
104
+
105
+ export function packageParserFor(rel) {
106
+ const base = path.basename(String(rel)).toLowerCase()
107
+ if (base.endsWith('.json')) return 'json'
108
+ // uv.lock / Cargo.lock are TOML; parsing them as TOML lets a lock requested directly
109
+ // fail with the actionable "use --lock-file/--lock-command" unsupported-format error.
110
+ if (base.endsWith('.toml') || base.endsWith('.lock')) return 'toml'
111
+ return null
112
+ }
113
+
114
+ // Minimal line-based TOML reader for the version-metadata subset. Returns
115
+ // { version, name, line } (line = index of the version assignment) or null when no
116
+ // supported table carries a version key.
117
+ export function parseTomlMetadata(raw) {
118
+ const lines = String(raw).split('\n')
119
+ let table = null
120
+ let name = null
121
+ let hit = null
122
+ for (let i = 0; i < lines.length; i += 1) {
123
+ const header = lines[i].match(/^\s*\[([^\]]+)\]\s*$/)
124
+ if (header) {
125
+ table = header[1].trim()
126
+ continue
127
+ }
128
+ if (table === 'project' && name == null) {
129
+ const nm = lines[i].match(/^\s*name\s*=\s*(["'])(.*?)\1/)
130
+ if (nm) name = nm[2]
131
+ }
132
+ if (hit == null && TOML_VERSION_TABLES.has(table)) {
133
+ const vm = lines[i].match(/^\s*version\s*=\s*(["'])(.*?)\1/)
134
+ if (vm) hit = { version: vm[2], line: i }
135
+ }
136
+ }
137
+ if (!hit) return null
138
+ return { version: hit.version, name, line: hit.line }
139
+ }
140
+
141
+ // Rewrite only the version literal on its own line, preserving the rest of the file
142
+ // byte-for-byte. Returns null when the file has no supported version key.
143
+ export function updateTomlVersion(raw, nextVersion) {
144
+ const parsed = parseTomlMetadata(raw)
145
+ if (!parsed) return null
146
+ const lines = String(raw).split('\n')
147
+ lines[parsed.line] = lines[parsed.line].replace(
148
+ /^(\s*version\s*=\s*)(["']).*?\2/,
149
+ `$1$2${nextVersion}$2`,
150
+ )
151
+ return lines.join('\n')
152
+ }
153
+
154
+ // { version, name? } or null when the raw content cannot be read with the given parser.
155
+ function readPackageVersion(raw, parser) {
156
+ if (parser === 'json') {
157
+ try {
158
+ const json = JSON.parse(raw)
159
+ return {
160
+ version: json.version != null ? String(json.version) : null,
161
+ name: json.name != null ? String(json.name) : null,
162
+ }
163
+ } catch {
164
+ return null
165
+ }
166
+ }
167
+ if (parser === 'toml') {
168
+ const parsed = parseTomlMetadata(raw)
169
+ return parsed ? { version: parsed.version, name: parsed.name } : null
170
+ }
171
+ return null
172
+ }
173
+
174
+ // Lock verification accepts uv.lock-style TOML and npm package-lock JSON. Both formats
175
+ // must identify the root project and hold the target version; an ambiguous or incomplete
176
+ // root entry never counts as proof.
177
+ export function verifyLockMetadata(raw, { name, version } = {}) {
178
+ if (!name) {
179
+ return { ok: false, reason: 'cannot verify lock metadata without a project name' }
180
+ }
181
+
182
+ const text = String(raw)
183
+ if (text.trimStart().startsWith('{')) {
184
+ let lock
185
+ try {
186
+ lock = JSON.parse(text)
187
+ } catch {
188
+ return { ok: false, reason: 'invalid npm package-lock JSON' }
189
+ }
190
+ const entries = []
191
+ if (lock && typeof lock === 'object' && !Array.isArray(lock) && ('name' in lock || 'version' in lock)) {
192
+ entries.push({ location: 'top-level', name: lock.name, version: lock.version })
193
+ }
194
+ const root = lock?.packages?.['']
195
+ if (root && typeof root === 'object' && !Array.isArray(root) && ('name' in root || 'version' in root)) {
196
+ entries.push({ location: 'packages[""]', name: root.name, version: root.version })
197
+ }
198
+ if (entries.length === 0) {
199
+ return { ok: false, reason: 'no npm root package metadata entry' }
200
+ }
201
+ for (const entry of entries) {
202
+ if (entry.name == null) {
203
+ return { ok: false, reason: `npm root package metadata at ${entry.location} is missing name` }
204
+ }
205
+ if (String(entry.name) !== name) {
206
+ return {
207
+ ok: false,
208
+ reason: `npm root package at ${entry.location} is named ${JSON.stringify(String(entry.name))}, expected ${JSON.stringify(name)}`,
209
+ }
210
+ }
211
+ if (entry.version == null) {
212
+ return { ok: false, reason: `npm root package metadata at ${entry.location} is missing version` }
213
+ }
214
+ if (String(entry.version) !== version) {
215
+ return {
216
+ ok: false,
217
+ reason: `${name} at ${entry.location} is ${JSON.stringify(String(entry.version))}, expected ${JSON.stringify(version)}`,
218
+ }
219
+ }
220
+ }
221
+ return { ok: true }
222
+ }
223
+
224
+ let current = null
225
+ const blocks = []
226
+ for (const line of text.split('\n')) {
227
+ if (/^\s*\[\[package\]\]/.test(line)) {
228
+ current = {}
229
+ blocks.push(current)
230
+ continue
231
+ }
232
+ if (/^\s*\[/.test(line)) {
233
+ current = null
234
+ continue
235
+ }
236
+ if (current) {
237
+ const kv = line.match(/^\s*(name|version)\s*=\s*(["'])(.*?)\2/)
238
+ if (kv && current[kv[1]] == null) current[kv[1]] = kv[3]
239
+ }
240
+ }
241
+ const entry = blocks.find((b) => b.name === name)
242
+ if (!entry) {
243
+ return { ok: false, reason: `no [[package]] entry named ${JSON.stringify(name)}` }
244
+ }
245
+ if (entry.version !== version) {
246
+ return {
247
+ ok: false,
248
+ reason: `${name} at ${JSON.stringify(entry.version ?? null)}, expected ${JSON.stringify(version)}`,
249
+ }
250
+ }
251
+ return { ok: true }
252
+ }
253
+
254
+ export function planMetadataSync({
255
+ cwd = process.cwd(),
256
+ version,
257
+ date,
258
+ changes = [],
259
+ packageFiles = ['package.json'],
260
+ changelogPath = 'CHANGELOG.md',
261
+ repo = null,
262
+ branch = null,
263
+ sources = null,
264
+ } = {}) {
265
+ if (!version) throw new Error('planMetadataSync: --version is required')
266
+ if (!date) throw new Error('planMetadataSync: --date is required')
267
+ const ver = String(version).replace(/^v/, '')
268
+ if (!sources) sources = filesystemSources(cwd, metadataDeclarations(packageFiles, changelogPath))
269
+ const absChangelog = path.resolve(cwd, changelogPath)
270
+ const currentChangelog = sources.get(changelogPath)?.raw ?? ''
271
+ const section = renderChangelogSection({ version: ver, date, changes, repo })
272
+ const inserted = insertSection(currentChangelog, section, ver)
273
+
274
+ const packages = []
275
+ for (const rel of packageFiles) {
276
+ const abs = path.resolve(cwd, rel)
277
+ const parser = packageParserFor(rel)
278
+ const source = sources.get(rel)
279
+ if (!source || source.missing) {
280
+ packages.push({
281
+ path: rel, abs, parser, from: null, to: ver,
282
+ missing: true, noop: false, error: `missing: ${rel}`,
283
+ })
284
+ continue
285
+ }
286
+ const raw = source.raw
287
+ if (parser == null) {
288
+ packages.push({
289
+ path: rel, abs, parser, from: null, to: ver, noop: false,
290
+ error: `unsupported-format: ${rel} (supported: .json version field, .toml [project]/[tool.poetry] version)`,
291
+ })
292
+ continue
293
+ }
294
+ const current = readPackageVersion(raw, parser)
295
+ if (!current || current.version == null) {
296
+ packages.push({
297
+ path: rel, abs, parser, from: null, to: ver, noop: false,
298
+ error: parser === 'toml'
299
+ ? `unsupported-format: ${rel} has no version key in [project] or [tool.poetry]; declare a repository command via --lock-file/--lock-command or drop the file`
300
+ : `unparseable: ${rel} is not valid ${parser} version metadata`,
301
+ })
302
+ continue
303
+ }
304
+ const from = current.version
305
+ if (from === ver) {
306
+ packages.push({ path: rel, abs, parser, from, to: ver, name: current.name, noop: true, next: raw })
307
+ continue
308
+ }
309
+ const next = parser === 'toml'
310
+ ? updateTomlVersion(raw, ver)
311
+ : `${JSON.stringify({ ...JSON.parse(raw), version: ver }, null, 2)}\n`
312
+ if (next == null) {
313
+ packages.push({
314
+ path: rel, abs, parser, from, to: ver, noop: false,
315
+ error: `unsupported-format: could not rewrite version in ${rel}`,
316
+ })
317
+ continue
318
+ }
319
+ packages.push({ path: rel, abs, parser, from, to: ver, name: current.name, noop: false, next })
320
+ }
321
+
322
+ return {
323
+ version: ver,
324
+ date,
325
+ branch: branch || defaultBranchName(ver),
326
+ changelogPath,
327
+ changelog: {
328
+ path: absChangelog,
329
+ rel: changelogPath,
330
+ next: inserted.text,
331
+ noop: !!inserted.noop,
332
+ },
333
+ packages,
334
+ cwd,
335
+ }
336
+ }
337
+
338
+ // Requested package files that could not be planned. open refuses while this is non-empty;
339
+ // plan reports it and exits 1. A missing/unparseable/unsupported file is never a noop.
340
+ export function planPackageErrors(plan) {
341
+ return (plan?.packages || []).filter((p) => p.error || p.missing)
342
+ }
343
+
344
+ // Post-apply gate: re-read every requested package file (and the declared lock, if any)
345
+ // from disk and prove it holds the target version. Runs before commit and before
346
+ // release.metadataPr is recorded — a PR record without this proof is the forex#198 bug.
347
+ export function verifyAppliedPlan(plan, { lock } = {}) {
348
+ const failures = []
349
+ for (const p of plan?.packages || []) {
350
+ if (p.error || p.missing) {
351
+ failures.push(`${p.path}: ${p.error || 'missing'}`)
352
+ continue
353
+ }
354
+ let raw
355
+ try {
356
+ raw = fs.readFileSync(path.resolve(plan.cwd, p.path), 'utf8')
357
+ } catch (err) {
358
+ failures.push(`${p.path}: unreadable after apply (${err.message})`)
359
+ continue
360
+ }
361
+ const current = readPackageVersion(raw, p.parser)
362
+ if (!current || current.version !== plan.version) {
363
+ failures.push(
364
+ `${p.path}: holds ${JSON.stringify(current?.version ?? null)} after apply, expected ${JSON.stringify(plan.version)}`,
365
+ )
366
+ }
367
+ }
368
+ if (lock) {
369
+ let raw = null
370
+ try {
371
+ raw = fs.readFileSync(path.resolve(plan.cwd, lock.rel), 'utf8')
372
+ } catch (err) {
373
+ failures.push(`${lock.rel}: unreadable after lock command (${err.message})`)
374
+ }
375
+ if (raw != null) {
376
+ const checked = verifyLockMetadata(raw, { name: lock.name, version: plan.version })
377
+ if (!checked.ok) failures.push(`${lock.rel}: ${checked.reason}`)
378
+ }
379
+ }
380
+ return { ok: failures.length === 0, failures }
381
+ }
382
+
383
+ // Post-commit gate: re-verify the COMMITTED tree. Commit hooks (pre-commit /
384
+ // commit-msg / prepare-commit-msg) run after the pre-commit verification and can
385
+ // rewrite — and re-stage — requested metadata, so HEAD is no longer proof by
386
+ // construction. `show(rel)` must run `git show HEAD:<rel>` and return the runner
387
+ // result ({ status, stdout, stderr }); fail closed when it is absent, when an entry
388
+ // is unreadable, or when any requested package file / declared lock in HEAD does not
389
+ // hold the target version (#960 round 2 — the recorded PR must never advertise a
390
+ // version the branch does not contain).
391
+ export function verifyCommittedPlan(plan, { lock, show } = {}) {
392
+ const failures = []
393
+ if (typeof show !== 'function') {
394
+ return { ok: false, failures: ['verifyCommittedPlan: a show runner (git show HEAD:<path>) is required'] }
395
+ }
396
+ for (const p of plan?.packages || []) {
397
+ if (p.error || p.missing) {
398
+ failures.push(`${p.path}: ${p.error || 'missing'}`)
399
+ continue
400
+ }
401
+ const r = show(p.path)
402
+ if ((r?.status ?? 1) !== 0) {
403
+ failures.push(`${p.path}: unreadable in committed tree (${String(r?.stderr || r?.stdout || 'git show failed').trim()})`)
404
+ continue
405
+ }
406
+ const current = readPackageVersion(r.stdout || '', p.parser)
407
+ if (!current || current.version !== plan.version) {
408
+ failures.push(
409
+ `${p.path}: committed tree holds ${JSON.stringify(current?.version ?? null)}, expected ${JSON.stringify(plan.version)}`,
410
+ )
411
+ }
412
+ }
413
+ if (lock) {
414
+ const r = show(lock.rel)
415
+ if ((r?.status ?? 1) !== 0) {
416
+ failures.push(`${lock.rel}: unreadable in committed tree (${String(r?.stderr || r?.stdout || 'git show failed').trim()})`)
417
+ } else {
418
+ const checked = verifyLockMetadata(r.stdout || '', { name: lock.name, version: plan.version })
419
+ if (!checked.ok) failures.push(`${lock.rel}: committed tree: ${checked.reason}`)
420
+ }
421
+ }
422
+ return { ok: failures.length === 0, failures }
423
+ }
424
+
425
+
426
+ export function applyMetadataFiles(plan, { runGit } = {}) {
427
+ if (!plan) throw new Error('applyMetadataFiles: plan required')
428
+ if (typeof runGit !== 'function') {
429
+ throw new Error('applyMetadataFiles: a disposable Git index runner is required')
430
+ }
431
+ stageMetadataPlan(runGit, plan)
432
+ return plan
433
+ }
434
+
435
+ export function validateMetadataPr(meta) {
436
+ if (!meta || typeof meta !== 'object') {
437
+ return { ok: false, reason: 'missing release.metadataPr' }
438
+ }
439
+ if (meta.status === 'n/a') {
440
+ if (!meta.reason || !String(meta.reason).trim()) {
441
+ return { ok: false, reason: 'n/a requires a reason' }
442
+ }
443
+ return { ok: true }
444
+ }
445
+ if (meta.status === 'opened' || meta.status === 'merged') {
446
+ if (meta.number == null && !meta.url) {
447
+ return { ok: false, reason: `${meta.status} requires number or url` }
448
+ }
449
+ return { ok: true }
450
+ }
451
+ if (meta.status === 'planned') {
452
+ // dry-run / record-plan intermediate — not complete for handoff, but structurally valid
453
+ return { ok: true, complete: false }
454
+ }
455
+ return { ok: false, reason: `unknown status ${JSON.stringify(meta.status)}` }
456
+ }
457
+
458
+ export function recordMetadataPr(manifest, entry) {
459
+ const m = structuredClone(manifest || {})
460
+ if (!m.release || typeof m.release !== 'object') m.release = {}
461
+ const checked = validateMetadataPr(entry)
462
+ if (!checked.ok) throw new Error(`recordMetadataPr: ${checked.reason}`)
463
+ m.release.metadataPr = {
464
+ ...entry,
465
+ source: entry.source || METADATA_SYNC_SOURCE,
466
+ recordedAt: entry.recordedAt || new Date().toISOString(),
467
+ }
468
+ return m
469
+ }
470
+
471
+ // Opt-in migration from a hand-written `release.metadataSync` to the canonical
472
+ // `release.metadataPr`. Hand-written records do NOT satisfy the completeness gate
473
+ // on their own; this helper lets an operator convert them under adapter control.
474
+ export function migrateMetadataSync(manifest) {
475
+ const m = manifest || {}
476
+ const r = m.release || {}
477
+ const legacy = r.metadataSync
478
+ if (legacy == null) {
479
+ throw new Error('migrateMetadataSync: no release.metadataSync to migrate')
480
+ }
481
+ if (typeof legacy === 'string') {
482
+ return {
483
+ status: 'opened',
484
+ number: parsePrNumber(legacy),
485
+ url: legacy,
486
+ source: METADATA_SYNC_SOURCE,
487
+ }
488
+ }
489
+ if (typeof legacy !== 'object' || Array.isArray(legacy)) {
490
+ throw new Error(`migrateMetadataSync: cannot migrate metadataSync of this shape (${typeof legacy})`)
491
+ }
492
+ if (legacy.status === 'n/a') {
493
+ return {
494
+ status: 'n/a',
495
+ reason: legacy.reason,
496
+ version: legacy.version || null,
497
+ source: METADATA_SYNC_SOURCE,
498
+ }
499
+ }
500
+ return {
501
+ status: 'opened',
502
+ number: legacy.number != null ? legacy.number : null,
503
+ url: legacy.url || null,
504
+ version: legacy.version || null,
505
+ source: METADATA_SYNC_SOURCE,
506
+ }
507
+ }
508
+
509
+ export function parsePrNumber(urlOrText) {
510
+ const text = String(urlOrText || '')
511
+ const m = text.match(/\/pull\/(\d+)/) || text.match(/#(\d+)\b/)
512
+ return m ? Number(m[1]) : null
513
+ }
514
+
515
+ // ---- CLI ------------------------------------------------------------------------------------
516
+
517
+ export function parseArgs(argv) {
518
+ const args = { packageFiles: [], positional: [] }
519
+ for (let i = 0; i < argv.length; i += 1) {
520
+ const a = argv[i]
521
+ if (a === '--help' || a === '-h') args.help = true
522
+ else if (a === '--manifest') args.manifest = argv[++i]
523
+ else if (a === '--version') args.version = argv[++i]
524
+ else if (a === '--sha') args.sha = argv[++i]
525
+ else if (a === '--date') args.date = argv[++i]
526
+ else if (a === '--changes') args.changes = argv[++i]
527
+ else if (a === '--package-file') args.packageFiles.push(argv[++i])
528
+ else if (a === '--cwd') args.cwd = argv[++i]
529
+ else if (a === '--changelog') args.changelog = argv[++i]
530
+ else if (a === '--base') args.base = argv[++i]
531
+ else if (a === '--branch') args.branch = argv[++i]
532
+ else if (a === '--title') args.title = argv[++i]
533
+ else if (a === '--body') args.body = argv[++i]
534
+ else if (a === '--reason') args.reason = argv[++i]
535
+ else if (a === '--repo') args.repo = argv[++i]
536
+ else if (a === '--watch-result') args.watchResult = argv[++i]
537
+ else if (a === '--lock-file') args.lockFile = argv[++i]
538
+ else if (a === '--lock-command') args.lockCommand = argv[++i]
539
+ else if (a === '--dry-run') args.dryRun = true
540
+ else if (a === '--record-plan') args.recordPlan = true
541
+ else if (a === '--json') args.json = true
542
+ else if (a.startsWith('-')) args.unknown = a
543
+ else args.positional.push(a)
544
+ }
545
+ args.cmd = args.positional[0] || null
546
+ if (args.packageFiles.length === 0) args.packageFiles = ['package.json']
547
+ return args
548
+ }
549
+
550
+ function loadJson(file) {
551
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
552
+ }
553
+
554
+ function writeJson(file, value) {
555
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
556
+ }
557
+
558
+ function parseChanges(raw, cwd, runGit = null) {
559
+ if (!raw) return []
560
+ if (raw.startsWith('[') || raw.startsWith('{')) {
561
+ const parsed = JSON.parse(raw)
562
+ return Array.isArray(parsed) ? parsed : [parsed]
563
+ }
564
+ const rel = safeRelativePath(raw, '--changes file')
565
+ const declarations = [{ rel, label: '--changes file' }]
566
+ const materialized = filesystemSources(cwd, declarations).get(rel)
567
+ const source = runGit ? gitTreeSources(runGit, cwd, declarations).get(rel) : materialized
568
+ if (!source || source.missing) {
569
+ throw new Error(`metadata-sync: --changes file ${JSON.stringify(rel)} must exist as a regular repository file`)
570
+ }
571
+ const parsed = JSON.parse(source.raw)
572
+ return Array.isArray(parsed) ? parsed : [parsed]
573
+ }
574
+
575
+ function defaultRun(cmd, args, { cwd, input } = {}) {
576
+ return spawnSync(cmd, args, { encoding: 'utf8', cwd, input })
577
+ }
578
+
579
+ function safeRelativePath(value, label) {
580
+ if (typeof value !== 'string' || value === '' || path.isAbsolute(value) || path.win32.isAbsolute(value)) {
581
+ throw new Error(`metadata-sync: ${label} must be a relative path inside the disposable worktree`)
582
+ }
583
+ const segments = value.split(/[\\/]/)
584
+ if (segments.includes('..')) throw new Error(`metadata-sync: ${label} may not contain .. segments`)
585
+ return value
586
+ }
587
+
588
+ function assertManifestOutsideCaller(file, cwd, runGit) {
589
+ const root = checked(runGit(['rev-parse', '--show-toplevel']), 'git rev-parse --show-toplevel').stdout.trim()
590
+ const rootReal = fs.realpathSync(root)
591
+ const manifestReal = fs.realpathSync(file)
592
+ const relative = path.relative(rootReal, manifestReal)
593
+ const inside = relative === '' || (
594
+ relative !== '..' &&
595
+ !relative.startsWith(`..${path.sep}`) &&
596
+ !path.isAbsolute(relative)
597
+ )
598
+ if (inside) {
599
+ throw new Error('metadata-sync: --manifest must live outside the caller repository so caller bytes/status remain unchanged')
600
+ }
601
+ }
602
+
603
+ function redact(value) {
604
+ return String(value || '')
605
+ .replace(/:\/\/[^\s/@:]+:[^\s/@]+@/g, '://[REDACTED]@')
606
+ .replace(/(gh[opsu]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})/g, '[REDACTED]')
607
+ .trim()
608
+ }
609
+
610
+ function checked(result, label) {
611
+ if ((result.status ?? 1) !== 0) {
612
+ throw new Error(`${label} failed: ${redact(result.stderr || result.stdout || '')}`.trim())
613
+ }
614
+ return result
615
+ }
616
+
617
+ function parseJsonOutput(result, label) {
618
+ checked(result, label)
619
+ try {
620
+ return JSON.parse(String(result.stdout || ''))
621
+ } catch (error) {
622
+ throw new Error(`${label} returned invalid JSON on stdout: ${error.message}`)
623
+ }
624
+ }
625
+
626
+ function sha256(buffer) {
627
+ return createHash('sha256').update(buffer).digest('hex')
628
+ }
629
+
630
+ function validateRepo(repo) {
631
+ if (typeof repo !== 'string' || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
632
+ throw new Error('metadata-sync: target repo must be an exact owner/name identity')
633
+ }
634
+ return repo
635
+ }
636
+
637
+ function repoFromRemoteUrl(value) {
638
+ const text = String(value || '').trim().replace(/\.git$/, '')
639
+ const scp = /^[^@\s]+@[^:]+:(.+)$/.exec(text)
640
+ if (scp) return scp[1].split('/').slice(-2).join('/')
641
+ try {
642
+ const parsed = new URL(text)
643
+ return parsed.pathname.replace(/^\//, '').split('/').slice(-2).join('/')
644
+ } catch {}
645
+ const parts = text.split(/[\\/]/).filter(Boolean)
646
+ return parts.slice(-2).join('/')
647
+ }
648
+
649
+ function resolveRef(runGit, ref, cwd) {
650
+ const result = runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { cwd })
651
+ return (result.status ?? 1) === 0 ? String(result.stdout || '').trim() : null
652
+ }
653
+
654
+ function gitTreeSources(runGit, cwd, declarations) {
655
+ const sources = new Map()
656
+ const seen = new Set()
657
+ for (const declaration of declarations) {
658
+ const rel = safeRelativePath(declaration.rel, declaration.label)
659
+ const normalized = path.normalize(rel)
660
+ if (normalized === '.' || normalized !== rel || rel.includes('\0')) {
661
+ throw new Error(`metadata-sync: ${declaration.label} must be a normalized relative path`)
662
+ }
663
+ if (seen.has(normalized)) {
664
+ throw new Error(`metadata-sync: metadata path ${JSON.stringify(normalized)} is declared more than once`)
665
+ }
666
+ seen.add(normalized)
667
+ const result = runGit(['ls-tree', '-z', 'HEAD', '--', normalized], { cwd })
668
+ checked(result, `inspect ${declaration.label}`)
669
+ const row = String(result.stdout || '')
670
+ if (!row) {
671
+ sources.set(normalized, { missing: true, raw: '' })
672
+ continue
673
+ }
674
+ const tab = row.indexOf('\t')
675
+ const fields = row.slice(0, tab).trim().split(/\s+/)
676
+ const foundPath = row.slice(tab + 1).replace(/\0$/, '')
677
+ if (tab < 0 || foundPath !== normalized || fields.length !== 3) {
678
+ throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} has ambiguous Git identity`)
679
+ }
680
+ const [mode, type, object] = fields
681
+ if (!['100644', '100755'].includes(mode) || type !== 'blob') {
682
+ throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} must be a regular repository file; symlinks and non-regular entries are rejected`)
683
+ }
684
+ const raw = checked(runGit(['cat-file', 'blob', object], { cwd }), `read ${declaration.label}`).stdout
685
+ sources.set(normalized, { missing: false, raw, mode })
686
+ }
687
+ return sources
688
+ }
689
+
690
+ function filesystemSources(cwd, declarations) {
691
+ if (!fs.constants.O_NOFOLLOW) {
692
+ throw new Error('metadata-sync: this platform cannot enforce no-follow metadata reads')
693
+ }
694
+ const root = path.resolve(cwd)
695
+ const rootReal = fs.realpathSync(root)
696
+ const sources = new Map()
697
+ const seen = new Set()
698
+ for (const declaration of declarations) {
699
+ const rel = safeRelativePath(declaration.rel, declaration.label)
700
+ const normalized = path.normalize(rel)
701
+ if (normalized === '.' || normalized !== rel || rel.includes('\0')) {
702
+ throw new Error(`metadata-sync: ${declaration.label} must be a normalized relative path`)
703
+ }
704
+ if (seen.has(normalized)) {
705
+ throw new Error(`metadata-sync: metadata path ${JSON.stringify(normalized)} is declared more than once`)
706
+ }
707
+ seen.add(normalized)
708
+ const abs = path.join(root, normalized)
709
+ let parent = path.dirname(abs)
710
+ let missingParent = false
711
+ while (true) {
712
+ let stat
713
+ try {
714
+ stat = fs.lstatSync(parent, { bigint: true })
715
+ } catch (error) {
716
+ if (error.code !== 'ENOENT') throw error
717
+ missingParent = true
718
+ parent = path.dirname(parent)
719
+ continue
720
+ }
721
+ const relative = path.relative(rootReal, fs.realpathSync(parent))
722
+ const contained = relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
723
+ if (stat.isSymbolicLink() || !stat.isDirectory() || !contained) {
724
+ throw new Error(`metadata-sync: parent of ${declaration.label} ${JSON.stringify(normalized)} must be a contained real directory`)
725
+ }
726
+ if (parent === root) break
727
+ parent = path.dirname(parent)
728
+ }
729
+ if (missingParent) {
730
+ sources.set(normalized, { missing: true, raw: '' })
731
+ continue
732
+ }
733
+ let before
734
+ try {
735
+ before = fs.lstatSync(abs, { bigint: true })
736
+ } catch (error) {
737
+ if (error.code !== 'ENOENT') throw error
738
+ sources.set(normalized, { missing: true, raw: '' })
739
+ continue
740
+ }
741
+ if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1n) {
742
+ throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} must be a single-link regular file; symlinks and non-regular files are rejected`)
743
+ }
744
+ const targetRelative = path.relative(rootReal, fs.realpathSync(abs))
745
+ if (targetRelative === '..' || targetRelative.startsWith(`..${path.sep}`) || path.isAbsolute(targetRelative)) {
746
+ throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} escapes the workspace`)
747
+ }
748
+ const fd = fs.openSync(abs, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
749
+ try {
750
+ const opened = fs.fstatSync(fd, { bigint: true })
751
+ if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino) {
752
+ throw new Error(`metadata-sync: ${declaration.label} ${JSON.stringify(normalized)} changed during validation`)
753
+ }
754
+ sources.set(normalized, { missing: false, raw: fs.readFileSync(fd, 'utf8') })
755
+ } finally {
756
+ fs.closeSync(fd)
757
+ }
758
+ }
759
+ return sources
760
+ }
761
+ function writePlannedMetadataFiles(plan, cwd) {
762
+ const entries = [
763
+ { rel: plan.changelog.rel, next: plan.changelog.next, noop: plan.changelog.noop },
764
+ ...plan.packages.map((item) => ({ rel: item.path, next: item.next, noop: item.noop || item.missing || item.error })),
765
+ ]
766
+ for (const entry of entries) {
767
+ if (entry.noop) continue
768
+ const target = path.resolve(cwd, entry.rel)
769
+ fs.mkdirSync(path.dirname(target), { recursive: true })
770
+ fs.writeFileSync(target, entry.next)
771
+ }
772
+ }
773
+
774
+ function createDryRunExecutionTree(cwd, plan) {
775
+ const executionTree = fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-sync-dry-run-'))
776
+ try {
777
+ fs.cpSync(cwd, executionTree, {
778
+ recursive: true,
779
+ filter(source) {
780
+ const relative = path.relative(cwd, source)
781
+ const firstSegment = relative.split(path.sep)[0]
782
+ return !['.git', 'node_modules'].includes(firstSegment)
783
+ },
784
+ })
785
+ writePlannedMetadataFiles(plan, executionTree)
786
+ return executionTree
787
+ } catch (error) {
788
+ fs.rmSync(executionTree, { recursive: true, force: true })
789
+ throw error
790
+ }
791
+ }
792
+
793
+ function runLockCommandAndVerify({ lockFile, lockCommand, plan, cwd, exec, stageLock = null }) {
794
+ if (!lockFile) return null
795
+ const lockRel = safeRelativePath(lockFile, '--lock-file')
796
+ const source = filesystemSources(cwd, [{ rel: lockRel, label: '--lock-file' }]).get(lockRel)
797
+ const lockBefore = source?.missing ? null : source.raw
798
+ const lockResult = exec(lockCommand, { cwd })
799
+ if ((lockResult.status ?? 1) !== 0) {
800
+ throw new Error(`metadata-sync: lock command failed (${lockCommand}): ${redact(lockResult.stderr || lockResult.stdout)}`)
801
+ }
802
+ const generated = filesystemSources(cwd, [{ rel: lockRel, label: '--lock-file' }]).get(lockRel)
803
+ if (!generated || generated.missing) {
804
+ throw new Error(`metadata-sync: declared lock ${lockRel} is missing after lock command`)
805
+ }
806
+ const name = plan.packages.find((item) => item.name)?.name || null
807
+ const lock = { rel: lockRel, raw: generated.raw, name }
808
+ if (generated.raw !== lockBefore && stageLock) stageLock(lock)
809
+ return lock
810
+ }
811
+
812
+ function versionChangelogPlan({ cwd, version, date, changes, repo, base, changelogRaw, runVersion }) {
813
+ const result = runVersion([
814
+ VERSION_SCRIPT, 'changelog', '--target-branch', base, '--production-branch', base,
815
+ '--version', version, '--date', date, '--changes', JSON.stringify(changes),
816
+ '--repo', repo, '--file', '-', '--dry-run', '--format', 'json',
817
+ ], { cwd, input: Buffer.from(changelogRaw) })
818
+ const payload = parseJsonOutput(result, 'version changelog --dry-run --format json')
819
+ const normalizedVersion = String(version).replace(/^v/, '')
820
+ if (payload.version !== normalizedVersion || payload.file !== '-' || typeof payload.markdown !== 'string') {
821
+ throw new Error('metadata-sync: version changelog JSON does not match the requested version/file contract')
822
+ }
823
+ return payload
824
+ }
825
+
826
+ function planFromSources({ cwd, version, date, changes, packageFiles, changelogPath, repo, branch, base, runVersion, sources }) {
827
+ const safeChangelog = safeRelativePath(changelogPath, '--changelog')
828
+ const safePackages = packageFiles.map((item) => safeRelativePath(item, '--package-file'))
829
+ const changelog = versionChangelogPlan({
830
+ cwd, version, date, changes, repo, base,
831
+ changelogRaw: sources.get(safeChangelog)?.raw ?? '', runVersion,
832
+ })
833
+ const plan = planMetadataSync({
834
+ cwd, version, date, changes: [], packageFiles: safePackages,
835
+ changelogPath: safeChangelog, repo, branch, sources,
836
+ })
837
+ plan.cwd = cwd
838
+ plan.changelog.next = changelog.markdown
839
+ plan.changelog.noop = changelog.noop
840
+ return plan
841
+ }
842
+
843
+ function metadataDeclarations(packageFiles, changelogPath, lockFile = null) {
844
+ return [
845
+ { rel: changelogPath, label: '--changelog' },
846
+ ...packageFiles.map((rel) => ({ rel, label: '--package-file' })),
847
+ ...(lockFile ? [{ rel: lockFile, label: '--lock-file' }] : []),
848
+ ]
849
+ }
850
+
851
+ function planInWorkspace(options) {
852
+ const sources = filesystemSources(
853
+ options.cwd,
854
+ metadataDeclarations(options.packageFiles, options.changelogPath, options.lockFile),
855
+ )
856
+ return planFromSources({ ...options, sources })
857
+ }
858
+
859
+ function planFromGitTree(runGit, options) {
860
+ const declarations = metadataDeclarations(options.packageFiles, options.changelogPath, options.lockFile)
861
+ filesystemSources(options.cwd, declarations)
862
+ const sources = gitTreeSources(runGit, options.cwd, declarations)
863
+ return planFromSources({ ...options, sources })
864
+ }
865
+
866
+ function stageMetadataPlan(runGit, plan) {
867
+ const entries = [
868
+ { rel: plan.changelog.rel, next: plan.changelog.next, noop: plan.changelog.noop },
869
+ ...plan.packages.map((item) => ({ rel: item.path, next: item.next, noop: item.noop || item.missing || item.error })),
870
+ ]
871
+ for (const entry of entries) {
872
+ if (entry.noop) continue
873
+ const blob = checked(
874
+ runGit(['hash-object', '-w', '--stdin'], { cwd: plan.cwd, input: entry.next }),
875
+ `stage metadata blob ${entry.rel}`,
876
+ ).stdout.trim()
877
+ if (!/^[0-9a-f]{40,64}$/.test(blob)) {
878
+ throw new Error(`metadata-sync: invalid blob identity for ${entry.rel}`)
879
+ }
880
+ checked(
881
+ runGit(['update-index', '--add', '--cacheinfo', `100644,${blob},${entry.rel}`], { cwd: plan.cwd }),
882
+ `stage metadata path ${entry.rel}`,
883
+ )
884
+ }
885
+ }
886
+
887
+ function contentIdentity(plan, lock = null) {
888
+ const entries = [
889
+ { path: plan.changelog.rel, content: plan.changelog.next },
890
+ ...plan.packages
891
+ .filter((item) => !item.missing && !item.error)
892
+ .map((item) => ({ path: item.path, content: item.next })),
893
+ ...(lock ? [{ path: lock.rel, content: lock.raw }] : []),
894
+ ]
895
+ return entries.sort((left, right) => left.path.localeCompare(right.path)).map((entry) => ({
896
+ path: entry.path,
897
+ sha256: sha256(Buffer.from(entry.content)),
898
+ }))
899
+ }
900
+ function assertCommitIdentity(runGit, { cwd, commit, baseSha, changedPaths, content, title, body }) {
901
+ const row = checked(runGit(['rev-list', '--parents', '-n', '1', commit], { cwd }), `inspect commit ${commit}`).stdout.trim().split(/\s+/)
902
+ if (row.length !== 2) {
903
+ throw new Error(`metadata-sync: commit ${commit} must have exactly one parent`)
904
+ }
905
+ if ((runGit(['merge-base', '--is-ancestor', row[1], baseSha], { cwd }).status ?? 1) !== 0) {
906
+ throw new Error(`metadata-sync: commit ${commit} is not based on an ancestor of fresh base ${baseSha}`)
907
+ }
908
+ const changed = checked(
909
+ runGit(['diff-tree', '--no-commit-id', '--name-only', '-r', commit], { cwd }),
910
+ `inspect changed paths ${commit}`,
911
+ ).stdout.split('\n').filter(Boolean).sort()
912
+ const expectedPaths = [...changedPaths].sort()
913
+ if (!sameJson(changed, expectedPaths)) {
914
+ throw new Error(`metadata-sync: commit ${commit} changed paths do not match expected metadata content`)
915
+ }
916
+ for (const expected of content) {
917
+ const blob = checked(runGit(['show', `${commit}:${expected.path}`], { cwd }), `read ${commit}:${expected.path}`).stdout
918
+ if (sha256(Buffer.from(blob)) !== expected.sha256) {
919
+ throw new Error(`metadata-sync: commit ${commit} content diverges at ${expected.path}`)
920
+ }
921
+ }
922
+ const actualMessage = checked(
923
+ runGit(['show', '-s', '--format=%B', commit], { cwd }),
924
+ `inspect message ${commit}`,
925
+ ).stdout.trim()
926
+ if (actualMessage !== `${title}\n\n${body}`.trim()) {
927
+ throw new Error(`metadata-sync: commit ${commit} message does not match the exact metadata identity`)
928
+ }
929
+ }
930
+
931
+ function sameJson(left, right) {
932
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null)
933
+ }
934
+
935
+ function assertRecordedIdentity(meta, expected) {
936
+ if (!meta) return
937
+ for (const field of ['repo', 'base', 'branch', 'version', 'sha']) {
938
+ if (meta[field] != null && meta[field] !== expected[field]) {
939
+ throw new Error(`metadata-sync: recorded ${field} ${JSON.stringify(meta[field])} does not match ${JSON.stringify(expected[field])}`)
940
+ }
941
+ }
942
+ if (meta.content != null && !sameJson(meta.content, expected.content)) {
943
+ throw new Error('metadata-sync: recorded expected content does not match the current metadata plan')
944
+ }
945
+ }
946
+
947
+ function normalizePr(raw) {
948
+ const headOwner = raw?.headRepositoryOwner?.login || null
949
+ const headName = raw?.headRepository?.name || null
950
+ return {
951
+ number: raw?.number ?? null,
952
+ url: raw?.url || null,
953
+ state: raw?.state || null,
954
+ base: raw?.baseRefName || null,
955
+ branch: raw?.headRefName || null,
956
+ commit: raw?.headRefOid || null,
957
+ repo: raw?.headRepository?.nameWithOwner || (headOwner && headName ? `${headOwner}/${headName}` : null),
958
+ mergeCommit: raw?.mergeCommit?.oid || null,
959
+ mergedAt: raw?.mergedAt || null,
960
+ }
961
+ }
962
+
963
+ function assertPrCoordinates(pr, expected) {
964
+ if (pr.repo !== expected.repo || pr.base !== expected.base || pr.branch !== expected.branch) {
965
+ throw new Error(
966
+ `metadata-sync: PR identity mismatch for #${pr.number ?? 'unknown'}; ` +
967
+ `expected ${expected.repo} ${expected.base}<-${expected.branch}`,
968
+ )
969
+ }
970
+ return pr
971
+ }
972
+
973
+ function assertPrIdentity(pr, expected) {
974
+ assertPrCoordinates(pr, expected)
975
+ if (pr.commit !== expected.commit) {
976
+ throw new Error(
977
+ `metadata-sync: PR identity mismatch for #${pr.number ?? 'unknown'}; ` +
978
+ `expected ${expected.repo} ${expected.base}<-${expected.branch}@${expected.commit}`,
979
+ )
980
+ }
981
+ return pr
982
+ }
983
+
984
+ const PR_FIELDS = 'number,url,state,baseRefName,headRefName,headRefOid,headRepository,headRepositoryOwner,mergeCommit,mergedAt'
985
+
986
+ function findExistingPr({ gh, cwd, repo, branch, recordedNumber }) {
987
+ if (recordedNumber != null) {
988
+ return normalizePr(parseJsonOutput(
989
+ gh(['pr', 'view', String(recordedNumber), '--repo', repo, '--json', PR_FIELDS], { cwd }),
990
+ `gh pr view ${recordedNumber}`,
991
+ ))
992
+ }
993
+ const rows = parseJsonOutput(
994
+ gh(['pr', 'list', '--repo', repo, '--state', 'all', '--head', branch, '--json', PR_FIELDS], { cwd }),
995
+ 'gh pr list',
996
+ )
997
+ if (!Array.isArray(rows)) throw new Error('metadata-sync: gh pr list did not return an array')
998
+ if (rows.length > 1) {
999
+ throw new Error(`metadata-sync: multiple PRs claim head ${branch}; refusing ambiguous adoption`)
1000
+ }
1001
+ return rows.length === 1 ? normalizePr(rows[0]) : null
1002
+ }
1003
+
1004
+ function createPr({ gh, cwd, repo, base, branch, title, body }) {
1005
+ const created = checked(gh([
1006
+ 'pr', 'create', '--repo', repo, '--base', base, '--head', branch,
1007
+ '--title', title, '--body', body,
1008
+ ], { cwd }), 'gh pr create')
1009
+ const url = String(created.stdout || '').trim().split('\n').filter(Boolean).pop()
1010
+ const number = parsePrNumber(url)
1011
+ if (number == null) throw new Error('metadata-sync: gh pr create returned no parseable PR URL')
1012
+ return findExistingPr({ gh, cwd, repo, branch, recordedNumber: number })
1013
+ }
1014
+
1015
+ function recordMergedDelivery({ args, cwd, git, gh, now }) {
1016
+ if (!args.manifest || !args.watchResult) {
1017
+ throw new Error('metadata-sync: record-delivery requires --manifest and --watch-result')
1018
+ }
1019
+ const runGit = (gitArgs) => git(gitArgs, { cwd })
1020
+ assertManifestOutsideCaller(args.manifest, cwd, runGit)
1021
+ const originRepo = validateRepo(repoFromRemoteUrl(
1022
+ checked(runGit(['remote', 'get-url', 'origin']), 'git remote get-url origin').stdout,
1023
+ ))
1024
+ if (!RELEASE_STATE_CONTRACT.fields.release.includes('metadataPr')) {
1025
+ throw new Error('metadata-sync: canonical release-state contract does not declare release.metadataPr')
1026
+ }
1027
+ const manifest = loadJson(args.manifest)
1028
+ const meta = manifest?.release?.metadataPr
1029
+ if (!meta || meta.source !== METADATA_SYNC_SOURCE || !['opened', 'merged'].includes(meta.status)) {
1030
+ throw new Error('metadata-sync: record-delivery requires a canonical opened or merged metadataPr record')
1031
+ }
1032
+ for (const field of ['repo', 'base', 'branch', 'commit', 'version', 'sha']) {
1033
+ if (typeof meta[field] !== 'string' || meta[field].trim() === '') {
1034
+ throw new Error(`metadata-sync: opened metadataPr is missing exact ${field} identity`)
1035
+ }
1036
+ }
1037
+ if (!Number.isInteger(meta.number) || !Array.isArray(meta.content) || meta.content.length === 0) {
1038
+ throw new Error('metadata-sync: opened metadataPr requires PR number and expected content')
1039
+ }
1040
+ if (meta.repo !== originRepo) {
1041
+ throw new Error(`metadata-sync: recorded repo ${meta.repo} does not match origin identity ${originRepo}`)
1042
+ }
1043
+ const watcher = loadJson(args.watchResult)
1044
+ if (watcher.outcome !== 'MERGED' || typeof watcher.mergedSha !== 'string' || !/^[0-9a-f]{40}$/.test(watcher.mergedSha)) {
1045
+ throw new Error('metadata-sync: watcher result is not a terminal MERGED verdict with mergedSha')
1046
+ }
1047
+
1048
+ const pr = assertPrIdentity(findExistingPr({
1049
+ gh, cwd, repo: validateRepo(meta.repo), branch: meta.branch, recordedNumber: meta.number,
1050
+ }), meta)
1051
+ if (pr.state !== 'MERGED' || pr.mergeCommit !== watcher.mergedSha || !pr.mergedAt) {
1052
+ throw new Error('metadata-sync: watcher result does not match the fresh merged PR observation')
1053
+ }
1054
+
1055
+ checked(runGit([
1056
+ 'fetch', '--quiet', '--no-tags', 'origin',
1057
+ `refs/heads/${meta.base}:refs/remotes/origin/${meta.base}`,
1058
+ ]), `fetch origin ${meta.base}`)
1059
+ const baseSha = resolveRef(runGit, `refs/remotes/origin/${meta.base}`, cwd)
1060
+ const commit = resolveRef(runGit, meta.commit, cwd)
1061
+ const mergedSha = resolveRef(runGit, watcher.mergedSha, cwd)
1062
+ if (commit !== meta.commit || mergedSha !== watcher.mergedSha || !baseSha) {
1063
+ throw new Error('metadata-sync: recorded commit, merge commit, or fresh base does not resolve exactly')
1064
+ }
1065
+ if ((runGit(['merge-base', '--is-ancestor', commit, mergedSha]).status ?? 1) !== 0) {
1066
+ throw new Error('metadata-sync: metadata commit is not an ancestor of the watcher merge commit')
1067
+ }
1068
+ if ((runGit(['merge-base', '--is-ancestor', mergedSha, baseSha]).status ?? 1) !== 0) {
1069
+ throw new Error(`metadata-sync: watcher merge commit is not an ancestor of fresh origin/${meta.base}`)
1070
+ }
1071
+
1072
+ const contentProof = meta.content.map((expected) => {
1073
+ if (typeof expected?.path !== 'string' || !/^[0-9a-f]{64}$/.test(expected?.sha256 || '')) {
1074
+ throw new Error('metadata-sync: malformed expected content identity')
1075
+ }
1076
+ safeRelativePath(expected.path, 'recorded content path')
1077
+ const blob = checked(runGit(['show', `${mergedSha}:${expected.path}`]), `read merged ${expected.path}`).stdout
1078
+ const actual = sha256(Buffer.from(blob))
1079
+ if (actual !== expected.sha256) {
1080
+ throw new Error(`metadata-sync: merged content mismatch for ${expected.path}`)
1081
+ }
1082
+ return { path: expected.path, sha256: actual }
1083
+ })
1084
+ const entry = {
1085
+ ...meta,
1086
+ status: 'merged',
1087
+ mergeCommit: mergedSha,
1088
+ mergedAt: pr.mergedAt,
1089
+ recordedAt: now(),
1090
+ delivery: {
1091
+ watcher: { outcome: 'MERGED', mergedSha },
1092
+ ancestry: { commit, mergeCommit: mergedSha, base: meta.base, baseSha },
1093
+ content: contentProof,
1094
+ },
1095
+ }
1096
+ writeJson(args.manifest, recordMetadataPr(manifest, entry))
1097
+ console.log(JSON.stringify({ ok: true, metadataPr: entry }, null, 2))
1098
+ return 0
1099
+ }
1100
+
1101
+ function openIsolated({ args, cwd, date, changes, git, gh, exec, runVersion, now }) {
1102
+ const base = args.base || 'main'
1103
+ const branch = args.branch || defaultBranchName(args.version)
1104
+ const runGit = (gitArgs, options = {}) => git(gitArgs, { cwd: options.cwd || cwd, input: options.input })
1105
+ if (!args.manifest) {
1106
+ throw new Error('metadata-sync: open requires --manifest outside the caller repository')
1107
+ }
1108
+ assertManifestOutsideCaller(args.manifest, cwd, runGit)
1109
+ const remoteUrl = checked(runGit(['remote', 'get-url', 'origin']), 'git remote get-url origin').stdout.trim()
1110
+ const derivedRepo = validateRepo(repoFromRemoteUrl(remoteUrl))
1111
+ const repo = validateRepo(args.repo || derivedRepo)
1112
+ if (repo !== derivedRepo) {
1113
+ throw new Error(`metadata-sync: requested repo ${repo} does not match origin identity ${derivedRepo}`)
1114
+ }
1115
+
1116
+ checked(runGit([
1117
+ 'fetch', '--quiet', '--no-tags', 'origin',
1118
+ `refs/heads/${base}:refs/remotes/origin/${base}`,
1119
+ ]), `fetch origin ${base}`)
1120
+ const baseSha = resolveRef(runGit, `refs/remotes/origin/${base}`, cwd)
1121
+ if (!baseSha) throw new Error(`metadata-sync: fetched base origin/${base} does not resolve`)
1122
+ const shippedSha = resolveRef(runGit, args.sha, cwd)
1123
+ if (!shippedSha) throw new Error(`metadata-sync: shipped SHA ${JSON.stringify(args.sha)} does not resolve`)
1124
+ if ((runGit(['merge-base', '--is-ancestor', shippedSha, baseSha]).status ?? 1) !== 0) {
1125
+ throw new Error(`metadata-sync: shipped SHA ${shippedSha} is not an ancestor of fresh origin/${base}`)
1126
+ }
1127
+
1128
+ const recorded = loadJson(args.manifest)?.release?.metadataPr || null
1129
+ const existingPr = findExistingPr({
1130
+ gh, cwd, repo, branch, recordedNumber: recorded?.number ?? null,
1131
+ })
1132
+ if (existingPr) {
1133
+ assertPrCoordinates(existingPr, { repo, base, branch })
1134
+ if (existingPr.state !== 'OPEN' || !/^[0-9a-f]{40}$/.test(existingPr.commit || '')) {
1135
+ throw new Error(
1136
+ `metadata-sync: existing PR #${existingPr.number} must be OPEN with an exact head commit; ` +
1137
+ 'use record-delivery for a merged PR',
1138
+ )
1139
+ }
1140
+ }
1141
+ runGit([
1142
+ 'fetch', '--quiet', '--no-tags', 'origin',
1143
+ `refs/heads/${branch}:refs/remotes/origin/${branch}`,
1144
+ ])
1145
+ runGit(['update-ref', '-d', `refs/remotes/origin/${branch}`])
1146
+ runGit(['fetch', '--quiet', '--no-tags', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`])
1147
+ const remoteCommit = resolveRef(runGit, `refs/remotes/origin/${branch}`, cwd)
1148
+ if (existingPr && !resolveRef(runGit, existingPr.commit, cwd)) {
1149
+ runGit(['fetch', '--quiet', '--no-tags', 'origin', `refs/pull/${existingPr.number}/head`])
1150
+ }
1151
+ const localCommit = resolveRef(runGit, `refs/heads/${branch}`, cwd)
1152
+ const recordedCommit = recorded?.commit || null
1153
+ const prCommit = existingPr?.commit || null
1154
+ const distinctCommits = new Set([localCommit, remoteCommit, recordedCommit, prCommit].filter(Boolean))
1155
+ if (distinctCommits.size > 1) {
1156
+ throw new Error(`metadata-sync: local, remote, recorded, and PR ${branch} artifacts diverge; refusing without force`)
1157
+ }
1158
+ if (prCommit && !resolveRef(runGit, prCommit, cwd)) {
1159
+ throw new Error(`metadata-sync: PR #${existingPr.number} head commit ${prCommit} cannot be fetched exactly`)
1160
+ }
1161
+
1162
+ const worktree = fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-sync-worktree-'))
1163
+ let executionTree = null
1164
+ let keepWorktree = false
1165
+ let published = remoteCommit != null
1166
+ let createdCommit = false
1167
+ try {
1168
+ checked(runGit(['worktree', 'add', '--detach', worktree, baseSha]), 'git worktree add')
1169
+ const worktreeChanges = parseChanges(args.changes, worktree, runGit)
1170
+ const plan = planFromGitTree(runGit, {
1171
+ cwd: worktree,
1172
+ version: args.version,
1173
+ date,
1174
+ changes: worktreeChanges,
1175
+ packageFiles: args.packageFiles,
1176
+ changelogPath: args.changelog || 'CHANGELOG.md',
1177
+ repo,
1178
+ branch,
1179
+ base,
1180
+ runVersion,
1181
+ lockFile: args.lockFile || null,
1182
+ })
1183
+ const packageErrors = planPackageErrors(plan)
1184
+ if (packageErrors.length) {
1185
+ throw new Error(`metadata-sync: refusing to open: ${packageErrors.map((item) => `${item.path} (${item.error})`).join('; ')}`)
1186
+ }
1187
+ filesystemSources(
1188
+ worktree,
1189
+ metadataDeclarations(args.packageFiles, args.changelog || 'CHANGELOG.md', args.lockFile || null),
1190
+ )
1191
+ applyMetadataFiles(plan, { runGit })
1192
+
1193
+ // Commands and ordinary commit hooks run only in this second disposable tree.
1194
+ executionTree = fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-sync-exec-'))
1195
+ const gitDir = checked(
1196
+ runGit(['rev-parse', '--git-dir'], { cwd: worktree }),
1197
+ 'resolve disposable Git directory',
1198
+ ).stdout.trim()
1199
+ fs.writeFileSync(path.join(executionTree, '.git'), `gitdir: ${path.resolve(worktree, gitDir)}\n`)
1200
+ checked(
1201
+ runGit(['checkout-index', '-a', '-f'], { cwd: executionTree }),
1202
+ 'materialize private metadata execution tree',
1203
+ )
1204
+
1205
+ const lock = runLockCommandAndVerify({
1206
+ lockFile: args.lockFile,
1207
+ lockCommand: args.lockCommand,
1208
+ plan,
1209
+ cwd: executionTree,
1210
+ exec,
1211
+ stageLock({ rel, raw }) {
1212
+ const lockBlob = checked(
1213
+ runGit(['hash-object', '-w', '--stdin'], { cwd: executionTree, input: raw }),
1214
+ `stage lock blob ${rel}`,
1215
+ ).stdout.trim()
1216
+ if (!/^[0-9a-f]{40,64}$/.test(lockBlob)) {
1217
+ throw new Error(`metadata-sync: invalid blob identity for ${rel}`)
1218
+ }
1219
+ checked(
1220
+ runGit(['update-index', '--add', '--cacheinfo', `100644,${lockBlob},${rel}`], { cwd: executionTree }),
1221
+ `stage lock path ${rel}`,
1222
+ )
1223
+ },
1224
+ })
1225
+
1226
+ const beforeCommit = verifyAppliedPlan({ ...plan, cwd: executionTree }, { lock })
1227
+ if (!beforeCommit.ok) {
1228
+ throw new Error(`metadata-sync: verification failed before commit: ${beforeCommit.failures.join('; ')}`)
1229
+ }
1230
+ const changedPaths = checked(
1231
+ runGit(['diff', '--cached', '--name-only'], { cwd: executionTree }),
1232
+ 'inspect staged metadata paths',
1233
+ ).stdout.split('\n').filter(Boolean).sort()
1234
+ if (changedPaths.length === 0) {
1235
+ if (args.manifest) {
1236
+ writeJson(args.manifest, recordMetadataPr(loadJson(args.manifest), {
1237
+ status: 'n/a',
1238
+ reason: 'metadata already present on base; nothing to commit',
1239
+ version: plan.version,
1240
+ sha: shippedSha,
1241
+ recordedAt: now(),
1242
+ }))
1243
+ }
1244
+ console.log(JSON.stringify({ ok: true, noop: true, reason: 'nothing to commit' }, null, 2))
1245
+ return 0
1246
+ }
1247
+ const content = contentIdentity(plan, lock)
1248
+ const title = args.title || `chore(release): metadata for v${plan.version}`
1249
+ const body = args.body || [
1250
+ `Post-release metadata sync for \`${plan.version}\` (shipped \`${shippedSha}\`).`,
1251
+ '',
1252
+ 'Cuts the CHANGELOG section and bumps package version fields after the final',
1253
+ 'release tag, preserving newer unreleased work on main.',
1254
+ '',
1255
+ 'Opened by `metadata-sync.mjs` (resolve-release / verified-sha-github-flow).',
1256
+ '',
1257
+ 'Refs: identity-v0.4.1 · #800 · identity#678',
1258
+ ].join('\n')
1259
+ assertRecordedIdentity(args.manifest ? loadJson(args.manifest)?.release?.metadataPr : null, {
1260
+ repo, base, branch, version: plan.version, sha: shippedSha, content,
1261
+ })
1262
+
1263
+ let commit = localCommit || remoteCommit || recordedCommit || prCommit
1264
+ if (commit) {
1265
+ assertCommitIdentity(runGit, { cwd: worktree, commit, baseSha, changedPaths, content, title, body })
1266
+ } else {
1267
+ checked(runGit(['commit', '-m', title, '-m', body], { cwd: executionTree }), 'git commit metadata')
1268
+ commit = resolveRef(runGit, 'HEAD', executionTree)
1269
+ createdCommit = true
1270
+ checked(
1271
+ runGit(['update-ref', `refs/heads/${branch}`, commit, '0'.repeat(40)]),
1272
+ `create local branch ${branch}`,
1273
+ )
1274
+ }
1275
+ assertCommitIdentity(runGit, { cwd: worktree, commit, baseSha, changedPaths, content, title, body })
1276
+
1277
+ const postCommitDisk = verifyAppliedPlan({ ...plan, cwd: executionTree }, { lock })
1278
+ const committed = verifyCommittedPlan(plan, {
1279
+ lock,
1280
+ show: (rel) => runGit(['show', `${commit}:${rel}`], { cwd: executionTree }),
1281
+ })
1282
+ if (!postCommitDisk.ok || !committed.ok) {
1283
+ throw new Error(
1284
+ `metadata-sync: post-commit verification failed (a commit hook mutated staged metadata?): ${[...postCommitDisk.failures, ...committed.failures].join('; ')}`,
1285
+ )
1286
+ }
1287
+
1288
+ if (!localCommit && !remoteCommit && (recordedCommit || prCommit)) {
1289
+ checked(
1290
+ runGit(['update-ref', `refs/heads/${branch}`, commit, '0'.repeat(40)]),
1291
+ `restore local branch ${branch}`,
1292
+ )
1293
+ }
1294
+
1295
+ let pushHookBypass = null
1296
+ if (!remoteCommit) {
1297
+ const pushArgs = ['push', 'origin', `refs/heads/${branch}:refs/heads/${branch}`]
1298
+ let pushed = runGit(pushArgs, { cwd: executionTree })
1299
+ if ((pushed.status ?? 1) !== 0) {
1300
+ const output = `${pushed.stderr || ''}\n${pushed.stdout || ''}`.trim()
1301
+ if (/mergify/i.test(output)) {
1302
+ pushHookBypass = 'pre-push'
1303
+ pushed = runGit([...pushArgs, '--no-verify'], { cwd: executionTree })
1304
+ if ((pushed.status ?? 1) !== 0) {
1305
+ throw new Error(`metadata-sync: git push --no-verify failed after known-hook retry: ${redact(pushed.stderr || pushed.stdout)}`)
1306
+ }
1307
+ } else {
1308
+ throw new Error(`metadata-sync: branch push rejected without force: ${redact(output)}`)
1309
+ }
1310
+ }
1311
+ published = true
1312
+ } else if (remoteCommit !== commit) {
1313
+ throw new Error(`metadata-sync: remote ${branch} points at ${remoteCommit}, not exact commit ${commit}`)
1314
+ }
1315
+
1316
+ const expectedPr = { repo, base, branch, commit }
1317
+ let pr = existingPr
1318
+ if (pr) pr = assertPrIdentity(pr, expectedPr)
1319
+ else pr = assertPrIdentity(createPr({ gh, cwd: executionTree, repo, base, branch, title, body }), expectedPr)
1320
+ const entry = {
1321
+ status: 'opened',
1322
+ number: pr.number,
1323
+ url: pr.url,
1324
+ repo,
1325
+ base,
1326
+ branch,
1327
+ commit,
1328
+ content,
1329
+ version: plan.version,
1330
+ sha: shippedSha,
1331
+ recordedAt: now(),
1332
+ ...(pushHookBypass ? { pushHookBypass } : {}),
1333
+ }
1334
+ if (args.manifest) writeJson(args.manifest, recordMetadataPr(loadJson(args.manifest), entry))
1335
+ console.log(JSON.stringify({
1336
+ ok: true,
1337
+ resumed: Boolean(localCommit || remoteCommit || recordedCommit || existingPr),
1338
+ metadataPr: entry,
1339
+ ...(pushHookBypass ? { push: { hookBypass: pushHookBypass } } : {}),
1340
+ ...(lock ? { lock: { file: lock.rel, command: args.lockCommand } } : {}),
1341
+ }, null, 2))
1342
+ return 0
1343
+ } catch (error) {
1344
+ const preservedCommit = resolveRef(runGit, `refs/heads/${branch}`, cwd)
1345
+ if (!published && createdCommit && !preservedCommit) keepWorktree = true
1346
+ if (keepWorktree) {
1347
+ throw new Error(`${error.message}; unpublished metadata worktree retained at ${JSON.stringify(worktree)}`)
1348
+ }
1349
+ if (!published && preservedCommit) {
1350
+ throw new Error(
1351
+ `${error.message}; unpublished metadata commit preserved at refs/heads/${branch} ${preservedCommit}`,
1352
+ )
1353
+ }
1354
+ throw error
1355
+ } finally {
1356
+ if (!keepWorktree) {
1357
+ runGit(['worktree', 'remove', '--force', worktree])
1358
+ fs.rmSync(worktree, { recursive: true, force: true })
1359
+ }
1360
+ if (executionTree) fs.rmSync(executionTree, { recursive: true, force: true })
1361
+ }
1362
+ }
1363
+
1364
+ export function main(argv, deps = {}) {
1365
+ const args = parseArgs(argv)
1366
+ if (args.help || args.cmd === 'help') {
1367
+ console.log(USAGE)
1368
+ return 0
1369
+ }
1370
+ if (args.unknown) {
1371
+ console.error(`metadata-sync: unknown flag ${args.unknown}`)
1372
+ return 2
1373
+ }
1374
+ if (!args.cmd || !['plan', 'open', 'record-delivery', 'na', 'migrate'].includes(args.cmd)) {
1375
+ console.error('metadata-sync: use plan|open|record-delivery|na|migrate (see --help)')
1376
+ return 2
1377
+ }
1378
+
1379
+ const git = deps.git || ((a, o) => defaultRun('git', a, o))
1380
+ const gh = deps.gh || ((a, o) => defaultRun('gh', a, o))
1381
+ const exec = deps.exec || ((cmd, { cwd: commandCwd } = {}) => spawnSync(cmd, { encoding: 'utf8', cwd: commandCwd, shell: true }))
1382
+ const runVersion = deps.runVersion || ((a, o) => defaultRun(process.execPath, a, o))
1383
+ const now = deps.now || (() => new Date().toISOString())
1384
+ const cwd = args.cwd || process.cwd()
1385
+
1386
+ try {
1387
+ if (args.cmd === 'record-delivery') {
1388
+ return recordMergedDelivery({ args, cwd, git, gh, now })
1389
+ }
1390
+
1391
+ if (args.cmd === 'migrate') {
1392
+ if (!args.manifest) {
1393
+ console.error('metadata-sync: migrate requires --manifest')
1394
+ return 2
1395
+ }
1396
+ const manifest = loadJson(args.manifest)
1397
+ const entry = migrateMetadataSync(manifest)
1398
+ const next = recordMetadataPr(manifest, entry)
1399
+ delete next.release.metadataSync
1400
+ writeJson(args.manifest, next)
1401
+ console.log(JSON.stringify({ ok: true, metadataPr: next.release.metadataPr }, null, 2))
1402
+ return 0
1403
+ }
1404
+
1405
+ if (args.cmd === 'na') {
1406
+ if (!args.manifest) {
1407
+ console.error('metadata-sync: na requires --manifest')
1408
+ return 2
1409
+ }
1410
+ if (!args.reason || !String(args.reason).trim()) {
1411
+ console.error('metadata-sync: na requires --reason <text>')
1412
+ return 2
1413
+ }
1414
+ const manifest = loadJson(args.manifest)
1415
+ const next = recordMetadataPr(manifest, {
1416
+ status: 'n/a',
1417
+ reason: args.reason,
1418
+ version: args.version || null,
1419
+ recordedAt: now(),
1420
+ })
1421
+ writeJson(args.manifest, next)
1422
+ console.log(JSON.stringify({ ok: true, metadataPr: next.release.metadataPr }, null, 2))
1423
+ return 0
1424
+ }
1425
+
1426
+ if (args.cmd === 'plan' || args.cmd === 'open') {
1427
+ if (!args.version) {
1428
+ console.error(`metadata-sync: ${args.cmd} requires --version`)
1429
+ return 2
1430
+ }
1431
+ if (args.cmd === 'open' && !args.sha) {
1432
+ console.error('metadata-sync: open requires --sha <shipped>')
1433
+ return 2
1434
+ }
1435
+ if ((args.lockFile == null) !== (args.lockCommand == null)) {
1436
+ console.error('metadata-sync: --lock-file and --lock-command must be declared together')
1437
+ return 2
1438
+ }
1439
+ const date = args.date || now().slice(0, 10)
1440
+ const changes = args.cmd === 'open' && !args.dryRun
1441
+ ? null
1442
+ : parseChanges(args.changes, cwd)
1443
+
1444
+ if (args.cmd === 'plan') {
1445
+ const plan = planInWorkspace({
1446
+ cwd,
1447
+ version: args.version,
1448
+ date,
1449
+ changes,
1450
+ packageFiles: args.packageFiles,
1451
+ changelogPath: args.changelog || 'CHANGELOG.md',
1452
+ repo: args.repo || null,
1453
+ branch: args.branch || null,
1454
+ base: args.base || 'main',
1455
+ runVersion,
1456
+ lockFile: args.lockFile || null,
1457
+ })
1458
+ console.log(JSON.stringify(plan, null, 2))
1459
+ const packageErrors = planPackageErrors(plan)
1460
+ if (packageErrors.length) {
1461
+ console.error(`metadata-sync: ${packageErrors.length} requested package file(s) failed: ${packageErrors.map((item) => `${item.path} (${item.error})`).join('; ')}`)
1462
+ return 1
1463
+ }
1464
+ return 0
1465
+ }
1466
+
1467
+ const base = args.base || 'main'
1468
+ const branch = args.branch || defaultBranchName(args.version)
1469
+
1470
+ if (args.dryRun) {
1471
+ const plan = planInWorkspace({
1472
+ cwd,
1473
+ version: args.version,
1474
+ date,
1475
+ changes,
1476
+ packageFiles: args.packageFiles,
1477
+ changelogPath: args.changelog || 'CHANGELOG.md',
1478
+ repo: args.repo || null,
1479
+ branch,
1480
+ base,
1481
+ runVersion,
1482
+ lockFile: args.lockFile || null,
1483
+ })
1484
+ const packageErrors = planPackageErrors(plan)
1485
+ if (packageErrors.length) {
1486
+ console.log(JSON.stringify({ dryRun: true, plan }, null, 2))
1487
+ console.error(`metadata-sync: ${packageErrors.length} requested package file(s) failed: ${packageErrors.map((item) => `${item.path} (${item.error})`).join('; ')}`)
1488
+ return 1
1489
+ }
1490
+ let executionTree = null
1491
+ try {
1492
+ if (args.lockFile) {
1493
+ executionTree = createDryRunExecutionTree(cwd, plan)
1494
+ const lock = runLockCommandAndVerify({
1495
+ lockFile: args.lockFile,
1496
+ lockCommand: args.lockCommand,
1497
+ plan,
1498
+ cwd: executionTree,
1499
+ exec,
1500
+ })
1501
+ const verified = verifyAppliedPlan({ ...plan, cwd: executionTree }, { lock })
1502
+ if (!verified.ok) {
1503
+ throw new Error(`metadata-sync: dry-run lock verification failed: ${verified.failures.join('; ')}`)
1504
+ }
1505
+ }
1506
+ } finally {
1507
+ if (executionTree) fs.rmSync(executionTree, { recursive: true, force: true })
1508
+ }
1509
+ if (args.recordPlan && args.manifest) {
1510
+ const manifest = loadJson(args.manifest)
1511
+ const next = recordMetadataPr(manifest, {
1512
+ status: 'planned',
1513
+ version: plan.version,
1514
+ sha: args.sha,
1515
+ branch: plan.branch,
1516
+ recordedAt: now(),
1517
+ })
1518
+ writeJson(args.manifest, next)
1519
+ }
1520
+ console.log(JSON.stringify({ dryRun: true, plan }, null, 2))
1521
+ return 0
1522
+ }
1523
+
1524
+ return openIsolated({
1525
+ args, cwd, date, changes, git, gh, exec, runVersion, now,
1526
+ })
1527
+
1528
+ }
1529
+ } catch (err) {
1530
+ console.error(`metadata-sync: ${err.message || err}`)
1531
+ return err.message && /requires|unknown|use plan/.test(err.message) ? 2 : 1
1532
+ }
1533
+ return 2
1534
+ }
1535
+
1536
+ if (isMainModule(import.meta.url)) {
1537
+ process.exit(main(process.argv.slice(2)))
1538
+ }