@intentius/chant 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (515) hide show
  1. package/dist/audit/catalog.d.ts +51 -1
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +20 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +14 -8
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/lineage.d.ts +10 -0
  8. package/dist/audit/lineage.d.ts.map +1 -0
  9. package/dist/audit/prior-art.d.ts +385 -0
  10. package/dist/audit/prior-art.d.ts.map +1 -0
  11. package/dist/audit/proof.d.ts +16 -0
  12. package/dist/audit/proof.d.ts.map +1 -1
  13. package/dist/audit/report-html.d.ts.map +1 -1
  14. package/dist/audit/report-model.d.ts +8 -1
  15. package/dist/audit/report-model.d.ts.map +1 -1
  16. package/dist/audit/report.d.ts.map +1 -1
  17. package/dist/audit/rules-doc.d.ts +3 -0
  18. package/dist/audit/rules-doc.d.ts.map +1 -1
  19. package/dist/audit/secrets.d.ts +15 -0
  20. package/dist/audit/secrets.d.ts.map +1 -1
  21. package/dist/audit/terraform-state.d.ts +46 -0
  22. package/dist/audit/terraform-state.d.ts.map +1 -0
  23. package/dist/build.d.ts +17 -1
  24. package/dist/build.d.ts.map +1 -1
  25. package/dist/claimed-fields.d.ts +161 -0
  26. package/dist/claimed-fields.d.ts.map +1 -0
  27. package/dist/cli/build-params-cli.d.ts +2 -2
  28. package/dist/cli/command-group.d.ts +14 -0
  29. package/dist/cli/command-group.d.ts.map +1 -1
  30. package/dist/cli/commands/audit.d.ts +17 -1
  31. package/dist/cli/commands/audit.d.ts.map +1 -1
  32. package/dist/cli/commands/build.d.ts.map +1 -1
  33. package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
  34. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  35. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  36. package/dist/cli/commands/import.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts +1 -0
  38. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon/templates/project.d.ts +7 -0
  40. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  41. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  42. package/dist/cli/commands/init.d.ts.map +1 -1
  43. package/dist/cli/commands/lint.d.ts.map +1 -1
  44. package/dist/cli/commands/onboard.d.ts +2 -0
  45. package/dist/cli/commands/onboard.d.ts.map +1 -1
  46. package/dist/cli/handlers/build.d.ts.map +1 -1
  47. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  48. package/dist/cli/handlers/operator.d.ts +82 -16
  49. package/dist/cli/handlers/operator.d.ts.map +1 -1
  50. package/dist/cli/handlers/run.d.ts +49 -42
  51. package/dist/cli/handlers/run.d.ts.map +1 -1
  52. package/dist/cli/main.d.ts +28 -1
  53. package/dist/cli/main.d.ts.map +1 -1
  54. package/dist/cli/mcp/op-tools.d.ts +16 -1
  55. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  56. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  57. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  58. package/dist/cli/registry.d.ts +26 -9
  59. package/dist/cli/registry.d.ts.map +1 -1
  60. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  61. package/dist/components/auto-release.d.ts +10 -20
  62. package/dist/components/auto-release.d.ts.map +1 -1
  63. package/dist/components/capability.d.ts +4 -4
  64. package/dist/components/cli-support.d.ts +20 -21
  65. package/dist/components/cli-support.d.ts.map +1 -1
  66. package/dist/components/component.d.ts +3 -3
  67. package/dist/components/component.d.ts.map +1 -1
  68. package/dist/components/driver-output.d.ts.map +1 -1
  69. package/dist/components/driver.d.ts +66 -36
  70. package/dist/components/driver.d.ts.map +1 -1
  71. package/dist/components/index.d.ts +1 -2
  72. package/dist/components/index.d.ts.map +1 -1
  73. package/dist/components/manifest-persistence.d.ts +1 -9
  74. package/dist/components/manifest-persistence.d.ts.map +1 -1
  75. package/dist/components/run-progress.d.ts +6 -6
  76. package/dist/components/run-progress.d.ts.map +1 -1
  77. package/dist/components/verbs/run-agent.d.ts +9 -15
  78. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  79. package/dist/components/verbs/verify.d.ts +2 -2
  80. package/dist/deep-observation.d.ts +17 -0
  81. package/dist/deep-observation.d.ts.map +1 -1
  82. package/dist/discovery/files.d.ts +4 -4
  83. package/dist/discovery/fold-import.d.ts.map +1 -1
  84. package/dist/discovery/param-deps.d.ts +33 -0
  85. package/dist/discovery/param-deps.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  87. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  88. package/dist/fold/foldable-helpers.d.ts +5 -1
  89. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  90. package/dist/fold-provenance.d.ts +236 -0
  91. package/dist/fold-provenance.d.ts.map +1 -0
  92. package/dist/graph-ops.d.ts +1 -1
  93. package/dist/held-elsewhere.d.ts +85 -0
  94. package/dist/held-elsewhere.d.ts.map +1 -0
  95. package/dist/import/ir-utils.d.ts +1 -1
  96. package/dist/import/ir-utils.d.ts.map +1 -1
  97. package/dist/import/parser.d.ts +33 -0
  98. package/dist/import/parser.d.ts.map +1 -1
  99. package/dist/index.d.ts +3 -0
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/kubectl-context.d.ts +3 -3
  102. package/dist/lexicon-config.d.ts +4 -4
  103. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  104. package/dist/lexicon.d.ts +104 -14
  105. package/dist/lexicon.d.ts.map +1 -1
  106. package/dist/lifecycle/change-set.d.ts +13 -0
  107. package/dist/lifecycle/change-set.d.ts.map +1 -1
  108. package/dist/lifecycle/deep-diff.d.ts +133 -6
  109. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  110. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  111. package/dist/lifecycle/gate-ledger.d.ts +71 -3
  112. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  113. package/dist/lifecycle/index.d.ts +1 -0
  114. package/dist/lifecycle/index.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +2 -2
  116. package/dist/lifecycle/run-ledger.d.ts +53 -0
  117. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  118. package/dist/lifecycle/symptoms.d.ts +3 -3
  119. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  120. package/dist/lint/config.d.ts +81 -2
  121. package/dist/lint/config.d.ts.map +1 -1
  122. package/dist/lint/post-synth.d.ts +55 -2
  123. package/dist/lint/post-synth.d.ts.map +1 -1
  124. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  125. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  126. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  127. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  128. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  129. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  130. package/dist/lint/rules/comp/index.d.ts +1 -1
  131. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  132. package/dist/lint/rules/op/index.d.ts +16 -0
  133. package/dist/lint/rules/op/index.d.ts.map +1 -0
  134. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  135. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  136. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  137. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  138. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  139. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  140. package/dist/lint/rules/op/support.d.ts +12 -0
  141. package/dist/lint/rules/op/support.d.ts.map +1 -0
  142. package/dist/lint/suppressions.d.ts +108 -0
  143. package/dist/lint/suppressions.d.ts.map +1 -0
  144. package/dist/live-endpoint.d.ts +1 -1
  145. package/dist/op/activities/activity-contracts.d.ts +54 -0
  146. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  147. package/dist/op/activities/apply.d.ts +262 -0
  148. package/dist/op/activities/apply.d.ts.map +1 -0
  149. package/dist/op/activities/build.d.ts +13 -0
  150. package/dist/op/activities/build.d.ts.map +1 -0
  151. package/dist/op/activities/converge.d.ts +110 -0
  152. package/dist/op/activities/converge.d.ts.map +1 -0
  153. package/dist/op/activities/env-teardown.d.ts +76 -0
  154. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  155. package/dist/op/activities/guard-validate.d.ts +64 -0
  156. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  157. package/dist/op/activities/http-check.d.ts +34 -0
  158. package/dist/op/activities/http-check.d.ts.map +1 -0
  159. package/dist/op/activities/index.d.ts +31 -0
  160. package/dist/op/activities/index.d.ts.map +1 -0
  161. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  162. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  163. package/dist/op/activities/lifecycle.d.ts +42 -0
  164. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  165. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  166. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  167. package/dist/op/activities/policy.d.ts +17 -0
  168. package/dist/op/activities/policy.d.ts.map +1 -0
  169. package/dist/op/activities/reconcile.d.ts +89 -0
  170. package/dist/op/activities/reconcile.d.ts.map +1 -0
  171. package/dist/op/activities/shell.d.ts +13 -0
  172. package/dist/op/activities/shell.d.ts.map +1 -0
  173. package/dist/op/activities/teardown.d.ts +10 -0
  174. package/dist/op/activities/teardown.d.ts.map +1 -0
  175. package/dist/op/activities/util.d.ts +7 -0
  176. package/dist/op/activities/util.d.ts.map +1 -0
  177. package/dist/op/activities/wait.d.ts +28 -0
  178. package/dist/op/activities/wait.d.ts.map +1 -0
  179. package/dist/op/activities/workflow-audit.d.ts +92 -0
  180. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  181. package/dist/op/activity-contract-registry.d.ts +76 -0
  182. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  183. package/dist/op/activity-contract.d.ts +25 -18
  184. package/dist/op/activity-contract.d.ts.map +1 -1
  185. package/dist/op/activity-failure.d.ts +28 -0
  186. package/dist/op/activity-failure.d.ts.map +1 -0
  187. package/dist/op/activity-profiles.d.ts +118 -0
  188. package/dist/op/activity-profiles.d.ts.map +1 -0
  189. package/dist/op/activity-registry.d.ts +22 -38
  190. package/dist/op/activity-registry.d.ts.map +1 -1
  191. package/dist/op/activity-runtime.d.ts +3 -14
  192. package/dist/op/activity-runtime.d.ts.map +1 -1
  193. package/dist/op/builders.d.ts +84 -29
  194. package/dist/op/builders.d.ts.map +1 -1
  195. package/dist/op/composites/apply-op.d.ts +113 -0
  196. package/dist/op/composites/apply-op.d.ts.map +1 -0
  197. package/dist/op/composites/converge-op.d.ts +120 -0
  198. package/dist/op/composites/converge-op.d.ts.map +1 -0
  199. package/dist/op/composites/index.d.ts +26 -0
  200. package/dist/op/composites/index.d.ts.map +1 -0
  201. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  202. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  203. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  204. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  205. package/dist/op/composites/reconcile-op.d.ts +57 -0
  206. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  207. package/dist/op/composites/watch-op.d.ts +60 -0
  208. package/dist/op/composites/watch-op.d.ts.map +1 -0
  209. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  210. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  211. package/dist/op/converge-rule.d.ts +12 -10
  212. package/dist/op/converge-rule.d.ts.map +1 -1
  213. package/dist/op/cron.d.ts +51 -0
  214. package/dist/op/cron.d.ts.map +1 -0
  215. package/dist/op/discover.d.ts +9 -0
  216. package/dist/op/discover.d.ts.map +1 -1
  217. package/dist/op/duration.d.ts +11 -0
  218. package/dist/op/duration.d.ts.map +1 -0
  219. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  220. package/dist/op/gate.d.ts +90 -0
  221. package/dist/op/gate.d.ts.map +1 -0
  222. package/dist/op/generate-pipeline.d.ts +9 -0
  223. package/dist/op/generate-pipeline.d.ts.map +1 -1
  224. package/dist/op/index.d.ts +23 -7
  225. package/dist/op/index.d.ts.map +1 -1
  226. package/dist/op/local-executor.d.ts +114 -28
  227. package/dist/op/local-executor.d.ts.map +1 -1
  228. package/dist/op/local-output.d.ts +11 -1
  229. package/dist/op/local-output.d.ts.map +1 -1
  230. package/dist/op/op-ir.d.ts +184 -0
  231. package/dist/op/op-ir.d.ts.map +1 -0
  232. package/dist/op/op-verb-class.d.ts +10 -2
  233. package/dist/op/op-verb-class.d.ts.map +1 -1
  234. package/dist/op/operator.d.ts +30 -5
  235. package/dist/op/operator.d.ts.map +1 -1
  236. package/dist/op/receipt-store.d.ts +1 -1
  237. package/dist/op/receipt-store.d.ts.map +1 -1
  238. package/dist/op/resource.d.ts +15 -1
  239. package/dist/op/resource.d.ts.map +1 -1
  240. package/dist/op/runtime.d.ts +200 -0
  241. package/dist/op/runtime.d.ts.map +1 -0
  242. package/dist/op/runtimes/local.d.ts +29 -0
  243. package/dist/op/runtimes/local.d.ts.map +1 -0
  244. package/dist/op/step-output-ref.d.ts +15 -13
  245. package/dist/op/step-output-ref.d.ts.map +1 -1
  246. package/dist/op/types.d.ts +79 -27
  247. package/dist/op/types.d.ts.map +1 -1
  248. package/dist/ownership.d.ts +1 -1
  249. package/dist/provenance.d.ts +21 -0
  250. package/dist/provenance.d.ts.map +1 -1
  251. package/dist/serializer-walker.d.ts.map +1 -1
  252. package/dist/stack-output.d.ts +5 -0
  253. package/dist/stack-output.d.ts.map +1 -1
  254. package/package.json +6 -1
  255. package/src/audit/catalog.test.ts +79 -3
  256. package/src/audit/catalog.ts +111 -13
  257. package/src/audit/core.test.ts +26 -0
  258. package/src/audit/core.ts +61 -17
  259. package/src/audit/discover.test.ts +39 -0
  260. package/src/audit/discover.ts +91 -15
  261. package/src/audit/lineage.ts +100 -0
  262. package/src/audit/prior-art.ts +153 -0
  263. package/src/audit/proof.test.ts +67 -0
  264. package/src/audit/proof.ts +70 -0
  265. package/src/audit/report-html.ts +9 -4
  266. package/src/audit/report-model.ts +16 -2
  267. package/src/audit/report.test.ts +33 -0
  268. package/src/audit/report.ts +5 -3
  269. package/src/audit/rules-doc.test.ts +34 -2
  270. package/src/audit/rules-doc.ts +82 -9
  271. package/src/audit/secrets.ts +15 -3
  272. package/src/audit/terraform-state.test.ts +144 -0
  273. package/src/audit/terraform-state.ts +131 -0
  274. package/src/build.ts +31 -1
  275. package/src/claimed-fields.test.ts +145 -0
  276. package/src/claimed-fields.ts +208 -0
  277. package/src/cli/build-params-cli.ts +2 -2
  278. package/src/cli/command-group.ts +14 -0
  279. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  280. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  281. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  282. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  283. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  284. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  285. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  286. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  287. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  288. package/src/cli/commands/audit.test.ts +189 -11
  289. package/src/cli/commands/audit.ts +73 -27
  290. package/src/cli/commands/build.test.ts +292 -9
  291. package/src/cli/commands/build.ts +156 -24
  292. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  293. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  294. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  295. package/src/cli/commands/import.ts +6 -0
  296. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  297. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  298. package/src/cli/commands/init-lexicon.test.ts +97 -2
  299. package/src/cli/commands/init-lexicon.ts +4 -2
  300. package/src/cli/commands/init.test.ts +35 -1
  301. package/src/cli/commands/init.ts +14 -3
  302. package/src/cli/commands/lint.test.ts +48 -0
  303. package/src/cli/commands/lint.ts +123 -0
  304. package/src/cli/commands/onboard.test.ts +75 -0
  305. package/src/cli/commands/onboard.ts +54 -23
  306. package/src/cli/handlers/build.test.ts +81 -1
  307. package/src/cli/handlers/build.ts +15 -1
  308. package/src/cli/handlers/graph.test.ts +3 -3
  309. package/src/cli/handlers/lifecycle.test.ts +195 -12
  310. package/src/cli/handlers/lifecycle.ts +123 -6
  311. package/src/cli/handlers/misc.ts +1 -1
  312. package/src/cli/handlers/operator.test.ts +183 -11
  313. package/src/cli/handlers/operator.ts +260 -55
  314. package/src/cli/handlers/run.test.ts +431 -1287
  315. package/src/cli/handlers/run.ts +365 -915
  316. package/src/cli/main.test.ts +23 -15
  317. package/src/cli/main.ts +88 -33
  318. package/src/cli/mcp/op-tools.ts +147 -88
  319. package/src/cli/mcp/resource-handlers.ts +10 -37
  320. package/src/cli/mcp/resources/context.ts +11 -12
  321. package/src/cli/mcp/server.test.ts +25 -27
  322. package/src/cli/mcp/server.ts +2 -2
  323. package/src/cli/registry.ts +26 -9
  324. package/src/codegen/docs.ts +1 -1
  325. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  326. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  327. package/src/components/README.md +3 -2
  328. package/src/components/auto-release.test.ts +4 -20
  329. package/src/components/auto-release.ts +17 -42
  330. package/src/components/capability.ts +4 -4
  331. package/src/components/cli-support.test.ts +10 -5
  332. package/src/components/cli-support.ts +52 -44
  333. package/src/components/component.schema.json +2 -2
  334. package/src/components/component.ts +3 -3
  335. package/src/components/driver-output.ts +14 -2
  336. package/src/components/driver.test.ts +92 -22
  337. package/src/components/driver.ts +193 -83
  338. package/src/components/index.ts +1 -6
  339. package/src/components/manifest-persistence.test.ts +2 -20
  340. package/src/components/manifest-persistence.ts +4 -24
  341. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  342. package/src/components/run-progress.ts +6 -6
  343. package/src/components/verbs/run-agent.ts +11 -17
  344. package/src/components/verbs/verify.ts +2 -2
  345. package/src/deep-observation.test.ts +75 -0
  346. package/src/deep-observation.ts +48 -0
  347. package/src/discovery/collect.test.ts +3 -3
  348. package/src/discovery/files.ts +4 -4
  349. package/src/discovery/fold-import.ts +105 -2
  350. package/src/discovery/param-deps.test.ts +112 -1
  351. package/src/discovery/param-deps.ts +193 -0
  352. package/src/discovery/sandbox/bundle.ts +15 -1
  353. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  354. package/src/discovery/sandbox/config-wire.ts +2 -2
  355. package/src/fold/fold.test.ts +56 -0
  356. package/src/fold/foldable-helpers.ts +81 -1
  357. package/src/fold-provenance-exclusion.test.ts +186 -0
  358. package/src/fold-provenance.test.ts +495 -0
  359. package/src/fold-provenance.ts +441 -0
  360. package/src/graph-ops.test.ts +2 -2
  361. package/src/graph-ops.ts +1 -1
  362. package/src/held-elsewhere.test.ts +102 -0
  363. package/src/held-elsewhere.ts +114 -0
  364. package/src/import/ir-utils.ts +11 -1
  365. package/src/import/parser.ts +35 -0
  366. package/src/index.ts +3 -0
  367. package/src/kubectl-context.ts +3 -3
  368. package/src/lexicon-config.test.ts +5 -5
  369. package/src/lexicon-config.ts +4 -4
  370. package/src/lexicon-plugin-helpers.ts +1 -1
  371. package/src/lexicon.ts +120 -14
  372. package/src/lifecycle/change-set.test.ts +49 -0
  373. package/src/lifecycle/change-set.ts +67 -0
  374. package/src/lifecycle/converge-ledger.ts +1 -1
  375. package/src/lifecycle/deep-diff.test.ts +267 -14
  376. package/src/lifecycle/deep-diff.ts +217 -14
  377. package/src/lifecycle/deep-observe.test.ts +3 -1
  378. package/src/lifecycle/deep-observe.ts +16 -6
  379. package/src/lifecycle/gate-ledger.test.ts +74 -2
  380. package/src/lifecycle/gate-ledger.ts +143 -36
  381. package/src/lifecycle/index.ts +1 -0
  382. package/src/lifecycle/release-ledger.ts +2 -2
  383. package/src/lifecycle/run-ledger.test.ts +268 -0
  384. package/src/lifecycle/run-ledger.ts +201 -0
  385. package/src/lifecycle/symptoms.ts +3 -3
  386. package/src/lint/config.test.ts +82 -1
  387. package/src/lint/config.ts +112 -3
  388. package/src/lint/policy.test.ts +1 -1
  389. package/src/lint/post-synth.test.ts +31 -0
  390. package/src/lint/post-synth.ts +54 -1
  391. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  392. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  393. package/src/lint/rules/comp/comp.test.ts +1 -1
  394. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +74 -0
  395. package/src/lint/rules/comp/index.ts +3 -3
  396. package/src/lint/rules/op/index.ts +23 -0
  397. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  398. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  399. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  400. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  401. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  402. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  403. package/src/lint/rules/op/support.ts +11 -0
  404. package/src/lint/suppressions.test.ts +163 -0
  405. package/src/lint/suppressions.ts +204 -0
  406. package/src/live-endpoint.ts +1 -1
  407. package/src/op/activities/activity-contracts.ts +66 -0
  408. package/src/op/activities/apply.test.ts +567 -0
  409. package/src/op/activities/apply.ts +662 -0
  410. package/src/op/activities/build.ts +26 -0
  411. package/src/op/activities/converge.test.ts +330 -0
  412. package/src/op/activities/converge.ts +449 -0
  413. package/src/op/activities/env-teardown.test.ts +187 -0
  414. package/src/op/activities/env-teardown.ts +171 -0
  415. package/src/op/activities/guard-validate.test.ts +139 -0
  416. package/src/op/activities/guard-validate.ts +223 -0
  417. package/src/op/activities/http-check.test.ts +48 -0
  418. package/src/op/activities/http-check.ts +55 -0
  419. package/src/op/activities/index.ts +119 -0
  420. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  421. package/src/op/activities/lexicon-upgrade.ts +673 -0
  422. package/src/op/activities/lifecycle.ts +85 -0
  423. package/src/op/activities/pipeline-audit.test.ts +67 -0
  424. package/src/op/activities/pipeline-audit.ts +201 -0
  425. package/src/op/activities/policy.test.ts +19 -0
  426. package/src/op/activities/policy.ts +37 -0
  427. package/src/op/activities/reconcile.test.ts +81 -0
  428. package/src/op/activities/reconcile.ts +187 -0
  429. package/src/op/activities/shell.ts +26 -0
  430. package/src/op/activities/teardown.ts +22 -0
  431. package/src/op/activities/util.ts +6 -0
  432. package/src/op/activities/wait.ts +69 -0
  433. package/src/op/activities/workflow-audit.test.ts +90 -0
  434. package/src/op/activities/workflow-audit.ts +203 -0
  435. package/src/op/activity-contract-registry.test.ts +98 -0
  436. package/src/op/activity-contract-registry.ts +114 -0
  437. package/src/op/activity-contract.test.ts +1 -1
  438. package/src/op/activity-contract.ts +37 -33
  439. package/src/op/activity-failure.ts +34 -0
  440. package/src/op/activity-profiles.test.ts +56 -0
  441. package/src/op/activity-profiles.ts +116 -0
  442. package/src/op/activity-registry.test.ts +37 -28
  443. package/src/op/activity-registry.ts +27 -56
  444. package/src/op/activity-runtime.ts +3 -47
  445. package/src/op/builders-exports.test.ts +0 -49
  446. package/src/op/builders-typed.test.ts +164 -0
  447. package/src/op/builders.ts +123 -39
  448. package/src/op/composites/apply-op.ts +208 -0
  449. package/src/op/composites/composites.test.ts +366 -0
  450. package/src/op/composites/converge-op.test.ts +168 -0
  451. package/src/op/composites/converge-op.ts +214 -0
  452. package/src/op/composites/index.ts +26 -0
  453. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  454. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  455. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  456. package/src/op/composites/pipeline-audit-op.ts +78 -0
  457. package/src/op/composites/reconcile-op.test.ts +51 -0
  458. package/src/op/composites/reconcile-op.ts +107 -0
  459. package/src/op/composites/watch-op.ts +108 -0
  460. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  461. package/src/op/composites/workflow-audit-op.ts +88 -0
  462. package/src/op/converge-rule.ts +12 -10
  463. package/src/op/cron.test.ts +128 -0
  464. package/src/op/cron.ts +149 -0
  465. package/src/op/discover-root.test.ts +105 -1
  466. package/src/op/discover.test.ts +6 -2
  467. package/src/op/discover.ts +84 -16
  468. package/src/op/duration.ts +21 -0
  469. package/src/op/effect-step.test.ts +26 -11
  470. package/src/op/emulator-lifecycle.ts +1 -2
  471. package/src/op/gate.ts +155 -0
  472. package/src/op/generate-pipeline.test.ts +65 -3
  473. package/src/op/generate-pipeline.ts +19 -2
  474. package/src/op/index.ts +39 -6
  475. package/src/op/local-executor.test.ts +163 -34
  476. package/src/op/local-executor.ts +370 -102
  477. package/src/op/local-output.test.ts +95 -11
  478. package/src/op/local-output.ts +33 -4
  479. package/src/op/op-ir.test.ts +281 -0
  480. package/src/op/op-ir.ts +417 -0
  481. package/src/op/op-verb-class.ts +19 -5
  482. package/src/op/op.test.ts +26 -5
  483. package/src/op/operator.test.ts +175 -6
  484. package/src/op/operator.ts +63 -16
  485. package/src/op/receipt-store.ts +1 -1
  486. package/src/op/resource.ts +20 -2
  487. package/src/op/runtime.ts +203 -0
  488. package/src/op/runtimes/local.test.ts +181 -0
  489. package/src/op/runtimes/local.ts +212 -0
  490. package/src/op/step-output-ref.ts +15 -13
  491. package/src/op/types.ts +84 -24
  492. package/src/ownership.ts +1 -1
  493. package/src/provenance.test.ts +6 -0
  494. package/src/provenance.ts +21 -1
  495. package/src/serializer-walker.test.ts +42 -0
  496. package/src/serializer-walker.ts +21 -2
  497. package/src/stack-output.ts +6 -1
  498. package/dist/cli/handlers/op-progress.d.ts +0 -90
  499. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  500. package/dist/cli/handlers/run-client.d.ts +0 -127
  501. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  502. package/dist/cli/handlers/run-report.d.ts +0 -5
  503. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  504. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  505. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  506. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  507. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  508. package/src/cli/handlers/op-progress.test.ts +0 -268
  509. package/src/cli/handlers/op-progress.ts +0 -248
  510. package/src/cli/handlers/run-client.test.ts +0 -82
  511. package/src/cli/handlers/run-client.ts +0 -219
  512. package/src/cli/handlers/run-report.test.ts +0 -62
  513. package/src/cli/handlers/run-report.ts +0 -122
  514. package/src/components/temporal-codegen-loader.ts +0 -41
  515. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -1,156 +1,188 @@
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 { createLocalOpRuntime } from "../../op/runtimes/local";
10
+ import type { OpRuntimeProvider, OpRunStatus } from "../../op/runtime";
10
11
  import { renderHuman, renderJson } from "../../op/local-output";
