@intentius/chant 0.90.0 → 0.93.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 (402) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/components.d.ts +8 -0
  4. package/dist/cli/handlers/components.d.ts.map +1 -1
  5. package/dist/cli/handlers/graph.d.ts.map +1 -1
  6. package/dist/cli/handlers/operator.d.ts +20 -0
  7. package/dist/cli/handlers/operator.d.ts.map +1 -1
  8. package/dist/cli/handlers/run.d.ts.map +1 -1
  9. package/dist/cli/main.d.ts.map +1 -1
  10. package/dist/cli/mcp/workspace-tools.d.ts +7 -4
  11. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  12. package/dist/cli/plugins.d.ts.map +1 -1
  13. package/dist/cli/registry.d.ts +42 -1
  14. package/dist/cli/registry.d.ts.map +1 -1
  15. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  16. package/dist/components/verbs/vuln-scan.d.ts +72 -0
  17. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  18. package/dist/config.d.ts +22 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/lexicon-module.d.ts +22 -0
  21. package/dist/lexicon-module.d.ts.map +1 -1
  22. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  23. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  24. package/dist/lifecycle/git.d.ts +40 -5
  25. package/dist/lifecycle/git.d.ts.map +1 -1
  26. package/dist/lifecycle/lease.d.ts +72 -16
  27. package/dist/lifecycle/lease.d.ts.map +1 -1
  28. package/dist/lifecycle/member-ledger.d.ts +3 -2
  29. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  30. package/dist/lifecycle/plan-ledger.d.ts +114 -0
  31. package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
  32. package/dist/lifecycle/run-ledger.d.ts +2 -0
  33. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  34. package/dist/lifecycle/symptoms.d.ts +51 -0
  35. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  36. package/dist/lifecycle/work-lease.d.ts +140 -0
  37. package/dist/lifecycle/work-lease.d.ts.map +1 -0
  38. package/dist/lint/rules/index.d.ts +4 -2
  39. package/dist/lint/rules/index.d.ts.map +1 -1
  40. package/dist/lint/rules/op/index.d.ts +14 -3
  41. package/dist/lint/rules/op/index.d.ts.map +1 -1
  42. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  43. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  44. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  45. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  46. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  47. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  48. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  49. package/dist/op/activities/activity-contracts.d.ts +203 -2
  50. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  51. package/dist/op/activities/apply.d.ts.map +1 -1
  52. package/dist/op/activities/change-coverage.d.ts +44 -0
  53. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  54. package/dist/op/activities/converge.d.ts +24 -1
  55. package/dist/op/activities/converge.d.ts.map +1 -1
  56. package/dist/op/activities/decide.d.ts +107 -0
  57. package/dist/op/activities/decide.d.ts.map +1 -0
  58. package/dist/op/activities/index.d.ts +10 -0
  59. package/dist/op/activities/index.d.ts.map +1 -1
  60. package/dist/op/activities/shell.d.ts +32 -0
  61. package/dist/op/activities/shell.d.ts.map +1 -1
  62. package/dist/op/activities/source-release.d.ts +86 -0
  63. package/dist/op/activities/source-release.d.ts.map +1 -0
  64. package/dist/op/activities/source-rollback.d.ts +100 -0
  65. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  66. package/dist/op/activities/work-evidence.d.ts +42 -0
  67. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  68. package/dist/op/activity-contract.d.ts +1 -0
  69. package/dist/op/activity-contract.d.ts.map +1 -1
  70. package/dist/op/activity-profiles.d.ts +16 -0
  71. package/dist/op/activity-profiles.d.ts.map +1 -1
  72. package/dist/op/builders.d.ts +84 -2
  73. package/dist/op/builders.d.ts.map +1 -1
  74. package/dist/op/composites/converge-op.d.ts +31 -4
  75. package/dist/op/composites/converge-op.d.ts.map +1 -1
  76. package/dist/op/decide-backend.d.ts +116 -0
  77. package/dist/op/decide-backend.d.ts.map +1 -0
  78. package/dist/op/decide-config.d.ts +120 -0
  79. package/dist/op/decide-config.d.ts.map +1 -0
  80. package/dist/op/decide-read-inputs.d.ts +40 -0
  81. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  82. package/dist/op/discover.d.ts +25 -0
  83. package/dist/op/discover.d.ts.map +1 -1
  84. package/dist/op/gate-wait.d.ts +43 -0
  85. package/dist/op/gate-wait.d.ts.map +1 -0
  86. package/dist/op/gate.d.ts +5 -2
  87. package/dist/op/gate.d.ts.map +1 -1
  88. package/dist/op/index.d.ts +18 -5
  89. package/dist/op/index.d.ts.map +1 -1
  90. package/dist/op/lifecycle-receipt-store.d.ts +34 -0
  91. package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
  92. package/dist/op/local-executor.d.ts +48 -4
  93. package/dist/op/local-executor.d.ts.map +1 -1
  94. package/dist/op/local-output.d.ts +2 -0
  95. package/dist/op/local-output.d.ts.map +1 -1
  96. package/dist/op/op-ir.d.ts +12 -1
  97. package/dist/op/op-ir.d.ts.map +1 -1
  98. package/dist/op/op-verb-class.d.ts.map +1 -1
  99. package/dist/op/operator.d.ts +126 -1
  100. package/dist/op/operator.d.ts.map +1 -1
  101. package/dist/op/runtime.d.ts +46 -5
  102. package/dist/op/runtime.d.ts.map +1 -1
  103. package/dist/op/runtimes/local.d.ts.map +1 -1
  104. package/dist/op/source-archive.d.ts +63 -0
  105. package/dist/op/source-archive.d.ts.map +1 -0
  106. package/dist/op/step-output-ref.d.ts +2 -2
  107. package/dist/op/step-output-ref.d.ts.map +1 -1
  108. package/dist/op/steward-points.d.ts +136 -0
  109. package/dist/op/steward-points.d.ts.map +1 -0
  110. package/dist/op/steward-turn.d.ts +53 -0
  111. package/dist/op/steward-turn.d.ts.map +1 -0
  112. package/dist/op/steward.d.ts +161 -0
  113. package/dist/op/steward.d.ts.map +1 -0
  114. package/dist/op/types.d.ts +58 -0
  115. package/dist/op/types.d.ts.map +1 -1
  116. package/dist/op/work-lease-decl.d.ts +18 -0
  117. package/dist/op/work-lease-decl.d.ts.map +1 -0
  118. package/dist/op/work-lease-run.d.ts +173 -0
  119. package/dist/op/work-lease-run.d.ts.map +1 -0
  120. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  121. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  122. package/dist/workspace/answers-ledger.d.ts +76 -0
  123. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  124. package/dist/workspace/box-isolation.d.ts +99 -0
  125. package/dist/workspace/box-isolation.d.ts.map +1 -0
  126. package/dist/workspace/changes-cli.d.ts +16 -0
  127. package/dist/workspace/changes-cli.d.ts.map +1 -0
  128. package/dist/workspace/changes.d.ts +200 -0
  129. package/dist/workspace/changes.d.ts.map +1 -0
  130. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  131. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  132. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  133. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  134. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  135. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  136. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  137. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  138. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  139. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  140. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  141. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  142. package/dist/workspace/chant-migrations.d.ts +99 -0
  143. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  144. package/dist/workspace/checks/box-isolation.d.ts +18 -0
  145. package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
  146. package/dist/workspace/checks/boxes.d.ts +104 -0
  147. package/dist/workspace/checks/boxes.d.ts.map +1 -0
  148. package/dist/workspace/checks/diagrams.d.ts +31 -0
  149. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  150. package/dist/workspace/checks/records.d.ts +8 -0
  151. package/dist/workspace/checks/records.d.ts.map +1 -1
  152. package/dist/workspace/checks.d.ts +13 -1
  153. package/dist/workspace/checks.d.ts.map +1 -1
  154. package/dist/workspace/compose-graph.d.ts +2 -0
  155. package/dist/workspace/compose-graph.d.ts.map +1 -1
  156. package/dist/workspace/conformance/index.d.ts.map +1 -1
  157. package/dist/workspace/decide.d.ts +229 -0
  158. package/dist/workspace/decide.d.ts.map +1 -0
  159. package/dist/workspace/decision-points.schema.json +137 -0
  160. package/dist/workspace/declaration.d.ts +103 -0
  161. package/dist/workspace/declaration.d.ts.map +1 -1
  162. package/dist/workspace/declaration.schema.json +285 -0
  163. package/dist/workspace/declared-kinds.d.ts +32 -0
  164. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  165. package/dist/workspace/generated-files.d.ts +7 -0
  166. package/dist/workspace/generated-files.d.ts.map +1 -1
  167. package/dist/workspace/intent.d.ts.map +1 -1
  168. package/dist/workspace/lineage-check.d.ts +3 -0
  169. package/dist/workspace/lineage-check.d.ts.map +1 -1
  170. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  171. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  172. package/dist/workspace/ls.d.ts +39 -1
  173. package/dist/workspace/ls.d.ts.map +1 -1
  174. package/dist/workspace/point-answer.schema.json +95 -0
  175. package/dist/workspace/points-cli.d.ts +113 -0
  176. package/dist/workspace/points-cli.d.ts.map +1 -0
  177. package/dist/workspace/points.d.ts +320 -0
  178. package/dist/workspace/points.d.ts.map +1 -0
  179. package/dist/workspace/reason-codes.d.ts +38 -0
  180. package/dist/workspace/reason-codes.d.ts.map +1 -1
  181. package/dist/workspace/record-assets.d.ts.map +1 -1
  182. package/dist/workspace/records-cli.d.ts +23 -1
  183. package/dist/workspace/records-cli.d.ts.map +1 -1
  184. package/dist/workspace/records-close.d.ts +1 -1
  185. package/dist/workspace/records-close.d.ts.map +1 -1
  186. package/dist/workspace/records-write.d.ts +11 -5
  187. package/dist/workspace/records-write.d.ts.map +1 -1
  188. package/dist/workspace/records.d.ts +42 -6
  189. package/dist/workspace/records.d.ts.map +1 -1
  190. package/dist/workspace/status-gates.d.ts +7 -2
  191. package/dist/workspace/status-gates.d.ts.map +1 -1
  192. package/dist/workspace/status-stewards.d.ts +179 -0
  193. package/dist/workspace/status-stewards.d.ts.map +1 -0
  194. package/dist/workspace/status.d.ts +68 -1
  195. package/dist/workspace/status.d.ts.map +1 -1
  196. package/dist/workspace/work-cli.d.ts +189 -0
  197. package/dist/workspace/work-cli.d.ts.map +1 -0
  198. package/dist/workspace/work-evidence.d.ts +74 -0
  199. package/dist/workspace/work-evidence.d.ts.map +1 -0
  200. package/dist/workspace/work.d.ts +43 -1
  201. package/dist/workspace/work.d.ts.map +1 -1
  202. package/package.json +1 -1
  203. package/src/cli/commands/audit.test.ts +9 -1
  204. package/src/cli/commands/build.ts +3 -2
  205. package/src/cli/commands/lint.ts +7 -1
  206. package/src/cli/handlers/components.test.ts +93 -0
  207. package/src/cli/handlers/components.ts +44 -3
  208. package/src/cli/handlers/graph.test.ts +20 -0
  209. package/src/cli/handlers/graph.ts +16 -2
  210. package/src/cli/handlers/operator.ts +141 -4
  211. package/src/cli/handlers/run.test.ts +173 -2
  212. package/src/cli/handlers/run.ts +174 -3
  213. package/src/cli/main.test.ts +50 -0
  214. package/src/cli/main.ts +97 -2
  215. package/src/cli/mcp/workspace-tools.test.ts +48 -19
  216. package/src/cli/mcp/workspace-tools.ts +110 -8
  217. package/src/cli/plugins.ts +11 -3
  218. package/src/cli/registry.ts +42 -1
  219. package/src/codegen/pinned-upgrade.ts +2 -1
  220. package/src/codegen/release-wiring.test.ts +49 -2
  221. package/src/components/capability-plugin-loader.ts +3 -3
  222. package/src/components/cli-support.ts +2 -2
  223. package/src/components/verbs/vuln-scan.test.ts +124 -1
  224. package/src/components/verbs/vuln-scan.ts +142 -1
  225. package/src/config.ts +11 -0
  226. package/src/lexicon-module.ts +56 -1
  227. package/src/lexicon-package.test.ts +96 -0
  228. package/src/lifecycle/converge-ledger.ts +20 -0
  229. package/src/lifecycle/git.ts +65 -15
  230. package/src/lifecycle/lease.test.ts +22 -0
  231. package/src/lifecycle/lease.ts +133 -29
  232. package/src/lifecycle/member-ledger.ts +3 -2
  233. package/src/lifecycle/plan-ledger.test.ts +148 -0
  234. package/src/lifecycle/plan-ledger.ts +158 -0
  235. package/src/lifecycle/run-ledger.ts +6 -0
  236. package/src/lifecycle/symptoms.ts +95 -0
  237. package/src/lifecycle/work-lease.test.ts +236 -0
  238. package/src/lifecycle/work-lease.ts +426 -0
  239. package/src/lint/rules/index.ts +6 -2
  240. package/src/lint/rules/op/index.ts +17 -4
  241. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  242. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  243. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  244. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  245. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  246. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  247. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  248. package/src/op/activities/activity-contracts.ts +190 -1
  249. package/src/op/activities/apply.ts +7 -6
  250. package/src/op/activities/change-coverage.ts +60 -0
  251. package/src/op/activities/converge.test.ts +55 -1
  252. package/src/op/activities/converge.ts +208 -29
  253. package/src/op/activities/decide.live.test.ts +78 -0
  254. package/src/op/activities/decide.test.ts +300 -0
  255. package/src/op/activities/decide.ts +267 -0
  256. package/src/op/activities/index.ts +21 -0
  257. package/src/op/activities/shell.test.ts +91 -1
  258. package/src/op/activities/shell.ts +91 -2
  259. package/src/op/activities/source-release.test.ts +118 -0
  260. package/src/op/activities/source-release.ts +168 -0
  261. package/src/op/activities/source-rollback.test.ts +131 -0
  262. package/src/op/activities/source-rollback.ts +239 -0
  263. package/src/op/activities/work-evidence.ts +62 -0
  264. package/src/op/activity-contract-registry.test.ts +2 -0
  265. package/src/op/activity-contract-registry.ts +2 -2
  266. package/src/op/activity-contract.test.ts +12 -0
  267. package/src/op/activity-contract.ts +6 -1
  268. package/src/op/activity-profiles.ts +29 -0
  269. package/src/op/activity-registry.test.ts +1 -1
  270. package/src/op/activity-registry.ts +2 -2
  271. package/src/op/builders.ts +124 -5
  272. package/src/op/command-gate.test.ts +192 -0
  273. package/src/op/composites/converge-op.test.ts +27 -1
  274. package/src/op/composites/converge-op.ts +69 -15
  275. package/src/op/decide-backend.test.ts +44 -0
  276. package/src/op/decide-backend.ts +212 -0
  277. package/src/op/decide-config.test.ts +52 -0
  278. package/src/op/decide-config.ts +80 -0
  279. package/src/op/decide-read-inputs.ts +117 -0
  280. package/src/op/discover.ts +71 -0
  281. package/src/op/gate-approval.ts +2 -2
  282. package/src/op/gate-wait.ts +57 -0
  283. package/src/op/gate.ts +8 -3
  284. package/src/op/generate-pipeline.ts +2 -2
  285. package/src/op/index.ts +34 -6
  286. package/src/op/lifecycle-receipt-store.test.ts +60 -0
  287. package/src/op/lifecycle-receipt-store.ts +61 -0
  288. package/src/op/local-executor.ts +367 -33
  289. package/src/op/local-output.test.ts +33 -0
  290. package/src/op/local-output.ts +33 -2
  291. package/src/op/op-ir.test.ts +10 -0
  292. package/src/op/op-ir.ts +18 -0
  293. package/src/op/op-verb-class.ts +1 -0
  294. package/src/op/operator.ts +360 -10
  295. package/src/op/runtime.ts +37 -8
  296. package/src/op/runtimes/local.ts +6 -1
  297. package/src/op/source-archive.ts +161 -0
  298. package/src/op/step-output-ref.ts +6 -2
  299. package/src/op/steward-points.test.ts +313 -0
  300. package/src/op/steward-points.ts +194 -0
  301. package/src/op/steward-turn.ts +79 -0
  302. package/src/op/steward.test.ts +296 -0
  303. package/src/op/steward.ts +277 -0
  304. package/src/op/types.ts +60 -0
  305. package/src/op/work-lease-decl.ts +80 -0
  306. package/src/op/work-lease-run.test.ts +345 -0
  307. package/src/op/work-lease-run.ts +395 -0
  308. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  309. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  310. package/src/workspace/answers-ledger.ts +118 -0
  311. package/src/workspace/box-isolation.test.ts +261 -0
  312. package/src/workspace/box-isolation.ts +205 -0
  313. package/src/workspace/changes-cli.ts +56 -0
  314. package/src/workspace/changes.schema.json +472 -0
  315. package/src/workspace/changes.test.ts +315 -0
  316. package/src/workspace/changes.ts +512 -0
  317. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  318. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  319. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  320. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  321. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  322. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  323. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  324. package/src/workspace/chant-migrations.ts +164 -0
  325. package/src/workspace/check-contract.test.ts +5 -1
  326. package/src/workspace/check.schema.json +25 -7
  327. package/src/workspace/checks/box-isolation.ts +68 -0
  328. package/src/workspace/checks/boxes.test.ts +276 -0
  329. package/src/workspace/checks/boxes.ts +442 -0
  330. package/src/workspace/checks/diagrams.test.ts +134 -0
  331. package/src/workspace/checks/diagrams.ts +108 -0
  332. package/src/workspace/checks/records.ts +72 -0
  333. package/src/workspace/checks.test.ts +7 -0
  334. package/src/workspace/checks.ts +27 -3
  335. package/src/workspace/compose-graph.ts +2 -0
  336. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  337. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  338. package/src/workspace/conformance/index.ts +5 -0
  339. package/src/workspace/decide.test.ts +224 -0
  340. package/src/workspace/decide.ts +709 -0
  341. package/src/workspace/decision-points.schema.json +137 -0
  342. package/src/workspace/declaration.schema.json +285 -0
  343. package/src/workspace/declaration.test.ts +58 -0
  344. package/src/workspace/declaration.ts +241 -0
  345. package/src/workspace/declared-kinds.test.ts +5 -5
  346. package/src/workspace/declared-kinds.ts +71 -3
  347. package/src/workspace/generated-files.ts +14 -0
  348. package/src/workspace/graph-cli.ts +1 -1
  349. package/src/workspace/graph.schema.json +9 -1
  350. package/src/workspace/intent.schema.json +12 -3
  351. package/src/workspace/intent.ts +3 -10
  352. package/src/workspace/lineage-check.ts +16 -1
  353. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  354. package/src/workspace/lineage-upgrade.test.ts +70 -0
  355. package/src/workspace/lineage-upgrade.ts +81 -10
  356. package/src/workspace/ls-contract.test.ts +57 -0
  357. package/src/workspace/ls.schema.json +46 -1
  358. package/src/workspace/ls.ts +62 -3
  359. package/src/workspace/point-answer.schema.json +95 -0
  360. package/src/workspace/points-cli.ts +284 -0
  361. package/src/workspace/points-write.schema.json +499 -0
  362. package/src/workspace/points.schema.json +743 -0
  363. package/src/workspace/points.test.ts +264 -0
  364. package/src/workspace/points.ts +564 -0
  365. package/src/workspace/read-contract.test.ts +18 -1
  366. package/src/workspace/reason-codes.test.ts +24 -1
  367. package/src/workspace/reason-codes.ts +49 -0
  368. package/src/workspace/record-assets.test.ts +3 -2
  369. package/src/workspace/record-assets.ts +4 -1
  370. package/src/workspace/records-amend.schema.json +2 -0
  371. package/src/workspace/records-cli.ts +28 -5
  372. package/src/workspace/records-close.schema.json +2 -0
  373. package/src/workspace/records-contract.test.ts +3 -2
  374. package/src/workspace/records-new.schema.json +2 -0
  375. package/src/workspace/records-review.schema.json +2 -0
  376. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  377. package/src/workspace/records-sessions-write.test.ts +2 -34
  378. package/src/workspace/records-write.e2e.test.ts +108 -0
  379. package/src/workspace/records-write.test.ts +32 -41
  380. package/src/workspace/records-write.ts +33 -15
  381. package/src/workspace/records.schema.json +53 -2
  382. package/src/workspace/records.test.ts +48 -3
  383. package/src/workspace/records.ts +142 -6
  384. package/src/workspace/status-contract.test.ts +189 -6
  385. package/src/workspace/status-gates.ts +9 -4
  386. package/src/workspace/status-stewards.ts +329 -0
  387. package/src/workspace/status.schema.json +335 -5
  388. package/src/workspace/status.ts +142 -6
  389. package/src/workspace/work-acceptance.test.ts +274 -0
  390. package/src/workspace/work-cli.e2e.test.ts +180 -0
  391. package/src/workspace/work-cli.ts +429 -0
  392. package/src/workspace/work-evidence.schema.json +212 -0
  393. package/src/workspace/work-evidence.ts +169 -0
  394. package/src/workspace/work-history.schema.json +349 -0
  395. package/src/workspace/work-history.test.ts +156 -0
  396. package/src/workspace/work-lease.schema.json +233 -0
  397. package/src/workspace/work-readiness-chud.test.ts +145 -0
  398. package/src/workspace/work.test.ts +3 -2
  399. package/src/workspace/work.ts +100 -1
  400. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  401. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  402. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -49,6 +49,16 @@ vi.mock("../../lifecycle/build-ledger-store", () => ({
49
49
  readBuildManifest: (...args: unknown[]) => readBuildManifestMock(...args),
50
50
  }));
