@intentius/chant 0.124.13 → 0.124.15

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 (991) hide show
  1. package/bin/chant +11 -0
  2. package/dist/agents/checks.js +326 -0
  3. package/dist/agents/discover.js +885 -0
  4. package/dist/agents/importer.js +17 -0
  5. package/dist/agents/index.js +12 -0
  6. package/dist/agents/types.js +19 -0
  7. package/dist/apply.js +151 -0
  8. package/dist/attrref.js +70 -0
  9. package/dist/audit/catalog.js +350 -0
  10. package/dist/audit/core.js +252 -0
  11. package/dist/audit/discover.js +633 -0
  12. package/dist/audit/fetch.js +666 -0
  13. package/dist/audit/lineage.js +91 -0
  14. package/dist/audit/nginx.js +366 -0
  15. package/dist/audit/prior-art.js +132 -0
  16. package/dist/audit/proof.js +321 -0
  17. package/dist/audit/report-html.js +194 -0
  18. package/dist/audit/report-model.js +198 -0
  19. package/dist/audit/report.js +102 -0
  20. package/dist/audit/rules-doc.js +144 -0
  21. package/dist/audit/secrets.js +405 -0
  22. package/dist/audit/terraform-state.js +140 -0
  23. package/dist/audit/wrangler.js +262 -0
  24. package/dist/behaviour-delta.js +401 -0
  25. package/dist/behaviour-engine.js +357 -0
  26. package/dist/behaviour-graph.js +83 -0
  27. package/dist/behaviour-http.js +188 -0
  28. package/dist/behaviour-kinds.js +172 -0
  29. package/dist/behaviour-overlay.js +50 -0
  30. package/dist/behaviour-predict.js +188 -0
  31. package/dist/behaviour-request.js +194 -0
  32. package/dist/behaviour.js +1325 -0
  33. package/dist/build-params.js +108 -0
  34. package/dist/build.d.ts +6 -0
  35. package/dist/build.d.ts.map +1 -1
  36. package/dist/build.js +669 -0
  37. package/dist/builder.js +44 -0
  38. package/dist/cdk/advise.js +30 -0
  39. package/dist/cdk/assembly.js +122 -0
  40. package/dist/cdk/graph.js +444 -0
  41. package/dist/cdk/tier-map.js +64 -0
  42. package/dist/cdk/types.js +24 -0
  43. package/dist/change-set.d.ts +24 -2
  44. package/dist/change-set.d.ts.map +1 -1
  45. package/dist/change-set.js +361 -0
  46. package/dist/child-project.js +21 -0
  47. package/dist/ci-jobs.d.ts +227 -0
  48. package/dist/ci-jobs.d.ts.map +1 -0
  49. package/dist/ci-jobs.js +448 -0
  50. package/dist/claimed-fields.js +135 -0
  51. package/dist/cli/build-options.js +70 -0
  52. package/dist/cli/build-params-cli.js +139 -0
  53. package/dist/cli/command-group.js +123 -0
  54. package/dist/cli/commands/audit-agents.js +326 -0
  55. package/dist/cli/commands/audit.js +571 -0
  56. package/dist/cli/commands/build.js +920 -0
  57. package/dist/cli/commands/carve-apply.js +153 -0
  58. package/dist/cli/commands/carve-bridge.js +201 -0
  59. package/dist/cli/commands/carve-emit.js +332 -0
  60. package/dist/cli/commands/carve-status.js +119 -0
  61. package/dist/cli/commands/carve.js +204 -0
  62. package/dist/cli/commands/check-lexicon-docs.js +79 -0
  63. package/dist/cli/commands/check-lexicon-examples.js +177 -0
  64. package/dist/cli/commands/check-lexicon-intrinsics.js +291 -0
  65. package/dist/cli/commands/check-lexicon-mcp.js +82 -0
  66. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  67. package/dist/cli/commands/check-lexicon-plugin.js +104 -0
  68. package/dist/cli/commands/check-lexicon.js +766 -0
  69. package/dist/cli/commands/describe.js +85 -0
  70. package/dist/cli/commands/diff.js +175 -0
  71. package/dist/cli/commands/doctor.js +350 -0
  72. package/dist/cli/commands/import-agents.js +155 -0
  73. package/dist/cli/commands/import.js +670 -0
  74. package/dist/cli/commands/init-lexicon/templates/codegen.js +194 -0
  75. package/dist/cli/commands/init-lexicon/templates/docs.js +100 -0
  76. package/dist/cli/commands/init-lexicon/templates/examples.js +29 -0
  77. package/dist/cli/commands/init-lexicon/templates/lint.js +34 -0
  78. package/dist/cli/commands/init-lexicon/templates/lsp.js +37 -0
  79. package/dist/cli/commands/init-lexicon/templates/plugin.js +108 -0
  80. package/dist/cli/commands/init-lexicon/templates/project.js +240 -0
  81. package/dist/cli/commands/init-lexicon/templates/spec.js +56 -0
  82. package/dist/cli/commands/init-lexicon/templates/tests.js +66 -0
  83. package/dist/cli/commands/init-lexicon.js +172 -0
  84. package/dist/cli/commands/init.js +381 -0
  85. package/dist/cli/commands/lexicon-rolling-upgrade.js +145 -0
  86. package/dist/cli/commands/lexicon-surface-diff.js +145 -0
  87. package/dist/cli/commands/licenses.js +125 -0
  88. package/dist/cli/commands/lint.d.ts.map +1 -1
  89. package/dist/cli/commands/lint.js +948 -0
  90. package/dist/cli/commands/list.js +63 -0
  91. package/dist/cli/commands/migrate.d.ts.map +1 -1
  92. package/dist/cli/commands/migrate.js +423 -0
  93. package/dist/cli/commands/onboard.js +403 -0
  94. package/dist/cli/commands/pinned-upgrade.js +191 -0
  95. package/dist/cli/commands/update.js +194 -0
  96. package/dist/cli/commands/vendor.js +217 -0
  97. package/dist/cli/conflict-check.js +104 -0
  98. package/dist/cli/debug.js +8 -0
  99. package/dist/cli/format.js +104 -0
  100. package/dist/cli/handlers/build.js +267 -0
  101. package/dist/cli/handlers/carve-apply.js +30 -0
  102. package/dist/cli/handlers/carve-bridge.js +28 -0
  103. package/dist/cli/handlers/carve-emit.js +50 -0
  104. package/dist/cli/handlers/carve-status.js +26 -0
  105. package/dist/cli/handlers/carve.js +40 -0
  106. package/dist/cli/handlers/change-set.js +46 -0
  107. package/dist/cli/handlers/components.js +730 -0
  108. package/dist/cli/handlers/dev.js +113 -0
  109. package/dist/cli/handlers/emulator.js +233 -0
  110. package/dist/cli/handlers/explain.js +54 -0
  111. package/dist/cli/handlers/fan-out.js +422 -0
  112. package/dist/cli/handlers/gate-resume.js +275 -0
  113. package/dist/cli/handlers/generate.js +84 -0
  114. package/dist/cli/handlers/graph.js +969 -0
  115. package/dist/cli/handlers/init.js +102 -0
  116. package/dist/cli/handlers/lifecycle.js +1727 -0
  117. package/dist/cli/handlers/lint.js +62 -0
  118. package/dist/cli/handlers/migrate.js +48 -0
  119. package/dist/cli/handlers/misc.js +391 -0
  120. package/dist/cli/handlers/operator.js +1162 -0
  121. package/dist/cli/handlers/override-approve.js +102 -0
  122. package/dist/cli/handlers/pr.js +503 -0
  123. package/dist/cli/handlers/promote.js +342 -0
  124. package/dist/cli/handlers/run-generate.js +206 -0
  125. package/dist/cli/handlers/run-wave.js +66 -0
  126. package/dist/cli/handlers/run.js +1191 -0
  127. package/dist/cli/handlers/scenario.js +325 -0
  128. package/dist/cli/handlers/search.js +924 -0
  129. package/dist/cli/handlers/serve.js +32 -0
  130. package/dist/cli/handlers/vendor.js +154 -0
  131. package/dist/cli/index.js +3 -0
  132. package/dist/cli/is-entry-point.js +23 -0
  133. package/dist/cli/lsp/capabilities.js +28 -0
  134. package/dist/cli/lsp/diagnostics.js +34 -0
  135. package/dist/cli/lsp/server.js +329 -0
  136. package/dist/cli/main.d.ts.map +1 -1
  137. package/dist/cli/main.js +2540 -0
  138. package/dist/cli/mcp/lifecycle-tools.js +80 -0
  139. package/dist/cli/mcp/op-tools.js +306 -0
  140. package/dist/cli/mcp/resource-handlers.js +258 -0
  141. package/dist/cli/mcp/resources/context.js +85 -0
  142. package/dist/cli/mcp/server.js +353 -0
  143. package/dist/cli/mcp/tools/build.js +111 -0
  144. package/dist/cli/mcp/tools/composites.js +95 -0
  145. package/dist/cli/mcp/tools/explain.js +166 -0
  146. package/dist/cli/mcp/tools/import.js +49 -0
  147. package/dist/cli/mcp/tools/lint.js +41 -0
  148. package/dist/cli/mcp/tools/scaffold.js +111 -0
  149. package/dist/cli/mcp/tools/search.js +112 -0
  150. package/dist/cli/mcp/types.js +1 -0
  151. package/dist/cli/mcp/workspace-plugins.js +91 -0
  152. package/dist/cli/mcp/workspace-tools.d.ts +1 -1
  153. package/dist/cli/mcp/workspace-tools.js +564 -0
  154. package/dist/cli/mcp-config.js +70 -0
  155. package/dist/cli/plugins.js +313 -0
  156. package/dist/cli/registry.js +23 -0
  157. package/dist/cli/reporters/stylish.js +329 -0
  158. package/dist/cli/version.js +15 -0
  159. package/dist/cli/watch.js +79 -0
  160. package/dist/codegen/bound-property-types.js +82 -0
  161. package/dist/codegen/coverage.js +119 -0
  162. package/dist/codegen/docs-file-markers.js +60 -0
  163. package/dist/codegen/docs-pages.js +115 -0
  164. package/dist/codegen/docs-rule-scanning.js +164 -0
  165. package/dist/codegen/docs-rules.js +76 -0
  166. package/dist/codegen/docs-sections.js +117 -0
  167. package/dist/codegen/docs-sidebar.js +65 -0
  168. package/dist/codegen/docs-types.js +4 -0
  169. package/dist/codegen/docs.js +356 -0
  170. package/dist/codegen/fetch.js +340 -0
  171. package/dist/codegen/generate-post-synth-barrel.js +63 -0
  172. package/dist/codegen/generate-registry.js +71 -0
  173. package/dist/codegen/generate-runtime-index.js +56 -0
  174. package/dist/codegen/generate-typescript.js +119 -0
  175. package/dist/codegen/generate.js +115 -0
  176. package/dist/codegen/json-patch.js +141 -0
  177. package/dist/codegen/json-schema-to-ts.js +344 -0
  178. package/dist/codegen/json-schema.js +246 -0
  179. package/dist/codegen/lexicon-regen.js +412 -0
  180. package/dist/codegen/naming.js +217 -0
  181. package/dist/codegen/okf-lexicon.js +245 -0
  182. package/dist/codegen/package.js +150 -0
  183. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  184. package/dist/codegen/pinned-upgrade.js +494 -0
  185. package/dist/codegen/registry.js +60 -0
  186. package/dist/codegen/rehype-base-url.mjs +68 -0
  187. package/dist/codegen/rolling-upgrade.js +217 -0
  188. package/dist/codegen/surface-snapshot.js +537 -0
  189. package/dist/codegen/testdata/postgres-tags.json +242 -0
  190. package/dist/codegen/topo-sort.js +36 -0
  191. package/dist/codegen/typecheck.js +115 -0
  192. package/dist/codegen/validate.js +200 -0
  193. package/dist/codegen/version-bump.js +35 -0
  194. package/dist/components/auto-release.js +143 -0
  195. package/dist/components/builders.js +47 -0
  196. package/dist/components/capability-plugin-loader.js +175 -0
  197. package/dist/components/capability-plugin-schema.js +46 -0
  198. package/dist/components/capability-plugin.js +99 -0
  199. package/dist/components/capability.js +60 -0
  200. package/dist/components/changed-files.js +72 -0
  201. package/dist/components/cli-support.js +427 -0
  202. package/dist/components/component.js +120 -0
  203. package/dist/components/component.schema.json +271 -0
  204. package/dist/components/config-defaults.js +130 -0
  205. package/dist/components/deploy-units.js +60 -0
  206. package/dist/components/discover.d.ts.map +1 -1
  207. package/dist/components/discover.js +189 -0
  208. package/dist/components/driver-output.js +86 -0
  209. package/dist/components/driver.js +728 -0
  210. package/dist/components/fan-out-output.js +165 -0
  211. package/dist/components/fan-out-record.js +94 -0
  212. package/dist/components/fan-out-run.js +373 -0
  213. package/dist/components/fan-out-support.js +60 -0
  214. package/dist/components/fan-out.js +273 -0
  215. package/dist/components/gate-plan.js +41 -0
  216. package/dist/components/gated-wave-pipeline.js +46 -0
  217. package/dist/components/index.js +24 -0
  218. package/dist/components/layers.js +28 -0
  219. package/dist/components/manifest-persistence.js +108 -0
  220. package/dist/components/pilots/alb-ecs.pilot.js +59 -0
  221. package/dist/components/pilots/dynamodb.pilot.js +51 -0
  222. package/dist/components/pilots/jar-emr.pilot.js +65 -0
  223. package/dist/components/pilots/lambda.pilot.js +52 -0
  224. package/dist/components/pilots/neo4j-fanout.pilot.js +68 -0
  225. package/dist/components/pilots/project.js +12 -0
  226. package/dist/components/pr-pipeline.d.ts.map +1 -1
  227. package/dist/components/pr-pipeline.js +135 -0
  228. package/dist/components/pr-run.js +363 -0
  229. package/dist/components/presets/ecs-fargate.js +72 -0
  230. package/dist/components/presets/index.js +28 -0
  231. package/dist/components/presets/lambda.js +34 -0
  232. package/dist/components/presets/single-host-compose.js +41 -0
  233. package/dist/components/promote.js +549 -0
  234. package/dist/components/registry.js +34 -0
  235. package/dist/components/run-progress.js +33 -0
  236. package/dist/components/sandbox/driver.d.ts.map +1 -1
  237. package/dist/components/sandbox/driver.js +93 -0
  238. package/dist/components/sandbox/run.js +115 -0
  239. package/dist/components/starter-plugin.js +104 -0
  240. package/dist/components/verbs/__tests__/mock-cloud-executor.js +68 -0
  241. package/dist/components/verbs/__tests__/mock-process-runner.js +45 -0
  242. package/dist/components/verbs/__tests__/mock-sbom-generator.js +56 -0
  243. package/dist/components/verbs/bom-writer.js +287 -0
  244. package/dist/components/verbs/build-archive.js +188 -0
  245. package/dist/components/verbs/build.js +142 -0
  246. package/dist/components/verbs/cloud-executor.js +110 -0
  247. package/dist/components/verbs/component-bom.js +125 -0
  248. package/dist/components/verbs/ensure-secret.js +50 -0
  249. package/dist/components/verbs/index.js +61 -0
  250. package/dist/components/verbs/license-policy.js +75 -0
  251. package/dist/components/verbs/lockfile-sbom-generator.js +191 -0
  252. package/dist/components/verbs/process-runner.js +78 -0
  253. package/dist/components/verbs/r2-sync.js +93 -0
  254. package/dist/components/verbs/reproducibility.js +62 -0
  255. package/dist/components/verbs/run-agent.js +577 -0
  256. package/dist/components/verbs/sbom-generator.js +119 -0
  257. package/dist/components/verbs/sbom.js +97 -0
  258. package/dist/components/verbs/shell.js +32 -0
  259. package/dist/components/verbs/sign.js +221 -0
  260. package/dist/components/verbs/stub.js +28 -0
  261. package/dist/components/verbs/tool-sbom-generator.js +154 -0
  262. package/dist/components/verbs/verify.js +112 -0
  263. package/dist/components/verbs/vex.js +131 -0
  264. package/dist/components/verbs/vuln-gate.js +152 -0
  265. package/dist/components/verbs/vuln-scan.js +288 -0
  266. package/dist/components/verbs/wait-verify.js +106 -0
  267. package/dist/components/verbs/wrangler.js +179 -0
  268. package/dist/components/wave-plan.js +102 -0
  269. package/dist/composite.d.ts +10 -2
  270. package/dist/composite.d.ts.map +1 -1
  271. package/dist/composite.js +313 -0
  272. package/dist/config-import.d.ts.map +1 -1
  273. package/dist/config-import.js +22 -0
  274. package/dist/config-sandbox.js +122 -0
  275. package/dist/config-static.js +206 -0
  276. package/dist/config.js +605 -0
  277. package/dist/content-digest.js +23 -0
  278. package/dist/declarable.js +114 -0
  279. package/dist/deep-observation.js +437 -0
  280. package/dist/detectLexicon.js +54 -0
  281. package/dist/discovery/cache.js +70 -0
  282. package/dist/discovery/collect.d.ts +2 -1
  283. package/dist/discovery/collect.d.ts.map +1 -1
  284. package/dist/discovery/collect.js +223 -0
  285. package/dist/discovery/cycles.js +95 -0
  286. package/dist/discovery/entity-wire-codec.js +391 -0
  287. package/dist/discovery/entity-wire.js +42 -0
  288. package/dist/discovery/files.js +121 -0
  289. package/dist/discovery/fold-import.d.ts.map +1 -1
  290. package/dist/discovery/fold-import.js +4073 -0
  291. package/dist/discovery/fold-rank.js +292 -0
  292. package/dist/discovery/graph.js +115 -0
  293. package/dist/discovery/host-composite.d.ts +35 -0
  294. package/dist/discovery/host-composite.d.ts.map +1 -0
  295. package/dist/discovery/host-composite.js +93 -0
  296. package/dist/discovery/import.d.ts.map +1 -1
  297. package/dist/discovery/import.js +65 -0
  298. package/dist/discovery/index.d.ts +9 -0
  299. package/dist/discovery/index.d.ts.map +1 -1
  300. package/dist/discovery/index.js +333 -0
  301. package/dist/discovery/param-deps.js +487 -0
  302. package/dist/discovery/resolve.js +39 -0
  303. package/dist/discovery/sandbox/bundle.js +214 -0
  304. package/dist/discovery/sandbox/child-errors.js +26 -0
  305. package/dist/discovery/sandbox/config-run.js +100 -0
  306. package/dist/discovery/sandbox/config-wire.js +183 -0
  307. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  308. package/dist/discovery/sandbox/driver.js +333 -0
  309. package/dist/discovery/sandbox/fork.js +229 -0
  310. package/dist/discovery/sandbox/policy-run.js +142 -0
  311. package/dist/discovery/sandbox/policy-wire.js +152 -0
  312. package/dist/discovery/sandbox/run.js +77 -0
  313. package/dist/discovery/specifier-resolve.js +320 -0
  314. package/dist/discovery/walk.js +284 -0
  315. package/dist/effect-receipt.js +340 -0
  316. package/dist/env.js +55 -0
  317. package/dist/errors.js +62 -0
  318. package/dist/fold/fold.js +1434 -0
  319. package/dist/fold/foldable-helpers.js +229 -0
  320. package/dist/fold/subset.js +696 -0
  321. package/dist/fold-provenance.js +414 -0
  322. package/dist/gate-decision.d.ts +20 -9
  323. package/dist/gate-decision.d.ts.map +1 -1
  324. package/dist/gate-decision.js +313 -0
  325. package/dist/gate-resume-run.js +154 -0
  326. package/dist/gated-waves.js +165 -0
  327. package/dist/governance.js +43 -0
  328. package/dist/graph-declared.js +45 -0
  329. package/dist/graph-detail.js +207 -0
  330. package/dist/graph-dot.js +54 -0
  331. package/dist/graph-effective.js +107 -0
  332. package/dist/graph-ir.js +623 -0
  333. package/dist/graph-layout.js +184 -0
  334. package/dist/graph-lens.js +105 -0
  335. package/dist/graph-mermaid.js +83 -0
  336. package/dist/graph-ops.js +39 -0
  337. package/dist/graph-refs.js +156 -0
  338. package/dist/held-elsewhere.js +87 -0
  339. package/dist/identity.js +251 -0
  340. package/dist/import/base-parser.js +34 -0
  341. package/dist/import/embedded.js +255 -0
  342. package/dist/import/generator.js +1 -0
  343. package/dist/import/ir-utils.js +85 -0
  344. package/dist/import/parser-options.js +45 -0
  345. package/dist/import/parser.js +1 -0
  346. package/dist/index.js +118 -0
  347. package/dist/intrinsic-interpolation.js +73 -0
  348. package/dist/intrinsic.js +31 -0
  349. package/dist/join-key.js +35 -0
  350. package/dist/kubectl-context.js +150 -0
  351. package/dist/lexicon-config.js +58 -0
  352. package/dist/lexicon-integrity.js +47 -0
  353. package/dist/lexicon-manifest.js +70 -0
  354. package/dist/lexicon-module.d.ts.map +1 -1
  355. package/dist/lexicon-module.js +241 -0
  356. package/dist/lexicon-output.js +218 -0
  357. package/dist/lexicon-plugin-helpers.js +104 -0
  358. package/dist/lexicon-schema.js +136 -0
  359. package/dist/lexicon.js +130 -0
  360. package/dist/lifecycle/affected.js +224 -0
  361. package/dist/lifecycle/assert-live.js +110 -0
  362. package/dist/lifecycle/build-ledger-store.js +160 -0
  363. package/dist/lifecycle/build-ledger.js +155 -0
  364. package/dist/lifecycle/change-set.js +356 -0
  365. package/dist/lifecycle/converge-ledger.js +161 -0
  366. package/dist/lifecycle/deep-diff.js +233 -0
  367. package/dist/lifecycle/deep-observe.js +147 -0
  368. package/dist/lifecycle/digest.js +143 -0
  369. package/dist/lifecycle/disruption.js +135 -0
  370. package/dist/lifecycle/gate-applied.js +113 -0
  371. package/dist/lifecycle/gate-audit.d.ts +69 -2
  372. package/dist/lifecycle/gate-audit.d.ts.map +1 -1
  373. package/dist/lifecycle/gate-audit.js +415 -0
  374. package/dist/lifecycle/gate-ledger-select.js +89 -0
  375. package/dist/lifecycle/gate-ledger.d.ts +24 -0
  376. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  377. package/dist/lifecycle/gate-ledger.js +201 -0
  378. package/dist/lifecycle/gate-origin.js +117 -0
  379. package/dist/lifecycle/gate-pending.js +58 -0
  380. package/dist/lifecycle/git.js +1182 -0
  381. package/dist/lifecycle/identity.js +58 -0
  382. package/dist/lifecycle/index.js +26 -0
  383. package/dist/lifecycle/lease.js +283 -0
  384. package/dist/lifecycle/ledger-port.js +1 -0
  385. package/dist/lifecycle/legacy-digest.js +91 -0
  386. package/dist/lifecycle/live-diff.js +274 -0
  387. package/dist/lifecycle/local-note.js +15 -0
  388. package/dist/lifecycle/member-ledger.js +122 -0
  389. package/dist/lifecycle/observation-baseline.js +155 -0
  390. package/dist/lifecycle/observe.js +375 -0
  391. package/dist/lifecycle/oras-referrer-lookup.js +82 -0
  392. package/dist/lifecycle/override-ledger.js +54 -0
  393. package/dist/lifecycle/plan-digest.js +110 -0
  394. package/dist/lifecycle/plan-ledger.js +129 -0
  395. package/dist/lifecycle/receipt-plan.js +192 -0
  396. package/dist/lifecycle/release-ledger.js +155 -0
  397. package/dist/lifecycle/release-read.js +64 -0
  398. package/dist/lifecycle/replay.js +193 -0
  399. package/dist/lifecycle/rollback.js +102 -0
  400. package/dist/lifecycle/run-ledger.js +183 -0
  401. package/dist/lifecycle/scenario-eval.js +204 -0
  402. package/dist/lifecycle/scenario.js +214 -0
  403. package/dist/lifecycle/snapshot.js +239 -0
  404. package/dist/lifecycle/status.js +447 -0
  405. package/dist/lifecycle/symptoms.js +117 -0
  406. package/dist/lifecycle/teardown.js +255 -0
  407. package/dist/lifecycle/types.js +1 -0
  408. package/dist/lifecycle/unobserved-gate.js +33 -0
  409. package/dist/lifecycle/wave-plans.js +68 -0
  410. package/dist/lifecycle/work-item-id.js +36 -0
  411. package/dist/lifecycle/work-lease.js +393 -0
  412. package/dist/lint/component-checks.js +87 -0
  413. package/dist/lint/config.js +483 -0
  414. package/dist/lint/declarative.js +122 -0
  415. package/dist/lint/discover.d.ts.map +1 -1
  416. package/dist/lint/discover.js +138 -0
  417. package/dist/lint/engine.js +246 -0
  418. package/dist/lint/knowledge-checks.js +64 -0
  419. package/dist/lint/named-checks.js +22 -0
  420. package/dist/lint/output-checks.js +87 -0
  421. package/dist/lint/output-docs.js +163 -0
  422. package/dist/lint/parser.js +33 -0
  423. package/dist/lint/pipeline-change-gate.js +70 -0
  424. package/dist/lint/policy-import.d.ts +1 -1
  425. package/dist/lint/policy-import.d.ts.map +1 -1
  426. package/dist/lint/policy-import.js +62 -0
  427. package/dist/lint/policy-sandbox.js +86 -0
  428. package/dist/lint/policy.js +122 -0
  429. package/dist/lint/post-synth.js +87 -0
  430. package/dist/lint/presets/relaxed.json +26 -0
  431. package/dist/lint/presets/strict.json +24 -24
  432. package/dist/lint/receipt-checks.js +83 -0
  433. package/dist/lint/rule-loader.d.ts.map +1 -1
  434. package/dist/lint/rule-loader.js +89 -0
  435. package/dist/lint/rule-registry.js +86 -0
  436. package/dist/lint/rule.js +1 -0
  437. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.js +7 -0
  438. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.js +7 -0
  439. package/dist/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.js +9 -0
  440. package/dist/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.js +11 -0
  441. package/dist/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.js +9 -0
  442. package/dist/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.js +25 -0
  443. package/dist/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.js +8 -0
  444. package/dist/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.js +21 -0
  445. package/dist/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.js +8 -0
  446. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.js +24 -0
  447. package/dist/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.js +17 -0
  448. package/dist/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.js +8 -0
  449. package/dist/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.js +18 -0
  450. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.js +20 -0
  451. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.js +8 -0
  452. package/dist/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.js +12 -0
  453. package/dist/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.js +16 -0
  454. package/dist/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.js +8 -0
  455. package/dist/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.js +12 -0
  456. package/dist/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.js +11 -0
  457. package/dist/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.js +11 -0
  458. package/dist/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.js +10 -0
  459. package/dist/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.js +16 -0
  460. package/dist/lint/rules/comp/comp001-publishes-never-applies.js +69 -0
  461. package/dist/lint/rules/comp/comp002-dangling-wiring-ref.js +84 -0
  462. package/dist/lint/rules/comp/comp003-mutating-no-rollback.js +103 -0
  463. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.js +70 -0
  464. package/dist/lint/rules/comp/comp005-capability-kind-is-noun.js +109 -0
  465. package/dist/lint/rules/comp/comp006-shell-needs-reason.js +49 -0
  466. package/dist/lint/rules/comp/comp007-composition-sprawl.js +64 -0
  467. package/dist/lint/rules/comp/index.js +33 -0
  468. package/dist/lint/rules/comp/support.js +129 -0
  469. package/dist/lint/rules/composite-scope.js +51 -0
  470. package/dist/lint/rules/cor017-composite-name-match.js +93 -0
  471. package/dist/lint/rules/cor018-composite-prefer-lexicon-type.js +136 -0
  472. package/dist/lint/rules/cor021-env-literal-name.js +100 -0
  473. package/dist/lint/rules/cor022-receipt-leaf.js +118 -0
  474. package/dist/lint/rules/cor024-receipt-secret-pointer.js +199 -0
  475. package/dist/lint/rules/declarable-naming-convention.js +62 -0
  476. package/dist/lint/rules/evl001-non-literal-expression.js +66 -0
  477. package/dist/lint/rules/evl002-control-flow-resource.js +69 -0
  478. package/dist/lint/rules/evl003-dynamic-property-access.js +44 -0
  479. package/dist/lint/rules/evl004-spread-non-const.js +94 -0
  480. package/dist/lint/rules/evl005-resource-block-body.js +41 -0
  481. package/dist/lint/rules/evl007-invalid-siblings.js +105 -0
  482. package/dist/lint/rules/evl009-composite-no-constant.js +158 -0
  483. package/dist/lint/rules/evl010-composite-no-transform.js +59 -0
  484. package/dist/lint/rules/evl011-symbolic-in-template.js +106 -0
  485. package/dist/lint/rules/export-required.js +115 -0
  486. package/dist/lint/rules/file-declarable-limit.js +88 -0
  487. package/dist/lint/rules/flat-declarations.js +62 -0
  488. package/dist/lint/rules/index.js +90 -0
  489. package/dist/lint/rules/no-cyclic-declarable-ref.js +138 -0
  490. package/dist/lint/rules/no-redundant-type-import.js +76 -0
  491. package/dist/lint/rules/no-redundant-value-cast.js +41 -0
  492. package/dist/lint/rules/no-string-ref.js +59 -0
  493. package/dist/lint/rules/no-unused-declarable-import.js +75 -0
  494. package/dist/lint/rules/no-unused-declarable.js +142 -0
  495. package/dist/lint/rules/op/index.js +28 -0
  496. package/dist/lint/rules/op/ops012-activity-contract.js +54 -0
  497. package/dist/lint/rules/op/ops013-step-output-ref.js +88 -0
  498. package/dist/lint/rules/op/ops014-converge-rule-refusals.js +114 -0
  499. package/dist/lint/rules/op/ops015-gate-approval.js +45 -0
  500. package/dist/lint/rules/op/support.js +11 -0
  501. package/dist/lint/rules/op/sys010-key-over-plain-http.js +100 -0
  502. package/dist/lint/rules/property-kind.js +56 -0
  503. package/dist/lint/rules/single-concern-file.js +71 -0
  504. package/dist/lint/rules/sys001-no-literal-key.js +60 -0
  505. package/dist/lint/selectors.js +168 -0
  506. package/dist/lint/suppressions.js +129 -0
  507. package/dist/lint/typescript.js +17 -0
  508. package/dist/live-endpoint.js +113 -0
  509. package/dist/live-read-session.js +76 -0
  510. package/dist/lsp/lexicon-providers.js +163 -0
  511. package/dist/lsp/types.js +5 -0
  512. package/dist/managed-fields.js +293 -0
  513. package/dist/mcp/types.js +4 -0
  514. package/dist/notify-verify.js +0 -1
  515. package/dist/notify.js +0 -1
  516. package/dist/notify.schema.json +72 -0
  517. package/dist/observation.js +266 -0
  518. package/dist/okf-read.js +134 -0
  519. package/dist/okf.js +275 -0
  520. package/dist/op/__fixtures__/decide-stub-backend.js +125 -0
  521. package/dist/op/activities/activity-contracts.d.ts +1 -1
  522. package/dist/op/activities/activity-contracts.js +313 -0
  523. package/dist/op/activities/apply.js +582 -0
  524. package/dist/op/activities/build.js +18 -0
  525. package/dist/op/activities/change-coverage.js +40 -0
  526. package/dist/op/activities/change-set.d.ts +8 -3
  527. package/dist/op/activities/change-set.d.ts.map +1 -1
  528. package/dist/op/activities/change-set.js +67 -0
  529. package/dist/op/activities/converge.js +492 -0
  530. package/dist/op/activities/decide.js +211 -0
  531. package/dist/op/activities/env-teardown.js +110 -0
  532. package/dist/op/activities/factory.js +591 -0
  533. package/dist/op/activities/forge-pull-request.js +82 -0
  534. package/dist/op/activities/gh-host.js +78 -0
  535. package/dist/op/activities/guard-validate.js +165 -0
  536. package/dist/op/activities/http-check.js +40 -0
  537. package/dist/op/activities/index.js +59 -0
  538. package/dist/op/activities/lexicon-upgrade.js +629 -0
  539. package/dist/op/activities/lifecycle.js +109 -0
  540. package/dist/op/activities/pipeline-audit.js +147 -0
  541. package/dist/op/activities/policy.js +26 -0
  542. package/dist/op/activities/predict-behaviour.js +341 -0
  543. package/dist/op/activities/propose-upgrade.js +165 -0
  544. package/dist/op/activities/reconcile.d.ts +28 -16
  545. package/dist/op/activities/reconcile.d.ts.map +1 -1
  546. package/dist/op/activities/reconcile.js +1011 -0
  547. package/dist/op/activities/shell.js +118 -0
  548. package/dist/op/activities/source-release.js +97 -0
  549. package/dist/op/activities/source-rollback.js +165 -0
  550. package/dist/op/activities/teardown.js +17 -0
  551. package/dist/op/activities/util.js +6 -0
  552. package/dist/op/activities/wait.js +51 -0
  553. package/dist/op/activities/work-evidence.js +35 -0
  554. package/dist/op/activities/workflow-audit.js +133 -0
  555. package/dist/op/activity-contract-registry.js +106 -0
  556. package/dist/op/activity-contract.js +232 -0
  557. package/dist/op/activity-failure.js +31 -0
  558. package/dist/op/activity-profiles.js +129 -0
  559. package/dist/op/activity-registry.js +105 -0
  560. package/dist/op/activity-runtime.js +28 -0
  561. package/dist/op/builders.d.ts +1 -1
  562. package/dist/op/builders.d.ts.map +1 -1
  563. package/dist/op/builders.js +801 -0
  564. package/dist/op/change-signal.js +118 -0
  565. package/dist/op/composites/apply-op.js +151 -0
  566. package/dist/op/composites/behaviour-op.js +65 -0
  567. package/dist/op/composites/converge-op.js +141 -0
  568. package/dist/op/composites/index.js +19 -0
  569. package/dist/op/composites/lexicon-upgrade-op.js +107 -0
  570. package/dist/op/composites/pipeline-audit-op.js +50 -0
  571. package/dist/op/composites/reconcile-op.js +84 -0
  572. package/dist/op/composites/watch-op.js +69 -0
  573. package/dist/op/composites/workflow-audit-op.js +57 -0
  574. package/dist/op/converge-rule.js +205 -0
  575. package/dist/op/cron.js +146 -0
  576. package/dist/op/decide-backend.js +181 -0
  577. package/dist/op/decide-config.js +68 -0
  578. package/dist/op/decide-read-inputs.js +141 -0
  579. package/dist/op/discover.d.ts.map +1 -1
  580. package/dist/op/discover.js +213 -0
  581. package/dist/op/duration.js +21 -0
  582. package/dist/op/emulator-freshness.js +80 -0
  583. package/dist/op/emulator-lifecycle.js +269 -0
  584. package/dist/op/factory.js +83 -0
  585. package/dist/op/gate-approval.js +172 -0
  586. package/dist/op/gate-name.js +86 -0
  587. package/dist/op/gate-plan-context.js +167 -0
  588. package/dist/op/gate-point-run.js +67 -0
  589. package/dist/op/gate-point.js +111 -0
  590. package/dist/op/gate-resume-plan.js +155 -0
  591. package/dist/op/gate-resume.js +31 -0
  592. package/dist/op/gate-review.js +290 -0
  593. package/dist/op/gate-run-locator.js +85 -0
  594. package/dist/op/gate-summary.js +100 -0
  595. package/dist/op/gate-wait.js +56 -0
  596. package/dist/op/gate.js +449 -0
  597. package/dist/op/generate-pipeline.js +124 -0
  598. package/dist/op/index.js +51 -0
  599. package/dist/op/lifecycle-receipt-store.js +45 -0
  600. package/dist/op/local-executor.js +1164 -0
  601. package/dist/op/local-output.js +140 -0
  602. package/dist/op/op-ir.js +317 -0
  603. package/dist/op/op-verb-class.js +131 -0
  604. package/dist/op/op-waves-notify.js +136 -0
  605. package/dist/op/op-waves-run.d.ts +1 -1
  606. package/dist/op/op-waves-run.d.ts.map +1 -1
  607. package/dist/op/op-waves-run.js +1657 -0
  608. package/dist/op/op-waves.js +409 -0
  609. package/dist/op/operator.js +743 -0
  610. package/dist/op/receipt-store.js +94 -0
  611. package/dist/op/resource.js +23 -0
  612. package/dist/op/run-context.js +28 -0
  613. package/dist/op/run-live.js +256 -0
  614. package/dist/op/runtime.js +21 -0
  615. package/dist/op/runtimes/local.js +195 -0
  616. package/dist/op/source-archive.js +131 -0
  617. package/dist/op/step-output-ref.js +397 -0
  618. package/dist/op/steward-beside.js +169 -0
  619. package/dist/op/steward-points.js +138 -0
  620. package/dist/op/steward-turn.js +59 -0
  621. package/dist/op/steward.js +317 -0
  622. package/dist/op/types.js +25 -0
  623. package/dist/op/work-lease-decl.js +81 -0
  624. package/dist/op/work-lease-run.js +393 -0
  625. package/dist/override-gate.js +284 -0
  626. package/dist/owner-chain.js +74 -0
  627. package/dist/ownership.js +92 -0
  628. package/dist/params.js +61 -0
  629. package/dist/plan-summary.js +711 -0
  630. package/dist/point-decision.js +211 -0
  631. package/dist/pr-forge.js +810 -0
  632. package/dist/pr-loop.js +203 -0
  633. package/dist/project-codegen.js +191 -0
  634. package/dist/project-root.js +74 -0
  635. package/dist/project-validation.js +50 -0
  636. package/dist/provenance.js +157 -0
  637. package/dist/pseudo-parameter.js +40 -0
  638. package/dist/reconcile.js +450 -0
  639. package/dist/resource-attributes.js +33 -0
  640. package/dist/runtime-adapter.js +77 -0
  641. package/dist/runtime.js +66 -0
  642. package/dist/secret-materialization.js +153 -0
  643. package/dist/secret-provenance.js +250 -0
  644. package/dist/serializer-walker.js +112 -0
  645. package/dist/serializer.js +1 -0
  646. package/dist/sort.js +45 -0
  647. package/dist/stack-output.js +115 -0
  648. package/dist/telemetry-attribution.js +149 -0
  649. package/dist/terraform/__fixtures__/build-graph.js +63 -0
  650. package/dist/terraform/adopt-state.js +40 -0
  651. package/dist/terraform/aws-resources.js +384 -0
  652. package/dist/terraform/bridge.js +204 -0
  653. package/dist/terraform/carve-provider.js +155 -0
  654. package/dist/terraform/carve.js +138 -0
  655. package/dist/terraform/data-source-shape.js +57 -0
  656. package/dist/terraform/emit-source.js +36 -0
  657. package/dist/terraform/excise.js +123 -0
  658. package/dist/terraform/graduate.js +128 -0
  659. package/dist/terraform/graph.js +435 -0
  660. package/dist/terraform/hcl2json.js +60 -0
  661. package/dist/terraform/manifest.js +149 -0
  662. package/dist/terraform/parse.js +81 -0
  663. package/dist/terraform/providers/aws.js +146 -0
  664. package/dist/terraform/providers/gcp.js +294 -0
  665. package/dist/terraform/providers/index.js +17 -0
  666. package/dist/terraform/providers/kubernetes.js +253 -0
  667. package/dist/terraform/score.js +151 -0
  668. package/dist/terraform/state.js +67 -0
  669. package/dist/terraform/tier-map.js +74 -0
  670. package/dist/terraform/types.js +13 -0
  671. package/dist/terraform/unified-diff.js +132 -0
  672. package/dist/testing.js +295 -0
  673. package/dist/toml-emit.js +153 -0
  674. package/dist/toml-parse.js +355 -0
  675. package/dist/toml-utils.js +60 -0
  676. package/dist/toml.js +428 -0
  677. package/dist/ts-loader.d.ts +46 -0
  678. package/dist/ts-loader.d.ts.map +1 -0
  679. package/dist/ts-loader.js +103 -0
  680. package/dist/types.js +1 -0
  681. package/dist/utils.js +61 -0
  682. package/dist/validation.js +20 -0
  683. package/dist/vendor/js-yaml-load.js +1662 -0
  684. package/dist/workspace/__fixtures__/contract-repo.d.ts +7 -0
  685. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  686. package/dist/workspace/__fixtures__/contract-repo.js +119 -0
  687. package/dist/workspace/__fixtures__/decide-workspace.js +112 -0
  688. package/dist/workspace/__fixtures__/sessions.d.ts +1 -1
  689. package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -1
  690. package/dist/workspace/__fixtures__/sessions.js +91 -0
  691. package/dist/workspace/__fixtures__/two-member-workspace.js +34 -0
  692. package/dist/workspace/agent-cli.js +99 -0
  693. package/dist/workspace/agent.schema.json +390 -0
  694. package/dist/workspace/answers-ledger.js +89 -0
  695. package/dist/workspace/box-cli.js +142 -0
  696. package/dist/workspace/box-factory-write.js +196 -0
  697. package/dist/workspace/box-factory-write.schema.json +342 -0
  698. package/dist/workspace/box-factory.js +88 -0
  699. package/dist/workspace/box-intent.js +126 -0
  700. package/dist/workspace/box-isolation.js +169 -0
  701. package/dist/workspace/box-listing-write.schema.json +204 -0
  702. package/dist/workspace/box-listing.js +298 -0
  703. package/dist/workspace/box-publish.js +352 -0
  704. package/dist/workspace/box-publish.schema.json +498 -0
  705. package/dist/workspace/box-services.js +39 -0
  706. package/dist/workspace/broker-protocol.js +359 -0
  707. package/dist/workspace/broker-protocol.schema.json +228 -0
  708. package/dist/workspace/change-set.schema.json +462 -0
  709. package/dist/workspace/changes-cli.js +56 -0
  710. package/dist/workspace/changes.js +446 -0
  711. package/dist/workspace/changes.schema.json +642 -0
  712. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.js +198 -0
  713. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.js +155 -0
  714. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.js +214 -0
  715. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.js +180 -0
  716. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.js +99 -0
  717. package/dist/workspace/chant-migrations/chud-lexicon-exit.js +847 -0
  718. package/dist/workspace/chant-migrations.js +113 -0
  719. package/dist/workspace/check.schema.json +433 -0
  720. package/dist/workspace/checks/box-isolation.js +62 -0
  721. package/dist/workspace/checks/boxes.js +491 -0
  722. package/dist/workspace/checks/diagrams.js +104 -0
  723. package/dist/workspace/checks/generated.js +419 -0
  724. package/dist/workspace/checks/ledgers.js +212 -0
  725. package/dist/workspace/checks/links.js +191 -0
  726. package/dist/workspace/checks/live.js +100 -0
  727. package/dist/workspace/checks/pipelines.js +188 -0
  728. package/dist/workspace/checks/records.js +169 -0
  729. package/dist/workspace/checks.js +426 -0
  730. package/dist/workspace/ci-green-cli.js +125 -0
  731. package/dist/workspace/ci-green-forge.js +83 -0
  732. package/dist/workspace/ci-green-workflow.d.ts +3 -1
  733. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  734. package/dist/workspace/ci-green-workflow.js +202 -0
  735. package/dist/workspace/ci-green.js +325 -0
  736. package/dist/workspace/ci-last-green.schema.json +158 -0
  737. package/dist/workspace/class-data.d.ts +84 -0
  738. package/dist/workspace/class-data.d.ts.map +1 -0
  739. package/dist/workspace/class-data.js +238 -0
  740. package/dist/workspace/compose-graph.js +235 -0
  741. package/dist/workspace/compose-reports.js +146 -0
  742. package/dist/workspace/composites.js +264 -0
  743. package/dist/workspace/composites.schema.json +617 -0
  744. package/dist/workspace/conformance/__fixture__/app/package.json +7 -0
  745. package/dist/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
  746. package/dist/workspace/conformance/__fixture__/decisions/decision.kind.mjs +54 -0
  747. package/dist/workspace/conformance/__fixture__/decisions/decision.schema.json +512 -0
  748. package/dist/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
  749. package/dist/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
  750. package/dist/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
  751. package/dist/workspace/conformance/__fixture__/delivery/package.json +7 -0
  752. package/dist/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
  753. package/dist/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
  754. package/dist/workspace/conformance/__fixtures__/reference-broker.js +205 -0
  755. package/dist/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +32 -0
  756. package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +135 -0
  757. package/dist/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  758. package/dist/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  759. package/dist/workspace/conformance/__writer_fixture__/decisions/points.json +228 -0
  760. package/dist/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  761. package/dist/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  762. package/dist/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +54 -0
  763. package/dist/workspace/conformance/__writer_fixture__/sessions/session.schema.json +289 -0
  764. package/dist/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
  765. package/dist/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
  766. package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +603 -0
  767. package/dist/workspace/conformance/broker.js +736 -0
  768. package/dist/workspace/conformance/index.js +673 -0
  769. package/dist/workspace/conformance/vitest.js +108 -0
  770. package/dist/workspace/conformance/writer.js +1080 -0
  771. package/dist/workspace/decide.js +879 -0
  772. package/dist/workspace/decision-points.schema.json +135 -135
  773. package/dist/workspace/declaration-write-cli.js +74 -0
  774. package/dist/workspace/declaration-write.js +238 -0
  775. package/dist/workspace/declaration.js +938 -0
  776. package/dist/workspace/declaration.schema.json +1413 -1413
  777. package/dist/workspace/declared-kinds.js +106 -0
  778. package/dist/workspace/environments.js +164 -0
  779. package/dist/workspace/evidence.schema.json +279 -0
  780. package/dist/workspace/export-cli.js +185 -0
  781. package/dist/workspace/export.js +499 -0
  782. package/dist/workspace/factory-rules.js +175 -0
  783. package/dist/workspace/generated-files.js +92 -0
  784. package/dist/workspace/glob.js +45 -0
  785. package/dist/workspace/graph-cache.js +352 -0
  786. package/dist/workspace/graph-cli.js +348 -0
  787. package/dist/workspace/graph.schema.json +1338 -0
  788. package/dist/workspace/identity-gate-rule.js +75 -0
  789. package/dist/workspace/identity.js +126 -0
  790. package/dist/workspace/import.js +283 -0
  791. package/dist/workspace/init.js +620 -0
  792. package/dist/workspace/intent-cli.js +263 -0
  793. package/dist/workspace/intent-joins.js +201 -0
  794. package/dist/workspace/intent-record.js +352 -0
  795. package/dist/workspace/intent-record.schema.json +864 -0
  796. package/dist/workspace/intent.js +1582 -0
  797. package/dist/workspace/intent.schema.json +2420 -0
  798. package/dist/workspace/json-edit.js +221 -0
  799. package/dist/workspace/jsonc.js +235 -0
  800. package/dist/workspace/kind-readers.js +103 -0
  801. package/dist/workspace/kinds.d.ts +3 -7
  802. package/dist/workspace/kinds.d.ts.map +1 -1
  803. package/dist/workspace/kinds.js +553 -0
  804. package/dist/workspace/lexicon-symbols.js +112 -0
  805. package/dist/workspace/lineage-adopt-cli.js +102 -0
  806. package/dist/workspace/lineage-adopt.js +360 -0
  807. package/dist/workspace/lineage-check.js +282 -0
  808. package/dist/workspace/lineage-cli.js +121 -0
  809. package/dist/workspace/lineage-hash-index.js +284 -0
  810. package/dist/workspace/lineage-init.js +507 -0
  811. package/dist/workspace/lineage-lock.js +387 -0
  812. package/dist/workspace/lineage-merge.js +45 -0
  813. package/dist/workspace/lineage-migrations.d.ts.map +1 -1
  814. package/dist/workspace/lineage-migrations.js +329 -0
  815. package/dist/workspace/lineage-provenance.js +36 -0
  816. package/dist/workspace/lineage-update.js +138 -0
  817. package/dist/workspace/lineage-upgrade-cli.js +131 -0
  818. package/dist/workspace/lineage-upgrade.js +889 -0
  819. package/dist/workspace/lineage-vendor.js +171 -0
  820. package/dist/workspace/lineage-version.js +128 -0
  821. package/dist/workspace/lineage-versions.js +275 -0
  822. package/dist/workspace/links.js +330 -0
  823. package/dist/workspace/ls-generated.js +95 -0
  824. package/dist/workspace/ls.js +277 -0
  825. package/dist/workspace/ls.schema.json +333 -0
  826. package/dist/workspace/member-commands.js +508 -0
  827. package/dist/workspace/member-handles.js +216 -0
  828. package/dist/workspace/member-pipeline.js +258 -0
  829. package/dist/workspace/member-run.js +117 -0
  830. package/dist/workspace/member-write.schema.json +193 -0
  831. package/dist/workspace/nested-graph.js +156 -0
  832. package/dist/workspace/nesting.js +38 -0
  833. package/dist/workspace/patch.js +301 -0
  834. package/dist/workspace/patch.schema.json +321 -0
  835. package/dist/workspace/pin-cli.js +55 -0
  836. package/dist/workspace/pin-integrity.js +100 -0
  837. package/dist/workspace/plan-summary.schema.json +155 -0
  838. package/dist/workspace/point-answer.schema.json +129 -129
  839. package/dist/workspace/points-cli.js +291 -0
  840. package/dist/workspace/points-write.schema.json +660 -0
  841. package/dist/workspace/points.js +521 -0
  842. package/dist/workspace/points.schema.json +916 -0
  843. package/dist/workspace/pr-report.schema.json +108 -0
  844. package/dist/workspace/principal-classes.d.ts +4 -11
  845. package/dist/workspace/principal-classes.d.ts.map +1 -1
  846. package/dist/workspace/principal-classes.js +162 -0
  847. package/dist/workspace/reason-codes.js +326 -0
  848. package/dist/workspace/record-assets.js +151 -0
  849. package/dist/workspace/record-decided.js +105 -0
  850. package/dist/workspace/record-sessions.js +96 -0
  851. package/dist/workspace/record-source.js +114 -0
  852. package/dist/workspace/records-amend.schema.json +284 -0
  853. package/dist/workspace/records-checkout.js +76 -0
  854. package/dist/workspace/records-cli.js +707 -0
  855. package/dist/workspace/records-close.js +116 -0
  856. package/dist/workspace/records-close.schema.json +278 -0
  857. package/dist/workspace/records-new.schema.json +202 -0
  858. package/dist/workspace/records-review.schema.json +367 -0
  859. package/dist/workspace/records-since.js +317 -0
  860. package/dist/workspace/records-since.schema.json +218 -0
  861. package/dist/workspace/records-write.js +1229 -0
  862. package/dist/workspace/records.js +1345 -0
  863. package/dist/workspace/records.schema.json +604 -0
  864. package/dist/workspace/reference-kinds/app/package.json +10 -0
  865. package/dist/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  866. package/dist/workspace/returns.js +303 -0
  867. package/dist/workspace/root-refusal.js +75 -0
  868. package/dist/workspace/run-statement.schema.json +622 -0
  869. package/dist/workspace/runs-cli.js +584 -0
  870. package/dist/workspace/runs-write.schema.json +994 -0
  871. package/dist/workspace/runs.js +708 -0
  872. package/dist/workspace/runs.schema.json +1257 -0
  873. package/dist/workspace/runtimes.js +76 -0
  874. package/dist/workspace/session-kinds.js +83 -0
  875. package/dist/workspace/signers.schema.json +206 -0
  876. package/dist/workspace/source-block.js +125 -0
  877. package/dist/workspace/squash.js +177 -0
  878. package/dist/workspace/status-gates.js +162 -0
  879. package/dist/workspace/status-stewards.js +330 -0
  880. package/dist/workspace/status.js +514 -0
  881. package/dist/workspace/status.schema.json +998 -0
  882. package/dist/workspace/steward-index.js +191 -0
  883. package/dist/workspace/symbols.js +188 -0
  884. package/dist/workspace/template-manifest.js +227 -0
  885. package/dist/workspace/template-pins.js +115 -0
  886. package/dist/workspace/trailer-joins.js +65 -0
  887. package/dist/workspace/trailers.js +111 -0
  888. package/dist/workspace/tree.js +136 -0
  889. package/dist/workspace/trust/attestor.js +63 -0
  890. package/dist/workspace/trust/dsse.js +108 -0
  891. package/dist/workspace/trust/evidence-cli.js +175 -0
  892. package/dist/workspace/trust/evidence.js +161 -0
  893. package/dist/workspace/trust/gate-seal.js +226 -0
  894. package/dist/workspace/trust/policy.js +190 -0
  895. package/dist/workspace/trust/provenance.js +180 -0
  896. package/dist/workspace/trust/rotation.js +298 -0
  897. package/dist/workspace/trust/run-commits.js +101 -0
  898. package/dist/workspace/trust/run-statement.js +182 -0
  899. package/dist/workspace/trust/seal.js +272 -0
  900. package/dist/workspace/trust/signers-cli.js +152 -0
  901. package/dist/workspace/trust/ssh-commit.js +178 -0
  902. package/dist/workspace/trust/test-repo.js +118 -0
  903. package/dist/workspace/trust/trust-files.js +193 -0
  904. package/dist/workspace/trust/verify-cli.js +63 -0
  905. package/dist/workspace/trust/verify.js +192 -0
  906. package/dist/workspace/which-chant.js +141 -0
  907. package/dist/workspace/wip-cli.js +95 -0
  908. package/dist/workspace/wip-write.schema.json +504 -0
  909. package/dist/workspace/wip.js +631 -0
  910. package/dist/workspace/wip.schema.json +334 -0
  911. package/dist/workspace/work-cli.js +448 -0
  912. package/dist/workspace/work-evidence.js +122 -0
  913. package/dist/workspace/work-evidence.schema.json +222 -0
  914. package/dist/workspace/work-history.schema.json +421 -0
  915. package/dist/workspace/work-lease.schema.json +246 -0
  916. package/dist/workspace/work-size.js +77 -0
  917. package/dist/workspace/work.js +283 -0
  918. package/dist/workspace/workspace-kinds.schema.json +259 -259
  919. package/dist/workspace/workspace-principals.schema.json +53 -53
  920. package/dist/workspace/write-lock-cli.js +105 -0
  921. package/dist/workspace/write-lock.js +424 -0
  922. package/dist/workspace/write-lock.schema.json +172 -0
  923. package/dist/workspace/write-scope.js +484 -0
  924. package/dist/yaml.js +459 -0
  925. package/package.json +2 -2
  926. package/src/build.ts +8 -0
  927. package/src/change-set.test.ts +48 -0
  928. package/src/change-set.ts +52 -2
  929. package/src/ci-jobs.test.ts +49 -0
  930. package/src/ci-jobs.ts +515 -0
  931. package/src/cli/commands/check-lexicon-plugin.ts +2 -0
  932. package/src/cli/commands/lint.ts +3 -0
  933. package/src/cli/commands/migrate.ts +2 -0
  934. package/src/cli/compiled-cli.e2e.test.ts +165 -0
  935. package/src/cli/main.ts +121 -27
  936. package/src/cli/mcp/workspace-tools.ts +1 -1
  937. package/src/codegen/pinned-upgrade.ts +2 -0
  938. package/src/components/discover.ts +2 -0
  939. package/src/components/pr-pipeline.ts +3 -2
  940. package/src/components/sandbox/driver.ts +3 -1
  941. package/src/composite.ts +15 -3
  942. package/src/config-import.ts +3 -0
  943. package/src/discovery/collect.ts +15 -1
  944. package/src/discovery/fold-build-parity.test.ts +71 -0
  945. package/src/discovery/fold-import.ts +25 -127
  946. package/src/discovery/fold-project-provenance.test.ts +21 -4
  947. package/src/discovery/host-composite.ts +101 -0
  948. package/src/discovery/import.ts +2 -0
  949. package/src/discovery/index.ts +11 -1
  950. package/src/discovery/sandbox/driver.ts +3 -1
  951. package/src/gate-decision-bundle.test.ts +5 -4
  952. package/src/gate-decision-identity.test.ts +59 -6
  953. package/src/gate-decision.ts +81 -21
  954. package/src/lexicon-module.ts +3 -0
  955. package/src/lifecycle/gate-audit.test.ts +55 -1
  956. package/src/lifecycle/gate-audit.ts +116 -3
  957. package/src/lifecycle/gate-ledger.test.ts +20 -1
  958. package/src/lifecycle/gate-ledger.ts +33 -1
  959. package/src/lint/config.ts +1 -1
  960. package/src/lint/discover.ts +28 -8
  961. package/src/lint/policy-import.ts +4 -1
  962. package/src/lint/rule-loader.ts +2 -0
  963. package/src/op/activities/activity-contracts.ts +1 -1
  964. package/src/op/activities/change-set.ts +11 -3
  965. package/src/op/activities/lexicon-upgrade.test.ts +2 -0
  966. package/src/op/activities/reconcile.test.ts +62 -2
  967. package/src/op/activities/reconcile.ts +46 -22
  968. package/src/op/builders.ts +1 -1
  969. package/src/op/discover.ts +3 -0
  970. package/src/op/op-waves-run.ts +4 -3
  971. package/src/op/op-waves.test.ts +2 -0
  972. package/src/op/work-lease-run.test.ts +12 -4
  973. package/src/ts-loader.test.ts +23 -0
  974. package/src/ts-loader.ts +122 -0
  975. package/src/workspace/__fixtures__/contract-repo.ts +17 -1
  976. package/src/workspace/__fixtures__/sessions.ts +3 -3
  977. package/src/workspace/change-set.schema.json +5 -3
  978. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +1 -1
  979. package/src/workspace/ci-green-workflow.test.ts +4 -4
  980. package/src/workspace/ci-green-workflow.ts +5 -2
  981. package/src/workspace/class-data.test.ts +64 -0
  982. package/src/workspace/class-data.ts +236 -0
  983. package/src/workspace/declaration.ts +1 -1
  984. package/src/workspace/kinds.ts +7 -36
  985. package/src/workspace/lineage-migrations.ts +2 -0
  986. package/src/workspace/points.ts +1 -1
  987. package/src/workspace/principal-classes.ts +12 -95
  988. package/src/workspace/records-formats.test.ts +7 -2
  989. package/src/workspace/records-sessions-write.test.ts +3 -3
  990. package/src/workspace/status-contract.e2e.test.ts +37 -0
  991. package/src/workspace/status-contract.test.ts +23 -21