12
+ import { loadPlugins } from "../plugins";
13
+ import { recordGateApproval } from "./operator";
11
14
  import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
12
15
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
13
16
  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
17
  import { renderDriverHuman, renderDriverJson } from "../../components/driver-output";
28
18
  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";
19
+ import { maybeRecordAutoRelease } from "../../components/auto-release";
20
+ import { maybePersistBuildManifest } from "../../components/manifest-persistence";
33
21
  import type { DriverComponentResult } from "../../components/driver";
34
- import type { BuildParamProvenance } from "../../provenance";
35
22
 
36
- function kebabToCamel(s: string): string {
37
- return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
38
- }
39
-
40
- function workflowFnName(opName: string): string {
41
- return kebabToCamel(opName) + "Workflow";
42
- }
23
+ /**
24
+ * The exit code a run that stopped at an unapproved gate uses (#2119).
25
+ * Deliberately not 1: a gate is a standing fact waiting on a human, and a CI
26
+ * job that treats it as a failure would page someone for a decision nobody has
27
+ * made yet.
28
+ */
29
+ export const GATED_EXIT_CODE = 3;
43
30
 
44
31
  /**
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.
32
+ * `run list/status/log/cancel --components` reported a component's *durable*
33
+ * run state — a run that outlives the CLI process and can be queried, signalled
34
+ * or cancelled afterwards (#589). #2116 removed the runtime that provided it.
35
+ * The local driver runs a component to completion inside this process, so there
36
+ * is no separate run left to ask about once the command returns.
37
+ *
38
+ * Returns 1; the caller returns it straight back.
49
39
  */