51
51
 
52
+ const persistReleasePlanMock = vi.fn();
53
+
54
+ vi.mock("../../lifecycle/plan-ledger", async () => {
55
+ const actual = await vi.importActual<typeof import("../../lifecycle/plan-ledger")>("../../lifecycle/plan-ledger");
56
+ return {
57
+ ...actual,
58
+ persistReleasePlan: (...args: unknown[]) => persistReleasePlanMock(...args),
59
+ };
60
+ });
61
+
52
62
  vi.mock("../../lifecycle/release-ledger", async () => {
53
63
  const actual = await vi.importActual<typeof import("../../lifecycle/release-ledger")>("../../lifecycle/release-ledger");
54
64
  return {
@@ -137,6 +147,7 @@ describe("components handlers", () => {
137
147
  discoverComponentsMock.mockReset().mockResolvedValue({ components: new Map(), sourceFiles: [], errors: [] });
138
148
  findBuildManifestByArtifactDigestMock.mockReset().mockResolvedValue(undefined);
139
149
  readBuildManifestMock.mockReset().mockResolvedValue(null);
150
+ persistReleasePlanMock.mockReset().mockResolvedValue({ commit: "a".repeat(40), written: true });
140
151
 
141
152
  delete process.env.GITHUB_RUN_ID;
142
153
  delete process.env.CI_PIPELINE_ID;
@@ -222,6 +233,88 @@ describe("components handlers", () => {
222
233
  await runComponentsReleaseRecord(ctx);
223
234
  expect(JSON.parse(stdoutBuf.join(""))).toMatchObject(record);
224
235
  });
236
+
237
+ describe("--plan (ws-055, #2733)", () => {
238
+ let planDir: string;
239
+
240
+ beforeEach(async () => {
241
+ planDir = await mkdtemp(join(tmpdir(), "chant-plan-"));
242
+ });
243
+
244
+ afterEach(async () => {
245
+ await rm(planDir, { recursive: true, force: true });
246
+ });
247
+
248
+ test("persists the plan and records its own digest, when --digest is omitted", async () => {
249
+ const planPath = join(planDir, "plan.json");
250
+ await writeFile(planPath, JSON.stringify({ digest: "sha256:fromplan", release: "r-1", units: [] }));
251
+ appendReleaseRecordMock.mockResolvedValue({
252
+ commit: "a".repeat(40),
253
+ record: { version: 1, component: "svc", env: "prod", digest: "sha256:fromplan", gitSha: "abc123headsha", runId: "local-123", timestamp: "2026-01-01T00:00:00.000Z", actor: "alice" },
254
+ });
255
+
256
+ const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
257
+ const exit = await runComponentsReleaseRecord(ctx);
258
+
259
+ expect(exit).toBe(0);
260
+ expect(persistReleasePlanMock).toHaveBeenCalledTimes(1);
261
+ expect(persistReleasePlanMock.mock.calls[0][0]).toEqual({ digest: "sha256:fromplan", release: "r-1", units: [] });
262
+ const [input] = appendReleaseRecordMock.mock.calls[0];
263
+ expect(input).toMatchObject({ digest: "sha256:fromplan" });
264
+ // The plan is persisted before the release record is appended, so the
265
+ // record's digest always names a plan this checkout can resolve.
266
+ expect(persistReleasePlanMock.mock.invocationCallOrder[0]).toBeLessThan(appendReleaseRecordMock.mock.invocationCallOrder[0]);
267
+ });
268
+
269
+ test("--digest agreeing with the plan's own digest is accepted", async () => {
270
+ const planPath = join(planDir, "plan.json");
271
+ await writeFile(planPath, JSON.stringify({ digest: "sha256:agree", release: "r-1" }));
272
+ appendReleaseRecordMock.mockResolvedValue({ commit: "a".repeat(40), record: { version: 1, component: "svc", env: "prod", digest: "sha256:agree", gitSha: "x", runId: "y", timestamp: "z", actor: "alice" } });
273
+
274
+ const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", digest: "sha256:agree", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
275
+ const exit = await runComponentsReleaseRecord(ctx);
276
+ expect(exit).toBe(0);
277
+ });
278
+
279
+ test("--digest disagreeing with the plan's own digest is refused", async () => {
280
+ const planPath = join(planDir, "plan.json");
281
+ await writeFile(planPath, JSON.stringify({ digest: "sha256:plandigest" }));
282
+
283
+ const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", digest: "sha256:other", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
284
+ const exit = await runComponentsReleaseRecord(ctx);
285
+ expect(exit).toBe(1);
286
+ expect(stderrBuf.join("\n")).toContain("does not match --release-plan");
287
+ expect(persistReleasePlanMock).not.toHaveBeenCalled();
288
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
289
+ });
290
+
291
+ test("a plan file with no digest field is refused", async () => {
292
+ const planPath = join(planDir, "plan.json");
293
+ await writeFile(planPath, JSON.stringify({ release: "r-1" }));
294
+
295
+ const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
296
+ const exit = await runComponentsReleaseRecord(ctx);
297
+ expect(exit).toBe(1);
298
+ expect(stderrBuf.join("\n")).toContain("missing its own \"digest\" field");
299
+ });
300
+
301
+ test("a plan file that is not valid JSON is refused", async () => {
302
+ const planPath = join(planDir, "plan.json");
303
+ await writeFile(planPath, "{not json");
304
+
305
+ const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
306
+ const exit = await runComponentsReleaseRecord(ctx);
307
+ expect(exit).toBe(1);
308
+ expect(stderrBuf.join("\n")).toContain("is not valid JSON");
309
+ });
310
+
311
+ test("a --plan path that can't be read is refused", async () => {
312
+ const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", releasePlanFile: join(planDir, "missing.json"), actor: "alice" }), plugins: [], serializers: [] };
313
+ const exit = await runComponentsReleaseRecord(ctx);
314
+ expect(exit).toBe(1);
315
+ expect(stderrBuf.join("\n")).toContain("Could not read --release-plan");
316
+ });
317
+ });
225
318
  });
226
319
 
227
320
  describe("runComponentsStatus", () => {
@@ -38,6 +38,7 @@ import {
38
38
  resolveRunId,
39
39
  InvalidReleaseRecordError,
40
40
  } from "../../lifecycle/release-ledger";
41
+ import { persistReleasePlan, InvalidReleasePlanError, type ReleasePlan } from "../../lifecycle/plan-ledger";
41
42
  import { reconcileStatus, liveEvidenceFromChangeSet, compareAcrossEnvironments, mergeLiveEvidence, type LiveComponentEvidence, type EntityLiveRead } from "../../lifecycle/status";
42
43
  import { commandBuildParams } from "../build-params-cli";
43
44
  import { buildChangeSet } from "../../lifecycle/change-set";
@@ -75,6 +76,14 @@ import { withoutReadFlags } from "../../lifecycle/legacy-digest";
75
76
  * never threaded in from elsewhere and never mocked in production code, per
76
77
  * #568: "the session cannot call Date.now() in some contexts; take the
77
78
  * timestamp from the environment/CLI at record time."
79
+ *
80
+ * `--release-plan <file>` (ws-055, #2733) takes `--digest`'s place: the file is a
81
+ * release plan JSON object naming its own `digest` field, persisted
82
+ * content-addressed to `_plans/<digest>.json` (../../lifecycle/plan-
83
+ * ledger.ts) before the release record is appended, so the record's digest
84
+ * always names a plan this checkout can resolve. Passing both requires them
85
+ * to agree — a plan is content-addressed by its own digest, so a caller
86
+ * naming a different one is a mistake, not an override.
78
87
  */
79
88
  export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<number> {
80
89
  const { args } = ctx;
@@ -87,11 +96,42 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
87
96
  }
88
97
 
89
98
  const component = args.component;
90
- const digest = args.digest;
99
+ let digest = args.digest;
100
+ let plan: ReleasePlan | undefined;
101
+ if (args.releasePlanFile) {
102
+ let raw: string;
103
+ try {
104
+ raw = await readFile(args.releasePlanFile, "utf-8");
105
+ } catch (err) {
106
+ console.error(formatError({ message: `Could not read --release-plan ${args.releasePlanFile}: ${err instanceof Error ? err.message : String(err)}` }));
107
+ return 1;
108
+ }
109
+ let parsed: unknown;
110
+ try {
111
+ parsed = JSON.parse(raw);
112
+ } catch (err) {
113
+ console.error(formatError({ message: `--release-plan ${args.releasePlanFile} is not valid JSON: ${err instanceof Error ? err.message : String(err)}` }));
114
+ return 1;
115
+ }
116
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
117
+ console.error(formatError({ message: `--release-plan ${args.releasePlanFile} must be a JSON object` }));
118
+ return 1;
119
+ }
120
+ plan = parsed as ReleasePlan;
121
+ if (typeof plan.digest !== "string" || plan.digest.length === 0) {
122
+ console.error(formatError({ message: `--release-plan ${args.releasePlanFile} is missing its own "digest" field — a release plan is content-addressed by its own digest` }));
123
+ return 1;
124
+ }
125
+ if (digest !== undefined && digest !== plan.digest) {
126
+ console.error(formatError({ message: `--digest ${digest} does not match --release-plan ${args.releasePlanFile}'s own digest ${plan.digest}` }));
127
+ return 1;
128
+ }
129
+ digest = plan.digest;
130
+ }
91
131
  if (!component || !digest) {
92
132
  console.error(formatError({
93
133
  message: "--component and --digest are required",
94
- hint: "chant components release <env> --component <name> --digest <sha256:...> [--git-sha <sha>] [--run-id <id>] [--actor <name>] [--approver <name>]",
134
+ hint: "chant components release <env> --component <name> --digest <sha256:...> [--release-plan <file>] [--git-sha <sha>] [--run-id <id>] [--actor <name>] [--approver <name>]",
95
135
  }));
96
136
  return 1;
97
137
  }
@@ -121,6 +161,7 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
121
161
  const timestamp = new Date().toISOString();
122
162
 
123
163
  try {
164
+ if (plan) await persistReleasePlan(plan);
124
165
  const { commit, record } = await appendReleaseRecord({
125
166
  component,
126
167
  env: environment,
@@ -142,7 +183,7 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
142
183
  }
143
184
  return 0;
144
185
  } catch (err) {
145
- if (err instanceof InvalidReleaseRecordError) {
186
+ if (err instanceof InvalidReleaseRecordError || err instanceof InvalidReleasePlanError) {
146
187
  console.error(formatError({ message: err.message }));
147
188
  return 1;
148
189
  }
@@ -3,6 +3,7 @@ import type { ParsedArgs } from "../registry";
3
3
  import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
4
4
  import { AttrRef } from "../../attrref";
5
5
  import { GRAPH_IR_VERSION } from "../../graph-ir";
6
+ import { NO_LEXICON_DETECTED_MESSAGE } from "../../detectLexicon";
6
7
 
7
8
  /**
8
9
  * The aws emulator capability, as the real plugin declares it. `--live`
@@ -469,6 +470,25 @@ describe("runGraph", () => {
469
470
  expect(exit).toBe(1);
470
471
  expect(stderrBuf.join("\n")).toContain("boom");
471
472
  });
473
+
474
+ // #2841 — an Ops-only project (`lexicons: []`, no lexicon import) has no
475
+ // lexicon to predict with. Detection's sentinel means "none", not a failed
476
+ // graph, so `chant workspace graph` does not report the member failed.
477
+ test("--format ir in a project with no lexicon graphs with no plugins", async () => {
478
+ lintClean(); discovered();
479
+ resolveLexMock.mockRejectedValue(new Error(NO_LEXICON_DETECTED_MESSAGE));
480
+ loadPluginsMock.mockResolvedValue([]);
481
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
482
+ expect(exit).toBe(0);
483
+ expect(loadPluginsMock).toHaveBeenCalledWith([]);
484
+ expect(JSON.parse(stdoutBuf.join("\n")).nodes.length).toBe(3);
485
+ });
486
+
487
+ test("--format ir still fails on a lexicon error other than none detected", async () => {
488
+ lintClean(); discovered();
489
+ resolveLexMock.mockRejectedValue(new Error("lexicon @intentius/chant-lexicon-nope is not installed"));
490
+ await expect(runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] })).rejects.toThrow("not installed");
491
+ });
472
492
  });
473
493
 
474
494
  describe("component DAG view (--components --format ir|layout)", () => {
@@ -21,6 +21,7 @@ import { toDot } from "../../graph-dot";
21
21
  import { getLayoutEngine, toLayoutInput, type NodeSize } from "../../graph-layout";
22
22
  import { lintCommand } from "../commands/lint";
23
23
  import { loadPlugins, resolveProjectLexicons, collectBuildRootContributors } from "../plugins";
24
+ import { isNoLexiconDetected } from "../../detectLexicon";
24
25
  import { readFileSync } from "node:fs";
25
26
  import { formatError, formatWarning, formatBold } from "../format";
26
27
  import type { CommandContext, ParsedArgs } from "../registry";
@@ -31,6 +32,19 @@ import type { BuildParamProvenance } from "../../provenance";
31
32
  import { discoverComponents } from "../../components/discover";
32
33
  import { cfnDeployStacks } from "./components";
33
34
 
35
+ /**
36
+ * The project's lexicons, or none for a project that declares none and imports
37
+ * none (an Ops-only member, `lexicons: []`): detection's NO_LEXICON_DETECTED
38
+ * sentinel means "no lexicon", as `chant lint` reads it, not a failed graph (#2841).
39
+ */
40
+ async function projectLexiconsOrNone(projectPath: string): Promise<string[]> {
41
+ try {
42
+ return await resolveProjectLexicons(projectPath);
43
+ } catch (err) {
44
+ if (isNoLexiconDetected(err)) return [];
45
+ throw err;
46
+ }
47
+ }
34
48
 
35
49
  /**
36
50
  * Resolve this invocation's declared build-time parameters, the same way
@@ -223,7 +237,7 @@ async function runGraphLive(
223
237
  // `graph` is not `requiresPlugins` (Op/source-graph modes must work without a
224
238
  // lexicon), so `ctx.plugins` is empty. The live path needs the project's
225
239
  // observation plugins — load them here, mirroring the lifecycle handlers.
226
- const plugins = ctx.plugins.length > 0 ? ctx.plugins : await loadPlugins(await resolveProjectLexicons(projectPath));
240
+ const plugins = ctx.plugins.length > 0 ? ctx.plugins : await loadPlugins(await projectLexiconsOrNone(projectPath));
227
241
  const declaredEnvNames = environmentNames(config.environments);
228
242
  if (declaredEnvNames && !matchesDeclaredEnvironment(config.environments, environment)) {
229
243
  console.error(formatError({
@@ -828,7 +842,7 @@ async function runGraphView(
828
842
  {
829
843
  const plugins = ctx.plugins.length > 0
830
844
  ? ctx.plugins
831
- : await loadPlugins(await resolveProjectLexicons(projectPath));
845
+ : await loadPlugins(await projectLexiconsOrNone(projectPath));
832
846
  const predicted = await predictOntoIr(ir, plugins as LexiconPlugin[], ctx.args, {
833
847
  // No environment on the declared path: the file is not deployed anywhere
834
848
  // yet, and naming one would claim it was.
@@ -11,7 +11,8 @@ import { loadChantConfig } from "../../config";
11
11
  import { build } from "../../build";
12
12
  import { isResourceDeclarable } from "../../declarable";
13
13
  import { collectBuildRootContributors, collectChangeSubscribers } from "../plugins";
14
- import { discoverOps } from "../../op/discover";
14
+ import { discoverOps, discoverStewards } from "../../op/discover";
15
+ import { pickSteward, stewardFormFor, stewardLeaseName, DEFAULT_STEWARD_ENV } from "../../op/steward";
15
16
  import { loadActivities, loadProfiles } from "../../op/activity-registry";
16
17
  import { parseDuration } from "../../op/local-executor";
17
18
  import {
@@ -21,13 +22,14 @@ import {
21
22
  formatRoundLine,
22
23
  formatSignalLine,
23
24
  DEFAULT_OPERATOR_INTERVAL_MS,
25
+ acquireStewardLease,
24
26
  type ChangeSubscriber,
25
27
  type OperatorSignalEvent,
26
28
  type OperatorTickEvent,
27
29
  } from "../../op/operator";
28
- import { readLease, DEFAULT_LEASE_TTL_MS } from "../../lifecycle/lease";
30
+ import { readLease, releaseLease, currentHolderId, DEFAULT_LEASE_TTL_MS } from "../../lifecycle/lease";
29
31
  import { readConvergeLedger, type ConvergeTickRecord } from "../../lifecycle/converge-ledger";
30
- import { readRunLedger } from "../../lifecycle/run-ledger";
32
+ import { readRunLedger, runEnvOf } from "../../lifecycle/run-ledger";
31
33
  import type { OpRunRecord } from "../../op/runtime";
32
34
  import type { GateResolutionRecord, PendingGateRecord } from "../../lifecycle/gate-ledger";
33
35
  import {
@@ -141,6 +143,7 @@ async function collectOperatorSubscribers(
141
143
  * leaving the daemon running); omitted, the daemon loops until Ctrl-C.
142
144
  */
143
145
  export async function runOperator(ctx: CommandContext): Promise<number> {
146
+ if (ctx.args.steward !== undefined) return runStewardOperator(ctx);
144
147
  const { ops, errors } = await discoverConvergeOps({ env: ctx.args.env });
145
148
  for (const err of errors) console.error(formatWarning({ message: err }));
146
149
 
@@ -205,6 +208,108 @@ export async function runOperator(ctx: CommandContext): Promise<number> {
205
208
  }
206
209
  }
207
210
 
211
+ // ── chant operator --steward ────────────────────────────────────────────────
212
+
213
+ /**
214
+ * `chant operator --steward [<name>] [--env <env>] [--interval] [--lease-ttl]
215
+ * [--once]` (#2731) — a steward's local form. The same Ops the steward runs on
216
+ * Fountain, on the same crons, one at a time, each under its operator lease,
217
+ * and the whole process under the steward's own lease.
218
+ *
219
+ * Refuses, before ticking anything:
220
+ * - an environment whose form is `fountain`, because there the composite's
221
+ * Agent and Teammate are the steward and this process would be a second
222
+ * writer beside them;
223
+ * - a steward whose lease another live process holds (a second local
224
+ * operator for it).
225
+ */
226
+ export async function runStewardOperator(ctx: CommandContext): Promise<number> {
227
+ const { stewards, errors, conflicts } = await discoverStewards();
228
+ for (const err of errors) console.error(formatWarning({ message: err }));
229
+ for (const conflict of conflicts) console.error(formatError({ message: conflict }));
230
+
231
+ const picked = pickSteward(stewards, ctx.args.steward ?? "");
232
+ if (typeof picked === "string") {
233
+ console.error(formatError({ message: picked }));
234
+ return 1;
235
+ }
236
+ const steward = picked;
237
+ const env = ctx.args.env ?? DEFAULT_STEWARD_ENV;
238
+ const form = stewardFormFor(steward, env);
239
+ if (form !== "local") {
240
+ console.error(formatError({
241
+ message: `steward "${steward.name}" runs on ${form} in environment "${env}", so a local operator for it would be a second writer`,
242
+ hint: `Its turns run on the fountain teammate "${steward.name}". Run this in an environment the declaration's form makes local, or change the form.`,
243
+ }));
244
+ return 1;
245
+ }
246
+
247
+ let activities, profiles;
248
+ try {
249
+ [activities, profiles] = await loadOperatorActivities();
250
+ } catch (err) {
251
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
252
+ return 1;
253
+ }
254
+
255
+ const intervalMs = ctx.args.interval ? parseDuration(ctx.args.interval) : DEFAULT_OPERATOR_INTERVAL_MS;
256
+ const leaseTtlMs = ctx.args.leaseTtl ? parseDuration(ctx.args.leaseTtl) : DEFAULT_LEASE_TTL_MS;
257
+ const holder = currentHolderId();
258
+
259
+ // Refuse a second local operator up front rather than letting it loop on
260
+ // `steward-busy` rounds: one steward is one writer.
261
+ const first = await acquireStewardLease(steward.name, holder, { ttlMs: leaseTtlMs });
262
+ if (!first.acquired) {
263
+ console.error(formatError({
264
+ message: `steward "${steward.name}" is already running (lease held by ${first.heldBy?.holder ?? "another process"} until ${first.heldBy?.expiresAt ?? "?"})`,
265
+ hint: "One steward is one writer. Stop the other operator, or wait for its lease to expire.",
266
+ }));
267
+ return 1;
268
+ }
269
+
270
+ const controller = new AbortController();
271
+ const onSigint = () => {
272
+ console.error(formatWarning({ message: "interrupted — stopping steward" }));
273
+ controller.abort();
274
+ };
275
+ process.once("SIGINT", onSigint);
276
+ process.once("SIGTERM", onSigint);
277
+
278
+ const printRound = (events: OperatorTickEvent[]) => {
279
+ for (const event of events) console.error(formatInfo(formatRoundLine(event)));
280
+ };
281
+
282
+ const scheduled = steward.ops.filter((op) => op.schedule !== undefined);
283
+ try {
284
+ if (ctx.args.once) {
285
+ const events = await runOperatorRound({ steward, holder, leaseTtlMs, activities, profiles, signal: controller.signal });
286
+ printRound(events);
287
+ return events.some((e) => e.kind === "tick-failed" || e.kind === "steward-busy") ? 1 : 0;
288
+ }
289
+ console.error(formatInfo(
290
+ `chant operator: steward ${steward.name} (local) runs ${scheduled.length} scheduled Op(s) of ${steward.ops.length}, ` +
291
+ `checking every ${intervalMs}ms (Ctrl-C to stop)`,
292
+ ));
293
+ await runOperatorForever({
294
+ steward,
295
+ holder,
296
+ intervalMs,
297
+ leaseTtlMs,
298
+ activities,
299
+ profiles,
300
+ signal: controller.signal,
301
+ onRound: printRound,
302
+ });
303
+ return 0;
304
+ } finally {
305
+ process.removeListener("SIGINT", onSigint);
306
+ process.removeListener("SIGTERM", onSigint);
307
+ // A courtesy: the lease expires on its own if this never runs.
308
+ const { record } = await readLease(stewardLeaseName(steward.name)).catch(() => ({ record: undefined }));
309
+ if (record?.holder === holder) await releaseLease(stewardLeaseName(steward.name), holder, record.token).catch(() => false);
310
+ }
311
+ }
312
+
208
313
  // ── chant operator status ───────────────────────────────────────────────────
209
314
 
210
315
  interface OpStatusLine {
@@ -308,6 +413,12 @@ async function statusFor(opName: string, env: string, cwd?: string): Promise<OpS
308
413
  export interface StandaloneGateLine {
309
414
  op: string;
310
415
  gate: string;
416
+ /**
417
+ * The Op whose newest run stopped at this gate, when the gate is a
418
+ * command's own rather than the Op's (#2779): a step running `chant
419
+ * workspace upgrade` stops its run at `workspace-upgrade` / `<scope>`.
420
+ */
421
+ run?: string;
311
422
  description?: string;
312
423
  expiresAt: string;
313
424
  url?: string;
@@ -353,6 +464,32 @@ export async function runOperatorStatus(ctx: CommandContext): Promise<number> {
353
464
  }
354
465
  }
355
466
 
467
+ // A gate a command decided inside an Op's step (#2779) is recorded under
468
+ // the command's op, which is not an `*.op.ts`, so the loop above never
469
+ // reads it. The Op's newest run names it; list it against that Op while it
470
+ // still stands.
471
+ for (const [opName, discovered] of [...allOps.entries()].sort(([a], [b]) => a.localeCompare(b))) {
472
+ let newest: OpRunRecord | undefined;
473
+ try {
474
+ newest = (await readRunLedger(runEnvOf(discovered.config), opName)).records.at(-1);
475
+ } catch {
476
+ continue;
477
+ }
478
+ const gateOp = newest?.status === "gated" ? newest.gate?.op : undefined;
479
+ if (!newest?.gate || !gateOp || gateOp === opName) continue;
480
+ const record = (await pendingGatesFor(gateOp)).find((r) => r.gate === newest.gate!.name);
481
+ if (!record || covered.has(`${gateOp} ${record.gate}`)) continue;
482
+ covered.add(`${gateOp} ${record.gate}`);
483
+ standalone.push({
484
+ op: gateOp,
485
+ gate: record.gate,
486
+ run: opName,
487
+ expiresAt: record.expiresAt,
488
+ ...(record.description ? { description: record.description } : {}),
489
+ ...(record.url ? { url: record.url } : {}),
490
+ });
491
+ }
492
+
356
493
  if (ops.length === 0 && standalone.length === 0) {
357
494
  console.error(formatWarning({ message: "No ConvergeOp declarations found" }));
358
495
  return 0;
@@ -389,7 +526,7 @@ export async function runOperatorStatus(ctx: CommandContext): Promise<number> {
389
526
  if (standalone.length > 0) {
390
527
  console.log(formatBold("pending gates (no converge tick)"));
391
528
  for (const g of standalone) {
392
- console.log(` - ${g.op} gate "${g.gate}" — resolve: chant approve ${g.op} ${g.gate}`);
529
+ console.log(` - ${g.op} gate "${g.gate}"${g.run ? ` (Op ${g.run}'s run stopped here)` : ""} — resolve: chant approve ${g.op} ${g.gate}`);
393
530
  if (g.description) console.log(` ${g.description}`);
394
531
  console.log(` expires: ${g.expiresAt}`);
395
532
  if (g.url) console.log(` approve at: ${g.url}`);