@intentius/chant 0.57.0 → 0.59.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 (558) 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 +36 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +26 -9
  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/carve-bridge.d.ts.map +1 -1
  34. package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
  35. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  36. package/dist/cli/commands/check-lexicon-examples.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 +13 -0
  44. package/dist/cli/commands/lint.d.ts.map +1 -1
  45. package/dist/cli/commands/onboard.d.ts +2 -0
  46. package/dist/cli/commands/onboard.d.ts.map +1 -1
  47. package/dist/cli/handlers/build.d.ts.map +1 -1
  48. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  49. package/dist/cli/handlers/operator.d.ts +82 -16
  50. package/dist/cli/handlers/operator.d.ts.map +1 -1
  51. package/dist/cli/handlers/run.d.ts +49 -42
  52. package/dist/cli/handlers/run.d.ts.map +1 -1
  53. package/dist/cli/main.d.ts +12 -1
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/mcp/op-tools.d.ts +16 -1
  56. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  58. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  59. package/dist/cli/plugins.d.ts +22 -0
  60. package/dist/cli/plugins.d.ts.map +1 -1
  61. package/dist/cli/registry.d.ts +40 -9
  62. package/dist/cli/registry.d.ts.map +1 -1
  63. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  64. package/dist/components/auto-release.d.ts +10 -20
  65. package/dist/components/auto-release.d.ts.map +1 -1
  66. package/dist/components/capability.d.ts +4 -4
  67. package/dist/components/cli-support.d.ts +24 -22
  68. package/dist/components/cli-support.d.ts.map +1 -1
  69. package/dist/components/component.d.ts +21 -6
  70. package/dist/components/component.d.ts.map +1 -1
  71. package/dist/components/driver-output.d.ts.map +1 -1
  72. package/dist/components/driver.d.ts +73 -37
  73. package/dist/components/driver.d.ts.map +1 -1
  74. package/dist/components/index.d.ts +1 -2
  75. package/dist/components/index.d.ts.map +1 -1
  76. package/dist/components/manifest-persistence.d.ts +1 -9
  77. package/dist/components/manifest-persistence.d.ts.map +1 -1
  78. package/dist/components/run-progress.d.ts +6 -6
  79. package/dist/components/run-progress.d.ts.map +1 -1
  80. package/dist/components/verbs/run-agent.d.ts +10 -22
  81. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  82. package/dist/components/verbs/verify.d.ts +2 -2
  83. package/dist/deep-observation.d.ts +17 -0
  84. package/dist/deep-observation.d.ts.map +1 -1
  85. package/dist/detectLexicon.d.ts +13 -0
  86. package/dist/detectLexicon.d.ts.map +1 -1
  87. package/dist/discovery/files.d.ts +4 -4
  88. package/dist/discovery/fold-import.d.ts.map +1 -1
  89. package/dist/discovery/param-deps.d.ts +33 -0
  90. package/dist/discovery/param-deps.d.ts.map +1 -1
  91. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  92. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  93. package/dist/fold/foldable-helpers.d.ts +5 -1
  94. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  95. package/dist/fold-provenance.d.ts +236 -0
  96. package/dist/fold-provenance.d.ts.map +1 -0
  97. package/dist/graph-ops.d.ts +1 -1
  98. package/dist/held-elsewhere.d.ts +85 -0
  99. package/dist/held-elsewhere.d.ts.map +1 -0
  100. package/dist/index.d.ts +3 -0
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/kubectl-context.d.ts +3 -3
  103. package/dist/lexicon-config.d.ts +4 -4
  104. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  105. package/dist/lexicon.d.ts +273 -16
  106. package/dist/lexicon.d.ts.map +1 -1
  107. package/dist/lifecycle/change-set.d.ts +13 -0
  108. package/dist/lifecycle/change-set.d.ts.map +1 -1
  109. package/dist/lifecycle/deep-diff.d.ts +133 -6
  110. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  111. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  112. package/dist/lifecycle/gate-ledger.d.ts +79 -3
  113. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  114. package/dist/lifecycle/index.d.ts +1 -0
  115. package/dist/lifecycle/index.d.ts.map +1 -1
  116. package/dist/lifecycle/release-ledger.d.ts +2 -2
  117. package/dist/lifecycle/run-ledger.d.ts +53 -0
  118. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  119. package/dist/lifecycle/symptoms.d.ts +3 -3
  120. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  121. package/dist/lint/config.d.ts +81 -2
  122. package/dist/lint/config.d.ts.map +1 -1
  123. package/dist/lint/post-synth.d.ts +55 -2
  124. package/dist/lint/post-synth.d.ts.map +1 -1
  125. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  126. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  127. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  128. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  129. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  130. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  131. package/dist/lint/rules/comp/index.d.ts +1 -1
  132. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  133. package/dist/lint/rules/op/index.d.ts +16 -0
  134. package/dist/lint/rules/op/index.d.ts.map +1 -0
  135. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  136. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  137. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  138. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  139. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  140. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  141. package/dist/lint/rules/op/support.d.ts +12 -0
  142. package/dist/lint/rules/op/support.d.ts.map +1 -0
  143. package/dist/lint/suppressions.d.ts +108 -0
  144. package/dist/lint/suppressions.d.ts.map +1 -0
  145. package/dist/live-endpoint.d.ts +1 -1
  146. package/dist/op/activities/activity-contracts.d.ts +54 -0
  147. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  148. package/dist/op/activities/apply.d.ts +262 -0
  149. package/dist/op/activities/apply.d.ts.map +1 -0
  150. package/dist/op/activities/build.d.ts +13 -0
  151. package/dist/op/activities/build.d.ts.map +1 -0
  152. package/dist/op/activities/converge.d.ts +110 -0
  153. package/dist/op/activities/converge.d.ts.map +1 -0
  154. package/dist/op/activities/env-teardown.d.ts +76 -0
  155. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  156. package/dist/op/activities/guard-validate.d.ts +64 -0
  157. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  158. package/dist/op/activities/http-check.d.ts +34 -0
  159. package/dist/op/activities/http-check.d.ts.map +1 -0
  160. package/dist/op/activities/index.d.ts +31 -0
  161. package/dist/op/activities/index.d.ts.map +1 -0
  162. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  163. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  164. package/dist/op/activities/lifecycle.d.ts +42 -0
  165. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  166. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  167. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  168. package/dist/op/activities/policy.d.ts +17 -0
  169. package/dist/op/activities/policy.d.ts.map +1 -0
  170. package/dist/op/activities/reconcile.d.ts +149 -0
  171. package/dist/op/activities/reconcile.d.ts.map +1 -0
  172. package/dist/op/activities/shell.d.ts +13 -0
  173. package/dist/op/activities/shell.d.ts.map +1 -0
  174. package/dist/op/activities/teardown.d.ts +10 -0
  175. package/dist/op/activities/teardown.d.ts.map +1 -0
  176. package/dist/op/activities/util.d.ts +7 -0
  177. package/dist/op/activities/util.d.ts.map +1 -0
  178. package/dist/op/activities/wait.d.ts +28 -0
  179. package/dist/op/activities/wait.d.ts.map +1 -0
  180. package/dist/op/activities/workflow-audit.d.ts +92 -0
  181. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  182. package/dist/op/activity-contract-registry.d.ts +76 -0
  183. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  184. package/dist/op/activity-contract.d.ts +25 -18
  185. package/dist/op/activity-contract.d.ts.map +1 -1
  186. package/dist/op/activity-failure.d.ts +28 -0
  187. package/dist/op/activity-failure.d.ts.map +1 -0
  188. package/dist/op/activity-profiles.d.ts +118 -0
  189. package/dist/op/activity-profiles.d.ts.map +1 -0
  190. package/dist/op/activity-registry.d.ts +22 -38
  191. package/dist/op/activity-registry.d.ts.map +1 -1
  192. package/dist/op/activity-runtime.d.ts +3 -14
  193. package/dist/op/activity-runtime.d.ts.map +1 -1
  194. package/dist/op/builders.d.ts +85 -30
  195. package/dist/op/builders.d.ts.map +1 -1
  196. package/dist/op/change-signal.d.ts +91 -0
  197. package/dist/op/change-signal.d.ts.map +1 -0
  198. package/dist/op/composites/apply-op.d.ts +118 -0
  199. package/dist/op/composites/apply-op.d.ts.map +1 -0
  200. package/dist/op/composites/converge-op.d.ts +120 -0
  201. package/dist/op/composites/converge-op.d.ts.map +1 -0
  202. package/dist/op/composites/index.d.ts +26 -0
  203. package/dist/op/composites/index.d.ts.map +1 -0
  204. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  205. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  206. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  207. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  208. package/dist/op/composites/reconcile-op.d.ts +57 -0
  209. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  210. package/dist/op/composites/watch-op.d.ts +60 -0
  211. package/dist/op/composites/watch-op.d.ts.map +1 -0
  212. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  213. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  214. package/dist/op/converge-rule.d.ts +12 -10
  215. package/dist/op/converge-rule.d.ts.map +1 -1
  216. package/dist/op/cron.d.ts +51 -0
  217. package/dist/op/cron.d.ts.map +1 -0
  218. package/dist/op/discover.d.ts +9 -0
  219. package/dist/op/discover.d.ts.map +1 -1
  220. package/dist/op/duration.d.ts +11 -0
  221. package/dist/op/duration.d.ts.map +1 -0
  222. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  223. package/dist/op/gate-name.d.ts +40 -0
  224. package/dist/op/gate-name.d.ts.map +1 -0
  225. package/dist/op/gate-summary.d.ts +43 -0
  226. package/dist/op/gate-summary.d.ts.map +1 -0
  227. package/dist/op/gate.d.ts +90 -0
  228. package/dist/op/gate.d.ts.map +1 -0
  229. package/dist/op/generate-pipeline.d.ts +9 -0
  230. package/dist/op/generate-pipeline.d.ts.map +1 -1
  231. package/dist/op/index.d.ts +29 -9
  232. package/dist/op/index.d.ts.map +1 -1
  233. package/dist/op/local-executor.d.ts +114 -28
  234. package/dist/op/local-executor.d.ts.map +1 -1
  235. package/dist/op/local-output.d.ts +11 -1
  236. package/dist/op/local-output.d.ts.map +1 -1
  237. package/dist/op/op-ir.d.ts +185 -0
  238. package/dist/op/op-ir.d.ts.map +1 -0
  239. package/dist/op/op-verb-class.d.ts +10 -2
  240. package/dist/op/op-verb-class.d.ts.map +1 -1
  241. package/dist/op/operator.d.ts +93 -5
  242. package/dist/op/operator.d.ts.map +1 -1
  243. package/dist/op/receipt-store.d.ts +1 -1
  244. package/dist/op/receipt-store.d.ts.map +1 -1
  245. package/dist/op/resource.d.ts +15 -1
  246. package/dist/op/resource.d.ts.map +1 -1
  247. package/dist/op/runtime.d.ts +200 -0
  248. package/dist/op/runtime.d.ts.map +1 -0
  249. package/dist/op/runtimes/local.d.ts +29 -0
  250. package/dist/op/runtimes/local.d.ts.map +1 -0
  251. package/dist/op/step-output-ref.d.ts +15 -13
  252. package/dist/op/step-output-ref.d.ts.map +1 -1
  253. package/dist/op/types.d.ts +96 -29
  254. package/dist/op/types.d.ts.map +1 -1
  255. package/dist/ownership.d.ts +1 -1
  256. package/dist/provenance.d.ts +21 -0
  257. package/dist/provenance.d.ts.map +1 -1
  258. package/dist/serializer-walker.d.ts.map +1 -1
  259. package/dist/terraform/bridge.d.ts +26 -7
  260. package/dist/terraform/bridge.d.ts.map +1 -1
  261. package/dist/terraform/carve-provider.d.ts +28 -2
  262. package/dist/terraform/carve-provider.d.ts.map +1 -1
  263. package/dist/terraform/data-source-shape.d.ts +77 -0
  264. package/dist/terraform/data-source-shape.d.ts.map +1 -0
  265. package/dist/terraform/graph.d.ts.map +1 -1
  266. package/dist/terraform/providers/kubernetes.d.ts +5 -0
  267. package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
  268. package/dist/terraform/tier-map.d.ts +16 -4
  269. package/dist/terraform/tier-map.d.ts.map +1 -1
  270. package/dist/terraform/types.d.ts +8 -0
  271. package/dist/terraform/types.d.ts.map +1 -1
  272. package/package.json +6 -1
  273. package/src/audit/catalog.test.ts +79 -3
  274. package/src/audit/catalog.ts +111 -13
  275. package/src/audit/core.test.ts +26 -0
  276. package/src/audit/core.ts +84 -18
  277. package/src/audit/discover.test.ts +39 -0
  278. package/src/audit/discover.ts +202 -18
  279. package/src/audit/lineage.ts +100 -0
  280. package/src/audit/prior-art.ts +153 -0
  281. package/src/audit/proof.test.ts +67 -0
  282. package/src/audit/proof.ts +70 -0
  283. package/src/audit/report-html.ts +9 -4
  284. package/src/audit/report-model.ts +16 -2
  285. package/src/audit/report.test.ts +33 -0
  286. package/src/audit/report.ts +5 -3
  287. package/src/audit/rules-doc.test.ts +34 -2
  288. package/src/audit/rules-doc.ts +82 -9
  289. package/src/audit/secrets.ts +15 -3
  290. package/src/audit/terraform-state.test.ts +144 -0
  291. package/src/audit/terraform-state.ts +131 -0
  292. package/src/build.ts +31 -1
  293. package/src/claimed-fields.test.ts +145 -0
  294. package/src/claimed-fields.ts +208 -0
  295. package/src/cli/build-params-cli.ts +2 -2
  296. package/src/cli/command-group.ts +14 -0
  297. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  298. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  299. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  300. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  301. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  302. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  303. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  304. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  305. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  306. package/src/cli/commands/audit.test.ts +191 -11
  307. package/src/cli/commands/audit.ts +78 -28
  308. package/src/cli/commands/build.test.ts +330 -9
  309. package/src/cli/commands/build.ts +170 -24
  310. package/src/cli/commands/carve-bridge.test.ts +237 -14
  311. package/src/cli/commands/carve-bridge.ts +21 -17
  312. package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
  313. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  314. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  315. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  316. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  317. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  318. package/src/cli/commands/init-lexicon.test.ts +97 -2
  319. package/src/cli/commands/init-lexicon.ts +4 -2
  320. package/src/cli/commands/init.test.ts +35 -1
  321. package/src/cli/commands/init.ts +14 -3
  322. package/src/cli/commands/lint.test.ts +194 -1
  323. package/src/cli/commands/lint.ts +205 -7
  324. package/src/cli/commands/onboard.test.ts +75 -0
  325. package/src/cli/commands/onboard.ts +54 -23
  326. package/src/cli/handlers/build.test.ts +81 -1
  327. package/src/cli/handlers/build.ts +15 -1
  328. package/src/cli/handlers/graph.test.ts +3 -3
  329. package/src/cli/handlers/lifecycle.test.ts +195 -12
  330. package/src/cli/handlers/lifecycle.ts +123 -6
  331. package/src/cli/handlers/misc.ts +1 -1
  332. package/src/cli/handlers/operator.test.ts +183 -11
  333. package/src/cli/handlers/operator.ts +341 -55
  334. package/src/cli/handlers/run.test.ts +568 -1295
  335. package/src/cli/handlers/run.ts +446 -912
  336. package/src/cli/main.test.ts +22 -16
  337. package/src/cli/main.ts +66 -33
  338. package/src/cli/mcp/op-tools.ts +147 -88
  339. package/src/cli/mcp/resource-handlers.ts +10 -37
  340. package/src/cli/mcp/resources/context.ts +11 -12
  341. package/src/cli/mcp/server.test.ts +25 -27
  342. package/src/cli/mcp/server.ts +2 -2
  343. package/src/cli/plugins.test.ts +68 -2
  344. package/src/cli/plugins.ts +39 -0
  345. package/src/cli/registry.ts +40 -9
  346. package/src/codegen/docs.ts +1 -1
  347. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  348. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  349. package/src/components/README.md +4 -3
  350. package/src/components/__fixtures__/neo4j-fanout.json +1 -1
  351. package/src/components/auto-release.test.ts +4 -20
  352. package/src/components/auto-release.ts +17 -42
  353. package/src/components/capability.ts +4 -4
  354. package/src/components/cli-support.test.ts +28 -12
  355. package/src/components/cli-support.ts +60 -47
  356. package/src/components/component-schema.test.ts +18 -2
  357. package/src/components/component.schema.json +18 -5
  358. package/src/components/component.test.ts +2 -2
  359. package/src/components/component.ts +27 -7
  360. package/src/components/config-defaults.test.ts +2 -2
  361. package/src/components/driver-output.ts +14 -2
  362. package/src/components/driver.test.ts +109 -20
  363. package/src/components/driver.ts +201 -84
  364. package/src/components/index.ts +1 -6
  365. package/src/components/manifest-persistence.test.ts +2 -20
  366. package/src/components/manifest-persistence.ts +4 -24
  367. package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
  368. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  369. package/src/components/run-progress.ts +6 -6
  370. package/src/components/verbs/run-agent.test.ts +19 -0
  371. package/src/components/verbs/run-agent.ts +12 -24
  372. package/src/components/verbs/verify.ts +2 -2
  373. package/src/deep-observation.test.ts +75 -0
  374. package/src/deep-observation.ts +48 -0
  375. package/src/detectLexicon.ts +18 -1
  376. package/src/discovery/collect.test.ts +3 -3
  377. package/src/discovery/files.ts +4 -4
  378. package/src/discovery/fold-import.test.ts +1 -1
  379. package/src/discovery/fold-import.ts +105 -2
  380. package/src/discovery/param-deps.test.ts +112 -1
  381. package/src/discovery/param-deps.ts +193 -0
  382. package/src/discovery/sandbox/bundle.ts +15 -1
  383. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  384. package/src/discovery/sandbox/config-wire.ts +2 -2
  385. package/src/fold/fold.test.ts +56 -0
  386. package/src/fold/foldable-helpers.ts +82 -2
  387. package/src/fold-provenance-exclusion.test.ts +186 -0
  388. package/src/fold-provenance.test.ts +495 -0
  389. package/src/fold-provenance.ts +441 -0
  390. package/src/graph-ops.test.ts +3 -3
  391. package/src/graph-ops.ts +1 -1
  392. package/src/held-elsewhere.test.ts +102 -0
  393. package/src/held-elsewhere.ts +114 -0
  394. package/src/index.ts +3 -0
  395. package/src/kubectl-context.ts +3 -3
  396. package/src/lexicon-config.test.ts +5 -5
  397. package/src/lexicon-config.ts +4 -4
  398. package/src/lexicon-plugin-helpers.ts +1 -1
  399. package/src/lexicon.ts +296 -16
  400. package/src/lifecycle/change-set.test.ts +49 -0
  401. package/src/lifecycle/change-set.ts +67 -0
  402. package/src/lifecycle/converge-ledger.ts +1 -1
  403. package/src/lifecycle/deep-diff.test.ts +267 -14
  404. package/src/lifecycle/deep-diff.ts +217 -14
  405. package/src/lifecycle/deep-observe.test.ts +3 -1
  406. package/src/lifecycle/deep-observe.ts +16 -6
  407. package/src/lifecycle/gate-ledger.test.ts +74 -2
  408. package/src/lifecycle/gate-ledger.ts +154 -36
  409. package/src/lifecycle/index.ts +1 -0
  410. package/src/lifecycle/release-ledger.ts +2 -2
  411. package/src/lifecycle/run-ledger.test.ts +268 -0
  412. package/src/lifecycle/run-ledger.ts +201 -0
  413. package/src/lifecycle/symptoms.ts +3 -3
  414. package/src/lint/config.test.ts +82 -1
  415. package/src/lint/config.ts +112 -3
  416. package/src/lint/pipeline-change-gate.test.ts +2 -2
  417. package/src/lint/policy.test.ts +1 -1
  418. package/src/lint/post-synth.test.ts +31 -0
  419. package/src/lint/post-synth.ts +54 -1
  420. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  421. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  422. package/src/lint/rules/comp/comp.test.ts +27 -1
  423. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +76 -0
  424. package/src/lint/rules/comp/index.ts +3 -3
  425. package/src/lint/rules/op/index.ts +23 -0
  426. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  427. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  428. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  429. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  430. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  431. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  432. package/src/lint/rules/op/support.ts +11 -0
  433. package/src/lint/suppressions.test.ts +163 -0
  434. package/src/lint/suppressions.ts +204 -0
  435. package/src/live-endpoint.ts +1 -1
  436. package/src/op/activities/activity-contracts.ts +66 -0
  437. package/src/op/activities/apply.test.ts +567 -0
  438. package/src/op/activities/apply.ts +662 -0
  439. package/src/op/activities/build.ts +26 -0
  440. package/src/op/activities/converge.test.ts +330 -0
  441. package/src/op/activities/converge.ts +449 -0
  442. package/src/op/activities/env-teardown.test.ts +187 -0
  443. package/src/op/activities/env-teardown.ts +171 -0
  444. package/src/op/activities/guard-validate.test.ts +139 -0
  445. package/src/op/activities/guard-validate.ts +223 -0
  446. package/src/op/activities/http-check.test.ts +48 -0
  447. package/src/op/activities/http-check.ts +55 -0
  448. package/src/op/activities/index.ts +119 -0
  449. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  450. package/src/op/activities/lexicon-upgrade.ts +673 -0
  451. package/src/op/activities/lifecycle.ts +85 -0
  452. package/src/op/activities/pipeline-audit.test.ts +67 -0
  453. package/src/op/activities/pipeline-audit.ts +201 -0
  454. package/src/op/activities/policy.test.ts +19 -0
  455. package/src/op/activities/policy.ts +37 -0
  456. package/src/op/activities/reconcile.test.ts +161 -0
  457. package/src/op/activities/reconcile.ts +354 -0
  458. package/src/op/activities/shell.ts +26 -0
  459. package/src/op/activities/teardown.ts +22 -0
  460. package/src/op/activities/util.ts +6 -0
  461. package/src/op/activities/wait.ts +69 -0
  462. package/src/op/activities/workflow-audit.test.ts +90 -0
  463. package/src/op/activities/workflow-audit.ts +203 -0
  464. package/src/op/activity-contract-registry.test.ts +98 -0
  465. package/src/op/activity-contract-registry.ts +114 -0
  466. package/src/op/activity-contract.test.ts +1 -1
  467. package/src/op/activity-contract.ts +37 -33
  468. package/src/op/activity-failure.ts +34 -0
  469. package/src/op/activity-profiles.test.ts +56 -0
  470. package/src/op/activity-profiles.ts +116 -0
  471. package/src/op/activity-registry.test.ts +37 -28
  472. package/src/op/activity-registry.ts +27 -56
  473. package/src/op/activity-runtime.ts +3 -47
  474. package/src/op/builders-exports.test.ts +0 -49
  475. package/src/op/builders-typed.test.ts +164 -0
  476. package/src/op/builders.ts +125 -41
  477. package/src/op/change-signal.test.ts +117 -0
  478. package/src/op/change-signal.ts +169 -0
  479. package/src/op/composites/apply-op.ts +218 -0
  480. package/src/op/composites/composites.test.ts +379 -0
  481. package/src/op/composites/converge-op.test.ts +168 -0
  482. package/src/op/composites/converge-op.ts +214 -0
  483. package/src/op/composites/index.ts +26 -0
  484. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  485. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  486. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  487. package/src/op/composites/pipeline-audit-op.ts +78 -0
  488. package/src/op/composites/reconcile-op.test.ts +51 -0
  489. package/src/op/composites/reconcile-op.ts +110 -0
  490. package/src/op/composites/watch-op.ts +108 -0
  491. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  492. package/src/op/composites/workflow-audit-op.ts +88 -0
  493. package/src/op/converge-rule.ts +12 -10
  494. package/src/op/cron.test.ts +128 -0
  495. package/src/op/cron.ts +149 -0
  496. package/src/op/discover-root.test.ts +105 -1
  497. package/src/op/discover.test.ts +6 -2
  498. package/src/op/discover.ts +84 -16
  499. package/src/op/duration.ts +21 -0
  500. package/src/op/effect-step.test.ts +29 -14
  501. package/src/op/emulator-lifecycle.ts +1 -2
  502. package/src/op/gate-name.test.ts +65 -0
  503. package/src/op/gate-name.ts +60 -0
  504. package/src/op/gate-summary.ts +84 -0
  505. package/src/op/gate.ts +155 -0
  506. package/src/op/generate-pipeline.test.ts +65 -3
  507. package/src/op/generate-pipeline.ts +19 -2
  508. package/src/op/index.ts +48 -8
  509. package/src/op/local-executor.test.ts +178 -34
  510. package/src/op/local-executor.ts +371 -102
  511. package/src/op/local-output.test.ts +95 -11
  512. package/src/op/local-output.ts +33 -4
  513. package/src/op/op-ir.test.ts +292 -0
  514. package/src/op/op-ir.ts +419 -0
  515. package/src/op/op-verb-class.test.ts +2 -2
  516. package/src/op/op-verb-class.ts +19 -5
  517. package/src/op/op.test.ts +28 -7
  518. package/src/op/operator.test.ts +543 -6
  519. package/src/op/operator.ts +203 -20
  520. package/src/op/receipt-store.ts +1 -1
  521. package/src/op/resource.ts +20 -2
  522. package/src/op/runtime.ts +203 -0
  523. package/src/op/runtimes/local.test.ts +181 -0
  524. package/src/op/runtimes/local.ts +212 -0
  525. package/src/op/step-output-ref.ts +15 -13
  526. package/src/op/types.ts +106 -26
  527. package/src/ownership.ts +1 -1
  528. package/src/provenance.test.ts +6 -0
  529. package/src/provenance.ts +21 -1
  530. package/src/serializer-walker.test.ts +42 -0
  531. package/src/serializer-walker.ts +21 -2
  532. package/src/terraform/aws-resources.test.ts +13 -4
  533. package/src/terraform/bridge.test.ts +22 -9
  534. package/src/terraform/bridge.ts +89 -28
  535. package/src/terraform/carve-provider.ts +38 -2
  536. package/src/terraform/data-source-shape.ts +95 -0
  537. package/src/terraform/graph.ts +35 -7
  538. package/src/terraform/providers/kubernetes.ts +48 -2
  539. package/src/terraform/tier-map.ts +21 -6
  540. package/src/terraform/types.ts +8 -0
  541. package/dist/cli/handlers/op-progress.d.ts +0 -90
  542. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  543. package/dist/cli/handlers/run-client.d.ts +0 -127
  544. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  545. package/dist/cli/handlers/run-report.d.ts +0 -5
  546. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  547. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  548. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  549. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  550. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  551. package/src/cli/handlers/op-progress.test.ts +0 -268
  552. package/src/cli/handlers/op-progress.ts +0 -248
  553. package/src/cli/handlers/run-client.test.ts +0 -82
  554. package/src/cli/handlers/run-client.ts +0 -219
  555. package/src/cli/handlers/run-report.test.ts +0 -62
  556. package/src/cli/handlers/run-report.ts +0 -122
  557. package/src/components/temporal-codegen-loader.ts +0 -41
  558. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -1,156 +1,237 @@