50
- function requireTemporalMode(ctx: CommandContext, what: string): boolean {
51
- if (ctx.args.temporal) return true;
40
+ function refuseDurableComponentSubcommand(what: string, hint: string): number {
52
41
  console.error(formatError({
53
- message: `\`${what}\` is not available in local mode`,
54
- hint: "pass --temporal or configure a profile",
42
+ message: `\`${what}\` read a component's durable run state, which #2116 removed`,
43
+ hint,
55
44
  }));
56
- return false;
45
+ return 1;
57
46
  }
58
47
 
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
- }
48
+ // ── The runtime seam (#2121) ────────────────────────────────────────────────
67
49
 
68
50
  /**
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).
51
+ * Resolve the one runtime this invocation talks to.
52
+ *
53
+ * `--on <name>` picks the named lexicon's `opRuntime` (`../../lexicon.ts`);
54
+ * without it, core's built-in `local` provider runs the Op in this process.
55
+ * Every `chant run` subcommand goes through whichever comes back, so there is
56
+ * one dispatch path rather than a branch per runtime.
57
+ *
58
+ * A lexicon already loaded into the command context wins the lookup — that is
59
+ * how a test hands in a stub, and how a command that loaded plugins for its
60
+ * own reasons avoids loading them twice. Otherwise the project's configured
61
+ * lexicons are loaded on demand: `chant run` is not a `requiresPlugins`
62
+ * command, and a plain local run must not start paying for plugin resolution.
63
+ *
64
+ * Returns `undefined` after printing an actionable error — an unconfigured
65
+ * name is answered with the configured list, a configured lexicon that hosts
66
+ * nothing is named outright.
75
67
  */
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
- }
68
+ async function resolveOpRuntime(ctx: CommandContext): Promise<OpRuntimeProvider | undefined> {
69
+ const on = ctx.args.on;
70
+ if (!on || on === "local") return createLocalOpRuntime({ projectPath: resolve(".") });
71
+
72
+ // Read the configured list straight from `chant.config.ts` rather than
73
+ // through `resolveProjectLexicons`: `--on` names a *configured* lexicon, and
74
+ // that helper's fallback is a source scan of the whole project, which is a
75
+ // long wait to be told a name is wrong.
76
+ let configured: string[] = [];
77
+ try {
78
+ configured = (await loadChantConfig(resolve("."))).config.lexicons ?? [];
79
+ } catch {
80
+ // No/unreadable chant.config.ts — the error below says so by listing nothing.
81
+ }
82
+
83
+ let plugin = ctx.plugins.find((p) => p.name === on);
84
+ if (!plugin && configured.includes(on)) {
85
+ plugin = (await loadPlugins([on]).catch(() => []))[0];
86
+ }
87
+
88
+ if (!plugin) {
89
+ const known = [...new Set([...ctx.plugins.map((p) => p.name), ...configured])];
90
+ console.error(formatError({
91
+ message: `--on ${on}: "${on}" is not a configured lexicon`,
92
+ hint: known.length > 0
93
+ ? `Configured lexicons: ${known.join(", ")}. Omit --on to run on the built-in local runtime.`
94
+ : "chant.config.ts configures no lexicons. Omit --on to run on the built-in local runtime.",
95
+ }));
96
+ return undefined;
91
97
  }
98
+
99
+ if (!plugin.opRuntime) {
100
+ console.error(formatError({
101
+ message: `--on ${on}: lexicon "${on}" does not host Op runs`,
102
+ hint: "It declares no opRuntime. Omit --on to run on the built-in local runtime.",
103
+ }));
104
+ return undefined;
105
+ }
106
+
107
+ return plugin.opRuntime;
108
+ }
109
+
110
+ /** An ISO-8601 instant trimmed to what a table cell has room for. */
111
+ function shortInstant(iso: string | undefined): string {
112
+ return iso ? iso.slice(0, 19).replace("T", " ") : "—";
113
+ }
114
+
115
+ /** Print what a runtime reports for a settled or in-flight run. */
116
+ function renderRuntimeStatus(label: string, name: string, runtime: string, status: OpRunStatus): void {
117
+ console.log(formatBold(`${label}: ${name}`));
118
+ console.log(` Runtime : ${runtime}`);
119
+ console.log(` Run ID : ${status.runId}`);
120
+ console.log(` State : ${status.state}`);
121
+ console.log(` Started : ${shortInstant(status.startedAt)}`);
122
+ if (status.endedAt) console.log(` Ended : ${shortInstant(status.endedAt)}`);
123
+ if (status.records) {
124
+ const settled = status.records.filter((r) => r.status !== "skipped").length;
125
+ console.log(` Steps : ${settled}/${status.records.length} settled`);
126
+ }
127
+ if (status.gate) {
128
+ console.log(` Gate : ${status.gate.name} (pending since ${status.gate.since})`);
129
+ }
130
+ if (status.error) console.log(` Error : ${status.error}`);
92
131
  }