@@ -0,0 +1,1657 @@
1
+ /**
2
+ * What an Op waves job runs (#3679): `chant run wave --spec <file> --wave <k>`.
3
+ * `./op-waves.ts` holds the spec and the jobs; this plans a wave's runs,
4
+ * decides its gate and applies it.
5
+ *
6
+ * - Plan: each run's `plan` command writes a plan file; its `planDigest` is
7
+ * the run's member digest, named by its target. With `planWave` (#3762)
8
+ * one call plans every run and lists each run's plan; the digest and the
9
+ * decision are the same as from per-run plans.
10
+ * - Hooks (#3762): read at base with the policy, run before and after the
11
+ * plan and the apply. One with `onFailure: approve` that fails holds the
12
+ * wave: it waits for an approval of its set digest whatever its policy.
13
+ * - Decide: the wave's policy comes from the spec file at the base commit
14
+ * (`git show <base>:<file>`), so a change cannot loosen its own gate. A
15
+ * wave missing there, or a file missing there, is `always`. A wave that
16
+ * needs an approval waits on `<gate>-wave-<k>` bound to
17
+ * {@link waveSetDigest} over its members, and exits 3 when nobody approved
18
+ * that digest.
19
+ * - Which approvals count is the wave's `approval`, read at base like its
20
+ * policy (#3684): `ledger`, `sealed` or `pr-review` (`./gate-review.ts`).
21
+ * Under `pr-review` the pull request's pipeline runs `--record-plans`
22
+ * ({@link recordOpWaveHeadPlans}), which plans every wave at the head and
23
+ * records the digests a review of that head approves.
24
+ * - Spend (#3784): with `approvals.spend` at base, an approval a wave applied
25
+ * under counts for nothing, and the wave records each approval it applies
26
+ * under in `_gates/<name>/applied.jsonl` (`../lifecycle/gate-applied.ts`)
27
+ * just before it applies (a wide wave's deciding job, before its shares).
28
+ * - Apply: each run's `apply` command, or one `applyWave` call for the
29
+ * approved set (#3762). A share job (`--share i`) re-plans its
30
+ * slice first and refuses, without applying it, a run whose plan digest is
31
+ * not the one the deciding job recorded (exit 4).
32
+ * - Hooks file (#3804): every apply call gets `CHANT_HOOKS`, a JSON file
33
+ * ({@link OpWaveHooksFile}) with the outcomes of the hooks that already ran
34
+ * and whether an approval stands. Under `hooks.apply.runBy: "apply"` (read
35
+ * at base) chant does not run the apply-stage hooks itself: the file hands
36
+ * them to the apply, which runs them under its own lock with
37
+ * {@link runOpWaveApplyHooks}, and their outcomes come back through
38
+ * `CHANT_HOOKS_RESULT`. A non-wave apply reads the same hooks at base with
39
+ * {@link opWaveHooksAtBase}.
40
+ * - Point (#3836): with `point` at base (the wave's, else the spec's), a wave
41
+ * that needs an approval asks that decision point through
42
+ * `evaluatePointGate` (`../point-decision.ts`) with `gate.planDigest` set to
43
+ * its set digest: an open question waits (exit 3, the pending fact cites
44
+ * it, `approve` is the `points answer` line), an answer the gate passes on
45
+ * records the resolution that cites it and the wave goes on, and any other
46
+ * answer refuses the wave (`refused.reason: "point"`, exit 8). Once the
47
+ * question is answered, `chant run resume --op <name>` starts the job again
48
+ * (`pointGateResumable`, #3835).
49
+ */
50
+ import { execFileSync, spawnSync } from "node:child_process";
51
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
52
+ import { dirname, isAbsolute, join, posix } from "node:path";
53
+ import { waveSetDigest } from "../gated-waves.js";
54
+ import { approveCommand, evaluateGate, gitGateLedgerPort } from "./gate.js";
55
+ import { isApprovalSpent } from "../lifecycle/gate-applied.js";
56
+ import { OP_WAVE_DEFAULT_APPLY, OP_WAVE_GATE_POLICIES, OP_WAVE_HOOK_MOMENTS, GATE_APPROVAL_SOURCES, assertOpWaveHooks, assertOpWavesSpec, opWaveApplyHooksRunBy, opWaveGateName, opWaveHooksAt, opWavePointAt, opWavePointProblems, opWaveRecordPath, opWaveShare, } from "./op-waves.js";
57
+ /** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
58
+ export const OP_WAVE_PENDING_RESERVED = [
59
+ "version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume",
60
+ "environment", "origin", "neverOverMcp", "approval", "approvalMode", "answer", "wave", "heldBy",
61
+ ];
62
+ export const OP_WAVE_APPLY_OUTCOMES = ["stood-down", "held", "replan"];
63
+ const spawnExec = (argv, cwd, env) => {
64
+ const result = spawnSync(argv[0], argv.slice(1), { cwd, stdio: "inherit", ...(env ? { env: { ...process.env, ...env } } : {}) });
65
+ if (result.error)
66
+ throw result.error;
67
+ return result.status ?? 1;
68
+ };
69
+ const gitShowAtBase = (base, path, cwd) => {
70
+ let sha;
71
+ try {
72
+ sha = execFileSync("git", ["rev-parse", "--verify", `${base}^{commit}`], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
73
+ }
74
+ catch {
75
+ throw new Error(`the gate policy is read at ${base}, which this checkout does not have. ` +
76
+ `Check out the full history (fetch-depth: 0 on GitHub and Forgejo, GIT_DEPTH: "0" on GitLab), or set the spec's base.`);
77
+ }
78
+ try {
79
+ const text = execFileSync("git", ["show", `${sha}:./${path}`], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
80
+ return { sha, text };
81
+ }
82
+ catch {
83
+ return { sha, text: null };
84
+ }
85
+ };
86
+ /** Read a spec file, refusing anything that is not one. */
87
+ export function parseOpWavesSpec(text, where) {
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(text);
91
+ }
92
+ catch (err) {
93
+ throw new Error(`${where}: ${err instanceof Error ? err.message : String(err)}`);
94
+ }
95
+ const waves = parsed?.waves;
96
+ const spec = (Array.isArray(waves) ? parsed : waves);
97
+ if (!spec || typeof spec !== "object" || !Array.isArray(spec.waves)) {
98
+ throw new Error(`${where} must hold an Op waves spec: { "name", "op", "plan", "waves": [...] }, or { "waves": <that>, "options": {...} }`);
99
+ }
100
+ return spec;
101
+ }
102
+ const noHooks = () => Object.fromEntries(OP_WAVE_HOOK_MOMENTS.map((m) => [m, []]));
103
+ /**
104
+ * The policy of wave `name`, as the spec file at `base` declares it. The
105
+ * file or the wave missing there reads as `always`: a change that adds a
106
+ * wave, or the file, waits for a person the first time. Its stage hooks are
107
+ * read there too (#3762), so a change cannot add, edit or remove a hook its
108
+ * own apply runs; the file or the wave missing there has none.
109
+ */
110
+ export function readOpWavePolicy(specFile, name, base, cwd, show = gitShowAtBase) {
111
+ const { sha, text } = show(base, specFile.replace(/^\.\//, "").split("\\").join(posix.sep), cwd);
112
+ const absent = { policy: "always", approval: "ledger", source: "absent-at-base", base: sha, hooks: noHooks(), spend: false, applyHooksRunBy: "chant", neverOverMcp: false };
113
+ if (text === null)
114
+ return absent;
115
+ let spec;
116
+ try {
117
+ spec = parseOpWavesSpec(text, `${specFile} at ${base}`);
118
+ }
119
+ catch {
120
+ return absent;
121
+ }
122
+ // The spend rule is the spec's, so it holds for a wave the base does not have yet.
123
+ const spend = spec.approvals?.spend === true;
124
+ const neverOverMcp = spec.neverOverMcp === true;
125
+ // #3836: the gate's decision point, the spec's for a wave the base does not have yet.
126
+ const asks = pointAtBase(spec, name, `${specFile} at ${sha.slice(0, 12)}`);
127
+ const wave = spec.waves.find((w) => w.name === name);
128
+ if (!wave)
129
+ return { ...absent, spend, neverOverMcp, ...asks };
130
+ const policy = wave.gate ?? "always";
131
+ // An approval mode this chant does not know reads as sealed: the strictest, so an unknown rule never loosens a gate.
132
+ const approval = wave.approval === undefined ? "ledger" : GATE_APPROVAL_SOURCES.includes(wave.approval) ? wave.approval : "sealed";
133
+ const rules = { policy: OP_WAVE_GATE_POLICIES.includes(policy) ? policy : "always", approval, source: "base", base: sha, hooks: noHooks(), spend, applyHooksRunBy: "chant", neverOverMcp, ...asks };
134
+ try {
135
+ assertOpWaveHooks(`${specFile} at ${sha.slice(0, 12)}`, spec.hooks);
136
+ assertOpWaveHooks(`${specFile} at ${sha.slice(0, 12)}, wave "${name}"`, wave.hooks);
137
+ rules.hooks = opWaveHooksAt(spec, name);
138
+ rules.applyHooksRunBy = opWaveApplyHooksRunBy(spec, name);
139
+ }
140
+ catch (err) {
141
+ // A hook at base no runner can run: none run, and the wave waits for a person rather than going out without them.
142
+ rules.hooksRefused = `the hooks at base cannot be run (${err instanceof Error ? err.message : String(err)})`;
143
+ }
144
+ return rules;
145
+ }
146
+ /** The point wave `name` asks at base (#3836), or why it cannot be asked. */
147
+ function pointAtBase(spec, name, where) {
148
+ const raw = spec.waves.find((w) => w.name === name)?.point ?? spec.point;
149
+ if (raw === undefined)
150
+ return {};
151
+ const problems = opWavePointProblems(raw);
152
+ if (problems.length > 0)
153
+ return { pointRefused: `the decision point at base cannot be asked (${where}: ${problems.join("; ")})` };
154
+ return { point: opWavePointAt(spec, name) };
155
+ }
156
+ /** Fill one argv template. A placeholder whose value is not given is left as it is. */
157
+ export function opWaveArgv(template, values) {
158
+ const pairs = [
159
+ ["{op}", values.op],
160
+ ["{target}", values.target],
161
+ ["{wave}", values.wave],
162
+ ["{plans}", values.plans],
163
+ ["{plan}", values.plan],
164
+ ["{runs}", values.runs],
165
+ ];
166
+ return template.map((part) => pairs.reduce((out, [key, value]) => (value === undefined ? out : out.replaceAll(key, value)), part));
167
+ }
168
+ /** Where a run's plan file goes. */
169
+ export function opWavePlanPath(spec, wave, target) {
170
+ return `.chant/op-waves/${spec.name}/wave-${wave}/${target.replace(/[^A-Za-z0-9._-]/g, "_")}.plan.json`;
171
+ }
172
+ /** Where a wave's runs file goes: the `{runs}` of a wave-level command or a hook (#3762). */
173
+ export function opWaveRunsPath(spec, wave) {
174
+ return `.chant/op-waves/${spec.name}/wave-${wave}/runs.json`;
175
+ }
176
+ /** Where a wave-level plan command writes its plans: its `{plans}` (#3762). */
177
+ export function opWavePlansPath(spec, wave) {
178
+ return `.chant/op-waves/${spec.name}/wave-${wave}/plans.json`;
179
+ }
180
+ function planRun(opts, waveName, run) {
181
+ const op = run.op ?? opts.spec.op;
182
+ const plan = opWavePlanPath(opts.spec, opts.wave, run.target);
183
+ mkdirSync(dirname(join(opts.cwd, plan)), { recursive: true });
184
+ const code = opts.exec(opWaveArgv(opts.spec.plan, { op, target: run.target, wave: waveName, plan }), opts.cwd);
185
+ if (code !== 0)
186
+ throw new Error(`planning ${run.target} exited ${code}`);
187
+ let file;
188
+ try {
189
+ file = JSON.parse(readFileSync(join(opts.cwd, plan), "utf-8"));
190
+ }
191
+ catch (err) {
192
+ throw new Error(`planning ${run.target} wrote no readable plan file at ${plan}: ${err instanceof Error ? err.message : String(err)}`);
193
+ }
194
+ if (typeof file.planDigest !== "string" || file.planDigest === "") {
195
+ throw new Error(`the plan file for ${run.target} (${plan}) has no planDigest`);
196
+ }
197
+ return member(run.target, op, file);
198
+ }
199
+ function member(target, op, file) {
200
+ const denied = file.denied;
201
+ if (denied !== undefined) {
202
+ const d = denied;
203
+ if (!d || typeof d !== "object" || !Array.isArray(d.rules) || d.rules.length === 0 || !d.rules.every((r) => typeof r === "string" && r.trim() !== "") || (d.reason !== undefined && typeof d.reason !== "string")) {
204
+ throw new Error(`the plan of ${target} has denied ${JSON.stringify(denied)}; a denial is { "rules": ["<rule id>", ...], "reason"?: "<text>" }`);
205
+ }
206
+ }
207
+ return {
208
+ member: target,
209
+ planDigest: file.planDigest,
210
+ op,
211
+ ...(file.destructive === true ? { destructive: true } : {}),
212
+ ...(file.empty === true ? { empty: true } : {}),
213
+ ...(file.denied ? { denied: { rules: file.denied.rules, ...(file.denied.reason ? { reason: file.denied.reason } : {}) } } : {}),
214
+ };
215
+ }
216
+ /** Write the `{runs}` file: the runs, with their plans once planned. */
217
+ function writeRunsFile(at, runs, members, digest, path = opWaveRunsPath(at.spec, at.k), discover = false) {
218
+ const file = {
219
+ wave: at.wave.name,
220
+ number: at.k,
221
+ ...(digest ? { digest } : {}),
222
+ ...(discover ? { discover: true } : {}),
223
+ runs: runs.map((run) => {
224
+ const m = members?.get(run.target);
225
+ return {
226
+ target: run.target,
227
+ op: run.op ?? at.spec.op,
228
+ ...(m ? { planDigest: m.planDigest, ...(m.destructive ? { destructive: true } : {}), ...(m.empty ? { empty: true } : {}) } : {}),
229
+ };
230
+ }),
231
+ };
232
+ mkdirSync(dirname(join(at.cwd, path)), { recursive: true });
233
+ writeFileSync(join(at.cwd, path), JSON.stringify(file, null, 2) + "\n");
234
+ return path;
235
+ }
236
+ /**
237
+ * Plan `runs` of wave `k`: one `plan` call per run, or one `planWave` call
238
+ * for all of them (#3762). A wave plan is written out as each run's plan file
239
+ * too, so a per-run `apply` or hook reads `{plan}` either way.
240
+ */
241
+ function planRuns(at, runs, discover = false) {
242
+ const { spec, k, wave, cwd, exec } = at;
243
+ if (!spec.planWave)
244
+ return { members: runs.map((run) => planRun({ spec, wave: k, cwd, exec }, wave.name, run)) };
245
+ const runsPath = writeRunsFile(at, runs, undefined, undefined, undefined, discover);
246
+ const plans = opWavePlansPath(spec, k);
247
+ rmSync(join(cwd, plans), { force: true });
248
+ const code = exec(opWaveArgv(spec.planWave, { op: spec.op, wave: wave.name, runs: runsPath, plans }), cwd);
249
+ if (code !== 0)
250
+ throw new Error(`planning wave ${k} (${wave.name}) exited ${code}`);
251
+ let parsed;
252
+ try {
253
+ parsed = JSON.parse(readFileSync(join(cwd, plans), "utf-8"));
254
+ }
255
+ catch (err) {
256
+ throw new Error(`planning wave ${k} (${wave.name}) wrote no readable plans file at ${plans}: ${err instanceof Error ? err.message : String(err)}`);
257
+ }
258
+ const entries = Array.isArray(parsed) ? parsed : parsed?.runs;
259
+ if (!Array.isArray(entries))
260
+ throw new Error(`the plans file of wave ${k} (${plans}) must hold { "runs": [{ "target", "planDigest" }] }`);
261
+ const wanted = new Set(runs.map((r) => r.target));
262
+ const byTarget = new Map();
263
+ // #3821: a discovering wave's runs are the ones the plans file names.
264
+ const found = [];
265
+ for (const entry of entries) {
266
+ const target = entry?.target;
267
+ if (discover && typeof target === "string" && target !== "" && !byTarget.has(target)) {
268
+ if (entry.op !== undefined && (typeof entry.op !== "string" || entry.op === ""))
269
+ throw new Error(`the plans file of wave ${k} (${plans}) gives ${target} an op that is not a name`);
270
+ found.push({ target, ...(entry.op && entry.op !== spec.op ? { op: entry.op } : {}) });
271
+ wanted.add(target);
272
+ }
273
+ if (typeof target !== "string" || !wanted.has(target)) {
274
+ throw new Error(`the plans file of wave ${k} (${plans}) plans ${JSON.stringify(target)}, which is not a run it was asked to plan`);
275
+ }
276
+ if (byTarget.has(target))
277
+ throw new Error(`the plans file of wave ${k} (${plans}) plans ${target} twice`);
278
+ if (typeof entry.planDigest !== "string" || entry.planDigest === "")
279
+ throw new Error(`the plans file of wave ${k} (${plans}) has no planDigest for ${target}`);
280
+ byTarget.set(target, entry);
281
+ }
282
+ const missing = runs.filter((r) => !byTarget.has(r.target)).map((r) => r.target);
283
+ if (missing.length > 0)
284
+ throw new Error(`the plans file of wave ${k} (${plans}) has no plan for ${missing.join(", ")}`);
285
+ if (discover)
286
+ runs = found;
287
+ const extras = Array.isArray(parsed) ? undefined : extraMembers(parsed.members, new Set(runs.map((r) => r.target)), `the plans file of wave ${k} (${plans})`);
288
+ const count = Array.isArray(parsed) ? undefined : parsed.waves;
289
+ if (count !== undefined && (!Number.isInteger(count) || count < 0))
290
+ throw new Error(`the plans file of wave ${k} (${plans}): \`waves\` must be the number of waves found, a whole number`);
291
+ const pending = Array.isArray(parsed) ? undefined : pendingFields(parsed.pending, `the plans file of wave ${k} (${plans})`);
292
+ const members = runs.map((run) => {
293
+ const m = member(run.target, run.op ?? spec.op, byTarget.get(run.target));
294
+ const path = join(cwd, opWavePlanPath(spec, k, run.target));
295
+ writeFileSync(path, JSON.stringify({ planDigest: m.planDigest, ...(m.destructive ? { destructive: true } : {}), ...(m.empty ? { empty: true } : {}), ...(m.denied ? { denied: m.denied } : {}) }) + "\n");
296
+ return m;
297
+ });
298
+ const overriders = Array.isArray(parsed) ? undefined : parsed.overriders;
299
+ const overridersSource = Array.isArray(parsed) ? undefined : parsed.overridersSource;
300
+ if (overridersSource !== undefined && (typeof overridersSource !== "string" || overridersSource.trim() === "")) {
301
+ throw new Error(`the plans file of wave ${k} (${plans}): \`overridersSource\` must be a label for where the overriders come from, such as "policy.override in terragucci.yml"`);
302
+ }
303
+ if (overriders !== undefined && (!Array.isArray(overriders) || !overriders.every((o) => typeof o === "string"))) {
304
+ throw new Error(`the plans file of wave ${k} (${plans}): \`overriders\` must be a list of principals`);
305
+ }
306
+ return {
307
+ members,
308
+ ...(pending ? { pending } : {}),
309
+ ...(overriders ? { overriders: overriders } : {}),
310
+ ...(overriders && overridersSource ? { overridersSource: overridersSource } : {}),
311
+ ...(extras ? { extras } : {}),
312
+ ...(count !== undefined ? { waves: count } : {}),
313
+ };
314
+ }
315
+ /** A plans file's `members` (#3821): digest members that are no run, each named once and by no run's target. */
316
+ function extraMembers(value, targets, where) {
317
+ if (value === undefined)
318
+ return undefined;
319
+ if (!Array.isArray(value))
320
+ throw new Error(`${where}: \`members\` must be a list of { "member", "planDigest" }`);
321
+ const seen = new Set();
322
+ const out = [];
323
+ for (const m of value) {
324
+ if (!m || typeof m.member !== "string" || m.member === "" || typeof m.planDigest !== "string" || m.planDigest === "")
325
+ throw new Error(`${where}: each of \`members\` needs a member and a planDigest`);
326
+ if (targets.has(m.member))
327
+ throw new Error(`${where}: member ${m.member} is also a run's target; a digest member that is no run needs a name of its own`);
328
+ if (seen.has(m.member))
329
+ throw new Error(`${where}: names member ${m.member} twice`);
330
+ seen.add(m.member);
331
+ out.push({ member: m.member, planDigest: m.planDigest });
332
+ }
333
+ return out.length > 0 ? out : undefined;
334
+ }
335
+ /** A plans file's `pending` (#3821): an object whose keys chant does not write itself, `description` aside. Undefined when absent or empty. */
336
+ function pendingFields(value, where) {
337
+ if (value === undefined)
338
+ return undefined;
339
+ if (value === null || typeof value !== "object" || Array.isArray(value))
340
+ throw new Error(`${where}: \`pending\` must be an object of fields for the pending fact`);
341
+ const reserved = Object.keys(value).filter((key) => OP_WAVE_PENDING_RESERVED.includes(key));
342
+ if (reserved.length > 0)
343
+ throw new Error(`${where}: \`pending\` sets ${reserved.join(", ")}, which chant writes itself`);
344
+ if ("description" in value && typeof value.description !== "string")
345
+ throw new Error(`${where}: pending.description must be a string`);
346
+ return Object.keys(value).length > 0 ? value : undefined;
347
+ }
348
+ /**
349
+ * A wave's set digest (#3821): {@link waveSetDigest} over its runs' plans,
350
+ * or under `digest.over: "changing"` over the runs whose plan is not empty,
351
+ * falling back to every run when all of them are empty. `extras`, the plans
352
+ * file's members that are no run (a cost), are always in it.
353
+ */
354
+ export function opWaveSetDigest(spec, members, extras = []) {
355
+ const changing = spec.digest?.over === "changing" ? members.filter((m) => m.empty !== true) : [];
356
+ return waveSetDigest([...(changing.length > 0 ? changing : members), ...extras]);
357
+ }
358
+ /** Read the hold file a hook wrote (#3821), or undefined when it wrote none or one that names no gate and digest. */
359
+ function readHoldRequest(path, log, label) {
360
+ if (!existsSync(path))
361
+ return undefined;
362
+ try {
363
+ const r = JSON.parse(readFileSync(path, "utf-8"));
364
+ if (typeof r.gate !== "string" || r.gate === "" || typeof r.digest !== "string" || r.digest === "")
365
+ throw new Error("it names no gate and digest");
366
+ if (r.op !== undefined && (typeof r.op !== "string" || r.op === ""))
367
+ throw new Error("its op is not a name");
368
+ return { gate: r.gate, digest: r.digest, ...(r.op ? { op: r.op } : {}), ...(typeof r.description === "string" ? { description: r.description } : {}), ...(r.pending ? { pending: pendingFields(r.pending, `the hold ${label} wrote`) ?? {} } : {}) };
369
+ }
370
+ catch (err) {
371
+ log(`${label} wrote a hold file that cannot be read (${err instanceof Error ? err.message : String(err)}), so it holds the wave on its set digest`);
372
+ return undefined;
373
+ }
374
+ }
375
+ /**
376
+ * Run one moment's hooks, in order: each once for the wave, or once per run
377
+ * for a `perRun` hook. A hook that fails stops the rest, unless its
378
+ * `onFailure` is `approve`: then it holds the wave and the hooks after it run.
379
+ */
380
+ function runHookList(at, moment, hooks, runs, runsPath, extraEnv = {}) {
381
+ const out = { holds: [], outcomes: [], otherGates: [] };
382
+ if (!hooks || hooks.length === 0)
383
+ return out;
384
+ const { name: specName, op, waveName, k, cwd, exec, log } = at;
385
+ // #3821: a hook that holds the wave on another gate names it in this file.
386
+ const holdPath = `.chant/op-waves/${specName}/wave-${k}/hold.json`;
387
+ const env = { CHANT_OP_WAVES: specName, CHANT_WAVE: waveName, CHANT_WAVE_NUMBER: String(k), CHANT_HOOK: moment, CHANT_RUNS: runsPath, CHANT_HOLD: holdPath, ...extraEnv };
388
+ for (const hook of hooks) {
389
+ const name = hook.name ?? (hook.command.join(" ").length > 60 ? `${hook.command.join(" ").slice(0, 57)}...` : hook.command.join(" "));
390
+ const calls = hook.perRun
391
+ ? runs.map((run) => ({
392
+ target: run.target,
393
+ argv: opWaveArgv(hook.command, { op: run.op, wave: waveName, target: run.target, runs: runsPath, ...(run.plan ? { plan: run.plan } : {}) }),
394
+ env: { ...env, CHANT_TARGET: run.target, ...(run.plan ? { CHANT_PLAN: run.plan } : {}) },
395
+ }))
396
+ : [{ target: undefined, argv: opWaveArgv(hook.command, { op, wave: waveName, runs: runsPath }), env: env }];
397
+ for (const call of calls) {
398
+ const label = `${moment} hook ${name}${call.target ? ` (${call.target})` : ""}`;
399
+ let code;
400
+ rmSync(join(cwd, holdPath), { force: true });
401
+ try {
402
+ code = exec(call.argv, cwd, call.env);
403
+ }
404
+ catch (err) {
405
+ log(`${label} did not run: ${err instanceof Error ? err.message : String(err)}`);
406
+ code = 127;
407
+ }
408
+ const base = { moment, name, ...(call.target ? { target: call.target } : {}), exit: code };
409
+ if (code === 0) {
410
+ out.outcomes.push({ ...base, outcome: "passed" });
411
+ continue;
412
+ }
413
+ if (hook.onFailure === "approve") {
414
+ out.outcomes.push({ ...base, outcome: "held" });
415
+ const request = readHoldRequest(join(cwd, holdPath), log, label);
416
+ if (request) {
417
+ out.otherGates.push({ label, name, moment, ...(call.target ? { target: call.target } : {}), argv: call.argv, env: call.env, request });
418
+ log(`${label} exited ${code} and holds wave ${k} (${waveName}) on gate ${request.op ?? specName} / ${request.gate}, digest ${request.digest}`);
419
+ continue;
420
+ }
421
+ out.holds.push(label);
422
+ log(`${label} exited ${code}, so wave ${k} (${waveName}) waits for an approval of its set digest`);
423
+ continue;
424
+ }
425
+ out.outcomes.push({ ...base, outcome: "failed" });
426
+ out.failed = `${label} exited ${code}`;
427
+ log(out.failed);
428
+ return out;
429
+ }
430
+ }
431
+ return out;
432
+ }
433
+ const placeOf = (ctx) => ({ name: ctx.spec.name, op: ctx.spec.op, waveName: ctx.wave.name, k: ctx.k, cwd: ctx.cwd, exec: ctx.exec, log: ctx.log });
434
+ const hookRuns = (spec, k, runs, members) => runs.map((run) => ({ target: run.target, op: run.op ?? spec.op, ...(members?.has(run.target) ? { plan: opWavePlanPath(spec, k, run.target) } : {}) }));
435
+ /**
436
+ * Run one moment's hooks in the wave job (#3762), recording each outcome on
437
+ * the result. A hook that held the wave on another gate (#3821) has that
438
+ * gate settled here: approved, the hook runs again with
439
+ * `CHANT_HOLD_APPROVED`; otherwise the wave waits on it. The re-run passes,
440
+ * or holds the wave on a further gate (#3847), settled the same way, up to
441
+ * {@link OP_WAVE_MAX_CHAINED_HOLDS} gates per hook call.
442
+ */
443
+ async function runHooks(ctx, moment, hooks, runs, members, digest, extraEnv = {}) {
444
+ if (!hooks || hooks.length === 0)
445
+ return { holds: [] };
446
+ const runsPath = writeRunsFile(ctx, runs, members, digest);
447
+ const out = runHookList(placeOf(ctx), moment, hooks, hookRuns(ctx.spec, ctx.k, runs, members), runsPath, extraEnv);
448
+ ctx.result.hooks.push(...out.outcomes);
449
+ const back = { holds: out.holds, ...(out.failed ? { failed: out.failed } : {}) };
450
+ if (out.failed)
451
+ return back;
452
+ for (const first of out.otherGates) {
453
+ // #3847: an approved re-run may hold the wave on a further gate (a second state migration that plans against the first one's states), settled in turn.
454
+ let hold = first;
455
+ const approved = [];
456
+ for (;;) {
457
+ const held = await settleOtherGate(ctx, hold);
458
+ if (held.status === "waiting") {
459
+ if (approved.length > 0)
460
+ held.after = approved;
461
+ back.waitingOn ?? (back.waitingOn = held);
462
+ break;
463
+ }
464
+ const now = { op: held.op, gate: held.gate, digest: held.digest, approvedBy: held.approvedBy, approvedAt: held.approvedAt };
465
+ approved.push(now);
466
+ const approvedEnv = {
467
+ ...hold.env,
468
+ CHANT_HOLD_APPROVED: JSON.stringify(now),
469
+ // Every hold this hook call was approved through so far, oldest first (#3847).
470
+ CHANT_HOLDS_APPROVED: JSON.stringify(approved),
471
+ };
472
+ const holdPath = join(ctx.cwd, hold.env.CHANT_HOLD);
473
+ rmSync(holdPath, { force: true });
474
+ let code;
475
+ try {
476
+ code = ctx.exec(hold.argv, ctx.cwd, approvedEnv);
477
+ }
478
+ catch (err) {
479
+ ctx.log(`${hold.label} did not run: ${err instanceof Error ? err.message : String(err)}`);
480
+ code = 127;
481
+ }
482
+ const target = hold.target ? { target: hold.target } : {};
483
+ if (code === 0) {
484
+ ctx.result.hooks.push({ moment, name: hold.name, ...target, exit: code, outcome: "passed" });
485
+ ctx.log(`${hold.label}: ${held.approvedBy} approved ${held.op} / ${held.gate} at ${held.digest}, and the hook ran again and passed`);
486
+ break;
487
+ }
488
+ const next = readHoldRequest(holdPath, ctx.log, hold.label);
489
+ const problem = !next
490
+ ? `${hold.label} exited ${code} after ${held.approvedBy} approved ${held.gate}`
491
+ : approved.some((a) => a.op === (next.op ?? ctx.spec.name) && a.gate === next.gate && a.digest === next.digest)
492
+ ? `${hold.label} exited ${code} after ${held.approvedBy} approved ${held.gate}, and holds the wave again on ${next.op ?? ctx.spec.name} / ${next.gate} at ${next.digest}, which is already approved`
493
+ : approved.length >= OP_WAVE_MAX_CHAINED_HOLDS
494
+ ? `${hold.label} held the wave on ${approved.length} gates in turn and asks for one more; a hook call holds on at most ${OP_WAVE_MAX_CHAINED_HOLDS}`
495
+ : undefined;
496
+ if (problem) {
497
+ ctx.result.hooks.push({ moment, name: hold.name, ...target, exit: code, outcome: "failed" });
498
+ back.failed = problem;
499
+ ctx.log(back.failed);
500
+ return back;
501
+ }
502
+ ctx.result.hooks.push({ moment, name: hold.name, ...target, exit: code, outcome: "held" });
503
+ ctx.log(`${hold.label}: ${held.approvedBy} approved ${held.op} / ${held.gate}; the hook ran again, exited ${code} and holds wave ${ctx.k} (${ctx.wave.name}) on gate ${next.op ?? ctx.spec.name} / ${next.gate}, digest ${next.digest}`);
504
+ hold = { ...hold, request: next };
505
+ }
506
+ }
507
+ return back;
508
+ }
509
+ /** How many gates one hook call holds a wave on in turn (#3847): the first hold and the holds its approved re-runs ask for. */
510
+ export const OP_WAVE_MAX_CHAINED_HOLDS = 10;
511
+ /**
512
+ * The gate ledger a wave decides by: the job's port, its `identity.gates`
513
+ * rule read at the wave's base (#3841), and the signers at base as a second
514
+ * rule when `approval` is `sealed`.
515
+ */
516
+ async function gatePort(options, cwd, gate, approval, base) {
517
+ const ledger = options.gates ?? gitGateLedgerPort({ cwd });
518
+ const atBase = (g) => ledger.approvalRule?.(g, { base }) ?? Promise.resolve(null);
519
+ if (approval !== "sealed")
520
+ return { ledger, port: ledger.approvalRule ? { ...ledger, approvalRule: atBase } : ledger };
521
+ // #3684: only an approval sealed by a signer listed at base counts.
522
+ const review = await import("./gate-review.js");
523
+ const sealed = review.sealedApprovalRule(gate, (options.trust ?? ((b) => review.trustPolicyAt(cwd, b)))(base));
524
+ return {
525
+ ledger,
526
+ port: {
527
+ ...ledger,
528
+ async approvalRule(g) {
529
+ const own = await atBase(g);
530
+ return g === gate ? review.bothRules(own, sealed) : own;
531
+ },
532
+ },
533
+ };
534
+ }
535
+ /** Log each approval of the digest a rule did not count (#3841), so a wait says why. */
536
+ function logRefused(log, label, check) {
537
+ if (check.satisfied)
538
+ return;
539
+ for (const { resolution: r, reason } of check.refused ?? [])
540
+ log(`${label}: the approval by ${r.resolvedBy} at ${r.timestamp} does not count: ${reason}`);
541
+ }
542
+ /**
543
+ * Settle a hold on another gate (#3821): an approval of that gate and digest
544
+ * under the wave's approval mode (`pr-review` counts the ledger alone, since
545
+ * no review planned that digest), else a pending fact for it.
546
+ */
547
+ async function settleOtherGate(ctx, hold) {
548
+ const { spec, k, wave, log } = ctx;
549
+ const rules = ctx.gateRule;
550
+ const op = hold.request.op ?? spec.name;
551
+ const mode = rules.approval === "sealed" ? "sealed" : "ledger";
552
+ const { port } = await gatePort(ctx.options, ctx.cwd, hold.request.gate, mode, rules.base);
553
+ const neverOverMcp = rules.neverOverMcp;
554
+ const now = ctx.options.now ?? new Date().toISOString();
555
+ const check = await evaluateGate(port, {
556
+ op,
557
+ gate: hold.request.gate,
558
+ planDigest: hold.request.digest,
559
+ description: hold.request.description ?? `wave ${k} (${wave.name}) of ${spec.name} is held on it by ${hold.label}`,
560
+ now,
561
+ // #3839: the wave a hold belongs to, as a field, whatever the hook's own description says.
562
+ fields: { ...hold.request.pending, approvalMode: rules.approval, wave: { number: k, name: wave.name, op: spec.name }, heldBy: { hook: hold.name, moment: hold.moment, ...(hold.target ? { target: hold.target } : {}) } },
563
+ ...(neverOverMcp ? { neverOverMcp: true } : {}),
564
+ });
565
+ const base = { op, gate: hold.request.gate, digest: hold.request.digest, hook: hold.label };
566
+ logRefused(log, `${op} / ${hold.request.gate}`, check);
567
+ if (check.satisfied)
568
+ return { ...base, status: "approved", approvedBy: check.resolution.resolvedBy, approvedAt: check.resolution.timestamp };
569
+ const approve = approveCommand(op, hold.request.gate, undefined, hold.request.digest, mode === "sealed");
570
+ log(`wave ${k} (${wave.name}) waits on ${op} / ${hold.request.gate}, which ${hold.label} holds it on: nobody approved ${hold.request.digest}. Approve: ${approve}`);
571
+ return {
572
+ ...base,
573
+ status: "waiting",
574
+ approve,
575
+ waitingSince: check.pending.timestamp,
576
+ recorded: check.recorded,
577
+ ...(check.mismatch ? { mismatch: check.mismatch } : {}),
578
+ };
579
+ }
580
+ /**
581
+ * Read the hooks file chant hands an apply command (#3804): the one at
582
+ * `path`, by default `CHANT_HOOKS`. Null when there is none, as for an apply
583
+ * that a wave job did not start.
584
+ */
585
+ export function readOpWaveHooksFile(path = process.env.CHANT_HOOKS, cwd = process.cwd()) {
586
+ if (!path)
587
+ return null;
588
+ const at = isAbsolute(path) ? path : join(cwd, path);
589
+ let parsed;
590
+ try {
591
+ parsed = JSON.parse(readFileSync(at, "utf-8"));
592
+ }
593
+ catch (err) {
594
+ throw new Error(`the hooks file ${path} cannot be read: ${err instanceof Error ? err.message : String(err)}`);
595
+ }
596
+ if (parsed?.version !== 1 || !Array.isArray(parsed.runs) || typeof parsed.hooks !== "object")
597
+ throw new Error(`${path} is not an Op waves hooks file (version 1)`);
598
+ return parsed;
599
+ }
600
+ /**
601
+ * Run one moment's hooks from a hooks file (#3804): what an apply command
602
+ * calls under its own lock, `before-apply` right before it changes anything
603
+ * and `after-apply` once it has, when the spec's `hooks.apply.runBy` at base
604
+ * is `apply`. Also what a product's apply outside the waves calls, with a
605
+ * file from {@link opWaveHooksAtBase}. A moment the file has no hooks for
606
+ * runs nothing and proceeds, so an apply can call it whoever runs the hooks.
607
+ *
608
+ * Each call adds its outcomes to the file's `result` (`CHANT_HOOKS_RESULT`),
609
+ * which chant reads back after the apply exits: the outcomes join the wave's
610
+ * result and decision record, and a hold makes the wave wait for, or find,
611
+ * an approval of its digest. `skip: "<reason>"` runs nothing and records the
612
+ * moment as skipped with the reason (#3811), for an apply that changes nothing:
613
+ * chant counts the moment as handled. A skip without a reason throws. `exec` runs one hook; inject it to run a hook
614
+ * in process or to keep its output.
615
+ */
616
+ export function runOpWaveApplyHooks(file, moment, options = {}) {
617
+ const cwd = options.cwd ?? process.cwd();
618
+ const log = options.log ?? ((line) => console.error(line));
619
+ let run;
620
+ if (options.skip !== undefined) {
621
+ const reason = options.skip.trim();
622
+ if (!reason)
623
+ throw new Error(`skipping the ${moment} hooks needs a reason (skip: "nothing to apply")`);
624
+ log(`${moment} hooks skipped: ${reason}`);
625
+ run = { moment, outcomes: [{ moment, name: "skip", exit: 0, outcome: "skipped", reason }], held: [], proceed: true };
626
+ }
627
+ else if (file.refused) {
628
+ log(`${file.refused}, so none of the ${moment} hooks run and the apply does not go on`);
629
+ run = { moment, outcomes: [], held: [file.refused], proceed: false };
630
+ }
631
+ else {
632
+ const place = { name: file.opWaves, op: file.op, waveName: file.wave, k: file.number, cwd, exec: options.exec ?? spawnExec, log };
633
+ const out = runHookList(place, moment, file.hooks[moment], file.runs, file.runsFile);
634
+ // A hold on another gate (#3821) is settled only by the wave job; under the apply's lock it stops the apply.
635
+ const others = out.otherGates.map((o) => `${o.label} on ${o.request.op ?? file.opWaves} / ${o.request.gate}`);
636
+ run = {
637
+ moment,
638
+ outcomes: out.outcomes,
639
+ ...(out.failed ? { failed: out.failed } : {}),
640
+ held: [...out.holds, ...others],
641
+ proceed: !out.failed && others.length === 0 && (out.holds.length === 0 || file.approved),
642
+ };
643
+ }
644
+ if (file.result) {
645
+ const at = isAbsolute(file.result) ? file.result : join(cwd, file.result);
646
+ let prior = { version: 1, moments: [], outcomes: [] };
647
+ if (existsSync(at)) {
648
+ try {
649
+ prior = JSON.parse(readFileSync(at, "utf-8"));
650
+ }
651
+ catch {
652
+ // A result file that cannot be read is started again.
653
+ }
654
+ }
655
+ const next = { version: 1, moments: [...prior.moments, moment], outcomes: [...prior.outcomes, ...run.outcomes] };
656
+ mkdirSync(dirname(at), { recursive: true });
657
+ writeFileSync(at, JSON.stringify(next, null, 2) + "\n");
658
+ }
659
+ return run;
660
+ }
661
+ /**
662
+ * The hooks of wave `wave` read at base, for an apply outside the waves
663
+ * (#3804): a product's own `apply <env>` that runs the same Op as the wave
664
+ * does, and the same hooks. All four moments are in the file; the apply runs
665
+ * the ones it has a stage for with {@link runOpWaveApplyHooks}. Nothing is
666
+ * approved: a held hook stops the apply. The wave or the file missing at
667
+ * base has no hooks, as in a wave job.
668
+ */
669
+ export function opWaveHooksAtBase(options) {
670
+ const cwd = options.cwd ?? process.cwd();
671
+ const spec = parseOpWavesSpec(readFileSync(isAbsolute(options.specFile) ? options.specFile : join(cwd, options.specFile), "utf-8"), options.specFile);
672
+ const k = spec.waves.findIndex((w) => w.name === options.wave) + 1;
673
+ const rules = readOpWavePolicy(options.specFile, options.wave, options.base ?? spec.base ?? "HEAD^1", cwd, options.show);
674
+ const runs = (options.runs ?? spec.waves[k - 1]?.runs ?? []).map((r) => ({ target: r.target, op: r.op ?? spec.op }));
675
+ const runsFile = `.chant/op-waves/${spec.name}/${options.wave.replace(/[^A-Za-z0-9._-]/g, "_")}.runs.json`;
676
+ const runsBody = { wave: options.wave, number: k, runs };
677
+ mkdirSync(dirname(join(cwd, runsFile)), { recursive: true });
678
+ writeFileSync(join(cwd, runsFile), JSON.stringify(runsBody, null, 2) + "\n");
679
+ const hooks = Object.fromEntries(Object.entries(rules.hooks).filter(([, list]) => list.length > 0));
680
+ return {
681
+ version: 1,
682
+ opWaves: spec.name,
683
+ op: spec.op,
684
+ wave: options.wave,
685
+ number: k,
686
+ base: rules.base,
687
+ approved: false,
688
+ runBy: "apply",
689
+ runs,
690
+ runsFile,
691
+ hooks,
692
+ outcomes: [],
693
+ ...(rules.hooksRefused ? { refused: rules.hooksRefused } : {}),
694
+ };
695
+ }
696
+ /** Whether `policy` lets a wave with these members through without an approval. */
697
+ export function opWaveNeedsApproval(policy, members) {
698
+ if (policy === "never")
699
+ return false;
700
+ if (members.every((m) => m.empty === true))
701
+ return false;
702
+ if (policy === "on-destructive")
703
+ return members.some((m) => m.destructive === true);
704
+ return true;
705
+ }
706
+ /** Log when the change's hooks are not the ones at base, which are the ones that run. */
707
+ function noteHooksAtBase(ctx, rules) {
708
+ const { hooks: atBase, base } = rules;
709
+ const own = opWaveHooksAt(ctx.spec, ctx.wave.name);
710
+ if (JSON.stringify(own) !== JSON.stringify(atBase) || opWaveApplyHooksRunBy(ctx.spec, ctx.wave.name) !== rules.applyHooksRunBy) {
711
+ ctx.log(`wave ${ctx.k} (${ctx.wave.name}): its hooks are read at ${base.slice(0, 12)}, and this change's differ; the ones at base run`);
712
+ }
713
+ }
714
+ /** Plan, decide and apply one wave, or one part of a wide wave. */
715
+ export async function runOpWave(options) {
716
+ const { spec } = options;
717
+ assertOpWavesSpec(spec);
718
+ const log = options.log ?? ((line) => console.error(line));
719
+ const last = spec.waves[spec.waves.length - 1];
720
+ const applied = [];
721
+ const rest = [];
722
+ for (let k = options.wave;; k++) {
723
+ let result;
724
+ let prior;
725
+ // #3821: an apply that came back "replan" plans the wave again, a bounded number of times.
726
+ for (let pass = 0;; pass++) {
727
+ result = await runOneWave(options, k, prior, pass);
728
+ if (!result.replan)
729
+ break;
730
+ if (pass + 1 >= OP_WAVE_MAX_REPLANS) {
731
+ log(`wave ${k}: its apply asked to plan again ${OP_WAVE_MAX_REPLANS} times, so the wave stops here`);
732
+ result.exitCode = 1;
733
+ break;
734
+ }
735
+ log(`wave ${k}: the apply applied nothing and asks to plan again${result.replan.reason ? ` (${result.replan.reason})` : ""}; planning again`);
736
+ prior = result.decision;
737
+ }
738
+ applied.push(...result.applied);
739
+ // #3821: a `rest` wave goes on through the waves its plan command found.
740
+ const found = result.decision?.discovered?.waves;
741
+ const onward = k >= spec.waves.length && last.rest === true && options.share === undefined && !options.decide && result.exitCode === 0 && found !== undefined && k < found;
742
+ if (rest.length > 0 || onward)
743
+ rest.push({ wave: k, exitCode: result.exitCode, ...(result.decision ? { digest: result.decision.digest } : {}) });
744
+ if (!onward)
745
+ return { ...result, applied, ...(rest.length > 0 ? { rest } : {}) };
746
+ log(`wave ${k} applied; the plan found ${found} waves, so this job goes on to wave ${k + 1}`);
747
+ }
748
+ }
749
+ /** How many times one wave job plans again when its apply asks to (#3821). */
750
+ export const OP_WAVE_MAX_REPLANS = 10;
751
+ /** Plan, decide and apply wave `k`, or one part of a wide wave. `prior`: the decision of the pass whose apply asked to plan again. */
752
+ async function runOneWave(options, k, prior, pass) {
753
+ const { spec } = options;
754
+ const cwd = options.cwd ?? process.cwd();
755
+ const exec = options.exec ?? spawnExec;
756
+ const log = options.log ?? ((line) => console.error(line));
757
+ const last = spec.waves[spec.waves.length - 1];
758
+ // #3821: past the spec's waves, a `rest` wave stands for each wave its plan command found.
759
+ let wave = spec.waves[k - 1] ?? (last.rest ? last : undefined);
760
+ if (!wave)
761
+ throw new Error(`Op waves "${spec.name}" has ${spec.waves.length} waves; there is no wave ${k}.`);
762
+ const shares = wave.shares ?? 1;
763
+ if (options.share !== undefined && (options.share < 1 || options.share > shares)) {
764
+ throw new Error(`wave ${k} (${wave.name}) has ${shares} share${shares === 1 ? "" : "s"}; there is no share ${options.share}.`);
765
+ }
766
+ const recordPath = join(cwd, opWaveRecordPath(spec, k));
767
+ const result = { exitCode: 0, applied: [], failed: [], moved: [], hooks: [] };
768
+ const ctx = { spec, k, wave, cwd, exec, log, result, options, gateRule: { approval: "ledger", base: "HEAD", neverOverMcp: spec.neverOverMcp === true } };
769
+ if (pass > 0)
770
+ log(`wave ${k} (${wave.name}): plan ${pass + 1}`);
771
+ if (options.share !== undefined) {
772
+ let decision;
773
+ try {
774
+ decision = JSON.parse(readFileSync(recordPath, "utf-8"));
775
+ }
776
+ catch {
777
+ throw new Error(`share ${options.share} of wave ${k} (${wave.name}) found no decision at ${opWaveRecordPath(spec, k)}. Run the deciding job first.`);
778
+ }
779
+ result.decision = decision;
780
+ ctx.gateRule = { approval: decision.approval, base: decision.base, neverOverMcp: decision.neverOverMcp === true };
781
+ if (decision.status === "waiting" || decision.status === "refused") {
782
+ log(`wave ${k} (${wave.name}) is ${decision.status === "refused" ? "refused by its policy" : `waiting on ${decision.heldOn?.gate ?? decision.gate}`}; nothing applied. ${decision.approve ?? ""}`.trim());
783
+ return { ...result, exitCode: decision.status === "refused" ? refusedExit(decision) : 3 };
784
+ }
785
+ // The share runs the hooks its deciding job read at base, around its own slice.
786
+ if (wave.discover) {
787
+ // #3821: a discovering wave's runs are the ones its deciding job planned.
788
+ wave = { ...wave, runs: decision.members.map((m) => ({ target: m.member, ...(m.op !== spec.op ? { op: m.op } : {}) })) };
789
+ ctx.wave = wave;
790
+ }
791
+ const slice = opWaveShare(wave.runs, options.share, shares);
792
+ const before = await runHooks(ctx, "before-plan", decision.hooks?.["before-plan"], slice);
793
+ if (before.failed)
794
+ return failed(ctx, `a hook before the plan failed, so nothing in share ${options.share} of wave ${k} was planned or applied`);
795
+ if (before.waitingOn)
796
+ return waitOnOther(ctx, decision, before.waitingOn);
797
+ const { members } = planRuns(ctx, slice);
798
+ const planned = new Map(members.map((m) => [m.member, m]));
799
+ const after = await runHooks(ctx, "after-plan", decision.hooks?.["after-plan"], slice, planned);
800
+ if (after.failed)
801
+ return failed(ctx, `a hook after the plan failed, so nothing in share ${options.share} of wave ${k} was applied`);
802
+ if (after.waitingOn)
803
+ return waitOnOther(ctx, decision, after.waitingOn);
804
+ const decided = new Map(decision.members.map((m) => [m.member, m.planDigest]));
805
+ const unmoved = [];
806
+ for (const run of slice) {
807
+ const now = planned.get(run.target);
808
+ if (decided.get(run.target) !== now.planDigest) {
809
+ log(`${run.target}: its plan moved since wave ${k} was decided, so it was not applied. ` +
810
+ `decided: ${decided.get(run.target) ?? "(not in the decision)"}; planned now: ${now.planDigest}`);
811
+ result.moved.push(run.target);
812
+ continue;
813
+ }
814
+ unmoved.push(run);
815
+ }
816
+ await applyStage(options, ctx, decision, unmoved, planned, [...before.holds, ...after.holds]);
817
+ if (result.exitCode === 0 && result.moved.length > 0)
818
+ result.exitCode = 4;
819
+ return result;
820
+ }
821
+ // The policy, the approval mode and the hooks, read at base before anything runs.
822
+ const rules = readOpWavePolicy(options.specFile, wave.name, options.base ?? spec.base ?? "HEAD^1", cwd, options.show);
823
+ const { policy, approval, source, base } = rules;
824
+ const neverOverMcp = rules.neverOverMcp || spec.neverOverMcp === true;
825
+ ctx.gateRule = { approval, base, neverOverMcp };
826
+ noteHooksAtBase(ctx, rules);
827
+ // #3836: a point at base that cannot be asked fails the wave; the ledger never stands in for it.
828
+ if (rules.pointRefused)
829
+ return failed(ctx, `wave ${k} (${wave.name}): ${rules.pointRefused}, so nothing in it was planned or applied`);
830
+ const own = opWavePointAt(spec, wave.name);
831
+ if (JSON.stringify(own) !== JSON.stringify(rules.point)) {
832
+ log(`wave ${k} (${wave.name}): its gate's decision point is read at ${base.slice(0, 12)} (${rules.point?.name ?? "none"}), and this change's differs (${own?.name ?? "none"}); the one at base holds`);
833
+ }
834
+ const holds = [];
835
+ if (rules.hooksRefused) {
836
+ holds.push(rules.hooksRefused);
837
+ log(`wave ${k} (${wave.name}): ${rules.hooksRefused}, so none run and the wave waits for an approval`);
838
+ }
839
+ const gate = opWaveGateName(spec, k);
840
+ const hooks = Object.fromEntries(Object.entries(rules.hooks).filter(([, list]) => list.length > 0));
841
+ const decision = {
842
+ wave: k,
843
+ name: wave.name,
844
+ op: spec.name,
845
+ gate,
846
+ policy,
847
+ approval,
848
+ policySource: source,
849
+ base,
850
+ digest: "",
851
+ members: [],
852
+ status: "not-required",
853
+ ...(Object.keys(hooks).length > 0 ? { hooks } : {}),
854
+ ...(rules.spend ? { spend: "base" } : spec.approvals?.spend === true ? { spend: "change" } : {}),
855
+ ...(rules.applyHooksRunBy === "apply" ? { applyHooksRunBy: "apply" } : {}),
856
+ ...(spec.digest?.over === "changing" ? { digestOver: "changing" } : {}),
857
+ ...(neverOverMcp ? { neverOverMcp: true } : {}),
858
+ ...(rules.point ? { point: { ...rules.point } } : {}),
859
+ };
860
+ const before = await runHooks(ctx, "before-plan", rules.hooks["before-plan"], wave.runs);
861
+ if (before.failed)
862
+ return failed(ctx, `a hook before the plan failed, so nothing in wave ${k} was planned or applied`);
863
+ if (before.waitingOn)
864
+ return decided(ctx, decision, recordPath, before.waitingOn);
865
+ const { members, pending, overriders, overridersSource, extras, waves: found } = planRuns(ctx, wave.runs, wave.discover === true);
866
+ if (wave.discover) {
867
+ wave = { ...wave, runs: members.map((m) => ({ target: m.member, ...(m.op !== spec.op ? { op: m.op } : {}) })) };
868
+ ctx.wave = wave;
869
+ decision.discovered = { ...(found !== undefined ? { waves: found } : {}) };
870
+ log(`wave ${k} (${wave.name}): the plan found ${members.length} run${members.length === 1 ? "" : "s"}${found !== undefined ? ` of ${found} waves` : ""}${members.length ? `: ${members.map((m) => m.member).join(", ")}` : ""}`);
871
+ }
872
+ const planned = new Map(members.map((m) => [m.member, m]));
873
+ const digest = opWaveSetDigest(spec, members, extras);
874
+ decision.digest = digest;
875
+ decision.members = members;
876
+ if (extras)
877
+ decision.extras = extras;
878
+ if (pending)
879
+ decision.pending = pending;
880
+ const after = await runHooks(ctx, "after-plan", rules.hooks["after-plan"], wave.runs, planned, digest);
881
+ if (after.failed)
882
+ return failed(ctx, `a hook after the plan failed, so nothing in wave ${k} was applied`);
883
+ holds.push(...before.holds, ...after.holds);
884
+ if (holds.length > 0)
885
+ decision.heldBy = holds;
886
+ if (after.waitingOn)
887
+ return decided(ctx, decision, recordPath, after.waitingOn);
888
+ // #3821: a run the product's policy denied applies only under an override; with none the wave is refused, and no approval is waited for.
889
+ const kept = prior !== undefined && (prior.status === "approved" || prior.status === "not-required") && prior.digest === digest;
890
+ if (kept) {
891
+ // The overrides this job applied under, and spent, still let the same plans through.
892
+ const byRun = new Map(prior.members.map((m) => [m.member, m.override]));
893
+ for (const m of members)
894
+ if (m.denied && byRun.get(m.member))
895
+ m.override = byRun.get(m.member);
896
+ if (prior.overridesSpentAt)
897
+ decision.overridesSpentAt = prior.overridesSpentAt;
898
+ }
899
+ if (members.some((m) => m.denied && !m.override))
900
+ await settleDenials(ctx, decision, overriders, overridersSource);
901
+ if (kept && prior.status === "approved" && decision.status !== "refused") {
902
+ // #3821: planned again after the apply asked to, to the same digest: the approval this job found still holds, and is not spent twice.
903
+ for (const key of ["via", "approvedBy", "approvedAt", "review", "spentAt"])
904
+ if (prior[key] !== undefined)
905
+ decision[key] = prior[key];
906
+ decision.status = "approved";
907
+ log(`wave ${k} (${wave.name}): planned again to the same digest, so the approval by ${prior.approvedBy} still holds`);
908
+ }
909
+ else if (decision.status !== "refused" && (opWaveNeedsApproval(policy, members) || holds.length > 0))
910
+ await settleGate(options, ctx, decision);
911
+ const code = await decided(ctx, decision, recordPath);
912
+ if (code.exitCode !== 0)
913
+ return code;
914
+ log(`wave ${k} (${wave.name}): gate ${policy} (read at ${base.slice(0, 12)}${source === "absent-at-base" ? ", where the wave is absent" : ""}), ` +
915
+ `digest ${digest}: ${decision.status}${decision.approvedBy ? ` by ${decision.approvedBy}` : ""}` +
916
+ (decision.via === "pr-review" ? ` (review of #${decision.review?.pr})` : "") +
917
+ (holds.length > 0 ? `; held by ${holds.join(", ")}` : ""));
918
+ // A wide wave's shares apply under the deciding job's approval and overrides, so they are spent here, once.
919
+ if (options.decide) {
920
+ if (!(await spendApproval(options, ctx, decision)) || !(await spendOverrides(ctx, decision)))
921
+ return ctx.result;
922
+ if (decision.spentAt || decision.overridesSpentAt)
923
+ writeRecord(recordPath, decision, result);
924
+ return result;
925
+ }
926
+ await applyStage(options, ctx, decision, wave.runs, planned, [], recordPath);
927
+ return result;
928
+ }
929
+ /**
930
+ * The gate has decided (#3821): write the decision record, run the
931
+ * `after-gate` hooks with the decision in their environment, and say whether
932
+ * the wave goes on. Returns the result with exit 3 when it waits, 7 when its
933
+ * policy refused it, 1 when an `after-gate` hook failed on a wave that would
934
+ * apply; exit 0 to go on. A hook that fails while the wave waits or is
935
+ * refused is logged and leaves the exit code alone.
936
+ */
937
+ async function decided(ctx, decision, recordPath, heldOn) {
938
+ const { k, wave, log, result } = ctx;
939
+ if (heldOn)
940
+ markHeldOn(decision, heldOn);
941
+ mkdirSync(dirname(recordPath), { recursive: true });
942
+ writeRecord(recordPath, decision, result);
943
+ result.decision = decision;
944
+ if (decision.approval === "pr-review" && decision.status === "waiting" && decision.review?.reason) {
945
+ log(`The pull request's review does not approve wave ${k}: ${decision.review.reason}`);
946
+ }
947
+ const hook = await runGateHooks(ctx, decision, recordPath);
948
+ if (decision.status === "waiting") {
949
+ log(`Nothing in wave ${k} or after it ran. ${decision.heldOn ? `It waits on ${decision.heldOn.op} / ${decision.heldOn.gate}.` : "Read the plans, then"} approve: ${decision.approve} (with --resume, the approval also starts this job again)`);
950
+ return { ...result, exitCode: 3 };
951
+ }
952
+ if (decision.status === "refused" && decision.refused?.reason === "point") {
953
+ log(`Nothing in wave ${k} (${wave.name}) or after it ran: ${decision.refused.message}`);
954
+ return { ...result, exitCode: 8 };
955
+ }
956
+ if (decision.status === "refused") {
957
+ log(`Nothing in wave ${k} (${wave.name}) or after it ran: the policy denied ${decision.refused?.runs.join(", ")}, and no override counts for ${decision.refused?.runs.length === 1 ? "it" : "them"}.`);
958
+ return { ...result, exitCode: 7 };
959
+ }
960
+ if (hook.failed)
961
+ return failed(ctx, `wave ${k} (${wave.name}): ${hook.failed}, so nothing in it was applied`);
962
+ return result;
963
+ }
964
+ /** A hold on another gate that waits makes the wave wait on it (#3821). */
965
+ function markHeldOn(decision, heldOn) {
966
+ decision.heldOn = heldOn;
967
+ decision.status = "waiting";
968
+ decision.approve = heldOn.approve;
969
+ if (heldOn.waitingSince)
970
+ decision.waitingSince = heldOn.waitingSince;
971
+ if (heldOn.recorded !== undefined)
972
+ decision.recorded = heldOn.recorded;
973
+ }
974
+ /** A share job held on another gate that waits (#3821). */
975
+ async function waitOnOther(ctx, decision, heldOn) {
976
+ markHeldOn(decision, heldOn);
977
+ ctx.log(`share ${ctx.options.share} of wave ${ctx.k} (${ctx.wave.name}) waits on ${heldOn.op} / ${heldOn.gate}; nothing in it applied. Approve: ${heldOn.approve}`);
978
+ return { ...ctx.result, decision, exitCode: 3 };
979
+ }
980
+ /**
981
+ * Run the `after-gate` hooks (#3821) once the gate decided. Their
982
+ * environment carries the decision: `CHANT_GATE_STATUS` (`approved`,
983
+ * `not-required`, `waiting` or `refused`), `CHANT_GATE_OP`, `CHANT_GATE`,
984
+ * `CHANT_GATE_DIGEST`, `CHANT_DECISION` (the decision record, with the
985
+ * standing pending fact, the approval of another digest and the denials),
986
+ * and when set `CHANT_GATE_APPROVE`, `CHANT_GATE_WAITING_SINCE` and
987
+ * `CHANT_GATE_RECORDED` (`true` when this run recorded the pending fact).
988
+ */
989
+ async function runGateHooks(ctx, decision, recordPath) {
990
+ const list = decision.hooks?.["after-gate"];
991
+ if (!list?.length)
992
+ return { holds: [] };
993
+ const on = decision.heldOn;
994
+ const env = {
995
+ CHANT_GATE_STATUS: decision.status,
996
+ CHANT_GATE_OP: on?.op ?? decision.op,
997
+ CHANT_GATE: on?.gate ?? decision.gate,
998
+ CHANT_GATE_DIGEST: on?.digest ?? decision.digest,
999
+ CHANT_DECISION: relativeTo(ctx.cwd, recordPath),
1000
+ ...(decision.approve ? { CHANT_GATE_APPROVE: decision.approve } : {}),
1001
+ ...(decision.waitingSince ? { CHANT_GATE_WAITING_SINCE: decision.waitingSince } : {}),
1002
+ ...(decision.recorded !== undefined ? { CHANT_GATE_RECORDED: String(decision.recorded) } : {}),
1003
+ };
1004
+ const planned = new Map(decision.members.map((m) => [m.member, m]));
1005
+ const out = await runHooks(ctx, "after-gate", list, ctx.wave.runs, planned.size > 0 ? planned : undefined, decision.digest || undefined, env);
1006
+ writeRecord(recordPath, decision, ctx.result);
1007
+ if (out.failed && decision.status !== "approved" && decision.status !== "not-required")
1008
+ ctx.log(`${out.failed}; the wave is ${decision.status} whatever the hook came to`);
1009
+ return out;
1010
+ }
1011
+ function relativeTo(cwd, path) {
1012
+ return path.startsWith(cwd + "/") ? path.slice(cwd.length + 1) : path;
1013
+ }
1014
+ /**
1015
+ * Decide each run the product's policy denied (#3821) against the override
1016
+ * ledger, `_gates/policy-override.jsonl`, as `@intentius/chant/override-gate`
1017
+ * decides: an override counts when a listed overrider wrote it for this run,
1018
+ * its plan digest and its rules, gave a reason, and (under `sealed`) sealed
1019
+ * it. Overridden runs carry their override; with any run still denied the
1020
+ * decision is `refused`, and each such run gets a pending fact on the
1021
+ * override ledger unless one stands.
1022
+ */
1023
+ async function settleDenials(ctx, decision, listed, source) {
1024
+ const { spec, k, wave, cwd, log, options } = ctx;
1025
+ const og = await import("../override-gate.js");
1026
+ const port = options.gates ?? gitGateLedgerPort({ cwd });
1027
+ const path = spec.override?.overriders ?? og.OVERRIDERS_PATH;
1028
+ const overriders = listed ?? og.overridersAt(cwd, decision.base, path).principals;
1029
+ const sealed = decision.approval === "sealed";
1030
+ const signers = sealed ? (options.signers ?? (await import("../gate-decision.js")).gateSignersAt.bind(null, cwd))(decision.base) : undefined;
1031
+ const ledger = await port.read(og.OVERRIDE_OP);
1032
+ const applied = ((await port.readApplied?.(og.OVERRIDE_OP)) ?? []);
1033
+ const now = options.now ?? new Date().toISOString();
1034
+ const still = [];
1035
+ let moved = false;
1036
+ for (const m of decision.members) {
1037
+ if (!m.denied || m.override)
1038
+ continue;
1039
+ const verdict = og.decideOverride({
1040
+ ledger,
1041
+ applied,
1042
+ subject: m.member,
1043
+ planDigest: m.planDigest,
1044
+ rules: m.denied.rules,
1045
+ overriders,
1046
+ overridersPath: listed ? (source ?? "the overriders the plans file names") : path,
1047
+ mode: sealed ? "sealed" : "ledger",
1048
+ ...(signers ? { signers } : {}),
1049
+ now,
1050
+ });
1051
+ if (verdict.status === "overridden") {
1052
+ const o = verdict.override;
1053
+ m.override = { by: o.by, at: o.at, digest: o.digest, reason: o.reason };
1054
+ log(`${m.member}: the policy denial (${o.rules.join(", ")}) is overridden by ${o.by} at ${o.at}${o.sealed ? ", sealed" : ""}: ${o.reason}`);
1055
+ continue;
1056
+ }
1057
+ still.push(m.member);
1058
+ for (const why of verdict.refusals)
1059
+ log(`${m.member}: an override does not count: ${why}`);
1060
+ if (verdict.status === "moved") {
1061
+ moved = true;
1062
+ log(`${m.member}: ${verdict.by} overrode an earlier plan or other rules (${verdict.was ?? "no digest"}); this plan and its rules give ${verdict.digest}, so that override counts for nothing`);
1063
+ }
1064
+ if (!verdict.standing) {
1065
+ const { version: _v, kind: _k, ...fact } = og.overridePending({
1066
+ subject: m.member,
1067
+ planDigest: m.planDigest,
1068
+ rules: m.denied.rules,
1069
+ timestamp: now,
1070
+ expiresAt: new Date(new Date(now).getTime() + 48 * 3600 * 1000).toISOString(),
1071
+ description: m.denied.reason ?? `wave ${k} (${wave.name}) of ${spec.name}: ${m.member}: plan ${m.planDigest} denied by ${og.sortedRules(m.denied.rules).join(", ")}`,
1072
+ });
1073
+ try {
1074
+ await port.appendPending({ ...fact, members: [{ member: m.member, planDigest: m.planDigest }], wave: { number: k, name: wave.name, op: spec.name } });
1075
+ }
1076
+ catch (err) {
1077
+ log(`${m.member}: the denial was not recorded for an override: ${err instanceof Error ? err.message : String(err)}`);
1078
+ }
1079
+ }
1080
+ log(`${m.member}: ${overriders.length ? `${overriders.join(", ")} may` : "nobody listed may"} override it with: ${og.overrideCommand(m.member, m.denied.rules, sealed)}`);
1081
+ }
1082
+ if (still.length > 0) {
1083
+ decision.status = "refused";
1084
+ decision.refused = { reason: moved ? "override-moved" : "policy", runs: still.sort(), ...(listed && source ? { overridersSource: source } : {}) };
1085
+ }
1086
+ }
1087
+ /**
1088
+ * Record each override the wave is about to apply under (#3821), in
1089
+ * `_gates/policy-override/applied.jsonl`, which spends it: written before
1090
+ * anything applies, once. False when the lines could not be written: the
1091
+ * wave then fails and applies nothing.
1092
+ */
1093
+ async function spendOverrides(ctx, decision) {
1094
+ const used = decision.members.filter((m) => m.override);
1095
+ if (used.length === 0 || decision.overridesSpentAt)
1096
+ return true;
1097
+ const { k, wave, cwd, log, options } = ctx;
1098
+ const og = await import("../override-gate.js");
1099
+ const port = options.gates ?? gitGateLedgerPort({ cwd });
1100
+ const now = options.now ?? new Date().toISOString();
1101
+ try {
1102
+ if (!port.appendApplied)
1103
+ throw new Error("this gate ledger cannot record an applied override");
1104
+ for (const m of used) {
1105
+ const record = og.appliedOverride({ subject: m.member, digest: m.override.digest, at: m.override.at, by: m.override.by }, { timestamp: now });
1106
+ await port.appendApplied(record);
1107
+ }
1108
+ }
1109
+ catch (err) {
1110
+ failed(ctx, `wave ${k} (${wave.name}) could not record the overrides it applies under, so nothing applied: ${err instanceof Error ? err.message : String(err)}`);
1111
+ return false;
1112
+ }
1113
+ decision.overridesSpentAt = now;
1114
+ log(`wave ${k} (${wave.name}) applies ${used.map((m) => m.member).join(", ")} under their overrides, which are now spent`);
1115
+ return true;
1116
+ }
1117
+ /** The exit code of a refused wave: 8 when its decision point refused it (#3836), 7 when the product's policy did (#3821). */
1118
+ function refusedExit(decision) {
1119
+ return decision.refused?.reason === "point" ? 8 : 7;
1120
+ }
1121
+ function failed(ctx, line) {
1122
+ ctx.log(line);
1123
+ ctx.result.exitCode = 1;
1124
+ return ctx.result;
1125
+ }
1126
+ /**
1127
+ * Decide whether an approval of the decision's digest stands, whatever its
1128
+ * policy: sets its status to `approved` or `waiting`. Called when the policy
1129
+ * asks for one, or when a hook holds the wave (#3762).
1130
+ */
1131
+ async function settleGate(options, ctx, decision) {
1132
+ if (decision.point)
1133
+ return settlePointGate(options, ctx, decision, decision.point);
1134
+ const { spec, k, wave, cwd } = ctx;
1135
+ const { gate, digest, approval, base, policy, members } = decision;
1136
+ const { ledger, port } = await gatePort(options, cwd, gate, approval, base);
1137
+ const now = options.now ?? new Date().toISOString();
1138
+ // #3784: under the spend rule at base, an approval a wave already applied under counts for nothing.
1139
+ const spent = decision.spend === "base" ? ((await ledger.readApplied?.(spec.name)) ?? []).filter((a) => a.gate === gate) : undefined;
1140
+ const held = decision.heldBy?.length ? `, held by ${decision.heldBy.join(", ")}` : "";
1141
+ const check = await evaluateGate(port, {
1142
+ op: spec.name,
1143
+ gate,
1144
+ planDigest: digest,
1145
+ description: (typeof decision.pending?.description === "string" ? decision.pending.description : undefined) ?? `wave ${k} (${wave.name}): ${members.length} run${members.length === 1 ? "" : "s"}, gate ${policy}, approval ${approval}${held}`,
1146
+ now,
1147
+ ...(spent ? { spent } : {}),
1148
+ // #3861: the approval mode the wave asked under, for readers deciding which approvals count.
1149
+ fields: { ...decision.pending, approvalMode: approval },
1150
+ ...(decision.neverOverMcp ? { neverOverMcp: true } : {}),
1151
+ });
1152
+ if (check.satisfied) {
1153
+ decision.status = "approved";
1154
+ decision.via = "ledger";
1155
+ decision.approvedBy = check.resolution.resolvedBy;
1156
+ decision.approvedAt = check.resolution.timestamp;
1157
+ return;
1158
+ }
1159
+ logRefused(ctx.log, `wave ${k} (${wave.name})`, check);
1160
+ // #3821: what the wait is, for the after-gate hooks and the record.
1161
+ decision.pendingFact = check.pending;
1162
+ decision.recorded = check.recorded;
1163
+ decision.waitingSince = check.pending.timestamp;
1164
+ if (check.mismatch)
1165
+ decision.mismatch = check.mismatch;
1166
+ const used = spent?.filter((a) => isApprovalSpent([a], { gate, planDigest: digest, timestamp: a.approvedAt })).pop();
1167
+ if (used) {
1168
+ ctx.log(`wave ${k} (${wave.name}): the approval of ${digest} by ${used.approvedBy} at ${used.approvedAt} is spent: a wave applied under it at ${used.timestamp}. Applying it again needs a new approval`);
1169
+ }
1170
+ if (approval === "pr-review") {
1171
+ const byReview = await approveByReview(options, { op: spec.name, gate, k, digest, cwd, now, port: ledger, ...(spent ? { spent } : {}) });
1172
+ decision.review = byReview.review;
1173
+ if (byReview.approved) {
1174
+ for (const key of ["pendingFact", "recorded", "waitingSince", "mismatch"])
1175
+ delete decision[key];
1176
+ decision.status = "approved";
1177
+ decision.via = "pr-review";
1178
+ decision.approvedBy = byReview.review.approvers.join(", ");
1179
+ decision.approvedAt = now;
1180
+ return;
1181
+ }
1182
+ decision.status = "waiting";
1183
+ decision.approve = approveCommand(spec.name, gate, undefined, digest);
1184
+ return;
1185
+ }
1186
+ decision.status = "waiting";
1187
+ decision.approve = approveCommand(spec.name, gate, undefined, digest, approval === "sealed");
1188
+ }
1189
+ /**
1190
+ * Decide a gate that asks a decision point (#3836), as an Op gate with a
1191
+ * `point` is decided (#3170): ask it with `gate.planDigest` set to the set
1192
+ * digest, then wait on an open question, pass on an answer the gate passes
1193
+ * on, or refuse the wave on any other. The wave's `approval` mode and spent
1194
+ * approvals do not apply: the point's deciders and quorum decide, and a
1195
+ * `chant approve` of the digest does not pass it.
1196
+ */
1197
+ async function settlePointGate(options, ctx, decision, point) {
1198
+ const { spec, k, wave, cwd, log } = ctx;
1199
+ const { gate, digest, policy, members } = decision;
1200
+ if (decision.approval !== "ledger")
1201
+ log(`wave ${k} (${wave.name}): its gate asks decision point ${point.name}, so its approval mode (${decision.approval}) does not apply`);
1202
+ const { evaluatePointGate } = await import("../point-decision.js");
1203
+ const asker = options.points ?? (await import("./gate-point-run.js")).workspaceGatePointAsker(cwd);
1204
+ const held = decision.heldBy?.length ? `, held by ${decision.heldBy.join(", ")}` : "";
1205
+ const check = await evaluatePointGate(options.gates ?? gitGateLedgerPort({ cwd }), asker, {
1206
+ op: spec.name,
1207
+ gate,
1208
+ point: { name: point.name, ...(point.pass ? { pass: point.pass } : {}), ...(point.store ? { store: point.store } : {}) },
1209
+ ...(point.inputs ? { inputs: point.inputs } : {}),
1210
+ description: (typeof decision.pending?.description === "string" ? decision.pending.description : undefined) ?? `wave ${k} (${wave.name}): ${members.length} run${members.length === 1 ? "" : "s"}, gate ${policy}, decision point ${point.name}${held}`,
1211
+ ...(point.timeout ? { timeout: point.timeout } : {}),
1212
+ planDigest: digest,
1213
+ ...(options.now ? { now: options.now } : {}),
1214
+ // #3863: the plans file's pending fields and the spec's neverOverMcp, as settleGate passes them.
1215
+ fields: { ...decision.pending, approvalMode: decision.approval },
1216
+ ...(decision.neverOverMcp ? { neverOverMcp: true } : {}),
1217
+ });
1218
+ if (check.satisfied) {
1219
+ decision.status = "approved";
1220
+ decision.via = "point";
1221
+ decision.approvedBy = check.resolution.resolvedBy;
1222
+ decision.approvedAt = check.resolution.timestamp;
1223
+ point.answer = check.answer;
1224
+ return;
1225
+ }
1226
+ if ("refused" in check) {
1227
+ decision.status = "refused";
1228
+ decision.refused = { reason: "point", runs: members.map((m) => m.member), message: check.refused };
1229
+ point.answer = check.answer;
1230
+ return;
1231
+ }
1232
+ decision.status = "waiting";
1233
+ decision.pendingFact = check.pending;
1234
+ decision.recorded = check.recorded;
1235
+ decision.waitingSince = check.pending.timestamp;
1236
+ point.question = check.waiting;
1237
+ decision.approve = `chant workspace points answer ${check.waiting.id} --answer <answer> --by <your name>`;
1238
+ log(`wave ${k} (${wave.name}) asks decision point ${point.name} about ${digest}: question ${check.waiting.id} is ${check.waiting.state}`);
1239
+ }
1240
+ /** Write the decision record, with every hook's outcome so far (#3804). */
1241
+ function writeRecord(recordPath, decision, result) {
1242
+ if (result.hooks.length > 0)
1243
+ decision.outcomes = [...result.hooks];
1244
+ writeFileSync(recordPath, JSON.stringify(decision, null, 2) + "\n");
1245
+ }
1246
+ /**
1247
+ * A hold waits only when nobody approved the decided digest: settle the gate
1248
+ * once for `holds`. False when the wave now waits (exit 3).
1249
+ */
1250
+ async function settleHolds(options, ctx, decision, holds, recordPath) {
1251
+ const { k, wave, log, result } = ctx;
1252
+ if (holds.length === 0 || decision.status === "approved")
1253
+ return true;
1254
+ decision.heldBy = [...(decision.heldBy ?? []), ...holds];
1255
+ await settleGate(options, ctx, decision);
1256
+ if (recordPath)
1257
+ writeRecord(recordPath, decision, result);
1258
+ if (decision.status === "refused") {
1259
+ // #3836: the gate's decision point refused the held wave.
1260
+ if (recordPath)
1261
+ await runGateHooks(ctx, decision, recordPath);
1262
+ log(`wave ${k} (${wave.name}) is held by ${holds.join(", ")}; nothing applied: ${decision.refused?.message ?? "refused"}`);
1263
+ result.exitCode = refusedExit(decision);
1264
+ return false;
1265
+ }
1266
+ if (decision.status === "waiting") {
1267
+ // #3821: the gate decided again, and the wave now waits: the after-gate hooks hear so.
1268
+ if (recordPath)
1269
+ await runGateHooks(ctx, decision, recordPath);
1270
+ log(`wave ${k} (${wave.name}) is held by ${holds.join(", ")}, and nobody approved ${decision.digest}; nothing applied. Approve: ${decision.approve}`);
1271
+ result.exitCode = 3;
1272
+ return false;
1273
+ }
1274
+ log(`wave ${k} (${wave.name}) is held by ${holds.join(", ")}, and ${decision.approvedBy} approved its digest`);
1275
+ return true;
1276
+ }
1277
+ /** Read what an apply wrote to `CHANT_APPLY_OUTCOME` (#3821), or undefined when it wrote nothing. A file that says something else is logged and ignored. */
1278
+ function readApplyOutcome(path, label, log) {
1279
+ if (!existsSync(path))
1280
+ return undefined;
1281
+ try {
1282
+ const o = JSON.parse(readFileSync(path, "utf-8"));
1283
+ if (!OP_WAVE_APPLY_OUTCOMES.includes(o.outcome))
1284
+ throw new Error(`outcome ${JSON.stringify(o.outcome)} is none of ${OP_WAVE_APPLY_OUTCOMES.join(", ")}`);
1285
+ if (o.reason !== undefined && typeof o.reason !== "string")
1286
+ throw new Error("its reason is not text");
1287
+ const holder = o.holder && typeof o.holder === "object" && typeof o.holder.run === "string" ? { run: o.holder.run, ...(typeof o.holder.url === "string" ? { url: o.holder.url } : {}) } : undefined;
1288
+ return { outcome: o.outcome, ...(o.reason ? { reason: o.reason } : {}), ...(holder ? { holder } : {}) };
1289
+ }
1290
+ catch (err) {
1291
+ log(`${label}: the apply wrote an outcome that cannot be read (${err instanceof Error ? err.message : String(err)}), so its exit code decides`);
1292
+ return undefined;
1293
+ }
1294
+ }
1295
+ /**
1296
+ * Call one apply command (#3804) with `CHANT_HOOKS` and `CHANT_HOOKS_RESULT`.
1297
+ * When the apply runs the apply-stage hooks, read their outcomes back: a hold
1298
+ * with no approval standing settles the gate and, once approved, calls the
1299
+ * apply again; an `after-apply` failure means the runs applied and the wave
1300
+ * fails; an apply that exited 0 without running its hooks fails the wave.
1301
+ */
1302
+ async function callApply(options, ctx, decision, runs, planned, argv, call, recordPath) {
1303
+ const { spec, k, wave, cwd, exec, log, result } = ctx;
1304
+ const byApply = decision.applyHooksRunBy === "apply";
1305
+ const dir = `.chant/op-waves/${spec.name}/wave-${k}`;
1306
+ const filePath = `${dir}/${call.key}.json`;
1307
+ const resultPath = `${dir}/${call.key}-result.json`;
1308
+ const outcomePath = `${dir}/${call.key}-outcome.json`;
1309
+ const handed = {};
1310
+ if (byApply) {
1311
+ for (const m of ["before-apply", "after-apply"])
1312
+ if (decision.hooks?.[m]?.length)
1313
+ handed[m] = decision.hooks[m];
1314
+ }
1315
+ for (let pass = 0; pass < 2; pass++) {
1316
+ const approved = decision.status === "approved";
1317
+ const file = {
1318
+ version: 1,
1319
+ opWaves: spec.name,
1320
+ op: spec.op,
1321
+ wave: wave.name,
1322
+ number: k,
1323
+ base: decision.base,
1324
+ digest: decision.digest,
1325
+ approved,
1326
+ ...(decision.heldBy?.length ? { heldBy: decision.heldBy } : {}),
1327
+ runBy: byApply ? "apply" : "chant",
1328
+ runs: hookRuns(spec, k, runs, planned),
1329
+ runsFile: call.runsPath,
1330
+ hooks: handed,
1331
+ outcomes: [...result.hooks],
1332
+ ...(byApply ? { result: resultPath } : {}),
1333
+ };
1334
+ mkdirSync(join(cwd, dir), { recursive: true });
1335
+ writeFileSync(join(cwd, filePath), JSON.stringify(file, null, 2) + "\n");
1336
+ rmSync(join(cwd, resultPath), { force: true });
1337
+ rmSync(join(cwd, outcomePath), { force: true });
1338
+ let code;
1339
+ try {
1340
+ code = exec(argv, cwd, { CHANT_HOOKS: filePath, CHANT_APPLY_OUTCOME: outcomePath, ...(byApply ? { CHANT_HOOKS_RESULT: resultPath } : {}) });
1341
+ }
1342
+ catch (err) {
1343
+ log(`${call.label}: the apply did not run: ${err instanceof Error ? err.message : String(err)}`);
1344
+ code = 127;
1345
+ }
1346
+ // #3821: an apply that applied nothing, and says why, is not a failure.
1347
+ const said = readApplyOutcome(join(cwd, outcomePath), call.label, log);
1348
+ if (said) {
1349
+ if (byApply) {
1350
+ try {
1351
+ const back = JSON.parse(readFileSync(join(cwd, resultPath), "utf-8"));
1352
+ if (Array.isArray(back?.outcomes))
1353
+ result.hooks.push(...back.outcomes);
1354
+ }
1355
+ catch {
1356
+ // No hooks ran.
1357
+ }
1358
+ }
1359
+ log(`${call.label}: the apply applied nothing: ${said.outcome}${said.reason ? ` (${said.reason})` : ""}${said.holder ? `, held by run ${said.holder.run}` : ""}`);
1360
+ return { outcome: "applied-nothing", said };
1361
+ }
1362
+ if (!byApply)
1363
+ return { outcome: code === 0 ? "applied" : "failed" };
1364
+ let back = null;
1365
+ try {
1366
+ back = JSON.parse(readFileSync(join(cwd, resultPath), "utf-8"));
1367
+ }
1368
+ catch {
1369
+ back = null;
1370
+ }
1371
+ const outcomes = Array.isArray(back?.outcomes) ? back.outcomes : [];
1372
+ const moments = Array.isArray(back?.moments) ? back.moments : [];
1373
+ result.hooks.push(...outcomes);
1374
+ const holds = outcomes.filter((o) => o.moment === "before-apply" && o.outcome === "held").map((o) => `${o.moment} hook ${o.name}${o.target ? ` (${o.target})` : ""}`);
1375
+ if (holds.length > 0 && !approved) {
1376
+ if (code === 0) {
1377
+ log(`${call.label}: the apply exited 0 although ${holds.join(", ")} held it and no approval stood; an apply applies nothing on a hold`);
1378
+ return { outcome: "failed" };
1379
+ }
1380
+ if (pass > 0 || !(await settleHolds(options, ctx, decision, holds, recordPath)))
1381
+ return { outcome: "waiting" };
1382
+ if (!(await spendApproval(options, ctx, decision)))
1383
+ return { outcome: "failed" };
1384
+ if (recordPath && decision.spentAt)
1385
+ writeRecord(recordPath, decision, result);
1386
+ continue;
1387
+ }
1388
+ const afterFailed = outcomes.find((o) => o.moment === "after-apply" && o.outcome === "failed");
1389
+ if (afterFailed)
1390
+ return { outcome: "applied", problem: `${call.label} applied, and then the after-apply hook ${afterFailed.name} failed` };
1391
+ if (code !== 0)
1392
+ return { outcome: "failed" };
1393
+ const skipped = ["before-apply", "after-apply"].filter((m) => handed[m] && !moments.includes(m));
1394
+ if (skipped.length > 0) {
1395
+ return { outcome: "applied", problem: `${call.label}: the apply exited 0 without running its ${skipped.join(" and ")} hooks (runOpWaveApplyHooks with CHANT_HOOKS), which the spec at base hands it` };
1396
+ }
1397
+ return { outcome: "applied" };
1398
+ }
1399
+ return { outcome: "failed" };
1400
+ }
1401
+ /**
1402
+ * Apply `runs` of a decided wave: the before-apply hooks, the apply (one call
1403
+ * per run, or one `applyWave` call for the set), then the after-apply hooks.
1404
+ * A hook that holds the wave here, or held a share job's plan, makes it wait
1405
+ * for an approval of the decided digest unless one already stands. Under
1406
+ * `hooks.apply.runBy: "apply"` the apply call runs the apply-stage hooks
1407
+ * itself, under its lock (#3804).
1408
+ */
1409
+ async function applyStage(options, ctx, decision, runs, planned, earlierHolds, recordPath) {
1410
+ const { spec, k, wave, log, result } = ctx;
1411
+ if (runs.length === 0)
1412
+ return;
1413
+ const byApply = decision.applyHooksRunBy === "apply";
1414
+ const holds = [...earlierHolds];
1415
+ if (!byApply) {
1416
+ const before = await runHooks(ctx, "before-apply", decision.hooks?.["before-apply"], runs, planned, decision.digest);
1417
+ if (before.failed) {
1418
+ failed(ctx, `a hook before the apply failed, so nothing in wave ${k} was applied`);
1419
+ return;
1420
+ }
1421
+ if (before.waitingOn) {
1422
+ markHeldOn(decision, before.waitingOn);
1423
+ if (recordPath) {
1424
+ writeRecord(recordPath, decision, result);
1425
+ await runGateHooks(ctx, decision, recordPath);
1426
+ }
1427
+ result.decision = decision;
1428
+ result.exitCode = 3;
1429
+ return;
1430
+ }
1431
+ holds.push(...before.holds);
1432
+ }
1433
+ if (!(await settleHolds(options, ctx, decision, holds, recordPath)))
1434
+ return;
1435
+ if (!(await spendApproval(options, ctx, decision)))
1436
+ return;
1437
+ if (!(await spendOverrides(ctx, decision)))
1438
+ return;
1439
+ if (recordPath && (decision.spentAt || decision.overridesSpentAt))
1440
+ writeRecord(recordPath, decision, result);
1441
+ const problems = [];
1442
+ let waiting = false;
1443
+ let nothing;
1444
+ if (spec.applyWave) {
1445
+ const runsPath = writeRunsFile(ctx, runs, planned, decision.digest);
1446
+ const argv = opWaveArgv(spec.applyWave, { op: spec.op, wave: wave.name, runs: runsPath, plans: opWavePlansPath(spec, k) });
1447
+ const got = await callApply(options, ctx, decision, runs, planned, argv, { label: `wave ${k} (${wave.name})`, key: "hooks", runsPath }, recordPath);
1448
+ if (got.outcome === "applied-nothing")
1449
+ nothing = got.said;
1450
+ else if (got.outcome === "applied") {
1451
+ result.applied.push(...runs.map((r) => r.target));
1452
+ if (got.problem)
1453
+ problems.push(got.problem);
1454
+ }
1455
+ else if (got.outcome === "waiting")
1456
+ waiting = true;
1457
+ else {
1458
+ log(`wave ${k} (${wave.name}): apply failed, so none of ${runs.map((r) => r.target).join(", ")} counts as applied`);
1459
+ result.failed.push(...runs.map((r) => r.target));
1460
+ }
1461
+ }
1462
+ else {
1463
+ for (const run of runs) {
1464
+ const key = run.target.replace(/[^A-Za-z0-9._-]/g, "_");
1465
+ // Under runBy "apply" each run's apply runs its own apply-stage hooks, over a runs file of that run alone.
1466
+ const runsPath = byApply ? writeRunsFile(ctx, [run], planned, decision.digest, `.chant/op-waves/${spec.name}/wave-${k}/${key}.runs.json`) : opWaveRunsPath(spec, k);
1467
+ const argv = opWaveArgv(spec.apply ?? OP_WAVE_DEFAULT_APPLY, { op: run.op ?? spec.op, target: run.target, wave: wave.name, plan: opWavePlanPath(spec, k, run.target) });
1468
+ const got = await callApply(options, ctx, decision, [run], planned, argv, { label: run.target, key: `${key}.hooks`, runsPath }, recordPath);
1469
+ if (got.outcome === "applied-nothing") {
1470
+ nothing = got.said;
1471
+ break;
1472
+ }
1473
+ if (got.outcome === "waiting") {
1474
+ waiting = true;
1475
+ break;
1476
+ }
1477
+ if (got.outcome === "applied") {
1478
+ result.applied.push(run.target);
1479
+ if (got.problem)
1480
+ problems.push(got.problem);
1481
+ }
1482
+ else {
1483
+ log(`${run.target}: apply failed`);
1484
+ result.failed.push(run.target);
1485
+ }
1486
+ }
1487
+ }
1488
+ if (recordPath && byApply)
1489
+ writeRecord(recordPath, decision, result);
1490
+ if (nothing) {
1491
+ // #3821: no run counts as failed; the wave stops here, or plans again.
1492
+ if (nothing.outcome === "replan") {
1493
+ result.replan = { ...(nothing.reason ? { reason: nothing.reason } : {}) };
1494
+ return;
1495
+ }
1496
+ result.outcome = nothing;
1497
+ result.exitCode = nothing.outcome === "stood-down" ? 6 : 5;
1498
+ if (recordPath)
1499
+ writeRecord(recordPath, decision, result);
1500
+ return;
1501
+ }
1502
+ if (waiting) {
1503
+ result.exitCode = decision.status === "refused" ? refusedExit(decision) : 3;
1504
+ return;
1505
+ }
1506
+ if (result.failed.length > 0) {
1507
+ result.exitCode = 1;
1508
+ return;
1509
+ }
1510
+ if (problems.length > 0) {
1511
+ for (const p of problems)
1512
+ log(p);
1513
+ result.exitCode = 1;
1514
+ return;
1515
+ }
1516
+ if (!byApply) {
1517
+ const after = await runHooks(ctx, "after-apply", decision.hooks?.["after-apply"], runs, planned, decision.digest);
1518
+ if (after.failed)
1519
+ failed(ctx, `wave ${k} (${wave.name}) applied, and then a hook after the apply failed`);
1520
+ }
1521
+ if (recordPath && result.hooks.length > 0)
1522
+ writeRecord(recordPath, decision, result);
1523
+ }
1524
+ /**
1525
+ * Record that the wave applies under its approval (#3784), which spends it:
1526
+ * one line in `_gates/<name>/applied.jsonl`, before anything applies, as
1527
+ * terragucci's wave gate writes it. Only an approved wave under the spend
1528
+ * rule records, and only once. False when the line could not be written: the
1529
+ * wave then fails and applies nothing, since an apply the record misses
1530
+ * would leave its approval standing.
1531
+ */
1532
+ async function spendApproval(options, ctx, decision) {
1533
+ if (!decision.spend || decision.status !== "approved" || decision.spentAt || !decision.approvedAt)
1534
+ return true;
1535
+ const { spec, k, wave, cwd, log } = ctx;
1536
+ const port = options.gates ?? gitGateLedgerPort({ cwd });
1537
+ const now = options.now ?? new Date().toISOString();
1538
+ const record = {
1539
+ version: 1,
1540
+ kind: "applied",
1541
+ op: spec.name,
1542
+ gate: decision.gate,
1543
+ planDigest: decision.digest,
1544
+ approvedAt: decision.approvedAt,
1545
+ approvedBy: decision.approvedBy ?? "",
1546
+ timestamp: now,
1547
+ wave: { number: k, name: wave.name },
1548
+ ...(decision.via === "pr-review" && decision.review ? { review: { ...(decision.review.pr !== undefined ? { pr: decision.review.pr } : {}), ...(decision.review.head ? { head: decision.review.head } : {}) } } : {}),
1549
+ };
1550
+ try {
1551
+ if (!port.appendApplied)
1552
+ throw new Error("this gate ledger cannot record an applied approval");
1553
+ const pushed = await port.appendApplied(record);
1554
+ if (!pushed.pushed && pushed.pushWarning)
1555
+ log(`wave ${k} (${wave.name}): ${pushed.pushWarning}`);
1556
+ }
1557
+ catch (err) {
1558
+ failed(ctx, `wave ${k} (${wave.name}) could not record that it applies under the approval by ${record.approvedBy}, so nothing applied: ${err instanceof Error ? err.message : String(err)}`);
1559
+ return false;
1560
+ }
1561
+ decision.spentAt = now;
1562
+ log(`wave ${k} (${wave.name}) applies under the approval by ${record.approvedBy} at ${record.approvedAt}, which is now spent`);
1563
+ return true;
1564
+ }
1565
+ /**
1566
+ * Under `pr-review` (#3684): the merged pull request's review approves the
1567
+ * wave when a writer other than the author approved its head, and the head
1568
+ * planned this wave's digest. The approval is then written to the ledger as a
1569
+ * resolution that names the review, so the record says how the wave passed.
1570
+ */
1571
+ async function approveByReview(options, at) {
1572
+ const review = await import("./gate-review.js");
1573
+ const head = options.head ?? execFileSync("git", ["rev-parse", "HEAD"], { cwd: at.cwd, encoding: "utf-8" }).trim();
1574
+ let merged;
1575
+ try {
1576
+ merged = await (options.reviews ?? review.reviewSourceFromEnv()).mergedReview(head);
1577
+ }
1578
+ catch (err) {
1579
+ return { approved: false, review: { reason: `the forge's review could not be read: ${err instanceof Error ? err.message : String(err)}` } };
1580
+ }
1581
+ if (!merged)
1582
+ return { approved: false, review: { reason: `no merged pull request produced ${head.slice(0, 12)}` } };
1583
+ // The head's plans are read only when a writer approved it.
1584
+ const plans = merged.approvers.length === 0 ? null : await (options.headPlans ?? ((name, h) => readHeadPlans(name, h, at.cwd)))(options.spec.name, merged.head);
1585
+ const { reviewApproval } = await import("../gate-decision.js");
1586
+ const outcome = reviewApproval(merged, plans?.waves.find((w) => w.wave === at.k)?.digest, at.digest);
1587
+ if (!outcome.approved)
1588
+ return outcome;
1589
+ // #3784: a review a wave already applied this digest under is spent, so it is not recorded again.
1590
+ if (at.spent && isApprovalSpent(at.spent, { gate: at.gate, planDigest: at.digest, review: { head: merged.head } })) {
1591
+ return {
1592
+ approved: false,
1593
+ review: { ...outcome.review, reason: `a wave already applied ${at.digest} under the review of #${merged.pr} at ${merged.head.slice(0, 12)}, so the review is spent` },
1594
+ };
1595
+ }
1596
+ const note = `approved by the review of #${merged.pr} at ${merged.head.slice(0, 12)}: ${merged.approvers.join(", ")}`;
1597
+ await at.port.appendResolution?.({
1598
+ op: at.op,
1599
+ gate: at.gate,
1600
+ resolvedBy: merged.approvers[0],
1601
+ timestamp: at.now,
1602
+ planDigest: at.digest,
1603
+ approver: { kind: "human" },
1604
+ note,
1605
+ ...(merged.url ? { url: merged.url } : {}),
1606
+ review: { pr: merged.pr, head: merged.head, approvers: merged.approvers },
1607
+ });
1608
+ return outcome;
1609
+ }
1610
+ async function readHeadPlans(name, head, cwd) {
1611
+ const { gitLedgerReadPort, requireLifecycleLedger } = await import("../lifecycle/git.js");
1612
+ const { readHeadPlans: read } = await import("../lifecycle/wave-plans.js");
1613
+ await requireLifecycleLedger({ cwd });
1614
+ return read(gitLedgerReadPort({ cwd }), name, head);
1615
+ }
1616
+ /**
1617
+ * `chant run wave --spec <file> --record-plans` (#3684): plan every wave at a
1618
+ * pull request's head and record each wave's digest on `chant/lifecycle`,
1619
+ * `_wave-plans/<name>/<head>.json`. A `pr-review` wave reads it after the
1620
+ * merge: the review of that head approves the digests it planned, and only
1621
+ * those.
1622
+ */
1623
+ export async function recordOpWaveHeadPlans(options) {
1624
+ const { spec } = options;
1625
+ assertOpWavesSpec(spec);
1626
+ const cwd = options.cwd ?? process.cwd();
1627
+ const exec = options.exec ?? spawnExec;
1628
+ const review = await import("./gate-review.js");
1629
+ const head = options.head ?? review.pullRequestHeadFromEnv() ?? execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim();
1630
+ const plans = {
1631
+ version: 1,
1632
+ name: spec.name,
1633
+ head,
1634
+ timestamp: options.now ?? new Date().toISOString(),
1635
+ waves: [],
1636
+ };
1637
+ // A `rest` wave (#3821) goes on through the waves the plan command found.
1638
+ const last = spec.waves[spec.waves.length - 1];
1639
+ for (let k = 1;; k++) {
1640
+ const wave = spec.waves[k - 1] ?? last;
1641
+ const { members, extras, waves: found } = planRuns({ spec, k, wave, cwd, exec }, wave.runs, wave.discover === true);
1642
+ plans.waves.push({ wave: k, name: wave.name, digest: opWaveSetDigest(spec, members, extras) });
1643
+ if (k >= spec.waves.length && !(last.rest && found !== undefined && k < found))
1644
+ break;
1645
+ }
1646
+ await (options.write ?? ((p) => writeHeadPlans(p, cwd)))(plans);
1647
+ return plans;
1648
+ }
1649
+ async function writeHeadPlans(plans, cwd) {
1650
+ const { pushLifecycle, requireLifecycleLedger, writeBlobToPath } = await import("../lifecycle/git.js");
1651
+ const { headPlansPath } = await import("./gate-review.js");
1652
+ await requireLifecycleLedger({ cwd });
1653
+ const { dir, file } = headPlansPath(plans.name, plans.head);
1654
+ await writeBlobToPath(dir, file, JSON.stringify(plans, null, 2) + "\n", `Op waves plans of ${plans.name} at ${plans.head.slice(0, 12)}`, { cwd });
1655
+ if (!(await pushLifecycle({ cwd })))
1656
+ throw new Error("the plans were recorded locally only: chant/lifecycle has no remote to push to");
1657
+ }