@intentius/chant 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (515) hide show
  1. package/dist/audit/catalog.d.ts +51 -1
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +20 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +14 -8
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/lineage.d.ts +10 -0
  8. package/dist/audit/lineage.d.ts.map +1 -0
  9. package/dist/audit/prior-art.d.ts +385 -0
  10. package/dist/audit/prior-art.d.ts.map +1 -0
  11. package/dist/audit/proof.d.ts +16 -0
  12. package/dist/audit/proof.d.ts.map +1 -1
  13. package/dist/audit/report-html.d.ts.map +1 -1
  14. package/dist/audit/report-model.d.ts +8 -1
  15. package/dist/audit/report-model.d.ts.map +1 -1
  16. package/dist/audit/report.d.ts.map +1 -1
  17. package/dist/audit/rules-doc.d.ts +3 -0
  18. package/dist/audit/rules-doc.d.ts.map +1 -1
  19. package/dist/audit/secrets.d.ts +15 -0
  20. package/dist/audit/secrets.d.ts.map +1 -1
  21. package/dist/audit/terraform-state.d.ts +46 -0
  22. package/dist/audit/terraform-state.d.ts.map +1 -0
  23. package/dist/build.d.ts +17 -1
  24. package/dist/build.d.ts.map +1 -1
  25. package/dist/claimed-fields.d.ts +161 -0
  26. package/dist/claimed-fields.d.ts.map +1 -0
  27. package/dist/cli/build-params-cli.d.ts +2 -2
  28. package/dist/cli/command-group.d.ts +14 -0
  29. package/dist/cli/command-group.d.ts.map +1 -1
  30. package/dist/cli/commands/audit.d.ts +17 -1
  31. package/dist/cli/commands/audit.d.ts.map +1 -1
  32. package/dist/cli/commands/build.d.ts.map +1 -1
  33. package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
  34. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  35. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  36. package/dist/cli/commands/import.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts +1 -0
  38. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon/templates/project.d.ts +7 -0
  40. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  41. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  42. package/dist/cli/commands/init.d.ts.map +1 -1
  43. package/dist/cli/commands/lint.d.ts.map +1 -1
  44. package/dist/cli/commands/onboard.d.ts +2 -0
  45. package/dist/cli/commands/onboard.d.ts.map +1 -1
  46. package/dist/cli/handlers/build.d.ts.map +1 -1
  47. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  48. package/dist/cli/handlers/operator.d.ts +82 -16
  49. package/dist/cli/handlers/operator.d.ts.map +1 -1
  50. package/dist/cli/handlers/run.d.ts +49 -42
  51. package/dist/cli/handlers/run.d.ts.map +1 -1
  52. package/dist/cli/main.d.ts +28 -1
  53. package/dist/cli/main.d.ts.map +1 -1
  54. package/dist/cli/mcp/op-tools.d.ts +16 -1
  55. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  56. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  57. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  58. package/dist/cli/registry.d.ts +26 -9
  59. package/dist/cli/registry.d.ts.map +1 -1
  60. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  61. package/dist/components/auto-release.d.ts +10 -20
  62. package/dist/components/auto-release.d.ts.map +1 -1
  63. package/dist/components/capability.d.ts +4 -4
  64. package/dist/components/cli-support.d.ts +20 -21
  65. package/dist/components/cli-support.d.ts.map +1 -1
  66. package/dist/components/component.d.ts +3 -3
  67. package/dist/components/component.d.ts.map +1 -1
  68. package/dist/components/driver-output.d.ts.map +1 -1
  69. package/dist/components/driver.d.ts +66 -36
  70. package/dist/components/driver.d.ts.map +1 -1
  71. package/dist/components/index.d.ts +1 -2
  72. package/dist/components/index.d.ts.map +1 -1
  73. package/dist/components/manifest-persistence.d.ts +1 -9
  74. package/dist/components/manifest-persistence.d.ts.map +1 -1
  75. package/dist/components/run-progress.d.ts +6 -6
  76. package/dist/components/run-progress.d.ts.map +1 -1
  77. package/dist/components/verbs/run-agent.d.ts +9 -15
  78. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  79. package/dist/components/verbs/verify.d.ts +2 -2
  80. package/dist/deep-observation.d.ts +17 -0
  81. package/dist/deep-observation.d.ts.map +1 -1
  82. package/dist/discovery/files.d.ts +4 -4
  83. package/dist/discovery/fold-import.d.ts.map +1 -1
  84. package/dist/discovery/param-deps.d.ts +33 -0
  85. package/dist/discovery/param-deps.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  87. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  88. package/dist/fold/foldable-helpers.d.ts +5 -1
  89. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  90. package/dist/fold-provenance.d.ts +236 -0
  91. package/dist/fold-provenance.d.ts.map +1 -0
  92. package/dist/graph-ops.d.ts +1 -1
  93. package/dist/held-elsewhere.d.ts +85 -0
  94. package/dist/held-elsewhere.d.ts.map +1 -0
  95. package/dist/import/ir-utils.d.ts +1 -1
  96. package/dist/import/ir-utils.d.ts.map +1 -1
  97. package/dist/import/parser.d.ts +33 -0
  98. package/dist/import/parser.d.ts.map +1 -1
  99. package/dist/index.d.ts +3 -0
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/kubectl-context.d.ts +3 -3
  102. package/dist/lexicon-config.d.ts +4 -4
  103. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  104. package/dist/lexicon.d.ts +104 -14
  105. package/dist/lexicon.d.ts.map +1 -1
  106. package/dist/lifecycle/change-set.d.ts +13 -0
  107. package/dist/lifecycle/change-set.d.ts.map +1 -1
  108. package/dist/lifecycle/deep-diff.d.ts +133 -6
  109. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  110. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  111. package/dist/lifecycle/gate-ledger.d.ts +71 -3
  112. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  113. package/dist/lifecycle/index.d.ts +1 -0
  114. package/dist/lifecycle/index.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +2 -2
  116. package/dist/lifecycle/run-ledger.d.ts +53 -0
  117. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  118. package/dist/lifecycle/symptoms.d.ts +3 -3
  119. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  120. package/dist/lint/config.d.ts +81 -2
  121. package/dist/lint/config.d.ts.map +1 -1
  122. package/dist/lint/post-synth.d.ts +55 -2
  123. package/dist/lint/post-synth.d.ts.map +1 -1
  124. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  125. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  126. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  127. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  128. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  129. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  130. package/dist/lint/rules/comp/index.d.ts +1 -1
  131. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  132. package/dist/lint/rules/op/index.d.ts +16 -0
  133. package/dist/lint/rules/op/index.d.ts.map +1 -0
  134. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  135. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  136. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  137. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  138. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  139. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  140. package/dist/lint/rules/op/support.d.ts +12 -0
  141. package/dist/lint/rules/op/support.d.ts.map +1 -0
  142. package/dist/lint/suppressions.d.ts +108 -0
  143. package/dist/lint/suppressions.d.ts.map +1 -0
  144. package/dist/live-endpoint.d.ts +1 -1
  145. package/dist/op/activities/activity-contracts.d.ts +54 -0
  146. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  147. package/dist/op/activities/apply.d.ts +262 -0
  148. package/dist/op/activities/apply.d.ts.map +1 -0
  149. package/dist/op/activities/build.d.ts +13 -0
  150. package/dist/op/activities/build.d.ts.map +1 -0
  151. package/dist/op/activities/converge.d.ts +110 -0
  152. package/dist/op/activities/converge.d.ts.map +1 -0
  153. package/dist/op/activities/env-teardown.d.ts +76 -0
  154. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  155. package/dist/op/activities/guard-validate.d.ts +64 -0
  156. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  157. package/dist/op/activities/http-check.d.ts +34 -0
  158. package/dist/op/activities/http-check.d.ts.map +1 -0
  159. package/dist/op/activities/index.d.ts +31 -0
  160. package/dist/op/activities/index.d.ts.map +1 -0
  161. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  162. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  163. package/dist/op/activities/lifecycle.d.ts +42 -0
  164. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  165. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  166. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  167. package/dist/op/activities/policy.d.ts +17 -0
  168. package/dist/op/activities/policy.d.ts.map +1 -0
  169. package/dist/op/activities/reconcile.d.ts +89 -0
  170. package/dist/op/activities/reconcile.d.ts.map +1 -0
  171. package/dist/op/activities/shell.d.ts +13 -0
  172. package/dist/op/activities/shell.d.ts.map +1 -0
  173. package/dist/op/activities/teardown.d.ts +10 -0
  174. package/dist/op/activities/teardown.d.ts.map +1 -0
  175. package/dist/op/activities/util.d.ts +7 -0
  176. package/dist/op/activities/util.d.ts.map +1 -0
  177. package/dist/op/activities/wait.d.ts +28 -0
  178. package/dist/op/activities/wait.d.ts.map +1 -0
  179. package/dist/op/activities/workflow-audit.d.ts +92 -0
  180. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  181. package/dist/op/activity-contract-registry.d.ts +76 -0
  182. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  183. package/dist/op/activity-contract.d.ts +25 -18
  184. package/dist/op/activity-contract.d.ts.map +1 -1
  185. package/dist/op/activity-failure.d.ts +28 -0
  186. package/dist/op/activity-failure.d.ts.map +1 -0
  187. package/dist/op/activity-profiles.d.ts +118 -0
  188. package/dist/op/activity-profiles.d.ts.map +1 -0
  189. package/dist/op/activity-registry.d.ts +22 -38
  190. package/dist/op/activity-registry.d.ts.map +1 -1
  191. package/dist/op/activity-runtime.d.ts +3 -14
  192. package/dist/op/activity-runtime.d.ts.map +1 -1
  193. package/dist/op/builders.d.ts +84 -29
  194. package/dist/op/builders.d.ts.map +1 -1
  195. package/dist/op/composites/apply-op.d.ts +113 -0
  196. package/dist/op/composites/apply-op.d.ts.map +1 -0
  197. package/dist/op/composites/converge-op.d.ts +120 -0
  198. package/dist/op/composites/converge-op.d.ts.map +1 -0
  199. package/dist/op/composites/index.d.ts +26 -0
  200. package/dist/op/composites/index.d.ts.map +1 -0
  201. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  202. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  203. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  204. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  205. package/dist/op/composites/reconcile-op.d.ts +57 -0
  206. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  207. package/dist/op/composites/watch-op.d.ts +60 -0
  208. package/dist/op/composites/watch-op.d.ts.map +1 -0
  209. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  210. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  211. package/dist/op/converge-rule.d.ts +12 -10
  212. package/dist/op/converge-rule.d.ts.map +1 -1
  213. package/dist/op/cron.d.ts +51 -0
  214. package/dist/op/cron.d.ts.map +1 -0
  215. package/dist/op/discover.d.ts +9 -0
  216. package/dist/op/discover.d.ts.map +1 -1
  217. package/dist/op/duration.d.ts +11 -0
  218. package/dist/op/duration.d.ts.map +1 -0
  219. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  220. package/dist/op/gate.d.ts +90 -0
  221. package/dist/op/gate.d.ts.map +1 -0
  222. package/dist/op/generate-pipeline.d.ts +9 -0
  223. package/dist/op/generate-pipeline.d.ts.map +1 -1
  224. package/dist/op/index.d.ts +23 -7
  225. package/dist/op/index.d.ts.map +1 -1
  226. package/dist/op/local-executor.d.ts +114 -28
  227. package/dist/op/local-executor.d.ts.map +1 -1
  228. package/dist/op/local-output.d.ts +11 -1
  229. package/dist/op/local-output.d.ts.map +1 -1
  230. package/dist/op/op-ir.d.ts +184 -0
  231. package/dist/op/op-ir.d.ts.map +1 -0
  232. package/dist/op/op-verb-class.d.ts +10 -2
  233. package/dist/op/op-verb-class.d.ts.map +1 -1
  234. package/dist/op/operator.d.ts +30 -5
  235. package/dist/op/operator.d.ts.map +1 -1
  236. package/dist/op/receipt-store.d.ts +1 -1
  237. package/dist/op/receipt-store.d.ts.map +1 -1
  238. package/dist/op/resource.d.ts +15 -1
  239. package/dist/op/resource.d.ts.map +1 -1
  240. package/dist/op/runtime.d.ts +200 -0
  241. package/dist/op/runtime.d.ts.map +1 -0
  242. package/dist/op/runtimes/local.d.ts +29 -0
  243. package/dist/op/runtimes/local.d.ts.map +1 -0
  244. package/dist/op/step-output-ref.d.ts +15 -13
  245. package/dist/op/step-output-ref.d.ts.map +1 -1
  246. package/dist/op/types.d.ts +79 -27
  247. package/dist/op/types.d.ts.map +1 -1
  248. package/dist/ownership.d.ts +1 -1
  249. package/dist/provenance.d.ts +21 -0
  250. package/dist/provenance.d.ts.map +1 -1
  251. package/dist/serializer-walker.d.ts.map +1 -1
  252. package/dist/stack-output.d.ts +5 -0
  253. package/dist/stack-output.d.ts.map +1 -1
  254. package/package.json +6 -1
  255. package/src/audit/catalog.test.ts +79 -3
  256. package/src/audit/catalog.ts +111 -13
  257. package/src/audit/core.test.ts +26 -0
  258. package/src/audit/core.ts +61 -17
  259. package/src/audit/discover.test.ts +39 -0
  260. package/src/audit/discover.ts +91 -15
  261. package/src/audit/lineage.ts +100 -0
  262. package/src/audit/prior-art.ts +153 -0
  263. package/src/audit/proof.test.ts +67 -0
  264. package/src/audit/proof.ts +70 -0
  265. package/src/audit/report-html.ts +9 -4
  266. package/src/audit/report-model.ts +16 -2
  267. package/src/audit/report.test.ts +33 -0
  268. package/src/audit/report.ts +5 -3
  269. package/src/audit/rules-doc.test.ts +34 -2
  270. package/src/audit/rules-doc.ts +82 -9
  271. package/src/audit/secrets.ts +15 -3
  272. package/src/audit/terraform-state.test.ts +144 -0
  273. package/src/audit/terraform-state.ts +131 -0
  274. package/src/build.ts +31 -1
  275. package/src/claimed-fields.test.ts +145 -0
  276. package/src/claimed-fields.ts +208 -0
  277. package/src/cli/build-params-cli.ts +2 -2
  278. package/src/cli/command-group.ts +14 -0
  279. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  280. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  281. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  282. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  283. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  284. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  285. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  286. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  287. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  288. package/src/cli/commands/audit.test.ts +189 -11
  289. package/src/cli/commands/audit.ts +73 -27
  290. package/src/cli/commands/build.test.ts +292 -9
  291. package/src/cli/commands/build.ts +156 -24
  292. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  293. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  294. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  295. package/src/cli/commands/import.ts +6 -0
  296. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  297. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  298. package/src/cli/commands/init-lexicon.test.ts +97 -2
  299. package/src/cli/commands/init-lexicon.ts +4 -2
  300. package/src/cli/commands/init.test.ts +35 -1
  301. package/src/cli/commands/init.ts +14 -3
  302. package/src/cli/commands/lint.test.ts +48 -0
  303. package/src/cli/commands/lint.ts +123 -0
  304. package/src/cli/commands/onboard.test.ts +75 -0
  305. package/src/cli/commands/onboard.ts +54 -23
  306. package/src/cli/handlers/build.test.ts +81 -1
  307. package/src/cli/handlers/build.ts +15 -1
  308. package/src/cli/handlers/graph.test.ts +3 -3
  309. package/src/cli/handlers/lifecycle.test.ts +195 -12
  310. package/src/cli/handlers/lifecycle.ts +123 -6
  311. package/src/cli/handlers/misc.ts +1 -1
  312. package/src/cli/handlers/operator.test.ts +183 -11
  313. package/src/cli/handlers/operator.ts +260 -55
  314. package/src/cli/handlers/run.test.ts +431 -1287
  315. package/src/cli/handlers/run.ts +365 -915
  316. package/src/cli/main.test.ts +23 -15
  317. package/src/cli/main.ts +88 -33
  318. package/src/cli/mcp/op-tools.ts +147 -88
  319. package/src/cli/mcp/resource-handlers.ts +10 -37
  320. package/src/cli/mcp/resources/context.ts +11 -12
  321. package/src/cli/mcp/server.test.ts +25 -27
  322. package/src/cli/mcp/server.ts +2 -2
  323. package/src/cli/registry.ts +26 -9
  324. package/src/codegen/docs.ts +1 -1
  325. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  326. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  327. package/src/components/README.md +3 -2
  328. package/src/components/auto-release.test.ts +4 -20
  329. package/src/components/auto-release.ts +17 -42
  330. package/src/components/capability.ts +4 -4
  331. package/src/components/cli-support.test.ts +10 -5
  332. package/src/components/cli-support.ts +52 -44
  333. package/src/components/component.schema.json +2 -2
  334. package/src/components/component.ts +3 -3
  335. package/src/components/driver-output.ts +14 -2
  336. package/src/components/driver.test.ts +92 -22
  337. package/src/components/driver.ts +193 -83
  338. package/src/components/index.ts +1 -6
  339. package/src/components/manifest-persistence.test.ts +2 -20
  340. package/src/components/manifest-persistence.ts +4 -24
  341. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  342. package/src/components/run-progress.ts +6 -6
  343. package/src/components/verbs/run-agent.ts +11 -17
  344. package/src/components/verbs/verify.ts +2 -2
  345. package/src/deep-observation.test.ts +75 -0
  346. package/src/deep-observation.ts +48 -0
  347. package/src/discovery/collect.test.ts +3 -3
  348. package/src/discovery/files.ts +4 -4
  349. package/src/discovery/fold-import.ts +105 -2
  350. package/src/discovery/param-deps.test.ts +112 -1
  351. package/src/discovery/param-deps.ts +193 -0
  352. package/src/discovery/sandbox/bundle.ts +15 -1
  353. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  354. package/src/discovery/sandbox/config-wire.ts +2 -2
  355. package/src/fold/fold.test.ts +56 -0
  356. package/src/fold/foldable-helpers.ts +81 -1
  357. package/src/fold-provenance-exclusion.test.ts +186 -0
  358. package/src/fold-provenance.test.ts +495 -0
  359. package/src/fold-provenance.ts +441 -0
  360. package/src/graph-ops.test.ts +2 -2
  361. package/src/graph-ops.ts +1 -1
  362. package/src/held-elsewhere.test.ts +102 -0
  363. package/src/held-elsewhere.ts +114 -0
  364. package/src/import/ir-utils.ts +11 -1
  365. package/src/import/parser.ts +35 -0
  366. package/src/index.ts +3 -0
  367. package/src/kubectl-context.ts +3 -3
  368. package/src/lexicon-config.test.ts +5 -5
  369. package/src/lexicon-config.ts +4 -4
  370. package/src/lexicon-plugin-helpers.ts +1 -1
  371. package/src/lexicon.ts +120 -14
  372. package/src/lifecycle/change-set.test.ts +49 -0
  373. package/src/lifecycle/change-set.ts +67 -0
  374. package/src/lifecycle/converge-ledger.ts +1 -1
  375. package/src/lifecycle/deep-diff.test.ts +267 -14
  376. package/src/lifecycle/deep-diff.ts +217 -14
  377. package/src/lifecycle/deep-observe.test.ts +3 -1
  378. package/src/lifecycle/deep-observe.ts +16 -6
  379. package/src/lifecycle/gate-ledger.test.ts +74 -2
  380. package/src/lifecycle/gate-ledger.ts +143 -36
  381. package/src/lifecycle/index.ts +1 -0
  382. package/src/lifecycle/release-ledger.ts +2 -2
  383. package/src/lifecycle/run-ledger.test.ts +268 -0
  384. package/src/lifecycle/run-ledger.ts +201 -0
  385. package/src/lifecycle/symptoms.ts +3 -3
  386. package/src/lint/config.test.ts +82 -1
  387. package/src/lint/config.ts +112 -3
  388. package/src/lint/policy.test.ts +1 -1
  389. package/src/lint/post-synth.test.ts +31 -0
  390. package/src/lint/post-synth.ts +54 -1
  391. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  392. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  393. package/src/lint/rules/comp/comp.test.ts +1 -1
  394. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +74 -0
  395. package/src/lint/rules/comp/index.ts +3 -3
  396. package/src/lint/rules/op/index.ts +23 -0
  397. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  398. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  399. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  400. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  401. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  402. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  403. package/src/lint/rules/op/support.ts +11 -0
  404. package/src/lint/suppressions.test.ts +163 -0
  405. package/src/lint/suppressions.ts +204 -0
  406. package/src/live-endpoint.ts +1 -1
  407. package/src/op/activities/activity-contracts.ts +66 -0
  408. package/src/op/activities/apply.test.ts +567 -0
  409. package/src/op/activities/apply.ts +662 -0
  410. package/src/op/activities/build.ts +26 -0
  411. package/src/op/activities/converge.test.ts +330 -0
  412. package/src/op/activities/converge.ts +449 -0
  413. package/src/op/activities/env-teardown.test.ts +187 -0
  414. package/src/op/activities/env-teardown.ts +171 -0
  415. package/src/op/activities/guard-validate.test.ts +139 -0
  416. package/src/op/activities/guard-validate.ts +223 -0
  417. package/src/op/activities/http-check.test.ts +48 -0
  418. package/src/op/activities/http-check.ts +55 -0
  419. package/src/op/activities/index.ts +119 -0
  420. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  421. package/src/op/activities/lexicon-upgrade.ts +673 -0
  422. package/src/op/activities/lifecycle.ts +85 -0
  423. package/src/op/activities/pipeline-audit.test.ts +67 -0
  424. package/src/op/activities/pipeline-audit.ts +201 -0
  425. package/src/op/activities/policy.test.ts +19 -0
  426. package/src/op/activities/policy.ts +37 -0
  427. package/src/op/activities/reconcile.test.ts +81 -0
  428. package/src/op/activities/reconcile.ts +187 -0
  429. package/src/op/activities/shell.ts +26 -0
  430. package/src/op/activities/teardown.ts +22 -0
  431. package/src/op/activities/util.ts +6 -0
  432. package/src/op/activities/wait.ts +69 -0
  433. package/src/op/activities/workflow-audit.test.ts +90 -0
  434. package/src/op/activities/workflow-audit.ts +203 -0
  435. package/src/op/activity-contract-registry.test.ts +98 -0
  436. package/src/op/activity-contract-registry.ts +114 -0
  437. package/src/op/activity-contract.test.ts +1 -1
  438. package/src/op/activity-contract.ts +37 -33
  439. package/src/op/activity-failure.ts +34 -0
  440. package/src/op/activity-profiles.test.ts +56 -0
  441. package/src/op/activity-profiles.ts +116 -0
  442. package/src/op/activity-registry.test.ts +37 -28
  443. package/src/op/activity-registry.ts +27 -56
  444. package/src/op/activity-runtime.ts +3 -47
  445. package/src/op/builders-exports.test.ts +0 -49
  446. package/src/op/builders-typed.test.ts +164 -0
  447. package/src/op/builders.ts +123 -39
  448. package/src/op/composites/apply-op.ts +208 -0
  449. package/src/op/composites/composites.test.ts +366 -0
  450. package/src/op/composites/converge-op.test.ts +168 -0
  451. package/src/op/composites/converge-op.ts +214 -0
  452. package/src/op/composites/index.ts +26 -0
  453. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  454. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  455. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  456. package/src/op/composites/pipeline-audit-op.ts +78 -0
  457. package/src/op/composites/reconcile-op.test.ts +51 -0
  458. package/src/op/composites/reconcile-op.ts +107 -0
  459. package/src/op/composites/watch-op.ts +108 -0
  460. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  461. package/src/op/composites/workflow-audit-op.ts +88 -0
  462. package/src/op/converge-rule.ts +12 -10
  463. package/src/op/cron.test.ts +128 -0
  464. package/src/op/cron.ts +149 -0
  465. package/src/op/discover-root.test.ts +105 -1
  466. package/src/op/discover.test.ts +6 -2
  467. package/src/op/discover.ts +84 -16
  468. package/src/op/duration.ts +21 -0
  469. package/src/op/effect-step.test.ts +26 -11
  470. package/src/op/emulator-lifecycle.ts +1 -2
  471. package/src/op/gate.ts +155 -0
  472. package/src/op/generate-pipeline.test.ts +65 -3
  473. package/src/op/generate-pipeline.ts +19 -2
  474. package/src/op/index.ts +39 -6
  475. package/src/op/local-executor.test.ts +163 -34
  476. package/src/op/local-executor.ts +370 -102
  477. package/src/op/local-output.test.ts +95 -11
  478. package/src/op/local-output.ts +33 -4
  479. package/src/op/op-ir.test.ts +281 -0
  480. package/src/op/op-ir.ts +417 -0
  481. package/src/op/op-verb-class.ts +19 -5
  482. package/src/op/op.test.ts +26 -5
  483. package/src/op/operator.test.ts +175 -6
  484. package/src/op/operator.ts +63 -16
  485. package/src/op/receipt-store.ts +1 -1
  486. package/src/op/resource.ts +20 -2
  487. package/src/op/runtime.ts +203 -0
  488. package/src/op/runtimes/local.test.ts +181 -0
  489. package/src/op/runtimes/local.ts +212 -0
  490. package/src/op/step-output-ref.ts +15 -13
  491. package/src/op/types.ts +84 -24
  492. package/src/ownership.ts +1 -1
  493. package/src/provenance.test.ts +6 -0
  494. package/src/provenance.ts +21 -1
  495. package/src/serializer-walker.test.ts +42 -0
  496. package/src/serializer-walker.ts +21 -2
  497. package/src/stack-output.ts +6 -1
  498. package/dist/cli/handlers/op-progress.d.ts +0 -90
  499. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  500. package/dist/cli/handlers/run-client.d.ts +0 -127
  501. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  502. package/dist/cli/handlers/run-report.d.ts +0 -5
  503. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  504. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  505. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  506. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  507. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  508. package/src/cli/handlers/op-progress.test.ts +0 -268
  509. package/src/cli/handlers/op-progress.ts +0 -248
  510. package/src/cli/handlers/run-client.test.ts +0 -82
  511. package/src/cli/handlers/run-client.ts +0 -219
  512. package/src/cli/handlers/run-report.test.ts +0 -62
  513. package/src/cli/handlers/run-report.ts +0 -122
  514. package/src/components/temporal-codegen-loader.ts +0 -41
  515. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -1,20 +1,37 @@