93
132
 
94
133
  // ── chant run list ──────────────────────────────────���─────────────────────────
95
134
 
96
135
  export async function runOpList(ctx: CommandContext): Promise<number> {
97
- if (ctx.args.components) return runComponentsList(ctx);
136
+ if (ctx.args.components) return runComponentsList();
137
+ return runOpListOnRuntime(ctx);
138
+ }
98
139
 
99
- if (!requireTemporalMode(ctx, "chant run list")) return 1;
100
- const { ops, errors } = await discoverOps();
140
+ /**
141
+ * `chant run list` on the resolved runtime (#2121) — discover every Op, ask
142
+ * the runtime what it knows about each, print one row per Op. The runtime
143
+ * answers for all of them in one call (`list`), so a hosted runtime can do it
144
+ * in one round trip instead of N.
145
+ *
146
+ * An Op the runtime has no run for prints with no state annotation.
147
+ */
148
+ async function runOpListOnRuntime(ctx: CommandContext): Promise<number> {
149
+ const runtime = await resolveOpRuntime(ctx);
150
+ if (!runtime) return 1;
101
151
 
102
- for (const err of errors) {
103
- console.error(formatError({ message: err }));
104
- }
152
+ const { ops, errors } = await discoverOps();
153
+ for (const err of errors) console.error(formatError({ message: err }));
105
154
 
106
155
  if (ops.size === 0) {
107
156
  console.error(formatWarning({ message: "No Op definitions found (*.op.ts)" }));
108
157
  return 0;
109
158
  }
110
159
 
160
+ let states: Map<string, OpRunStatus | undefined>;
161
+ try {
162
+ states = await runtime.list([...ops.values()].map((d) => d.config));
163
+ } catch (err) {
164
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
165
+ return 1;
166
+ }
167
+
111
168
  console.log(
112
- "NAME".padEnd(22) +
169
+ "NAME".padEnd(26) +
113
170
  "PHASES".padEnd(8) +
114
- "TASK-QUEUE".padEnd(20) +
115
171
  "DEPENDS".padEnd(20) +
116
172
  "OVERVIEW",
117
173
  );
118
174
 
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
175
  for (const [name, { config }] of ops) {
141
176
  const phases = String(config.phases.length);
142
- const tq = config.taskQueue ?? config.name;
143
177
  const deps = config.depends?.join(",") ?? "—";
144
178
  const overview = config.overview.length > 36
145
179
  ? config.overview.slice(0, 33) + "..."
146
180
  : config.overview;
147
- const status = runStatus?.get(name);
148
- const statusStr = status ? ` [${status}]` : "";
181
+ const state = states.get(name)?.state;
149
182
 
150
183
  console.log(
151
- (name + statusStr).padEnd(22) +
184
+ (name + (state ? ` [${state}]` : "")).padEnd(26) +
152
185
  phases.padEnd(8) +
153
- tq.padEnd(20) +
154
186
  deps.padEnd(20) +
155
187
  overview,
156
188
  );
@@ -160,89 +192,21 @@ export async function runOpList(ctx: CommandContext): Promise<number> {
160
192
  }
161
193
 
162
194
  /**
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.
195
+ * `chant run list --components` (#599) listed discovered components annotated
196
+ * with each one's durable run status. Discovery alone is what `chant list
197
+ * --components` already prints, so with the status column gone (#2116) this
198
+ * subcommand has nothing of its own left to say.
179
199
  */
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;
200
+ function runComponentsList(): Promise<number> {
201
+ return Promise.resolve(refuseDurableComponentSubcommand(
202
+ "chant run list --components",
203
+ "Run `chant list --components` to see discovered components.",
204
+ ));
240
205
  }
241
206
 
242
207
  // ── chant run status <name> ───────────────────────────────────────────────────
243
208
 
244
209
  export async function runOpStatus(ctx: CommandContext): Promise<number> {
245
- if (!requireTemporalMode(ctx, "chant run status")) return 1;
246
210
  const name = ctx.args.extraPositional;
247
211
  if (!name) {
248
212
  const label = ctx.args.components ? "Component" : "Op";
@@ -250,84 +214,44 @@ export async function runOpStatus(ctx: CommandContext): Promise<number> {
250
214
  return 1;
251
215
  }
252
216
 
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})`);
217
+ if (ctx.args.components) {
218
+ return refuseDurableComponentSubcommand(
219
+ "chant run status --components",
220
+ "Run `chant components status` for a component's release and build state.",
221
+ );
286
222
  }
287
-
288
- return 0;
223
+ return runOpStatusOnRuntime(ctx, name);
289
224
  }
290
225
 
291
226
  /**
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).
227
+ * `chant run status <name>` on the resolved runtime (#2121). A runtime with
228
+ * no record of the Op is not an error: it prints one line saying so and exits
229
+ * 0, because "this runtime has never run it" is a true answer to the question.
297
230
  */
298
- async function runComponentStatus(ctx: CommandContext, name: string): Promise<number> {
299
- const projectPath = resolve(".");
300
- let client, desc: WorkflowExecutionDescription, history: WorkflowHistoryRaw;
231
+ async function runOpStatusOnRuntime(ctx: CommandContext, name: string): Promise<number> {
232
+ const runtime = await resolveOpRuntime(ctx);
233
+ if (!runtime) return 1;
234
+
235
+ let status: OpRunStatus | undefined;
301
236
  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);
237
+ status = await runtime.status(name);
306
238
  } catch (err) {
307
239
  console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
308
240
  return 1;
309
241
  }
310
242
 
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`);
243
+ if (!status) {
244
+ console.error(formatInfo(`No run of Op "${name}" is recorded on the "${runtime.name}" runtime.`));
245
+ return 0;
322
246
  }
323
247
 
248
+ renderRuntimeStatus("Op", name, runtime.name, status);
324
249
  return 0;
325
250
  }