1
- import { resolve, join, dirname } from "node:path";
2
- import { existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
3
- import { createConnection } from "node:net";
4
- import { spawn as spawnChild, type ChildProcess } from "node:child_process";
1
+ import { resolve, dirname } from "node:path";
2
+ import { writeFileSync, mkdirSync, readFileSync } from "node:fs";
5
3
  import { loadChantConfig, resolveAutoReleaseDisabled, type ChantConfig } from "../../config";
6
4
  import { discoverOps } from "../../op/discover";
7
5
  import type { OpConfig } from "../../op/types";
8
6
  import { loadActivities, loadProfiles } from "../../op/activity-registry";
9
- import { runOpLocally, findGate, findPolicyGateStep, LocalGateUnsupportedError, OpRunFailure, type StepRecord } from "../../op/local-executor";
7
+ import { runOpLocally, findPolicyGateStep, OpRunFailure, type StepRecord } from "../../op/local-executor";
8
+ import { approveCommand } from "../../op/gate";
9
+ import { writeGatedRunSummary, type GatedRunSummary } from "../../op/gate-summary";
10
+ import { createLocalOpRuntime } from "../../op/runtimes/local";
11
+ import type { OpRuntimeProvider, OpRunStatus } from "../../op/runtime";
10
12
  import { renderHuman, renderJson } from "../../op/local-output";
13
+ import { loadPlugins } from "../plugins";
14
+ import { recordGateApproval } from "./operator";
11
15
  import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
12
16
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
13
17
  import type { CommandContext } from "../registry";
14
- import {
15
- loadTemporalClient,
16
- connectionOptions,
17
- resolveWorkflowId,
18
- resolveProfile,
19
- fetchNormalizedHistory,
20
- type WorkflowHandleRaw,
21
- type WorkflowExecutionDescription,
22
- type WorkflowHistoryRaw,
23
- } from "./run-client";
24
- import { generateReport, writeReport } from "./run-report";
25
- import { extractIndexedStepRecords, stepRecordEmitter, countActivities, queryGateState } from "./op-progress";
26
- import { runComponents, resolveComponentTargets, findComponentGate, listComponents } from "../../components/cli-support";
27
18
  import { renderDriverHuman, renderDriverJson } from "../../components/driver-output";
28
19
  import { ndjsonProgressSink } from "../../components/run-progress";
29
- import { loadComponentTemporalCodegen } from "../../components/temporal-codegen-loader";
30
- import { applyConfigDefaults } from "../../components/config-defaults";
31
- import { maybeRecordAutoRelease, extractRunDigestFromPhaseOutputs } from "../../components/auto-release";
32
- import { maybePersistBuildManifest, extractRunManifestFromPhaseOutputs } from "../../components/manifest-persistence";
20
+ import { maybeRecordAutoRelease } from "../../components/auto-release";
21
+ import { maybePersistBuildManifest } from "../../components/manifest-persistence";
33
22
  import type { DriverComponentResult } from "../../components/driver";
34
- import type { BuildParamProvenance } from "../../provenance";
35
23
 
36
- function kebabToCamel(s: string): string {
37
- return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
24
+ /**
25
+ * The exit code a run that stopped at an unapproved gate uses (#2119).
26
+ * Deliberately not 1: a gate is a standing fact waiting on a human, and a CI
27
+ * job that treats it as a failure would page someone for a decision nobody has
28
+ * made yet.
29
+ */
30
+ export const GATED_EXIT_CODE = 3;
31
+
32
+ /**
33
+ * The exit code this invocation gives a gated run — {@link GATED_EXIT_CODE}
34
+ * unless `--gated-exit <code>` asked for another one (#2243).
35
+ *
36
+ * The mapping lives here rather than in a shell wrapper in every generated
37
+ * pipeline, so one rule covers every forge: GitHub Actions has no neutral
38
+ * conclusion for a `run:` step, so a push-to-main apply that stops at its gate
39
+ * paints the branch red on every merge until someone approves. `--gated-exit
40
+ * 0` is how the job that knows a pending approval is not a failure says so.
41
+ *
42
+ * Only the gated outcome is remapped. A failed run still returns 1, so the
43
+ * flag can never hide a broken apply. Returns `undefined` after printing the
44
+ * refusal when the value is not a process exit status.
45
+ */
46
+ function resolveGatedExitCode(ctx: CommandContext): number | undefined {
47
+ const raw = ctx.args.gatedExit;
48
+ if (raw === undefined) return GATED_EXIT_CODE;
49
+ if (!Number.isInteger(raw) || raw < 0 || raw > 255) {
50
+ console.error(formatError({
51
+ message: "--gated-exit expects a whole number from 0 to 255",
52
+ hint: `Got ${Number.isNaN(raw) ? "a non-numeric value" : String(raw)}. ` +
53
+ `Pass --gated-exit 0 to make a run that stopped at a gate a success for CI; ` +
54
+ `omit the flag for the default ${GATED_EXIT_CODE}.`,
55
+ }));
56
+ return undefined;
57
+ }
58
+ return raw;
38
59
  }
39
60
 
40
- function workflowFnName(opName: string): string {
41
- return kebabToCamel(opName) + "Workflow";
61
+ /**
62
+ * What a gated run leaves behind for CI, beyond the stderr block the renderers
63
+ * already print (#2243): the same gate, approve command and ledger path
64
+ * appended to whatever file `GITHUB_STEP_SUMMARY` names, so the run page says
65
+ * what is pending without anyone opening the log.
66
+ *
67
+ * Also says on stderr that the exit code was remapped, when it was. A job that
68
+ * passes `--gated-exit 0` reports success, and the one line that explains why
69
+ * a zero-exit run applied nothing belongs next to the gate itself.
70
+ */
71
+ function reportGatedRun(summary: GatedRunSummary, exitCode: number): void {
72
+ writeGatedRunSummary(summary);
73
+ if (exitCode !== GATED_EXIT_CODE) {
74
+ console.error(formatInfo(
75
+ `gated: exiting ${exitCode} because --gated-exit asked for it. Nothing after the gate ran.`,
76
+ ));
77
+ }
42
78
  }
43
79
 
44
80
  /**
45
- * Guard for Temporal-only commands (run list/status/log/signal/cancel, --report).
46
- * These query durable run state that only exists under Temporal. Returns true
47
- * when `--temporal` was passed; otherwise prints an actionable message and the
48
- * caller should return non-zero.
81
+ * `run list/status/log/cancel --components` reported a component's *durable*
82
+ * run state — a run that outlives the CLI process and can be queried, signalled
83
+ * or cancelled afterwards (#589). #2116 removed the runtime that provided it.
84
+ * The local driver runs a component to completion inside this process, so there
85
+ * is no separate run left to ask about once the command returns.
86
+ *
87
+ * Returns 1; the caller returns it straight back.
49
88
  */
50
- function requireTemporalMode(ctx: CommandContext, what: string): boolean {
51
- if (ctx.args.temporal) return true;
89
+ function refuseDurableComponentSubcommand(what: string, hint: string): number {
52
90
  console.error(formatError({
53
- message: `\`${what}\` is not available in local mode`,
54
- hint: "pass --temporal or configure a profile",
91
+ message: `\`${what}\` read a component's durable run state, which #2116 removed`,
92
+ hint,
55
93
  }));
56
- return false;
94
+ return 1;
57
95
  }
58
96
 
59
- export async function makeTemporalClient(profileName: string | undefined, projectPath: string) {
60
- const { config } = await loadChantConfig(projectPath);
61
- const profile = resolveProfile(config as Record<string, unknown>, profileName);
62
- const { Connection, Client } = await loadTemporalClient();
63
- const connection = await Connection.connect(connectionOptions(profile));
64
- const client = new Client({ connection, namespace: profile.namespace });
65
- return { client, profile, config };
66
- }
97
+ // ── The runtime seam (#2121) ────────────────────────────────────────────────
67
98
 
68
99
  /**
69
- * Register the Keyword search attributes the generated workflow upserts
70
- * (`OpName`, `Phase`, plus any op-declared) on the namespace, so the first
71
- * workflow task does not fail with `BadSearchAttributes`. Registered one at a
72
- * time so an already-present attribute (`ALREADY_EXISTS`) does not block the
73
- * rest. Only used for the autoStart dev server — a real server (autoStart=false)
74
- * is expected to have these pre-registered (it may reject registration).
100
+ * Resolve the one runtime this invocation talks to.
101
+ *
102
+ * `--on <name>` picks the named lexicon's `opRuntime` (`../../lexicon.ts`);
103
+ * without it, core's built-in `local` provider runs the Op in this process.
104
+ * Every `chant run` subcommand goes through whichever comes back, so there is
105
+ * one dispatch path rather than a branch per runtime.
106
+ *
107
+ * A lexicon already loaded into the command context wins the lookup — that is
108
+ * how a test hands in a stub, and how a command that loaded plugins for its
109
+ * own reasons avoids loading them twice. Otherwise the project's configured
110
+ * lexicons are loaded on demand: `chant run` is not a `requiresPlugins`
111
+ * command, and a plain local run must not start paying for plugin resolution.
112
+ *
113
+ * Returns `undefined` after printing an actionable error — an unconfigured
114
+ * name is answered with the configured list, a configured lexicon that hosts
115
+ * nothing is named outright.
75
116
  */
76
- async function ensureSearchAttributes(client: unknown, namespace: string, names: string[]): Promise<void> {
77
- const operatorService = (
78
- client as { connection?: { operatorService?: { addSearchAttributes?: (r: unknown) => Promise<unknown> } } }
79
- ).connection?.operatorService;
80
- if (!operatorService?.addSearchAttributes) return;
81
- const KEYWORD = 2; // Temporal IndexedValueType KEYWORD
82
- for (const name of names) {
83
- try {
84
- await operatorService.addSearchAttributes({ namespace, searchAttributes: { [name]: KEYWORD } });
85
- } catch (err) {
86
- const msg = err instanceof Error ? err.message : String(err);
87
- if (!/already|exists/i.test(msg)) {
88
- console.error(formatWarning({ message: `Could not register search attribute "${name}": ${msg}` }));
89
- }
90
- }
117
+ async function resolveOpRuntime(ctx: CommandContext): Promise<OpRuntimeProvider | undefined> {
118
+ const on = ctx.args.on;
119
+ if (!on || on === "local") return createLocalOpRuntime({ projectPath: resolve(".") });
120
+
121
+ // Read the configured list straight from `chant.config.ts` rather than
122
+ // through `resolveProjectLexicons`: `--on` names a *configured* lexicon, and
123
+ // that helper's fallback is a source scan of the whole project, which is a
124
+ // long wait to be told a name is wrong.
125
+ let configured: string[] = [];
126
+ try {
127
+ configured = (await loadChantConfig(resolve("."))).config.lexicons ?? [];
128
+ } catch {
129
+ // No/unreadable chant.config.ts — the error below says so by listing nothing.
130
+ }
131
+
132
+ let plugin = ctx.plugins.find((p) => p.name === on);
133
+ if (!plugin && configured.includes(on)) {
134
+ plugin = (await loadPlugins([on]).catch(() => []))[0];
135
+ }
136
+
137
+ if (!plugin) {
138
+ const known = [...new Set([...ctx.plugins.map((p) => p.name), ...configured])];
139
+ console.error(formatError({
140
+ message: `--on ${on}: "${on}" is not a configured lexicon`,
141
+ hint: known.length > 0
142
+ ? `Configured lexicons: ${known.join(", ")}. Omit --on to run on the built-in local runtime.`
143
+ : "chant.config.ts configures no lexicons. Omit --on to run on the built-in local runtime.",
144
+ }));
145
+ return undefined;
91
146
  }
147
+
148
+ if (!plugin.opRuntime) {
149
+ console.error(formatError({
150
+ message: `--on ${on}: lexicon "${on}" does not host Op runs`,
151
+ hint: "It declares no opRuntime. Omit --on to run on the built-in local runtime.",
152
+ }));
153
+ return undefined;
154
+ }
155
+
156
+ return plugin.opRuntime;
157
+ }
158
+
159
+ /** An ISO-8601 instant trimmed to what a table cell has room for. */
160
+ function shortInstant(iso: string | undefined): string {
161
+ return iso ? iso.slice(0, 19).replace("T", " ") : "—";
162
+ }
163
+
164
+ /** Print what a runtime reports for a settled or in-flight run. */
165
+ function renderRuntimeStatus(label: string, name: string, runtime: string, status: OpRunStatus): void {
166
+ console.log(formatBold(`${label}: ${name}`));
167
+ console.log(` Runtime : ${runtime}`);
168
+ console.log(` Run ID : ${status.runId}`);
169
+ console.log(` State : ${status.state}`);
170
+ console.log(` Started : ${shortInstant(status.startedAt)}`);
171
+ if (status.endedAt) console.log(` Ended : ${shortInstant(status.endedAt)}`);
172
+ if (status.records) {
173
+ const settled = status.records.filter((r) => r.status !== "skipped").length;
174
+ console.log(` Steps : ${settled}/${status.records.length} settled`);
175
+ }
176
+ if (status.gate) {
177
+ console.log(` Gate : ${status.gate.name} (pending since ${status.gate.since})`);
178
+ }
179
+ if (status.error) console.log(` Error : ${status.error}`);
92
180
  }
93
181
 
94
182
  // ── chant run list ──────────────────────────────────���─────────────────────────
95
183
 
96
184
  export async function runOpList(ctx: CommandContext): Promise<number> {
97
- if (ctx.args.components) return runComponentsList(ctx);
185
+ if (ctx.args.components) return runComponentsList();
186
+ return runOpListOnRuntime(ctx);
187
+ }
98
188
 
99
- if (!requireTemporalMode(ctx, "chant run list")) return 1;
100
- const { ops, errors } = await discoverOps();
189
+ /**
190
+ * `chant run list` on the resolved runtime (#2121) — discover every Op, ask
191
+ * the runtime what it knows about each, print one row per Op. The runtime
192
+ * answers for all of them in one call (`list`), so a hosted runtime can do it
193
+ * in one round trip instead of N.
194
+ *
195
+ * An Op the runtime has no run for prints with no state annotation.
196
+ */
197
+ async function runOpListOnRuntime(ctx: CommandContext): Promise<number> {
198
+ const runtime = await resolveOpRuntime(ctx);
199
+ if (!runtime) return 1;
101
200
 
102
- for (const err of errors) {
103
- console.error(formatError({ message: err }));
104
- }
201
+ const { ops, errors } = await discoverOps();
202
+ for (const err of errors) console.error(formatError({ message: err }));
105
203
 
106
204
  if (ops.size === 0) {
107
205
  console.error(formatWarning({ message: "No Op definitions found (*.op.ts)" }));
108
206
  return 0;
109
207
  }
110
208
 
209
+ let states: Map<string, OpRunStatus | undefined>;
210
+ try {
211
+ states = await runtime.list([...ops.values()].map((d) => d.config));
212
+ } catch (err) {
213
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
214
+ return 1;
215
+ }
216
+
111
217
  console.log(
112
- "NAME".padEnd(22) +
218
+ "NAME".padEnd(26) +
113
219
  "PHASES".padEnd(8) +
114
- "TASK-QUEUE".padEnd(20) +
115
220
  "DEPENDS".padEnd(20) +
116
221
  "OVERVIEW",
117
222
  );
118
223
 
119
- let runStatus: Map<string, string> | undefined;
120
- try {
121
- const projectPath = resolve(".");
122
- const { config } = await loadChantConfig(projectPath);
123
- const profile = resolveProfile(config as Record<string, unknown>, ctx.args.profile);
124
- const { Connection, Client } = await loadTemporalClient();
125
- const connection = await Connection.connect(connectionOptions(profile));
126
- const client = new Client({ connection, namespace: profile.namespace });
127
- runStatus = new Map();
128
- for (const [name] of ops) {
129
- try {
130
- const desc = await client.workflow.getHandle(resolveWorkflowId(name)).describe();
131
- runStatus.set(name, desc.status.name);
132
- } catch {
133
- runStatus.set(name, "—");
134
- }
135
- }
136
- } catch {
137
- // Temporal not available — degrade gracefully
138
- }
139
-
140
224
  for (const [name, { config }] of ops) {
141
225
  const phases = String(config.phases.length);
142
- const tq = config.taskQueue ?? config.name;
143
226
  const deps = config.depends?.join(",") ?? "—";
144
227
  const overview = config.overview.length > 36
145
228
  ? config.overview.slice(0, 33) + "..."
146
229
  : config.overview;
147
- const status = runStatus?.get(name);
148
- const statusStr = status ? ` [${status}]` : "";
230
+ const state = states.get(name)?.state;
149
231
 
150
232
  console.log(
151
- (name + statusStr).padEnd(22) +
233
+ (name + (state ? ` [${state}]` : "")).padEnd(26) +
152
234
  phases.padEnd(8) +
153
- tq.padEnd(20) +
154
235
  deps.padEnd(20) +
155
236
  overview,
156
237
  );
@@ -160,89 +241,21 @@ export async function runOpList(ctx: CommandContext): Promise<number> {
160
241
  }
161
242
 
162
243
  /**
163
- * `chant run list --components` (#599) — the component counterpart of
164
- * `runOpList` above, mirrored shape-for-shape: discover, print a header,
165
- * best-effort annotate with Temporal run status, print one row per entry.
166
- * Kept behind the same `requireTemporalMode` gate as the Op path — the whole
167
- * point of this subcommand is the run-status annotation, and components have
168
- * no local notion of a "run" to list either (mirrors the "Temporal-only
169
- * subcommand guards" test coverage for the Op list/status/log/signal/cancel
170
- * set).
171
- *
172
- * Discovery + column shape reuses `listComponents` (../../components/cli-
173
- * support.ts), the same helper `chant list --components` already uses, so
174
- * archetype inference/field projection isn't duplicated here — this function
175
- * only adds the Temporal status column. Status lookups use
176
- * `componentWorkflowId`, the id space `runOpSignal`/`runOpCancel` established
177
- * for components (#589) — kept distinct from `resolveWorkflowId`'s Op id
178
- * space so an Op and a component can share a name.
244
+ * `chant run list --components` (#599) listed discovered components annotated
245
+ * with each one's durable run status. Discovery alone is what `chant list
246
+ * --components` already prints, so with the status column gone (#2116) this
247
+ * subcommand has nothing of its own left to say.
179
248
  */
180
- async function runComponentsList(ctx: CommandContext): Promise<number> {
181
- if (!requireTemporalMode(ctx, "chant run list --components")) return 1;
182
-
183
- const projectPath = resolve(".");
184
- const result = await listComponents(projectPath, ctx.args.sandbox);
185
-
186
- if (!result.success) {
187
- for (const err of result.errors) console.error(formatError({ message: err }));
188
- return 1;
189
- }
190
-
191
- if (result.components.length === 0) {
192
- console.error(formatWarning({ message: "No component definitions found (*.component.ts)" }));
193
- return 0;
194
- }
195
-
196
- console.log(
197
- "NAME".padEnd(22) +
198
- "ARCHETYPE".padEnd(18) +
199
- "PHASES".padEnd(8) +
200
- "DEPENDS".padEnd(20) +
201
- "FILE",
202
- );
203
-
204
- let runStatus: Map<string, string> | undefined;
205
- try {
206
- const { config } = await loadChantConfig(projectPath);
207
- const profile = resolveProfile(config as Record<string, unknown>, ctx.args.profile);
208
- const { Connection, Client } = await loadTemporalClient();
209
- const connection = await Connection.connect(connectionOptions(profile));
210
- const client = new Client({ connection, namespace: profile.namespace });
211
- runStatus = new Map();
212
- for (const { name } of result.components) {
213
- try {
214
- const desc = await client.workflow.getHandle(componentWorkflowId(name)).describe();
215
- runStatus.set(name, desc.status.name);
216
- } catch {
217
- runStatus.set(name, "—");
218
- }
219
- }
220
- } catch {
221
- // Temporal not available — degrade gracefully
222
- }
223
-
224
- for (const c of result.components) {
225
- const phases = String(c.phases.length);
226
- const deps = c.dependsOn.join(",") || "—";
227
- const status = runStatus?.get(c.name);
228
- const statusStr = status ? ` [${status}]` : "";
229
-
230
- console.log(
231
- (c.name + statusStr).padEnd(22) +
232
- c.archetype.padEnd(18) +
233
- phases.padEnd(8) +
234
- deps.padEnd(20) +
235
- c.filePath,
236
- );
237
- }
238
-
239
- return 0;
249
+ function runComponentsList(): Promise<number> {
250
+ return Promise.resolve(refuseDurableComponentSubcommand(
251
+ "chant run list --components",
252
+ "Run `chant list --components` to see discovered components.",
253
+ ));
240
254
  }
241
255
 
242
256
  // ── chant run status <name> ───────────────────────────────────────────────────
243
257
 
244
258
  export async function runOpStatus(ctx: CommandContext): Promise<number> {
245
- if (!requireTemporalMode(ctx, "chant run status")) return 1;
246
259
  const name = ctx.args.extraPositional;
247
260
  if (!name) {
248
261
  const label = ctx.args.components ? "Component" : "Op";
@@ -250,84 +263,44 @@ export async function runOpStatus(ctx: CommandContext): Promise<number> {
250
263
  return 1;
251
264
  }
252
265
 
253
- if (ctx.args.components) return runComponentStatus(ctx, name);
254
-
255
- const projectPath = resolve(".");
256
- let client, desc: WorkflowExecutionDescription, history: WorkflowHistoryRaw, handle: WorkflowHandleRaw;
257
- try {
258
- ({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
259
- handle = client.workflow.getHandle(resolveWorkflowId(name));
260
- desc = await handle.describe();
261
- history = await fetchNormalizedHistory(handle);
262
- } catch (err) {
263
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
264
- return 1;
265
- }
266
-
267
- console.log(formatBold(`Op: ${name}`));
268
- console.log(` Workflow ID : ${desc.workflowId}`);
269
- console.log(` Run ID : ${desc.runId}`);
270
- console.log(` Status : ${desc.status.name}`);
271
- console.log(` Task Queue : ${desc.taskQueue}`);
272
- console.log(` Started : ${desc.startTime.toISOString()}`);
273
- if (desc.closeTime) console.log(` Closed : ${desc.closeTime.toISOString()}`);
274
-
275
- const { completed, scheduled } = countActivities(history);
276
- if (scheduled > 0) {
277
- console.log(` Activities : ${completed}/${scheduled} completed`);
278
- }
279
-
280
- // Queryable gate state (#1676) — undefined means the query itself failed
281
- // (an ungated Op never registers a "gateState" handler), so print nothing;
282
- // null means the query answered "no gate pending" — also nothing to print.
283
- const gate = await queryGateState(handle);
284
- if (gate) {
285
- console.log(` Gate : ${gate.signalName}${gate.description ? ` — ${gate.description}` : ""} (waiting since ${gate.since})`);
266
+ if (ctx.args.components) {
267
+ return refuseDurableComponentSubcommand(
268
+ "chant run status --components",
269
+ "Run `chant components status` for a component's release and build state.",
270
+ );
286
271
  }
287
-
288
- return 0;
272
+ return runOpStatusOnRuntime(ctx, name);
289
273
  }
290
274
 
291
275
  /**
292
- * `chant run status <name> --components` (#599) — the component counterpart
293
- * of `runOpStatus` above. Identical describe+history rendering; the only
294
- * difference is the workflow id (`componentWorkflowId`, not
295
- * `resolveWorkflowId`) and the "Component:" label, mirroring how
296
- * `runOpSignal`/`runOpCancel` already distinguish the two id spaces (#589).
276
+ * `chant run status <name>` on the resolved runtime (#2121). A runtime with
277
+ * no record of the Op is not an error: it prints one line saying so and exits
278
+ * 0, because "this runtime has never run it" is a true answer to the question.
297
279
  */
298
- async function runComponentStatus(ctx: CommandContext, name: string): Promise<number> {
299
- const projectPath = resolve(".");
300
- let client, desc: WorkflowExecutionDescription, history: WorkflowHistoryRaw;
280
+ async function runOpStatusOnRuntime(ctx: CommandContext, name: string): Promise<number> {
281
+ const runtime = await resolveOpRuntime(ctx);
282
+ if (!runtime) return 1;
283
+
284
+ let status: OpRunStatus | undefined;
301
285
  try {
302
- ({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
303
- const handle = client.workflow.getHandle(componentWorkflowId(name));
304
- desc = await handle.describe();
305
- history = await fetchNormalizedHistory(handle);
286
+ status = await runtime.status(name);
306
287
  } catch (err) {
307
288
  console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
308
289
  return 1;
309
290
  }
310
291
 
311
- console.log(formatBold(`Component: ${name}`));
312
- console.log(` Workflow ID : ${desc.workflowId}`);
313
- console.log(` Run ID : ${desc.runId}`);
314
- console.log(` Status : ${desc.status.name}`);
315
- console.log(` Task Queue : ${desc.taskQueue}`);
316
- console.log(` Started : ${desc.startTime.toISOString()}`);
317
- if (desc.closeTime) console.log(` Closed : ${desc.closeTime.toISOString()}`);
318
-
319
- const { completed, scheduled } = countActivities(history);
320
- if (scheduled > 0) {
321
- console.log(` Activities : ${completed}/${scheduled} completed`);
292
+ if (!status) {
293
+ console.error(formatInfo(`No run of Op "${name}" is recorded on the "${runtime.name}" runtime.`));
294
+ return 0;
322
295
  }
323
296
 
297
+ renderRuntimeStatus("Op", name, runtime.name, status);
324
298
  return 0;
325
299
  }
326
300
 
327
301
  // ── chant run log <name> ──────────────────────────────────────────────────────
328
302
 
329
303
  export async function runOpLog(ctx: CommandContext): Promise<number> {
330
- if (!requireTemporalMode(ctx, "chant run log")) return 1;
331
304
  const name = ctx.args.extraPositional;
332
305
  if (!name) {
333
306
  const label = ctx.args.components ? "Component" : "Op";
@@ -335,151 +308,128 @@ export async function runOpLog(ctx: CommandContext): Promise<number> {
335
308
  return 1;
336
309
  }
337
310
 
338
- if (ctx.args.components) return runComponentLog(ctx, name);
339
-
340
- const projectPath = resolve(".");
341
- let client;
342
- try {
343
- ({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
344
- } catch (err) {
345
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
346
- return 1;
347
- }
348
-
349
- console.log(
350
- "RUN-ID".padEnd(36) +
351
- "STATUS".padEnd(16) +
352
- "STARTED".padEnd(26) +
353
- "CLOSED",
354
- );
355
-
356
- try {
357
- const fnName = workflowFnName(name);
358
- for await (const run of client.workflow.list({ query: `WorkflowType = "${fnName}"` })) {
359
- const start = run.startTime.toISOString().slice(0, 19).replace("T", " ");
360
- const close = run.closeTime ? run.closeTime.toISOString().slice(0, 19).replace("T", " ") : "—";
361
- console.log(
362
- run.runId.padEnd(36) +
363
- run.status.name.padEnd(16) +
364
- start.padEnd(26) +
365
- close,
366
- );
367
- }
368
- } catch (err) {
369
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
370
- return 1;
311
+ if (ctx.args.components) {
312
+ return refuseDurableComponentSubcommand(
313
+ "chant run log --components",
314
+ "Run `chant components status` for a component's recorded releases.",
315
+ );
371
316
  }
372
-
373
- return 0;
317
+ return runOpLogOnRuntime(ctx, name);
374
318
  }
375
319
 
376
320
  /**
377
- * `chant run log <name> --components` (#599) — the component counterpart of
378
- * `runOpLog` above. Same table shape; the `WorkflowType` query needs the
379
- * component's generated workflow function name rather than
380
- * `workflowFnName`'s Op convention, so this loads the Temporal component
381
- * codegen module (`../../components/temporal-codegen-loader.ts`, the same
382
- * loader `runComponentTemporal` already uses) purely for its
383
- * `componentWorkflowFnName` helper — no compilation happens here, `log` only
384
- * reads past run history.
321
+ * `chant run log <name>` on the resolved runtime (#2121) — the runtime's own
322
+ * run history, newest first, `--limit <n>` rows at most.
385
323
  */
386
- async function runComponentLog(ctx: CommandContext, name: string): Promise<number> {
387
- const projectPath = resolve(".");
388
- let client;
324
+ async function runOpLogOnRuntime(ctx: CommandContext, name: string): Promise<number> {
325
+ const runtime = await resolveOpRuntime(ctx);
326
+ if (!runtime) return 1;
327
+
328
+ let records;
389
329
  try {
390
- ({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
330
+ records = await runtime.log(name, ctx.args.limit === undefined ? undefined : { limit: ctx.args.limit });
391
331
  } catch (err) {
392
332
  console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
393
333
  return 1;
394
334
  }
395
335
 
396
- let fnName: string;
397
- try {
398
- const codegen = await loadComponentTemporalCodegen();
399
- fnName = codegen.componentWorkflowFnName(name);
400
- } catch (err) {
401
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
402
- return 1;
336
+ if (records.length === 0) {
337
+ console.error(formatInfo(`No run of Op "${name}" is recorded on the "${runtime.name}" runtime.`));
338
+ return 0;
403
339
  }
404
340
 
405
341
  console.log(
406
- "RUN-ID".padEnd(36) +
407
- "STATUS".padEnd(16) +
342
+ "RUN-ID".padEnd(38) +
343
+ "STATUS".padEnd(10) +
408
344
  "STARTED".padEnd(26) +
409
- "CLOSED",
345
+ "ENDED",
410
346
  );
411
-
412
- try {
413
- for await (const run of client.workflow.list({ query: `WorkflowType = "${fnName}"` })) {
414
- const start = run.startTime.toISOString().slice(0, 19).replace("T", " ");
415
- const close = run.closeTime ? run.closeTime.toISOString().slice(0, 19).replace("T", " ") : "—";
416
- console.log(
417
- run.runId.padEnd(36) +
418
- run.status.name.padEnd(16) +
419
- start.padEnd(26) +
420
- close,
421
- );
422
- }
423
- } catch (err) {
424
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
425
- return 1;
347
+ for (const record of records) {
348
+ console.log(
349
+ record.id.padEnd(38) +
350
+ record.status.padEnd(10) +
351
+ shortInstant(record.started).padEnd(26) +
352
+ shortInstant(record.ended),
353
+ );
426
354
  }
427
355
 
428
356
  return 0;
429
357
  }
430
358
 
431
- // ── chant run signal <name> <signal> ─────────────────────────────────────────
359
+ // ── chant run approve <op> <gate> ────────────────────────────────────────────
432
360
 
433
361
  /**
434
- * `chant run signal <name> <signal> [--components]` — sends a signal to a
435
- * running Op workflow by default, or a component workflow when `--components`
436
- * is passed (#589): the durable path's gate is otherwise unclearable from the
437
- * CLI. Resolves the workflow id via `componentWorkflowId` (`chant-component-
438
- * <name>`) instead of `resolveWorkflowId` (`chant-op-<name>`) in that case —
439
- * the two id spaces are kept distinct so an Op and a component can share a
440
- * name without colliding.
362
+ * `chant run approve <op> <gate> [--approver] [--url] [--note]` (#2121) — the
363
+ * rename of `chant run signal`, and a different act.
364
+ *
365
+ * A gate resolution is a fact, not a message: the ledger write is the whole
366
+ * of it, and it is the same write `chant approve` performs
367
+ * (`recordGateApproval`, ./operator.ts), so a resolution recorded here reads
368
+ * back identically. The runtime is told afterwards, through its optional
369
+ * `resolveGate`, purely so a runtime that parks a run mid-flight can wake it
370
+ * now rather than on its next tick. A runtime without that method is not a
371
+ * failure — the fact is recorded either way, and the next run re-reads it.
441
372
  */
442
- export async function runOpSignal(ctx: CommandContext): Promise<number> {
443
- if (!requireTemporalMode(ctx, "chant run signal")) return 1;
444
- const name = ctx.args.extraPositional;
445
- const signalName = ctx.args.extraPositional2;
446
-
447
- if (!name || !signalName) {
448
- console.error(formatError({ message: "Usage: chant run signal <name> <signal-name>" }));
373
+ export async function runOpApprove(ctx: CommandContext): Promise<number> {
374
+ const opName = ctx.args.extraPositional;
375
+ const gate = ctx.args.extraPositional2;
376
+ if (!opName || !gate) {
377
+ console.error(formatError({ message: "Usage: chant run approve <op> <gate>" }));
449
378
  return 1;
450
379
  }
451
380
 
452
- const projectPath = resolve(".");
453
- const workflowId = ctx.args.components ? componentWorkflowId(name) : resolveWorkflowId(name);
454
- // Approver identity for an approval gate (#1035): supplied via --approver, or
455
- // resolved from the CI/user environment the same way `chant components
456
- // release` resolves --actor. Rides the gate signal payload so Temporal
457
- // persists "who approved" in the workflow history. Optional — a signal that
458
- // is not an approval gate (or an approver who declines to identify) sends no
459
- // payload and the gate still clears.
460
- const approver = ctx.args.approver ?? process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER;
461
- let handle: WorkflowHandleRaw;
381
+ const runtime = await resolveOpRuntime(ctx);
382
+ if (!runtime) return 1;
383
+
384
+ const outcome = await recordGateApproval(opName, gate, {
385
+ actor: ctx.args.approver ?? ctx.args.actor,
386
+ note: ctx.args.note,
387
+ url: ctx.args.url,
388
+ });
389
+ if (!outcome.ok) return 1;
390
+
391
+ if (!runtime.resolveGate) {
392
+ console.error(formatInfo(
393
+ `The "${runtime.name}" runtime reads the resolution when the op next runs — re-run \`chant run ${opName}\`.`,
394
+ ));
395
+ return 0;
396
+ }
397
+
462
398
  try {
463
- const { client } = await makeTemporalClient(ctx.args.profile, projectPath);
464
- handle = client.workflow.getHandle(workflowId);
465
- await handle.signal(signalName, ...(approver ? [{ approver }] : []));
399
+ await runtime.resolveGate(opName, gate, outcome.record);
466
400
  } catch (err) {
467
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
401
+ console.error(formatWarning({
402
+ message:
403
+ `The resolution is recorded, but the "${runtime.name}" runtime could not be woken: ` +
404
+ (err instanceof Error ? err.message : String(err)),
405
+ hint: `Re-run \`chant run ${opName} --on ${runtime.name}\` once the runtime is reachable.`,
406
+ }));
468
407
  return 1;
469
408
  }
470
409
 
471
- console.error(formatSuccess(
472
- `Signal "${signalName}" sent to ${ctx.args.components ? "component" : "Op"} "${name}"` +
473
- (approver ? ` (approver: ${approver})` : ""),
474
- ));
410
+ console.error(formatSuccess(`Runtime "${runtime.name}" was notified of the resolution.`));
475
411
  return 0;
476
412
  }
477
413
 
414
+ /**
415
+ * `chant run signal` was renamed `chant run approve` (#2121). Registered so
416
+ * the old spelling says where the verb went instead of being read as an Op
417
+ * named "signal".
418
+ */
419
+ export function runOpSignalRenamed(ctx: CommandContext): Promise<number> {
420
+ const opName = ctx.args.extraPositional ?? "<op>";
421
+ const gate = ctx.args.extraPositional2 ?? "<gate>";
422
+ console.error(formatError({
423
+ message: "`chant run signal` is now `chant run approve`",
424
+ hint: `A gate is resolved by recording the fact, not by sending a message: chant run approve ${opName} ${gate}`,
425
+ }));
426
+ return Promise.resolve(1);
427
+ }
428
+
478
429
  // ── chant run cancel <name> ───────────────────────────────────────────────────
479
430
 
480
- /** `chant run cancel <name> [--components]` — cancels an Op workflow by default, or a component workflow when `--components` is passed (#589), mirroring `runOpSignal`'s id resolution. */
431
+ /** `chant run cancel <name>` — asks the resolved runtime (#2121) to stop the Op's active run. */
481
432
  export async function runOpCancel(ctx: CommandContext): Promise<number> {
482
- if (!requireTemporalMode(ctx, "chant run cancel")) return 1;
483
433
  const name = ctx.args.extraPositional;
484
434
  if (!name) {
485
435
  console.error(formatError({ message: "Op name is required: chant run cancel <name>" }));
@@ -488,65 +438,52 @@ export async function runOpCancel(ctx: CommandContext): Promise<number> {
488
438
 
489
439
  if (!ctx.args.force) {
490
440
  console.error(formatWarning({
491
- message: `Cancelling "${name}" will stop the active workflow run`,
441
+ message: `Cancelling "${name}" will stop the active run`,
492
442
  hint: "Use --force to confirm cancellation",
493
443
  }));
494
444
  return 1;
495
445
  }
496
446
 
497
- const projectPath = resolve(".");
498
- const workflowId = ctx.args.components ? componentWorkflowId(name) : resolveWorkflowId(name);
499
- let handle: WorkflowHandleRaw;
447
+ if (ctx.args.components) {
448
+ return refuseDurableComponentSubcommand(
449
+ "chant run cancel --components",
450
+ "A component run lives and dies with the `chant run --components` process — interrupt that instead.",
451
+ );
452
+ }
453
+
454
+ return runOpCancelOnRuntime(ctx, name);
455
+ }
456
+
457
+ /**
458
+ * `chant run cancel <name>` on the resolved runtime (#2121). `--force` is
459
+ * already checked by the caller, so a runtime only ever sees a confirmed
460
+ * request; a runtime with nothing to cancel says so in its own words and the
461
+ * command exits non-zero.
462
+ */
463
+ async function runOpCancelOnRuntime(ctx: CommandContext, name: string): Promise<number> {
464
+ const runtime = await resolveOpRuntime(ctx);
465
+ if (!runtime) return 1;
466
+
500
467
  try {
501
- const { client } = await makeTemporalClient(ctx.args.profile, projectPath);
502
- handle = client.workflow.getHandle(workflowId);
503
- await handle.cancel();
468
+ await runtime.cancel(name, { force: true });
504
469
  } catch (err) {
505
470
  console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
506
471
  return 1;
507
472
  }
508
473
 
509
- console.error(formatSuccess(`Cancellation requested for ${ctx.args.components ? "component" : "Op"} "${name}"`));
474
+ console.error(formatSuccess(`Cancellation requested for Op "${name}" on the "${runtime.name}" runtime`));
510
475
  return 0;
511
476
  }
512
477
 
513
478
  // ── chant run <name> — main command ───────────────────────────────────────────
514
479
 
515
- const TERMINAL_STATUSES = new Set(["COMPLETED", "FAILED", "CANCELLED", "TERMINATED", "TIMED_OUT"]);
516
- const POLL_INTERVAL_MS = 3000;
517
-
518
- async function waitForTemporalServer(address: string, maxWaitMs = 30_000): Promise<void> {
519
- const [host, portStr] = address.split(":");
520
- const port = parseInt(portStr ?? "7233", 10);
521
- const deadline = Date.now() + maxWaitMs;
522
- while (Date.now() < deadline) {
523
- try {
524
- await new Promise<void>((res, rej) => {
525
- const socket = createConnection({ host, port }, () => { socket.destroy(); res(); });
526
- socket.on("error", rej);
527
- socket.setTimeout(1000, () => { socket.destroy(); rej(new Error("timeout")); });
528
- });
529
- return;
530
- } catch {
531
- await new Promise((r) => setTimeout(r, 1000));
532
- }
533
- }
534
- throw new Error(`Temporal server at ${address} did not become ready within ${maxWaitMs / 1000}s`);
535
- }
536
-
537
- function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
538
- const { completed, scheduled } = countActivities(history);
539
- process.stderr.write(
540
- `\r${formatInfo(`[${opName}]`)} ${completed}/${scheduled} activities completed`,
541
- );
542
- }
543
-
544
480
  /**
545
481
  * `chant run <name>` dispatcher.
546
482
  *
547
- * Local mode is the default — it runs the Op in-process with no Temporal
548
- * server. `--temporal` opts into a cluster (gates, schedules, durable resume).
549
- * `--report` reads a past durable run and is therefore Temporal-only.
483
+ * Every run goes through the runtime resolved by `--on` (#2121); without it,
484
+ * core's built-in local provider runs the Op in this process, where a `gate`
485
+ * is decided against the gate ledger and ends the run pending approval
486
+ * (#2119), exit code 3.
550
487
  *
551
488
  * `chant run --components <name|all>` (#585) is a separate target: discovered
552
489
  * `Component` declarations dispatched through the interpret driver
@@ -555,22 +492,19 @@ function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
555
492
  * runComponentGraph(ctx)` branch (../handlers/graph.ts).
556
493
  *
557
494
  * chant #1116 — `--report` with `--components` is checked and hard-errored
558
- * before that dispatch. There is no preview/dry-run mode for the component
559
- * driver: unlike the Op path (where `--report` reads a past Temporal run),
560
- * `runOpComponents` has never read `ctx.args.report` at all, so the flag was
561
- * silently ignored and the command fell through to a real dispatch — observed
562
- * live reaching an actual cloud shell-out. Erroring here is the safe minimum
563
- * called out on the issue; a real preview is future work.
495
+ * before that dispatch, and #2116 retired `--report` on the Op path too: it
496
+ * rendered a past durable run's workflow history, which no runtime keeps any
497
+ * more. Both refuse rather than falling through to a real dispatch — the
498
+ * component case was observed live reaching an actual cloud shell-out.
564
499
  */
565
500
  /**
566
501
  * Pre-flight for `chant run <op> --sandbox` on an Op containing a `policyGate`
567
502
  * step (chant #2003). `--sandbox` is a global flag, and `../main.ts` arms the
568
503
  * process-wide policy latch off it for every command — so the gate's
569
504
  * `loadPolicyChecks` refuses mid-run with a message addressed to a chant
570
- * maintainer ("This is a chant bug"), or, under `--temporal`, as a
571
- * non-retryable activity failure inside the workflow. Neither is an answer a
572
- * user can act on. Refuse here instead, before anything runs, naming the
573
- * combination and what to do about it.
505
+ * maintainer ("This is a chant bug"), which is not an answer a user can act
506
+ * on. Refuse here instead, before anything runs, naming the combination and
507
+ * what to do about it.
574
508
  *
575
509
  * Keyed on the flag rather than on whether the project declares
576
510
  * `lint.policies`: the flag is what arms the latch, and a gate that builds the
@@ -597,18 +531,14 @@ export async function runOp(ctx: CommandContext): Promise<number> {
597
531
  return 1;
598
532
  }
599
533
  if (ctx.args.components) return runOpComponents(ctx);
600
- if (ctx.args.local && ctx.args.temporal) {
534
+ if (ctx.args.report) {
601
535
  console.error(formatError({
602
- message: "--local and --temporal are mutually exclusive",
603
- hint: "omit both for local mode (the default), or pass exactly one",
536
+ message: "`chant run --report` rendered a past durable run, which #2116 removed",
537
+ hint: "Run `chant run log <op>` for the runtime's own run history.",
604
538
  }));
605
539
  return 1;
606
540
  }
607
- if (ctx.args.report) {
608
- if (!requireTemporalMode(ctx, "chant run --report")) return 1;
609
- return runOpTemporal(ctx);
610
- }
611
- return ctx.args.temporal ? runOpTemporal(ctx) : runOpLocal(ctx);
541
+ return runOpOnRuntime(ctx);
612
542
  }
613
543
 
614
544
  // ── Auto-release recording post-run (#597) ──────────────────────────────────
@@ -689,28 +619,18 @@ async function recordAutoReleasesForRun(
689
619
  // ── chant run --components <name|all> ────────────────────────────────────────
690
620
 
691
621
  /**
692
- * `chant run --components <name|all> [--env <env>] [--temporal]` (#585, and
693
- * #589 for `--temporal`) — the interpret driver's CLI entrypoint. `args.path`
694
- * is the component name (or `all`), matching `chant run <name>`'s Op-dispatch
695
- * convention exactly (`args.path` is the Op name there too) rather than a
696
- * project directory — components are always discovered from the current
697
- * working directory, the same way Op discovery (`discoverOps()`) never takes
698
- * a project-path argument either.
622
+ * `chant run --components <name|all> [--env <env>]` (#585) — the interpret
623
+ * driver's CLI entrypoint, and since #2116 the only component run path there
624
+ * is. `args.path` is the component name (or `all`), matching `chant run
625
+ * <name>`'s Op-dispatch convention exactly (`args.path` is the Op name there
626
+ * too) rather than a project directory — components are always discovered from
627
+ * the current working directory, the same way Op discovery (`discoverOps()`)
628
+ * never takes a project-path argument either.
699
629
  *
700
- * Local mode (the default) resolves the selector through `runComponents`
701
- * (`../../components/cli-support.ts`) and runs it on the local in-process
702
- * executor; gated components are rejected before any step runs (matching
703
- * `runOpLocal`'s pre-flight `findGate` guard) with an actionable message
704
- * pointing at `--temporal`.
705
- *
706
- * `--temporal` (#589) takes the durable path instead: compiles the named
707
- * component's composition to a Temporal workflow/worker (mirroring
708
- * `Temporal::Op` codegen — see `runComponentTemporal` below), and gates are
709
- * now ACCEPTED — a gate is durable wait-for-signal there, not a local
710
- * in-process block. Scoped to a single named component (`all --temporal` is
711
- * out of scope for #589: coordinating N durable workflows' cross-component
712
- * `dependsOn` durably is a follow-up, not part of "compile a component's
713
- * `deploy` composition into a durable orchestrator").
630
+ * The selector resolves through the runtime's `runComponents` and runs on the
631
+ * local in-process driver; a `gate` is decided against the gate ledger when
632
+ * the driver reaches it (#2119), and one nobody has approved ends the run with
633
+ * exit code 3 and the `chant approve` line, exactly as `chant run <op>` does.
714
634
  *
715
635
  * On a successful run, auto-emits one release-ledger record per component
716
636
  * that published a digest (#597, `recordAutoReleasesForRun` above) — a
@@ -723,8 +643,8 @@ async function recordAutoReleasesForRun(
723
643
  * chant #1108 — resolves this invocation's declared build-time parameters
724
644
  * (`chant.config.ts`'s `buildParams`, against `--param`/`--params-file`/a
725
645
  * declared `env` mapping) the exact same way `chant build` does
726
- * (`resolveCliBuildParams`, shared with `buildCommand`), BEFORE either the
727
- * local or `--temporal` path discovers/imports any `*.component.ts` file.
646
+ * (`resolveCliBuildParams`, shared with `buildCommand`), BEFORE anything
647
+ * discovers/imports a `*.component.ts` file.
728
648
  * Before this, `params.*` (`@intentius/chant/params`) was always `{}` under
729
649
  * this command, no matter what a component's `chant.config.ts` declared or a
730
650
  * CI job's environment supplied — see chant #1108. `chant.config.ts` is
@@ -742,6 +662,9 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
742
662
  return 1;
743
663
  }
744
664
 
665
+ const gatedExit = resolveGatedExitCode(ctx);
666
+ if (gatedExit === undefined) return 1;
667
+
745
668
  const projectPath = resolve(".");
746
669
  const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
747
670
  const paramsResolution = resolveCliBuildParams(config.buildParams, {
@@ -754,7 +677,15 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
754
677
  return 1;
755
678
  }
756
679
 
757
- if (ctx.args.temporal) return runComponentTemporal(ctx, selector, config, paramsResolution.provenance);
680
+ const runtime = await resolveOpRuntime(ctx);
681
+ if (!runtime) return 1;
682
+ if (!runtime.runComponents) {
683
+ console.error(formatError({
684
+ message: `--components is not supported on the "${runtime.name}" runtime`,
685
+ hint: "Omit --on to run components on the built-in local runtime.",
686
+ }));
687
+ return 1;
688
+ }
758
689
 
759
690
  const env = ctx.args.env ?? "local";
760
691
  // Seed cross-component/cross-stack outputs from upstream jobs' dumped files
@@ -778,7 +709,7 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
778
709
  // `undefined` and every `onProgress?.(...)` call in the driver is a no-op —
779
710
  // behavior is byte-for-byte unchanged from before this flag existed.
780
711
  const onProgress = ctx.args.progressJson ? ndjsonProgressSink() : undefined;
781
- const result = await runComponents(projectPath, selector, {
712
+ const result = await runtime.runComponents(projectPath, selector, {
782
713
  env: ctx.args.env,
783
714
  componentOutputs: seededOutputs,
784
715
  onProgress,
@@ -794,17 +725,7 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
794
725
  writeFileSync(dumpPath, JSON.stringify(result.run.componentOutputs, null, 2));
795
726
  }
796
727
 
797
- if (result.gateUnsupported) {
798
- console.error(formatError({
799
- message:
800
- `component "${result.gateUnsupported.component}": gate "${result.gateUnsupported.signalName}" is not ` +
801
- `supported on the local executor — gates need a durable runtime.`,
802
- hint: "Re-run with `chant run --components " + result.gateUnsupported.component + " --temporal`.",
803
- }));
804
- return 1;
805
- }
806
-
807
- if (!result.success && !result.run) {
728
+ if (!result.success && !result.run && !result.gated) {
808
729
  console.error(formatError({ message: result.error ?? "Failed to run component(s)" }));
809
730
  return 1;
810
731
  }
@@ -813,6 +734,29 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
813
734
  if (ctx.args.json) renderDriverJson(result.run); else renderDriverHuman(result.run);
814
735
  }
815
736
 
737
+ // Gated (#2119): the same fact-and-stop the Op path takes, and the same
738
+ // exit code. Nothing to release — the component stopped short of finishing.
739
+ if (result.gated) {
740
+ const { gate } = result.gated;
741
+ console.error(formatWarning({
742
+ message: `component "${result.gated.component}" is gated on "${gate.gate}" — pending approval`,
743
+ }));
744
+ console.error(formatInfo(`approve : ${approveCommand(gate.op, gate.gate)}`));
745
+ if (gate.url) console.error(formatInfo(`approve at: ${gate.url}`));
746
+ console.error(formatInfo(`expires : ${gate.expiresAt}`));
747
+ reportGatedRun(
748
+ {
749
+ op: gate.op,
750
+ gate: gate.gate,
751
+ ...(gate.description ? { description: gate.description } : {}),
752
+ expiresAt: gate.expiresAt,
753
+ ...(gate.url ? { url: gate.url } : {}),
754
+ },
755
+ gatedExit,
756
+ );
757
+ return gatedExit;
758
+ }
759
+
816
760
  if (result.success && result.run) {
817
761
  const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord);
818
762
  await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
@@ -821,259 +765,31 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
821
765
  return result.success ? 0 : 1;
822
766
  }
823
767
 
824
- // ── chant run --components <name> --temporal (#589) ─────────────────────────
825
-
826
- function componentWorkflowId(componentName: string): string {
827
- return `chant-component-${componentName}`;
828
- }
829
-
830
768
  /**
831
- * `chant run --components <name> --temporal` (#589, epic #551 §5/§8) — the
832
- * durable counterpart to `runOpComponents`'s local path. Mirrors
833
- * `runOpTemporal`'s shape exactly (build+spawn worker, submit workflow, poll,
834
- * report) but "build" here means compiling the component's composition
835
- * on-the-fly via `loadComponentTemporalCodegen` (the Temporal lexicon's
836
- * `serializeComponent`, mirroring `serializeOps`) rather than reading a
837
- * pre-existing `dist/ops/<name>/worker.ts` — components have no separate
838
- * `chant build` step that already produced generated files the way Ops do, so
839
- * this generates `dist/components/<name>/{workflow,worker,activities}.ts`
840
- * itself before spawning.
769
+ * `chant run <name>` on the resolved runtime (#2121) — the built-in `local`
770
+ * provider by default, a lexicon's `opRuntime` under `--on <name>`.
841
771
  *
842
- * Unlike the local executor, a `gate` anywhere in the component's
843
- * composition is fully supported: the generated workflow durably waits for
844
- * the signal (see `serializeComponent`'s codegen), survivable across a worker
845
- * crash and clearable via `chant run signal <name> <signal> --components
846
- * --temporal` (`runOpSignal` above, extended for components alongside this
847
- * issue).
848
- *
849
- * `config`/`buildParams` are resolved once by the caller (`runOpComponents`,
850
- * chant #1108) BEFORE this function runs, so `resolveComponentTargets`
851
- * below — which discovers/imports the target `*.component.ts` file — sees
852
- * `params.*` (`@intentius/chant/params`) already populated, the same
853
- * guarantee the local executor path gets.
772
+ * This function knows nothing about how a run happens. It discovers the Op,
773
+ * runs the CLI-level pre-flights that are about flags rather than execution
774
+ * (`--sandbox` over a `policyGate` step, #2003), hands the config to the
775
+ * runtime, and renders whatever comes back. On the local runtime that is the
776
+ * executor's own `OpRunResult`, so `--json` and the human render are exactly
777
+ * what they were before the seam existed; a runtime that reports only a state
778
+ * gets the terser render instead.
854
779
  */
855
- async function runComponentTemporal(
856
- ctx: CommandContext,
857
- selector: string,
858
- config: ChantConfig,
859
- buildParams: BuildParamProvenance[],
860
- ): Promise<number> {
861
- if (selector === "all") {
862
- console.error(formatError({
863
- message: "`chant run --components all --temporal` is not supported",
864
- hint: "Pass a single component name — durable execution is compiled and run per component (see epic #551 #589).",
865
- }));
866
- return 1;
867
- }
868
-
869
- const projectPath = resolve(".");
870
- const resolved = await resolveComponentTargets(projectPath, selector, ctx.args.sandbox, buildParams);
871
- if (!resolved.success || resolved.targets.length === 0) {
872
- console.error(formatError({ message: resolved.error ?? `Component "${selector}" not found` }));
873
- return 1;
874
- }
875
- const component = resolved.targets[0];
876
-
877
- // Config + profile (mirrors runOpTemporal). `config` was already loaded by
878
- // the caller to resolve build-time parameters (chant #1108); reused here
879
- // rather than loading chant.config.ts a second time.
880
- const chantConfig = config;
881
- let profile;
882
- try {
883
- profile = resolveProfile(chantConfig as Record<string, unknown>, ctx.args.profile);
884
- } catch (err) {
885
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
886
- return 1;
887
- }
888
-
889
- // Compile the component to workflow/worker/activities under dist/components/<name>/.
890
- let codegen;
891
- try {
892
- codegen = await loadComponentTemporalCodegen();
893
- } catch (err) {
894
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
895
- return 1;
896
- }
897
-
898
- // (#629) Fill `chant.config.ts`'s `sbom`/`signing`/`vulnPolicy` defaults into
899
- // every recognized step BEFORE codegen — same pass the interpret/local path
900
- // runs in `runComponents` (../../components/cli-support.ts). The Temporal path
901
- // *inlines* the resolved composition into the generated workflow/activities,
902
- // so if we serialized the raw component the durable path would silently drop
903
- // project-level supply-chain defaults the local path honors. (The GitLab
904
- // generate path needs no equivalent: it emits thin `chant run --components`
905
- // trigger jobs, so defaults are applied at run time inside the triggered job.)
906
- const resolvedComponent = applyConfigDefaults(component, chantConfig);
907
- const files = codegen.serializeComponent(resolvedComponent, { env: ctx.args.env });
908
- for (const [relPath, content] of Object.entries(files)) {
909
- const outPath = join(projectPath, "dist", relPath);
910
- mkdirSync(dirname(outPath), { recursive: true });
911
- writeFileSync(outPath, content);
912
- }
913
-
914
- const workerPath = join(projectPath, "dist", "components", component.name, "worker.ts");
915
-
916
- // autoStart: spin up temporal server if needed (mirrors runOpTemporal).
917
- if (profile.autoStart) {
918
- console.error(formatInfo("autoStart: checking Temporal server..."));
919
- try {
920
- await waitForTemporalServer(profile.address, 2000);
921
- console.error(formatInfo("Temporal server already running."));
922
- } catch {
923
- console.error(formatInfo("Starting temporal server start-dev..."));
924
- spawnChild("temporal", ["server", "start-dev"], {
925
- cwd: projectPath,
926
- stdio: "ignore",
927
- detached: true,
928
- }).unref();
929
- await waitForTemporalServer(profile.address, 30_000);
930
- console.error(formatSuccess("Temporal server ready."));
931
- }
932
- }
933
-
934
- let client;
935
- try {
936
- ({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
937
- } catch (err) {
938
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
939
- return 1;
940
- }
941
-
942
- // Register the search attributes the generated component workflow upserts
943
- // (ComponentName/Phase) on the autoStart dev server (mirrors runOpTemporal).
944
- if (profile.autoStart) {
945
- await ensureSearchAttributes(client, profile.namespace, ["ComponentName", "Phase"]);
946
- }
947
-
948
- const profileName = ctx.args.profile ??
949
- (((chantConfig as Record<string, unknown>).temporal as Record<string, unknown> | undefined)?.defaultProfile as string | undefined) ??
950
- "local";
951
-
952
- console.error(formatInfo(`Spawning worker for component "${component.name}" (profile: ${profileName})...`));
953
- const workerProcess: ChildProcess = spawnChild("npx", ["tsx", workerPath], {
954
- cwd: projectPath,
955
- env: { ...process.env, TEMPORAL_PROFILE: profileName },
956
- stdio: ["ignore", "ignore", "inherit"],
957
- });
958
-
959
- const workflowId = componentWorkflowId(component.name);
960
- const fnName = codegen.componentWorkflowFnName(component.name);
961
- const taskQueue = profile.taskQueue ?? component.name;
962
-
963
- let handle: WorkflowHandleRaw;
964
- try {
965
- handle = await client.workflow.start(fnName, {
966
- taskQueue,
967
- workflowId,
968
- workflowIdConflictPolicy: "FAIL",
969
- });
970
- console.error(formatSuccess(`Workflow started: ${workflowId}`));
971
- } catch (err) {
972
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
973
- try { workerProcess.kill(); } catch { /* ignore */ }
974
- return 1;
975
- }
976
-
977
- const gate = findComponentGate(component);
978
- if (gate) {
979
- console.error(formatInfo(
980
- `Component has a gate ("${gate.signalName}") — unblock it with: chant run signal ${component.name} ${gate.signalName} --components --temporal`,
981
- ));
982
- }
983
-
984
- let finalDesc: WorkflowExecutionDescription | undefined;
985
-
986
- try {
987
- while (true) {
988
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
989
- const desc = await handle.describe();
990
- const history = await fetchNormalizedHistory(handle);
991
- renderProgress(component.name, history);
992
- if (TERMINAL_STATUSES.has(desc.status.name)) {
993
- process.stderr.write("\n");
994
- finalDesc = desc;
995
- break;
996
- }
997
- }
998
- } catch (err) {
999
- process.stderr.write("\n");
1000
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1001
- return 1;
1002
- } finally {
1003
- try { workerProcess.kill(); } catch { /* ignore */ }
1004
- }
1005
-
1006
- if (!finalDesc) return 1;
1007
-
1008
- const status = finalDesc.status.name;
1009
- console.error(status === "COMPLETED"
1010
- ? formatSuccess(`Component "${component.name}" completed successfully.`)
1011
- : formatError({ message: `Component "${component.name}" ended with status: ${status}` }),
1012
- );
1013
-
1014
- if (status === "COMPLETED") {
1015
- // (#597) Auto-emit a release record post-run — never inside the generated
1016
- // workflow (determinism). The workflow returns its final phaseOutputs
1017
- // (see serializeComponent's codegen) precisely so this read is possible;
1018
- // handle.result() on an already-COMPLETED workflow is a plain historical
1019
- // read, not a new activity/side effect.
1020
- let digest: string | undefined;
1021
- let manifest: ReturnType<typeof extractRunManifestFromPhaseOutputs>;
1022
- try {
1023
- const workflowResult = await handle.result() as { phaseOutputs?: Record<string, Record<string, unknown>> } | undefined;
1024
- digest = extractRunDigestFromPhaseOutputs(workflowResult?.phaseOutputs);
1025
- manifest = extractRunManifestFromPhaseOutputs(workflowResult?.phaseOutputs);
1026
- } catch {
1027
- digest = undefined;
1028
- manifest = undefined;
1029
- }
1030
-
1031
- const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} }));
1032
- const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord);
1033
- const env = ctx.args.env ?? "local";
1034
- const outcome = await maybeRecordAutoRelease(
1035
- // A Temporal workflow run id lives in the orchestrator's id space (#2045).
1036
- { component: component.name, env, success: true, digest, runId: finalDesc.runId, runOrigin: { forge: "op" } },
1037
- { disabled },
1038
- );
1039
- if (!outcome.recorded && outcome.reason === "error") {
1040
- console.error(formatWarning({ message: `release record for "${component.name}"@${env} was not recorded: ${outcome.error}` }));
1041
- } else if (outcome.recorded) {
1042
- console.error(formatInfo(
1043
- `Recorded release: ${formatBold(component.name)}@${env} -> ${outcome.record.digest} (commit ${outcome.commit.slice(0, 7)})`,
1044
- ));
1045
- }
1046
-
1047
- // (#609) Persist the run's build manifest post-run, same opt-out flag as
1048
- // the release record above — see ../../components/manifest-persistence.ts.
1049
- const manifestOutcome = await maybePersistBuildManifest({ success: true, manifest }, { disabled });
1050
- if (!manifestOutcome.persisted && manifestOutcome.reason === "error") {
1051
- console.error(formatWarning({ message: `build manifest for "${component.name}"@${env} was not persisted: ${manifestOutcome.error}` }));
1052
- } else if (manifestOutcome.persisted) {
1053
- console.error(formatInfo(
1054
- `Persisted build manifest: ${formatBold(component.name)} -> ${manifestOutcome.manifestDigest} (commit ${manifestOutcome.commit.slice(0, 7)})`,
1055
- ));
1056
- }
1057
- }
1058
-
1059
- return status === "COMPLETED" ? 0 : 1;
1060
- }
1061
-
1062
- /**
1063
- * Run an Op in-process via the local executor — no Temporal worker, server, or
1064
- * built `worker.ts`. Reads the Op config straight from discovery and resolves
1065
- * activities from the Temporal lexicon package.
1066
- */
1067
- export async function runOpLocal(ctx: CommandContext): Promise<number> {
780
+ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
1068
781
  const opName = ctx.args.path;
1069
782
  if (!opName || opName === ".") {
1070
783
  console.error(formatError({
1071
784
  message: "Op name is required: chant run <name>",
1072
- hint: "Run `chant run list --temporal` to see available Ops",
785
+ hint: "Run `chant run list` to see available Ops",
1073
786
  }));
1074
787
  return 1;
1075
788
  }
1076
789
 
790
+ const gatedExit = resolveGatedExitCode(ctx);
791
+ if (gatedExit === undefined) return 1;
792
+
1077
793
  const { ops, errors } = await discoverOps();
1078
794
  for (const err of errors) console.error(formatWarning({ message: err }));
1079
795
 
@@ -1094,32 +810,12 @@ export async function runOpLocal(ctx: CommandContext): Promise<number> {
1094
810
  // Pre-flight: --sandbox cannot cover a policyGate step (#2003).
1095
811
  if (refusesPolicyGateUnderSandbox(ctx, config, opName)) return 1;
1096
812
 
1097
- // Pre-flight: gates/schedules need a durable runtime — fail before any step.
1098
- const gate = findGate(config);
1099
- if (gate) {
1100
- console.error(formatError({
1101
- message: new LocalGateUnsupportedError(gate.signalName).message,
1102
- }));
1103
- return 1;
1104
- }
813
+ const runtime = await resolveOpRuntime(ctx);
814
+ if (!runtime) return 1;
1105
815
 
1106
- // The project's configured lexicons decide which cloud appliers to load
1107
- // (aws → floci, gcp → gcpApply, azure → az group). Best-effort: an unreadable
1108
- // config just yields the temporal base activities.
1109
- let lexicons: string[] = [];
1110
- try {
1111
- lexicons = (await loadChantConfig(process.cwd())).config.lexicons ?? [];
1112
- } catch {
1113
- // No/invalid chant.config — fall back to base activities only.
1114
- }
1115
-
1116
- let activities, profiles;
1117
- try {
1118
- [activities, profiles] = await Promise.all([loadActivities(lexicons), loadProfiles()]);
1119
- } catch (err) {
1120
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1121
- return 1;
1122
- }
816
+ // `--progress-json` streams one NDJSON StepRecord per settled step, fed by
817
+ // whatever the runtime reports through `progress`.
818
+ const progress = ctx.args.progressJson ? ndjsonProgressSink<StepRecord>() : undefined;
1123
819
 
1124
820
  // Ctrl-C aborts in-flight activities (kills their child processes) instead of
1125
821
  // orphaning them. The handler is removed in `finally` so it never leaks.
@@ -1131,18 +827,57 @@ export async function runOpLocal(ctx: CommandContext): Promise<number> {
1131
827
  process.once("SIGINT", onSigint);
1132
828
 
1133
829
  try {
1134
- const result = await runOpLocally(config, activities, profiles, controller.signal);
1135
- if (ctx.args.json) renderJson(result); else renderHuman(result);
1136
- return 0;
830
+ const handle = await runtime.start(config, {
831
+ env: ctx.args.env,
832
+ ...(ctx.args.profile !== undefined ? { profile: ctx.args.profile } : {}),
833
+ progress,
834
+ signal: controller.signal,
835
+ });
836
+ const status = await handle.result();
837
+
838
+ if (status.result) {
839
+ // The executor's own render already prints a gate, its approve line
840
+ // and its expiry (#2119).
841
+ if (ctx.args.json) renderJson(status.result); else renderHuman(status.result);
842
+ } else {
843
+ renderRuntimeStatus("Op", opName, runtime.name, status);
844
+ if (status.state === "gated" && status.gate) {
845
+ console.error(formatWarning({
846
+ message: `Op "${opName}" is waiting on gate "${status.gate.name}"`,
847
+ hint: `Record the resolution with: ${approveCommand(opName, status.gate.name)}`,
848
+ }));
849
+ }
850
+ }
851
+
852
+ // Exit 3 for a gated run (#2119) — a distinct code so CI can tell
853
+ // "waiting on a human" from a broken op, and retry the one but not the
854
+ // other. `--gated-exit <code>` remaps that one outcome and nothing else
855
+ // (#2243).
856
+ if (status.state === "gated") {
857
+ // The local runtime carries the whole pending fact on its result; a
858
+ // runtime that reports only a state carries the gate's name alone.
859
+ const pending = status.result?.gate;
860
+ const gate = pending?.gate ?? status.gate?.name;
861
+ if (gate) {
862
+ reportGatedRun(
863
+ {
864
+ op: opName,
865
+ gate,
866
+ ...(pending?.description ? { description: pending.description } : {}),
867
+ ...(pending?.expiresAt ? { expiresAt: pending.expiresAt } : {}),
868
+ ...(pending?.url ? { url: pending.url } : {}),
869
+ },
870
+ gatedExit,
871
+ );
872
+ }
873
+ return gatedExit;
874
+ }
875
+ return status.state === "completed" ? 0 : 1;
1137
876
  } catch (err) {
1138
877
  if (err instanceof OpRunFailure) {
1139
878
  if (ctx.args.json) renderJson(err.result); else renderHuman(err.result);
1140
879
  return 1;
1141
880
  }
1142
- if (err instanceof LocalGateUnsupportedError) {
1143
- console.error(formatError({ message: err.message }));
1144
- return 1;
1145
- }
1146
881
  console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1147
882
  return 1;
1148
883
  } finally {
@@ -1150,213 +885,12 @@ export async function runOpLocal(ctx: CommandContext): Promise<number> {
1150
885
  }
1151
886
  }
1152
887
 
1153
- async function runOpTemporal(ctx: CommandContext): Promise<number> {
1154
- const opName = ctx.args.path;
1155
-
1156
- if (!opName || opName === ".") {
1157
- console.error(formatError({
1158
- message: "Op name is required: chant run <name>",
1159
- hint: "Run `chant run list` to see available Ops",
1160
- }));
1161
- return 1;
1162
- }
1163
-
1164
- // Discover Ops
1165
- const { ops, errors } = await discoverOps();
1166
- for (const err of errors) console.error(formatWarning({ message: err }));
1167
-
1168
- const discovered = ops.get(opName);
1169
- if (!discovered) {
1170
- const names = [...ops.keys()];
1171
- console.error(formatError({
1172
- message: `Op "${opName}" not found`,
1173
- hint: names.length > 0
1174
- ? `Available: ${names.join(", ")}`
1175
- : "No *.op.ts files found — create one or run `chant run list`",
1176
- }));
1177
- return 1;
1178
- }
1179
-
1180
- const { config } = discovered;
1181
-
1182
- // Pre-flight: --sandbox cannot cover a policyGate step (#2003). Refused here
1183
- // too, so the durable path fails as a CLI error rather than as a
1184
- // non-retryable activity failure buried in workflow history.
1185
- if (refusesPolicyGateUnderSandbox(ctx, config, opName)) return 1;
1186
-
1187
- const projectPath = resolve(".");
1188
-
1189
- // Load config + profile
1190
- const { config: chantConfig } = await loadChantConfig(projectPath);
1191
- let profile;
1192
- try {
1193
- profile = resolveProfile(chantConfig as Record<string, unknown>, ctx.args.profile);
1194
- } catch (err) {
1195
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1196
- return 1;
1197
- }
1198
-
1199
- // Handle --report flag: just print the last run report
1200
- if (ctx.args.report) {
1201
- let client, desc: WorkflowExecutionDescription, history: WorkflowHistoryRaw;
1202
- try {
1203
- ({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
1204
- const handle = client.workflow.getHandle(resolveWorkflowId(opName));
1205
- desc = await handle.describe();
1206
- history = await fetchNormalizedHistory(handle);
1207
- } catch (err) {
1208
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1209
- return 1;
1210
- }
1211
- const md = generateReport(opName, config, desc, history);
1212
- process.stdout.write(md);
1213
- return 0;
1214
- }
1215
-
1216
- // Check built worker exists
1217
- const workerPath = join(projectPath, "dist", "ops", opName, "worker.ts");
1218
- if (!existsSync(workerPath)) {
1219
- console.error(formatError({
1220
- message: `dist/ops/${opName}/worker.ts not found`,
1221
- hint: "Run `chant build` first to generate the worker",
1222
- }));
1223
- return 1;
1224
- }
1225
-
1226
- // autoStart: spin up temporal server if needed
1227
- if (profile.autoStart) {
1228
- console.error(formatInfo("autoStart: checking Temporal server..."));
1229
- try {
1230
- await waitForTemporalServer(profile.address, 2000);
1231
- console.error(formatInfo("Temporal server already running."));
1232
- } catch {
1233
- console.error(formatInfo("Starting temporal server start-dev..."));
1234
- spawnChild("temporal", ["server", "start-dev"], {
1235
- cwd: projectPath,
1236
- stdio: "ignore",
1237
- detached: true,
1238
- }).unref();
1239
- await waitForTemporalServer(profile.address, 30_000);
1240
- console.error(formatSuccess("Temporal server ready."));
1241
- }
1242
- }
1243
-
1244
- // Load Temporal client
1245
- let client;
1246
- try {
1247
- ({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
1248
- } catch (err) {
1249
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1250
- return 1;
1251
- }
1252
-
1253
- // On the autoStart dev server, register the search attributes the generated
1254
- // workflow upserts (OpName/Phase + any op-declared) so the first workflow task
1255
- // does not fail with BadSearchAttributes.
1256
- if (profile.autoStart) {
1257
- await ensureSearchAttributes(client, profile.namespace, [
1258
- "OpName",
1259
- "Phase",
1260
- ...Object.keys((config as { searchAttributes?: Record<string, string> }).searchAttributes ?? {}),
1261
- ]);
1262
- }
1263
-
1264
- // Spawn worker process
1265
- const profileName = ctx.args.profile ??
1266
- (((chantConfig as Record<string, unknown>).temporal as Record<string, unknown> | undefined)?.defaultProfile as string | undefined) ??
1267
- "local";
1268
-
1269
- console.error(formatInfo(`Spawning worker for Op "${opName}" (profile: ${profileName})...`));
1270
- const workerProcess: ChildProcess = spawnChild("npx", ["tsx", workerPath], {
1271
- cwd: projectPath,
1272
- env: { ...process.env, TEMPORAL_PROFILE: profileName },
1273
- stdio: ["ignore", "ignore", "inherit"],
1274
- });
1275
-
1276
- // Submit workflow
1277
- const workflowId = resolveWorkflowId(opName);
1278
- const fnName = workflowFnName(opName);
1279
- const taskQueue = profile.taskQueue ?? opName;
1280
-
1281
- let handle: WorkflowHandleRaw;
1282
- try {
1283
- handle = await client.workflow.start(fnName, {
1284
- taskQueue,
1285
- workflowId,
1286
- workflowIdConflictPolicy: "FAIL",
1287
- });
1288
- console.error(formatSuccess(`Workflow started: ${workflowId}`));
1289
- } catch (err) {
1290
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1291
- return 1;
1292
- }
1293
-
1294
- // Poll for progress until terminal state.
1295
- //
1296
- // `--progress-json` (chant #1676): stream one NDJSON StepRecord per line to
1297
- // stdout as each declared step settles — the durable-path counterpart to
1298
- // `chant run <name> --json`'s local-executor record shape (op-progress.ts).
1299
- // Purely additive: when the flag is absent `emitNewRecords` stays undefined
1300
- // and every call site is a no-op, so poll-loop behavior is unchanged.
1301
- //
1302
- // Dedup is by declared-step identity, not by an emitted count (chant #2032):
1303
- // the terminal `final: true` pass *inserts* a `"skipped"` record at each
1304
- // never-reached step's declared position, so the list is not append-only and
1305
- // a count watermark shifts off the end of it.
1306
- const emitProgress = ctx.args.progressJson ? ndjsonProgressSink<StepRecord>() : undefined;
1307
- const emitNewRecords = emitProgress ? stepRecordEmitter(emitProgress) : undefined;
1308
-
1309
- let finalDesc: WorkflowExecutionDescription | undefined;
1310
- let finalHistory: WorkflowHistoryRaw | undefined;
1311
-
1312
- try {
1313
- while (true) {
1314
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1315
- const desc = await handle.describe();
1316
- const history = await fetchNormalizedHistory(handle);
1317
-
1318
- renderProgress(opName, history);
1319
- emitNewRecords?.(extractIndexedStepRecords(config, history));
1320
-
1321
- if (TERMINAL_STATUSES.has(desc.status.name)) {
1322
- process.stderr.write("\n");
1323
- emitNewRecords?.(extractIndexedStepRecords(config, history, { final: true }));
1324
- finalDesc = desc;
1325
- finalHistory = history;
1326
- break;
1327
- }
1328
- }
1329
- } catch (err) {
1330
- process.stderr.write("\n");
1331
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1332
- return 1;
1333
- } finally {
1334
- // Kill worker process (best-effort)
1335
- try { workerProcess.kill(); } catch { /* ignore */ }
1336
- }
1337
-
1338
- if (!finalDesc || !finalHistory) return 1;
1339
-
1340
- const status = finalDesc.status.name;
1341
- console.error(status === "COMPLETED"
1342
- ? formatSuccess(`Op "${opName}" completed successfully.`)
1343
- : formatError({ message: `Op "${opName}" ended with status: ${status}` }),
1344
- );
1345
-
1346
- // Write deployment report
1347
- const md = generateReport(opName, config, finalDesc, finalHistory);
1348
- const reportPath = writeReport(opName, md);
1349
- console.error(formatInfo(`Report written to ${reportPath}`));
1350
-
1351
- return status === "COMPLETED" ? 0 : 1;
1352
- }
1353
-
1354
888
  // ── fallback ────────────────────────────────────────────────────────────────���─
1355
889
 
1356
890
  export function runOpUnknown(ctx: CommandContext): Promise<number> {
1357
891
  console.error(formatError({
1358
892
  message: `Unknown run subcommand: ${ctx.args.extraPositional ?? ctx.args.path}`,
1359
- hint: "Available: chant run <name>, run list, run status, run signal, run cancel, run log",
893
+ hint: "Available: chant run <name>, run list, run status, run approve, run cancel, run log",
1360
894
  }));
1361
895
  return Promise.resolve(1);
1362
896
  }