1
1
  /**
2
- * Local Op executor — runs an Op's phases in-process with no Temporal worker.
2
+ * Local Op executor — runs an Op's phases in this process.
3
3
  *
4
- * A first-class peer to Temporal mode for dev loops, CI, and drift/observation
5
- * Ops. Provides phase sequencing, parallel phases, per-step retry + timeout via
4
+ * The default runtime (#2121), and the one dev loops, CI, and drift/observation
5
+ * Ops use. Provides phase sequencing, parallel phases, per-step retry + timeout via
6
6
  * activity profiles, `outcomeAttribute` capture, and `onFailure` compensation.
7
- * Gates and schedules are unsupported and rejected before any phase runs.
8
7
  *
9
- * The executor is deliberately decoupled from the Temporal lexicon: activity
8
+ * A gate is a fact, not a wait (#2119). Reaching one, the executor consults the
9
+ * gate ledger through `./gate.ts`: a resolution newer than the gate's newest
10
+ * pending fact lets the step pass, carrying the approver onto its record;
11
+ * anything else records the pending fact and ends the run with status `gated`.
12
+ * No later phase runs and `onFailure` does not run — a gate is not a failure,
13
+ * and there is nothing to compensate for. The next run re-evaluates from the
14
+ * ledger, so `chant approve <op> <gate>` followed by `chant run <op>` is the
15
+ * whole loop.
16
+ *
17
+ * The executor is deliberately decoupled from any lexicon: activity
10
18
  * implementations and profiles are passed in (loaded dynamically by the CLI),
11
- * so core never statically depends on `@intentius/chant-lexicon-temporal`.
19
+ * so core never statically depends on one.
12
20
  */