326
251
 
327
252
  // ── chant run log <name> ──────────────────────────────────────────────────────
328
253
 
329
254
  export async function runOpLog(ctx: CommandContext): Promise<number> {
330
- if (!requireTemporalMode(ctx, "chant run log")) return 1;
331
255
  const name = ctx.args.extraPositional;
332
256
  if (!name) {
333
257
  const label = ctx.args.components ? "Component" : "Op";
@@ -335,151 +259,128 @@ export async function runOpLog(ctx: CommandContext): Promise<number> {
335
259
  return 1;
336
260
  }
337
261
 
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;
262
+ if (ctx.args.components) {
263
+ return refuseDurableComponentSubcommand(
264
+ "chant run log --components",
265
+ "Run `chant components status` for a component's recorded releases.",
266
+ );
371
267
  }
372
-
373
- return 0;
268
+ return runOpLogOnRuntime(ctx, name);
374
269
  }
375
270
 
376
271
  /**
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.
272
+ * `chant run log <name>` on the resolved runtime (#2121) — the runtime's own
273
+ * run history, newest first, `--limit <n>` rows at most.
385
274
  */
386
- async function runComponentLog(ctx: CommandContext, name: string): Promise<number> {
387
- const projectPath = resolve(".");
388
- let client;
275
+ async function runOpLogOnRuntime(ctx: CommandContext, name: string): Promise<number> {
276
+ const runtime = await resolveOpRuntime(ctx);
277
+ if (!runtime) return 1;
278
+
279
+ let records;
389
280
  try {
390
- ({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
281
+ records = await runtime.log(name, ctx.args.limit === undefined ? undefined : { limit: ctx.args.limit });
391
282
  } catch (err) {
392
283
  console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
393
284
  return 1;
394
285
  }
395
286
 
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;
287
+ if (records.length === 0) {
288
+ console.error(formatInfo(`No run of Op "${name}" is recorded on the "${runtime.name}" runtime.`));
289
+ return 0;
403
290
  }
404
291
 
405
292
  console.log(
406
- "RUN-ID".padEnd(36) +
407
- "STATUS".padEnd(16) +
293
+ "RUN-ID".padEnd(38) +
294
+ "STATUS".padEnd(10) +
408
295
  "STARTED".padEnd(26) +
409
- "CLOSED",
296
+ "ENDED",
410
297
  );
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;
298
+ for (const record of records) {
299
+ console.log(
300
+ record.id.padEnd(38) +
301
+ record.status.padEnd(10) +
302
+ shortInstant(record.started).padEnd(26) +
303
+ shortInstant(record.ended),
304
+ );
426
305
  }
427
306
 
428
307
  return 0;
429
308
  }
430
309
 
431
- // ── chant run signal <name> <signal> ─────────────────────────────────────────
310
+ // ── chant run approve <op> <gate> ────────────────────────────────────────────
432
311
 
433
312
  /**
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.
313
+ * `chant run approve <op> <gate> [--approver] [--url] [--note]` (#2121) — the
314
+ * rename of `chant run signal`, and a different act.
315
+ *
316
+ * A gate resolution is a fact, not a message: the ledger write is the whole
317
+ * of it, and it is the same write `chant approve` performs
318
+ * (`recordGateApproval`, ./operator.ts), so a resolution recorded here reads
319
+ * back identically. The runtime is told afterwards, through its optional
320
+ * `resolveGate`, purely so a runtime that parks a run mid-flight can wake it
321
+ * now rather than on its next tick. A runtime without that method is not a
322
+ * failure — the fact is recorded either way, and the next run re-reads it.
441
323
  */
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>" }));
324
+ export async function runOpApprove(ctx: CommandContext): Promise<number> {
325
+ const opName = ctx.args.extraPositional;
326
+ const gate = ctx.args.extraPositional2;
327
+ if (!opName || !gate) {
328
+ console.error(formatError({ message: "Usage: chant run approve <op> <gate>" }));
449
329
  return 1;
450
330
  }
451
331
 
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;
332
+ const runtime = await resolveOpRuntime(ctx);
333
+ if (!runtime) return 1;
334
+
335
+ const outcome = await recordGateApproval(opName, gate, {
336
+ actor: ctx.args.approver ?? ctx.args.actor,
337
+ note: ctx.args.note,
338
+ url: ctx.args.url,
339
+ });
340
+ if (!outcome.ok) return 1;
341
+
342
+ if (!runtime.resolveGate) {
343
+ console.error(formatInfo(
344
+ `The "${runtime.name}" runtime reads the resolution when the op next runs — re-run \`chant run ${opName}\`.`,
345
+ ));
346
+ return 0;
347
+ }
348
+
462
349
  try {
463
- const { client } = await makeTemporalClient(ctx.args.profile, projectPath);
464
- handle = client.workflow.getHandle(workflowId);
465
- await handle.signal(signalName, ...(approver ? [{ approver }] : []));
350
+ await runtime.resolveGate(opName, gate, outcome.record);
466
351
  } catch (err) {
467
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
352
+ console.error(formatWarning({
353
+ message:
354
+ `The resolution is recorded, but the "${runtime.name}" runtime could not be woken: ` +
355
+ (err instanceof Error ? err.message : String(err)),
356
+ hint: `Re-run \`chant run ${opName} --on ${runtime.name}\` once the runtime is reachable.`,
357
+ }));
468
358
  return 1;
469
359
  }
470
360
 
471
- console.error(formatSuccess(
472
- `Signal "${signalName}" sent to ${ctx.args.components ? "component" : "Op"} "${name}"` +
473
- (approver ? ` (approver: ${approver})` : ""),
474
- ));
361
+ console.error(formatSuccess(`Runtime "${runtime.name}" was notified of the resolution.`));
475
362
  return 0;
476
363
  }
477
364
 
365
+ /**
366
+ * `chant run signal` was renamed `chant run approve` (#2121). Registered so
367
+ * the old spelling says where the verb went instead of being read as an Op
368
+ * named "signal".
369
+ */
370
+ export function runOpSignalRenamed(ctx: CommandContext): Promise<number> {
371
+ const opName = ctx.args.extraPositional ?? "<op>";
372
+ const gate = ctx.args.extraPositional2 ?? "<gate>";
373
+ console.error(formatError({
374
+ message: "`chant run signal` is now `chant run approve`",
375
+ hint: `A gate is resolved by recording the fact, not by sending a message: chant run approve ${opName} ${gate}`,
376
+ }));
377
+ return Promise.resolve(1);
378
+ }
379
+
478
380
  // ── chant run cancel <name> ───────────────────────────────────────────────────
479
381
 
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. */
382
+ /** `chant run cancel <name>` — asks the resolved runtime (#2121) to stop the Op's active run. */
481
383
  export async function runOpCancel(ctx: CommandContext): Promise<number> {
482
- if (!requireTemporalMode(ctx, "chant run cancel")) return 1;
483
384
  const name = ctx.args.extraPositional;
484
385
  if (!name) {
485
386
  console.error(formatError({ message: "Op name is required: chant run cancel <name>" }));
@@ -488,65 +389,52 @@ export async function runOpCancel(ctx: CommandContext): Promise<number> {
488
389
 
489
390
  if (!ctx.args.force) {
490
391
  console.error(formatWarning({
491
- message: `Cancelling "${name}" will stop the active workflow run`,
392
+ message: `Cancelling "${name}" will stop the active run`,
492
393
  hint: "Use --force to confirm cancellation",
493
394
  }));
494
395
  return 1;
495
396
  }
496
397
 
497
- const projectPath = resolve(".");
498
- const workflowId = ctx.args.components ? componentWorkflowId(name) : resolveWorkflowId(name);
499
- let handle: WorkflowHandleRaw;
398
+ if (ctx.args.components) {
399
+ return refuseDurableComponentSubcommand(
400
+ "chant run cancel --components",
401
+ "A component run lives and dies with the `chant run --components` process — interrupt that instead.",
402
+ );
403
+ }
404
+
405
+ return runOpCancelOnRuntime(ctx, name);
406
+ }
407
+
408
+ /**
409
+ * `chant run cancel <name>` on the resolved runtime (#2121). `--force` is
410
+ * already checked by the caller, so a runtime only ever sees a confirmed
411
+ * request; a runtime with nothing to cancel says so in its own words and the
412
+ * command exits non-zero.
413
+ */
414
+ async function runOpCancelOnRuntime(ctx: CommandContext, name: string): Promise<number> {
415
+ const runtime = await resolveOpRuntime(ctx);
416
+ if (!runtime) return 1;
417
+
500
418
  try {
501
- const { client } = await makeTemporalClient(ctx.args.profile, projectPath);
502
- handle = client.workflow.getHandle(workflowId);
503
- await handle.cancel();
419
+ await runtime.cancel(name, { force: true });
504
420
  } catch (err) {
505
421
  console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
506
422
  return 1;
507
423
  }
508
424
 
509
- console.error(formatSuccess(`Cancellation requested for ${ctx.args.components ? "component" : "Op"} "${name}"`));
425
+ console.error(formatSuccess(`Cancellation requested for Op "${name}" on the "${runtime.name}" runtime`));
510
426
  return 0;
511
427
  }
512
428
 
513
429
  // ── chant run <name> — main command ───────────────────────────────────────────
514
430
 
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
431
  /**
545
432
  * `chant run <name>` dispatcher.
546
433
  *
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.
434
+ * Every run goes through the runtime resolved by `--on` (#2121); without it,
435
+ * core's built-in local provider runs the Op in this process, where a `gate`
436
+ * is decided against the gate ledger and ends the run pending approval
437
+ * (#2119), exit code 3.
550
438
  *
551
439
  * `chant run --components <name|all>` (#585) is a separate target: discovered
552
440
  * `Component` declarations dispatched through the interpret driver
@@ -555,22 +443,19 @@ function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
555
443
  * runComponentGraph(ctx)` branch (../handlers/graph.ts).
556
444
  *
557
445
  * 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.
446
+ * before that dispatch, and #2116 retired `--report` on the Op path too: it
447
+ * rendered a past durable run's workflow history, which no runtime keeps any
448
+ * more. Both refuse rather than falling through to a real dispatch — the
449
+ * component case was observed live reaching an actual cloud shell-out.
564
450
  */
565
451
  /**
566
452
  * Pre-flight for `chant run <op> --sandbox` on an Op containing a `policyGate`
567
453
  * step (chant #2003). `--sandbox` is a global flag, and `../main.ts` arms the
568
454
  * process-wide policy latch off it for every command — so the gate's
569
455
  * `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.
456
+ * maintainer ("This is a chant bug"), which is not an answer a user can act
457
+ * on. Refuse here instead, before anything runs, naming the combination and
458
+ * what to do about it.
574
459
  *
575
460
  * Keyed on the flag rather than on whether the project declares
576
461
  * `lint.policies`: the flag is what arms the latch, and a gate that builds the
@@ -597,18 +482,14 @@ export async function runOp(ctx: CommandContext): Promise<number> {
597
482
  return 1;
598
483
  }
599
484
  if (ctx.args.components) return runOpComponents(ctx);
600
- if (ctx.args.local && ctx.args.temporal) {
485
+ if (ctx.args.report) {
601
486
  console.error(formatError({
602
- message: "--local and --temporal are mutually exclusive",
603
- hint: "omit both for local mode (the default), or pass exactly one",
487
+ message: "`chant run --report` rendered a past durable run, which #2116 removed",
488
+ hint: "Run `chant run log <op>` for the runtime's own run history.",
604
489
  }));
605
490
  return 1;
606
491
  }
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);
492
+ return runOpOnRuntime(ctx);
612
493
  }
613
494
 
614
495
  // ── Auto-release recording post-run (#597) ──────────────────────────────────
@@ -689,28 +570,18 @@ async function recordAutoReleasesForRun(
689
570
  // ── chant run --components <name|all> ────────────────────────────────────────
690
571
 
691
572
  /**
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.
699
- *
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`.
573
+ * `chant run --components <name|all> [--env <env>]` (#585) — the interpret
574
+ * driver's CLI entrypoint, and since #2116 the only component run path there
575
+ * is. `args.path` is the component name (or `all`), matching `chant run
576
+ * <name>`'s Op-dispatch convention exactly (`args.path` is the Op name there
577
+ * too) rather than a project directory — components are always discovered from
578
+ * the current working directory, the same way Op discovery (`discoverOps()`)
579
+ * never takes a project-path argument either.
705
580
  *
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").
581
+ * The selector resolves through the runtime's `runComponents` and runs on the
582
+ * local in-process driver; a `gate` is decided against the gate ledger when
583
+ * the driver reaches it (#2119), and one nobody has approved ends the run with
584
+ * exit code 3 and the `chant approve` line, exactly as `chant run <op>` does.
714
585
  *
715
586
  * On a successful run, auto-emits one release-ledger record per component
716
587
  * that published a digest (#597, `recordAutoReleasesForRun` above) — a
@@ -723,8 +594,8 @@ async function recordAutoReleasesForRun(
723
594
  * chant #1108 — resolves this invocation's declared build-time parameters
724
595
  * (`chant.config.ts`'s `buildParams`, against `--param`/`--params-file`/a
725
596
  * 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.
597
+ * (`resolveCliBuildParams`, shared with `buildCommand`), BEFORE anything
598
+ * discovers/imports a `*.component.ts` file.
728
599
  * Before this, `params.*` (`@intentius/chant/params`) was always `{}` under
729
600
  * this command, no matter what a component's `chant.config.ts` declared or a
730
601
  * CI job's environment supplied — see chant #1108. `chant.config.ts` is
@@ -754,7 +625,15 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
754
625
  return 1;
755
626
  }
756
627
 
757
- if (ctx.args.temporal) return runComponentTemporal(ctx, selector, config, paramsResolution.provenance);
628
+ const runtime = await resolveOpRuntime(ctx);
629
+ if (!runtime) return 1;
630
+ if (!runtime.runComponents) {
631
+ console.error(formatError({
632
+ message: `--components is not supported on the "${runtime.name}" runtime`,
633
+ hint: "Omit --on to run components on the built-in local runtime.",
634
+ }));
635
+ return 1;
636
+ }
758
637
 
759
638
  const env = ctx.args.env ?? "local";
760
639
  // Seed cross-component/cross-stack outputs from upstream jobs' dumped files
@@ -778,7 +657,7 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
778
657
  // `undefined` and every `onProgress?.(...)` call in the driver is a no-op —
779
658
  // behavior is byte-for-byte unchanged from before this flag existed.
780
659
  const onProgress = ctx.args.progressJson ? ndjsonProgressSink() : undefined;
781
- const result = await runComponents(projectPath, selector, {
660
+ const result = await runtime.runComponents(projectPath, selector, {
782
661
  env: ctx.args.env,
783
662
  componentOutputs: seededOutputs,
784
663
  onProgress,
@@ -794,17 +673,7 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
794
673
  writeFileSync(dumpPath, JSON.stringify(result.run.componentOutputs, null, 2));
795
674
  }
796
675
 
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) {
676
+ if (!result.success && !result.run && !result.gated) {
808
677
  console.error(formatError({ message: result.error ?? "Failed to run component(s)" }));
809
678
  return 1;
810
679
  }
@@ -813,6 +682,19 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
813
682
  if (ctx.args.json) renderDriverJson(result.run); else renderDriverHuman(result.run);
814
683
  }
815
684
 
685
+ // Gated (#2119): the same fact-and-stop the Op path takes, and the same
686
+ // exit code. Nothing to release — the component stopped short of finishing.
687
+ if (result.gated) {
688
+ const { gate } = result.gated;
689
+ console.error(formatWarning({
690
+ message: `component "${result.gated.component}" is gated on "${gate.gate}" — pending approval`,
691
+ }));
692
+ console.error(formatInfo(`approve : ${approveCommand(gate.op, gate.gate)}`));
693
+ if (gate.url) console.error(formatInfo(`approve at: ${gate.url}`));
694
+ console.error(formatInfo(`expires : ${gate.expiresAt}`));
695
+ return GATED_EXIT_CODE;
696
+ }
697
+
816
698
  if (result.success && result.run) {
817
699
  const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord);
818
700
  await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
@@ -821,255 +703,24 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
821
703
  return result.success ? 0 : 1;
822
704
  }
823
705
 
824
- // ── chant run --components <name> --temporal (#589) ─────────────────────────
825
-
826
- function componentWorkflowId(componentName: string): string {
827
- return `chant-component-${componentName}`;
828
- }
829
-
830
706
  /**
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.
841
- *
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).
707
+ * `chant run <name>` on the resolved runtime (#2121) — the built-in `local`
708
+ * provider by default, a lexicon's `opRuntime` under `--on <name>`.
848
709
  *
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.
854
- */
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.
710
+ * This function knows nothing about how a run happens. It discovers the Op,
711
+ * runs the CLI-level pre-flights that are about flags rather than execution
712
+ * (`--sandbox` over a `policyGate` step, #2003), hands the config to the
713
+ * runtime, and renders whatever comes back. On the local runtime that is the
714
+ * executor's own `OpRunResult`, so `--json` and the human render are exactly
715
+ * what they were before the seam existed; a runtime that reports only a state
716
+ * gets the terser render instead.
1066
717
  */
1067
- export async function runOpLocal(ctx: CommandContext): Promise<number> {
718
+ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
1068
719
  const opName = ctx.args.path;
1069
720
  if (!opName || opName === ".") {
1070
721
  console.error(formatError({
1071
722
  message: "Op name is required: chant run <name>",
1072
- hint: "Run `chant run list --temporal` to see available Ops",
723
+ hint: "Run `chant run list` to see available Ops",
1073
724
  }));
1074
725
  return 1;
1075
726
  }
@@ -1094,32 +745,12 @@ export async function runOpLocal(ctx: CommandContext): Promise<number> {
1094
745
  // Pre-flight: --sandbox cannot cover a policyGate step (#2003).
1095
746
  if (refusesPolicyGateUnderSandbox(ctx, config, opName)) return 1;
1096
747
 
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
- }
1105
-
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
- }
748
+ const runtime = await resolveOpRuntime(ctx);
749
+ if (!runtime) return 1;
1115
750
 
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
- }
751
+ // `--progress-json` streams one NDJSON StepRecord per settled step, fed by
752
+ // whatever the runtime reports through `progress`.
753
+ const progress = ctx.args.progressJson ? ndjsonProgressSink<StepRecord>() : undefined;
1123
754
 
1124
755
  // Ctrl-C aborts in-flight activities (kills their child processes) instead of
1125
756
  // orphaning them. The handler is removed in `finally` so it never leaks.
@@ -1131,18 +762,38 @@ export async function runOpLocal(ctx: CommandContext): Promise<number> {
1131
762
  process.once("SIGINT", onSigint);
1132
763
 
1133
764
  try {
1134
- const result = await runOpLocally(config, activities, profiles, controller.signal);
1135
- if (ctx.args.json) renderJson(result); else renderHuman(result);
1136
- return 0;
765
+ const handle = await runtime.start(config, {
766
+ env: ctx.args.env,
767
+ ...(ctx.args.profile !== undefined ? { profile: ctx.args.profile } : {}),
768
+ progress,
769
+ signal: controller.signal,
770
+ });
771
+ const status = await handle.result();
772
+
773
+ if (status.result) {
774
+ // The executor's own render already prints a gate, its approve line
775
+ // and its expiry (#2119).
776
+ if (ctx.args.json) renderJson(status.result); else renderHuman(status.result);
777
+ } else {
778
+ renderRuntimeStatus("Op", opName, runtime.name, status);
779
+ if (status.state === "gated" && status.gate) {
780
+ console.error(formatWarning({
781
+ message: `Op "${opName}" is waiting on gate "${status.gate.name}"`,
782
+ hint: `Record the resolution with: ${approveCommand(opName, status.gate.name)}`,
783
+ }));
784
+ }
785
+ }
786
+
787
+ // Exit 3 for a gated run (#2119) — a distinct code so CI can tell
788
+ // "waiting on a human" from a broken op, and retry the one but not the
789
+ // other.
790
+ if (status.state === "gated") return GATED_EXIT_CODE;
791
+ return status.state === "completed" ? 0 : 1;
1137
792
  } catch (err) {
1138
793
  if (err instanceof OpRunFailure) {
1139
794
  if (ctx.args.json) renderJson(err.result); else renderHuman(err.result);
1140
795
  return 1;
1141
796
  }
1142
- if (err instanceof LocalGateUnsupportedError) {
1143
- console.error(formatError({ message: err.message }));
1144
- return 1;
1145
- }
1146
797
  console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1147
798
  return 1;
1148
799
  } finally {
@@ -1150,213 +801,12 @@ export async function runOpLocal(ctx: CommandContext): Promise<number> {
1150
801
  }
1151
802
  }
1152
803
 
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
804
  // ── fallback ────────────────────────────────────────────────────────────────���─
1355
805
 
1356
806
  export function runOpUnknown(ctx: CommandContext): Promise<number> {
1357
807
  console.error(formatError({
1358
808
  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",
809
+ hint: "Available: chant run <name>, run list, run status, run approve, run cancel, run log",
1360
810
  }));
1361
811
  return Promise.resolve(1);
1362
812
  }