13
21
 
22
+ import { outcomeAttributesOf } from "./types";
14
23
  import type { OpConfig, PhaseDefinition, ActivityStep, GateStep, EffectStep, StepDefinition } from "./types";
15
24
  import { resolveActivity, type ActivityFn, type ActivityProfile } from "./activity-registry";
16
25
  import type { ReceiptReadResult } from "./receipt-store";
17
26
  import { isStepOutputRef } from "./step-output-ref";
27
+ import { parseDuration } from "./duration";
28
+ import { evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "./gate";
29
+ import type { PendingGateRecord } from "../lifecycle/gate-ledger";
30
+ import { appendRunRecord, buildRunRecord } from "../lifecycle/run-ledger";
31
+ import type { OpRunRecord } from "./runtime";
32
+ import { randomUUID } from "node:crypto";
33
+
34
+ export { parseDuration } from "./duration";
18
35
 
19
36
  // ── Records ─────────────────────────────────────────────────────────────────
20
37
 
@@ -22,38 +39,53 @@ export interface StepRecord {
22
39
  phase: string;
23
40
  fn: string;
24
41
  /**
25
- * Present for a local-executor record. Absent for one reconstructed from
26
- * Temporal workflow history (op-progress.ts) — an activity's scheduled
27
- * input isn't decoded there, so the field is simply omitted rather than
28
- * populated with a guess.
42
+ * Present for a local-executor record. A runtime that reconstructs records
43
+ * from its own run history may not have an activity's scheduled input to
44
+ * hand, and omits the field rather than guessing at it.
29
45
  */
30
46
  args?: Record<string, unknown>;
31
47
  status: "ok" | "fail" | "skipped";
32
48
  durationMs: number;
49
+ /**
50
+ * The first outcome attribute the step published, kept singular because a
51
+ * step publishing one is the ordinary case and every reader of this field
52
+ * predates the plural form. {@link StepRecord.outcomes} is the whole list.
53
+ */
33
54
  outcome?: { name: string; value: unknown };
55
+ /** Every outcome attribute the step published, in authored order (#2105). Absent when it published none. */
56
+ outcomes?: Array<{ name: string; value: unknown }>;
34
57
  error?: string;
58
+ /** Set on a `gate` step that passed (#2119): who resolved it, when, and at what address. */
59
+ approval?: { gate: string; resolvedBy: string; timestamp: string; url?: string };
35
60
  }
36
61
 
37
62
  export interface OpRunResult {
38
63
  op: string;
39
64
  records: StepRecord[];
40
65
  totalMs: number;
41
- ok: boolean;
66
+ /**
67
+ * Three-state outcome (#2119). `gated` is neither success nor failure: the
68
+ * run reached a gate nobody has approved, recorded the fact, and stopped.
69
+ * The CLI exits 3 for it so CI can tell "waiting on a human" from "broken".
70
+ *
71
+ * Written to the run ledger as the record's own `status` (#2118).
72
+ */
73
+ status: "ok" | "fail" | "gated";
74
+ /** ISO-8601 start of this run. */
75
+ startedAt: string;
76
+ /** Present when `status === "gated"`: the pending fact the run ended on. */
77
+ gate?: PendingGateRecord;
78
+ /**
79
+ * The run's ledger record (#2118) — always built, whether or not it was
80
+ * appended. `chant run <op> --json` prints exactly this, so what a caller
81
+ * reads on stdout and what a later `readRunLedger` reads back are the same
82
+ * document.
83
+ */
84
+ record: OpRunRecord;
42
85
  }
43
86
 
44
87
  // ── Errors ────────────────────────────────────────────────────────────────��─
45
88
 
46
- /** Thrown when an Op contains a gate (or schedule) that local mode cannot run. */
47
- export class LocalGateUnsupportedError extends Error {
48
- constructor(public readonly signalName: string) {
49
- super(
50
- `gate "${signalName}" is not supported in local mode — gates and schedules ` +
51
- `need a durable runtime. Re-run with --temporal.`,
52
- );
53
- this.name = "LocalGateUnsupportedError";
54
- }
55
- }
56
-
57
89
  /** Thrown on terminal Op failure; carries the partial run result for rendering. */
58
90
  export class OpRunFailure extends Error {
59
91
  constructor(public readonly result: OpRunResult) {
@@ -70,6 +102,18 @@ class PhaseFailure extends Error {
70
102
  }
71
103
  }
72
104
 
105
+ /** Internal: a phase stopped on an unapproved gate. Not a failure — no compensation follows it. */
106
+ class GateStop extends Error {
107
+ constructor(
108
+ public readonly records: StepRecord[],
109
+ public readonly pending: PendingGateRecord,
110
+ public readonly phase: string,
111
+ ) {
112
+ super(`gate "${pending.gate}" is pending approval`);
113
+ this.name = "GateStop";
114
+ }
115
+ }
116
+
73
117
  // ── Helpers ───────────────────────────────────────────────────────────────��─
74
118
 
75
119
  const DEFAULT_PROFILE = "fastIdempotent";
@@ -79,19 +123,6 @@ const isActivity = (s: StepDefinition): s is ActivityStep => s.kind === "activit
79
123
  const isGate = (s: StepDefinition): s is GateStep => s.kind === "gate";
80
124
  const isEffect = (s: StepDefinition): s is EffectStep => s.kind === "effect";
81
125
 
82
- /** Parse a Temporal duration string ("5m", "30s", "1h30m", "100ms") to ms. */
83
- export function parseDuration(s: string): number {
84
- const units: Record<string, number> = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
85
- let total = 0;
86
- let matched = false;
87
- for (const m of s.matchAll(/(\d+)(ms|s|m|h|d)/g)) {
88
- total += Number(m[1]) * units[m[2]];
89
- matched = true;
90
- }
91
- if (!matched) throw new Error(`unparseable duration: "${s}"`);
92
- return total;
93
- }
94
-
95
126
  /** Resolve a dot-path into a value; returns the whole value when path is absent. */
96
127
  function resolvePath(value: unknown, path?: string): unknown {
97
128
  if (!path) return value;
@@ -120,22 +151,6 @@ function resolveStepOutputRefs(value: unknown, resultsById: ReadonlyMap<string,
120
151
  return value;
121
152
  }
122
153
 
123
- /** Find the first gate step anywhere in the Op (phases + onFailure, including
124
- * gates nested inside effect steps), if any. */
125
- export function findGate(config: OpConfig): GateStep | undefined {
126
- const all = [...config.phases, ...(config.onFailure ?? [])];
127
- for (const phase of all) {
128
- for (const step of phase.steps) {
129
- if (isGate(step)) return step;
130
- if (isEffect(step)) {
131
- const nested = step.steps.find(isGate);
132
- if (nested) return nested;
133
- }
134
- }
135
- }
136
- return undefined;
137
- }
138
-
139
154
  /**
140
155
  * Find the first `policyGate` step anywhere in the Op (phases + `onFailure`,
141
156
  * including steps nested inside effect steps), if any.
@@ -147,7 +162,8 @@ export function findGate(config: OpConfig): GateStep | undefined {
147
162
  * shown to a user who passed a documented flag. The combination cannot be
148
163
  * honoured until the gate can build and load policies inside the boundary
149
164
  * (#1157), so both `chant run` paths pre-flight it with this and refuse before
150
- * any phase executes, the same shape as {@link findGate}'s pre-flight.
165
+ * any phase executes. This is the one pre-flight refusal left on the Op path —
166
+ * a `gate` no longer is one (#2119).
151
167
  */
152
168
  export function findPolicyGateStep(config: OpConfig): ActivityStep | undefined {
153
169
  const all = [...config.phases, ...(config.onFailure ?? [])];
@@ -217,9 +233,9 @@ interface RanStep {
217
233
  /**
218
234
  * Run one activity step with retry + timeout. Never throws — returns a
219
235
  * record. Any {@link StepOutputRef} in `step.args` is resolved against
220
- * `resultsById` before the activity is called (#1290) — the local executor
221
- * is a first-class peer of the Temporal path, so an unresolved placeholder
222
- * must never reach an activity function; see `resolveStepOutputRefs`.
236
+ * `resultsById` before the activity is called (#1290) — an unresolved
237
+ * placeholder must never reach an activity function, on this runtime or any
238
+ * other; see `resolveStepOutputRefs`.
223
239
  */
224
240
  async function runStep(
225
241
  step: ActivityStep,
@@ -241,8 +257,8 @@ async function runStep(
241
257
  }
242
258
 
243
259
  const profile = profiles[step.profile ?? DEFAULT_PROFILE] ?? {};
244
- const timeoutMs = profile.startToCloseTimeout
245
- ? parseDuration(profile.startToCloseTimeout)
260
+ const timeoutMs = profile.timeout
261
+ ? parseDuration(profile.timeout)
246
262
  : FALLBACK_TIMEOUT_MS;
247
263
  const maxAttempts =
248
264
  profile.retry?.maximumAttempts && profile.retry.maximumAttempts > 0
@@ -261,11 +277,10 @@ async function runStep(
261
277
  const result = await callWithTimeout(fn, args, timeoutMs, signal);
262
278
  if (step.id) resultsById.set(step.id, result);
263
279
  const record: StepRecord = { ...base, status: "ok", durationMs: Date.now() - start };
264
- if (step.outcomeAttribute) {
265
- record.outcome = {
266
- name: step.outcomeAttribute.name,
267
- value: resolvePath(result, step.outcomeAttribute.from),
268
- };
280
+ const attrs = outcomeAttributesOf(step);
281
+ if (attrs.length > 0) {
282
+ record.outcomes = attrs.map((a) => ({ name: a.name, value: resolvePath(result, a.from) }));
283
+ record.outcome = record.outcomes[0];
269
284
  }
270
285
  return { record, result };
271
286
  } catch (err) {
@@ -307,6 +322,75 @@ function skippedRecord(phaseName: string, fn: string, args?: Record<string, unkn
307
322
  return { phase: phaseName, fn, args: args ?? {}, status: "skipped", durationMs: 0 };
308
323
  }
309
324
 
325
+ // ── Gate steps (#2119) ───────────────────────────────────────────────────────
326
+
327
+ /** What a run needs to decide a gate against the ledger — see `./gate.ts`. */
328
+ interface GateContext {
329
+ op: string;
330
+ port: GateLedgerPort;
331
+ now?: string;
332
+ runId?: string;
333
+ /** Called once per settled step, in production order (#2121) — what `--progress-json` streams from. */
334
+ onRecord?: (record: StepRecord) => void;
335
+ }
336
+
337
+ /** Collect records and hand each to the caller's progress sink in one move. */
338
+ function pushRecord(sink: StepRecord[], ctx: GateContext, ...recs: StepRecord[]): void {
339
+ sink.push(...recs);
340
+ for (const r of recs) ctx.onRecord?.(r);
341
+ }
342
+
343
+ /** The record name a gate step lands under, so a reader (and a JSON consumer) can pick it out of `records`. */
344
+ function gateFn(step: GateStep): string {
345
+ return `gate:${step.signalName}`;
346
+ }
347
+
348
+ /**
349
+ * Decide one gate against the ledger. A resolution newer than the gate's
350
+ * newest pending fact passes it, and the approver lands on the step record; a
351
+ * pending fact is recorded (or left standing) otherwise, and the caller stops
352
+ * the run.
353
+ */
354
+ async function runGateStep(
355
+ step: GateStep,
356
+ phaseName: string,
357
+ gates: GateContext,
358
+ ): Promise<{ record: StepRecord; pending?: PendingGateRecord }> {
359
+ const start = Date.now();
360
+ const check = await evaluateGate(gates.port, {
361
+ op: gates.op,
362
+ gate: step.signalName,
363
+ ...(step.description ? { description: step.description } : {}),
364
+ ...(step.timeout ? { timeout: step.timeout } : {}),
365
+ ...(gates.runId ? { runId: gates.runId } : {}),
366
+ ...(gates.now ? { now: gates.now } : {}),
367
+ });
368
+
369
+ if (check.satisfied) {
370
+ const { resolution } = check;
371
+ return {
372
+ record: {
373
+ phase: phaseName,
374
+ fn: gateFn(step),
375
+ args: {},
376
+ status: "ok",
377
+ durationMs: Date.now() - start,
378
+ approval: {
379
+ gate: step.signalName,
380
+ resolvedBy: resolution.resolvedBy,
381
+ timestamp: resolution.timestamp,
382
+ ...(resolution.url ? { url: resolution.url } : {}),
383
+ },
384
+ },
385
+ };
386
+ }
387
+
388
+ return {
389
+ record: { phase: phaseName, fn: gateFn(step), args: {}, status: "skipped", durationMs: Date.now() - start },
390
+ pending: check.pending,
391
+ };
392
+ }
393
+
310
394
  /**
311
395
  * Run one effect step: read-compare-run-write. On a match the nested steps are
312
396
  * recorded as skipped ("effect already applied") and nothing is written. On a
@@ -314,6 +398,11 @@ function skippedRecord(phaseName: string, fn: string, args?: Record<string, unkn
314
398
  * succeeds is the receipt written — last, once (the sole writer, #1703
315
399
  * decision 3). Any failure leaves the receipt untouched (stale), so the next
316
400
  * run re-proposes the effect.
401
+ *
402
+ * A nested `gate` is decided in authored order like any other nested step
403
+ * (#2119). Unapproved, it stops the effect before the receipt is written, so
404
+ * the next run re-reads the receipt, re-proposes the effect, and re-evaluates
405
+ * the gate — the receipt stays honest about what actually happened.
317
406
  */
318
407
  async function runEffectStep(
319
408
  step: EffectStep,
@@ -321,17 +410,18 @@ async function runEffectStep(
321
410
  activities: Map<string, ActivityFn>,
322
411
  profiles: Record<string, ActivityProfile>,
323
412
  resultsById: Map<string, unknown>,
413
+ gates: GateContext,
324
414
  signal?: AbortSignal,
325
- ): Promise<{ records: StepRecord[]; failed: boolean }> {
415
+ ): Promise<{ records: StepRecord[]; failed: boolean; pending?: PendingGateRecord }> {
326
416
  const records: StepRecord[] = [];
327
417
 
328
418
  const read = await runStep(receiptReadStep(step), phaseName, activities, profiles, resultsById, signal);
329
- records.push(read.record);
419
+ pushRecord(records, gates, read.record);
330
420
  if (read.record.status === "fail") return { records, failed: true };
331
421
 
332
422
  const result = read.result as Partial<ReceiptReadResult> | undefined;
333
423
  if (typeof result?.expectation !== "string") {
334
- records.push({
424
+ pushRecord(records, gates, {
335
425
  phase: phaseName,
336
426
  fn: `effect:${step.receipt.name}`,
337
427
  args: {},
@@ -346,22 +436,44 @@ async function runEffectStep(
346
436
  if (result.current === expectation) {
347
437
  // Effect already applied — skip the nested steps, write nothing.
348
438
  for (const nested of step.steps) {
349
- if (nested.kind === "activity") records.push(skippedRecord(phaseName, nested.fn, nested.args));
439
+ pushRecord(records, gates,
440
+ nested.kind === "activity"
441
+ ? skippedRecord(phaseName, nested.fn, nested.args)
442
+ : skippedRecord(phaseName, gateFn(nested)),
443
+ );
350
444
  }
351
445
  return { records, failed: false };
352
446
  }
353
447
 
354
- // Gates are pre-flighted by findGate; only activities remain here.
355
- const nestedActivities = step.steps.filter(isActivity);
356
- for (let i = 0; i < nestedActivities.length; i++) {
357
- const ran = await runStep(nestedActivities[i], phaseName, activities, profiles, resultsById, signal);
358
- records.push(ran.record);
448
+ const skipRest = (from: number) => {
449
+ for (const skipped of step.steps.slice(from)) {
450
+ pushRecord(records, gates,
451
+ skipped.kind === "activity"
452
+ ? skippedRecord(phaseName, skipped.fn, skipped.args)
453
+ : skippedRecord(phaseName, gateFn(skipped)),
454
+ );
455
+ }
456
+ pushRecord(records, gates, skippedRecord(phaseName, "receiptWrite"));
457
+ };
458
+
459
+ for (let i = 0; i < step.steps.length; i++) {
460
+ const nested = step.steps[i];
461
+ if (isGate(nested)) {
462
+ const { record, pending } = await runGateStep(nested, phaseName, gates);
463
+ pushRecord(records, gates, record);
464
+ if (pending) {
465
+ // Receipt left untouched — the next run re-proposes the effect and
466
+ // re-evaluates the gate against whatever the ledger says by then.
467
+ skipRest(i + 1);
468
+ return { records, failed: false, pending };
469
+ }
470
+ continue;
471
+ }
472
+ const ran = await runStep(nested, phaseName, activities, profiles, resultsById, signal);
473
+ pushRecord(records, gates, ran.record);
359
474
  if (ran.record.status === "fail") {
360
475
  // Receipt left untouched (stale) — the next run re-proposes the effect.
361
- for (const skipped of nestedActivities.slice(i + 1)) {
362
- records.push(skippedRecord(phaseName, skipped.fn, skipped.args));
363
- }
364
- records.push(skippedRecord(phaseName, "receiptWrite"));
476
+ skipRest(i + 1);
365
477
  return { records, failed: true };
366
478
  }
367
479
  }
@@ -380,25 +492,22 @@ async function runEffectStep(
380
492
  resultsById,
381
493
  signal,
382
494
  );
383
- records.push(wrote.record);
495
+ pushRecord(records, gates, wrote.record);
384
496
  return { records, failed: wrote.record.status === "fail" };
385
497
  }
386
498
 
387
- /** Run a phase. Throws PhaseFailure (with records so far) if any step fails. */
499
+ /**
500
+ * Run a phase. Throws `PhaseFailure` (with the records so far) if any step
501
+ * fails, or `GateStop` if a `gate` step in it is still pending approval.
502
+ */
388
503
  async function runPhase(
389
504
  phase: PhaseDefinition,
390
505
  activities: Map<string, ActivityFn>,
391
506
  profiles: Record<string, ActivityProfile>,
392
507
  resultsById: Map<string, unknown>,
508
+ gates: GateContext,
393
509
  signal?: AbortSignal,
394
510
  ): Promise<StepRecord[]> {
395
- // Defensive: gates are pre-flighted, but never execute one if it slips
396
- // through — including a gate nested inside an effect step.
397
- const gate =
398
- phase.steps.find(isGate) ??
399
- phase.steps.filter(isEffect).flatMap((e) => e.steps).find(isGate);
400
- if (gate) throw new LocalGateUnsupportedError(gate.signalName);
401
-
402
511
  if (phase.parallel) {
403
512
  const eff = phase.steps.find(isEffect);
404
513
  if (eff) {
@@ -406,32 +515,65 @@ async function runPhase(
406
515
  `effect step "${eff.receipt.name}" cannot run in a parallel phase — read-compare-run-write is ordered`,
407
516
  );
408
517
  }
518
+ // Gates in a parallel phase are decided before the fan-out: a gate is a
519
+ // read of the ledger, not work, and starting activities that a pending
520
+ // gate is about to strand would defeat the point of stopping at it.
521
+ const gateRecords: StepRecord[] = [];
522
+ for (const step of phase.steps.filter(isGate)) {
523
+ const { record, pending } = await runGateStep(step, phase.name, gates);
524
+ pushRecord(gateRecords, gates, record);
525
+ if (pending) {
526
+ for (const skipped of phase.steps.filter(isActivity)) {
527
+ pushRecord(gateRecords, gates, skippedRecord(phase.name, skipped.fn, skipped.args));
528
+ }
529
+ throw new GateStop(gateRecords, pending, phase.name);
530
+ }
531
+ }
409
532
  const steps = phase.steps.filter(isActivity);
410
- const records = (
533
+ const ran = (
411
534
  await Promise.all(steps.map((s) => runStep(s, phase.name, activities, profiles, resultsById, signal)))
412
535
  ).map((r) => r.record);
536
+ for (const r of ran) gates.onRecord?.(r);
537
+ const records = gateRecords.concat(ran);
413
538
  if (records.some((r) => r.status === "fail")) throw new PhaseFailure(records);
414
539
  return records;
415
540
  }
416
541
 
417
- const steps = phase.steps.filter((s): s is ActivityStep | EffectStep => !isGate(s));
542
+ const steps = phase.steps;
418
543
  const records: StepRecord[] = [];
419
544
 
420
545
  const skipRemaining = (from: number) => {
421
546
  for (const skipped of steps.slice(from)) {
422
547
  if (isEffect(skipped)) {
423
- records.push(skippedRecord(phase.name, `effect:${skipped.receipt.name}`));
548
+ pushRecord(records, gates, skippedRecord(phase.name, `effect:${skipped.receipt.name}`));
549
+ } else if (isGate(skipped)) {
550
+ pushRecord(records, gates, skippedRecord(phase.name, gateFn(skipped)));
424
551
  } else {
425
- records.push(skippedRecord(phase.name, skipped.fn, skipped.args));
552
+ pushRecord(records, gates, skippedRecord(phase.name, skipped.fn, skipped.args));
426
553
  }
427
554
  }
428
555
  };
429
556
 
430
557
  for (let i = 0; i < steps.length; i++) {
431
558
  const step = steps[i];
559
+ if (isGate(step)) {
560
+ const { record, pending } = await runGateStep(step, phase.name, gates);
561
+ pushRecord(records, gates, record);
562
+ if (pending) {
563
+ skipRemaining(i + 1);
564
+ throw new GateStop(records, pending, phase.name);
565
+ }
566
+ continue;
567
+ }
432
568
  if (isEffect(step)) {
433
- const { records: effRecords, failed } = await runEffectStep(step, phase.name, activities, profiles, resultsById, signal);
434
- records.push(...effRecords);
569
+ const { records: effRecords, failed, pending } = await runEffectStep(
570
+ step, phase.name, activities, profiles, resultsById, gates, signal,
571
+ );
572
+ records.push(...effRecords); // already emitted by runEffectStep
573
+ if (pending) {
574
+ skipRemaining(i + 1);
575
+ throw new GateStop(records, pending, phase.name);
576
+ }
435
577
  if (failed) {
436
578
  skipRemaining(i + 1);
437
579
  throw new PhaseFailure(records);
@@ -439,7 +581,7 @@ async function runPhase(
439
581
  continue;
440
582
  }
441
583
  const { record } = await runStep(step, phase.name, activities, profiles, resultsById, signal);
442
- records.push(record);
584
+ pushRecord(records, gates, record);
443
585
  if (record.status === "fail") {
444
586
  // Mark the remaining steps in this phase as skipped, then abort.
445
587
  skipRemaining(i + 1);
@@ -451,20 +593,78 @@ async function runPhase(
451
593
 
452
594
  // ── Public API ────────────────────────────────────────────────────────────��─
453
595
 
596
+ /** How a run reaches the gate ledger, and what it calls itself there (#2119). */
597
+ export interface RunOpOptions {
598
+ /**
599
+ * Where gate facts are read and written. Defaults to the `chant/lifecycle`
600
+ * orphan branch under `cwd`; a test (or a caller that already holds the
601
+ * ledger) passes `memoryGateLedgerPort()` from `./gate.ts` instead.
602
+ */
603
+ gates?: GateLedgerPort;
604
+ /** Working directory for the default git-backed gate ledger. */
605
+ cwd?: string;
606
+ /** ISO-8601 "now", so a gate decision is deterministic under test. */
607
+ now?: string;
608
+ /** Identifies this run on any pending fact it records. */
609
+ runId?: string;
610
+ /**
611
+ * Called once per settled step, in the order the records are produced
612
+ * (#2121) — what the local op runtime (./runtimes/local.ts) feeds
613
+ * `--progress-json` from. Side-effect free when omitted.
614
+ */
615
+ onRecord?: (record: StepRecord) => void;
616
+ /**
617
+ * Append the run's {@link OpRunRecord} to the run ledger
618
+ * (`../lifecycle/run-ledger.ts`) when the run settles (#2118). Opt-in on
619
+ * purpose: the executor is called from unit tests and in-process harnesses
620
+ * whose working directory is an ordinary checkout, and a run there must not
621
+ * write to the lifecycle branch. The local runtime (./runtimes/local.ts) and
622
+ * `chant operator` pass it; nothing else does.
623
+ */
624
+ ledger?: { cwd?: string };
625
+ /**
626
+ * Called when the ledger append itself fails. The run's own outcome is
627
+ * already decided by then, so a ledger failure is reported to the caller,
628
+ * never promoted into a run failure.
629
+ */
630
+ onLedgerError?: (err: unknown) => void;
631
+ }
632
+
454
633
  /**
455
- * Execute an Op locally. Resolves with the run result on success; rejects with
456
- * `OpRunFailure` (carrying the partial result) on terminal failure, after
457
- * running any `onFailure` phases in reverse order. Throws
458
- * `LocalGateUnsupportedError` up front if the Op contains a gate.
634
+ * Execute an Op locally.
635
+ *
636
+ * Resolves with the run result when every phase succeeds (`status: "ok"`), and
637
+ * also when the run stopped at an unapproved gate (`status: "gated"`, with the
638
+ * pending fact on `result.gate`) — a gate is a fact, not an error, so it is not
639
+ * thrown. Rejects with `OpRunFailure` (carrying the partial result) on terminal
640
+ * failure, after running any `onFailure` phases in reverse order; a gated run
641
+ * runs no `onFailure` phase, because nothing failed and nothing was left
642
+ * half-applied to compensate for.
643
+ *
644
+ * Whichever of the three ways it settles, the run carries an
645
+ * {@link OpRunRecord} on `result.record` (#2118). With `options.ledger` that
646
+ * record is also appended to `<env>/runs__<op>.jsonl` on the lifecycle branch
647
+ * before this returns or rejects, so an Op's outcome outlives the process that
648
+ * produced it.
459
649
  */
460
650
  export async function runOpLocally(
461
651
  config: OpConfig,
462
652
  activities: Map<string, ActivityFn>,
463
653
  profiles: Record<string, ActivityProfile>,
464
654
  signal?: AbortSignal,
655
+ options: RunOpOptions = {},
465
656
  ): Promise<OpRunResult> {
466
- const gate = findGate(config);
467
- if (gate) throw new LocalGateUnsupportedError(gate.signalName);
657
+ // The run id is the gate ledger's and the run ledger's alike: a pending gate
658
+ // fact and the record naming that gate carry the same string.
659
+ const runId = options.runId ?? randomUUID();
660
+
661
+ const gates: GateContext = {
662
+ op: config.name,
663
+ port: options.gates ?? gitGateLedgerPort(options.cwd ? { cwd: options.cwd } : undefined),
664
+ ...(options.now ? { now: options.now } : {}),
665
+ runId,
666
+ ...(options.onRecord ? { onRecord: options.onRecord } : {}),
667
+ };
468
668
 
469
669
  // Effect steps are ordered (read-compare-run-write): refuse them in a
470
670
  // parallel phase up front, with the phase named, rather than mid-run.
@@ -480,6 +680,33 @@ export async function runOpLocally(
480
680
 
481
681
  const records: StepRecord[] = [];
482
682
  const start = Date.now();
683
+ const startedAt = options.now ?? new Date(start).toISOString();
684
+
685
+ /**
686
+ * Build the run's ledger record, append it when asked, and hand it back for
687
+ * the result. Called once, at whichever of the three exits the run takes.
688
+ */
689
+ const settle = async (
690
+ settled: StepRecord[],
691
+ status: OpRunResult["status"],
692
+ gate?: PendingGateRecord,
693
+ ): Promise<OpRunRecord> => {
694
+ const input = buildRunRecord(config, settled, {
695
+ started: startedAt,
696
+ ended: options.now ?? new Date().toISOString(),
697
+ status,
698
+ id: runId,
699
+ ...(gate ? { gate: { name: gate.gate, since: gate.timestamp } } : {}),
700
+ });
701
+ const record: OpRunRecord = { version: 1, ...input, id: runId };
702
+ if (!options.ledger) return record;
703
+ try {
704
+ return (await appendRunRecord(input, options.ledger)).record;
705
+ } catch (err) {
706
+ options.onLedgerError?.(err);
707
+ return record;
708
+ }
709
+ };
483
710
 
484
711
  // Completed step id → its activity result (#1290). Populated as main-phase
485
712
  // steps finish, so a later step's step-output references resolve to real
@@ -489,9 +716,36 @@ export async function runOpLocally(
489
716
  try {
490
717
  for (const phase of config.phases) {
491
718
  if (signal?.aborted) throw new PhaseFailure([]);
492
- records.push(...(await runPhase(phase, activities, profiles, resultsById, signal)));
719
+ records.push(...(await runPhase(phase, activities, profiles, resultsById, gates, signal)));
493
720
  }
494
721
  } catch (err) {
722
+ // A pending gate ends the run where it stands: no later phase, and no
723
+ // `onFailure` compensation — nothing failed, so there is nothing to undo.
724
+ if (err instanceof GateStop) {
725
+ records.push(...err.records);
726
+ const stoppedAt = config.phases.findIndex((p) => p.name === err.phase);
727
+ for (const phase of config.phases.slice(stoppedAt + 1)) {
728
+ for (const step of phase.steps) {
729
+ records.push(
730
+ isEffect(step)
731
+ ? skippedRecord(phase.name, `effect:${step.receipt.name}`)
732
+ : isGate(step)
733
+ ? skippedRecord(phase.name, gateFn(step))
734
+ : skippedRecord(phase.name, step.fn, step.args),
735
+ );
736
+ }
737
+ }
738
+ return {
739
+ op: config.name,
740
+ records,
741
+ totalMs: Date.now() - start,
742
+ status: "gated",
743
+ startedAt,
744
+ gate: err.pending,
745
+ record: await settle(records, "gated", err.pending),
746
+ };
747
+ }
748
+
495
749
  if (err instanceof PhaseFailure) records.push(...err.records);
496
750
 
497
751
  // Compensation: run onFailure phases in reverse order (best-effort). Skipped
@@ -499,17 +753,31 @@ export async function runOpLocally(
499
753
  if (!signal?.aborted) {
500
754
  for (const phase of [...(config.onFailure ?? [])].reverse()) {
501
755
  try {
502
- records.push(...(await runPhase(phase, activities, profiles, resultsById, signal)));
756
+ records.push(...(await runPhase(phase, activities, profiles, resultsById, gates, signal)));
503
757
  } catch (compErr) {
504
- if (compErr instanceof PhaseFailure) records.push(...compErr.records);
758
+ if (compErr instanceof PhaseFailure || compErr instanceof GateStop) records.push(...compErr.records);
505
759
  }
506
760
  }
507
761
  }
508
762
 
509
- throw new OpRunFailure({ op: config.name, records, totalMs: Date.now() - start, ok: false });
763
+ throw new OpRunFailure({
764
+ op: config.name,
765
+ records,
766
+ totalMs: Date.now() - start,
767
+ status: "fail",
768
+ startedAt,
769
+ record: await settle(records, "fail"),
770
+ });
510
771
  }
511
772
 
512
- return { op: config.name, records, totalMs: Date.now() - start, ok: true };
773
+ return {
774
+ op: config.name,
775
+ records,
776
+ totalMs: Date.now() - start,
777
+ status: "ok",
778
+ startedAt,
779
+ record: await settle(records, "ok"),
780
+ };
513
781
  }
514
782
 
515
783
  function errMessage(err: unknown): string {