@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
@@ -50,16 +50,42 @@ export function renderHuman(result: OpRunResult, write: Writer = stderr): void {
50
50
  if (record.refusal) {
51
51
  write(` [refused] ${record.refusal}`);
52
52
  }
53
+ if (record.point) {
54
+ write(` [waiting] decision point ${record.point.point}: ${record.point.id} is ${record.point.state}`);
55
+ }
53
56
  if (record.error) {
54
57
  write(` ${record.error}`);
55
58
  }
56
59
  }
57
60
 
61
+ // The work lease (#2748): which item the run held, and how it ended.
62
+ const work = result.workLease;
63
+ if (work?.item) {
64
+ const end = work.lost
65
+ ? `lost (${work.lost})`
66
+ : work.released
67
+ ? `released ${work.outcome}`
68
+ : "not released; it runs out on its own";
69
+ write(`[work] ${work.item} held by ${work.holder}${work.branch ? ` on ${work.branch}` : ""}: ${end}`);
70
+ } else if (work?.refusal) {
71
+ write(`[work] nothing claimed: ${work.refusal}`);
72
+ }
73
+
58
74
  const total = `${(result.totalMs / 1000).toFixed(1)}s`;
59
75
  if (result.status === "ok") {
60
76
  write(`Op "${result.op}" completed in ${total}`);
61
77
  return;
62
78
  }
79
+ if (result.status === "waiting" && result.point) {
80
+ // An open decision point (#2749): a fact like a gate. A person answers the
81
+ // question, through hud or at a shell, and the next run reads the answer.
82
+ const { point } = result;
83
+ write(`Op "${result.op}" is waiting on decision point "${point.point}" after ${total}`);
84
+ write(` question: ${point.id} (${point.state}) at ${point.path}`);
85
+ if (point.subject) write(` subject : ${point.subject}`);
86
+ write(` answer : ${pointAnswerCommand(point.id)}`);
87
+ return;
88
+ }
63
89
  if (result.status === "fail" || !result.gate) {
64
90
  write(`Op "${result.op}" failed after ${total}`);
65
91
  return;
@@ -74,7 +100,7 @@ export function renderHuman(result: OpRunResult, write: Writer = stderr): void {
74
100
  // #2300: the plan the approval will be bound to. Printed before the
75
101
  // command, because it is what the command approves.
76
102
  if (gate.planDigest) write(` plan : ${gate.planDigest}`);
77
- write(` approve : ${approveCommand(gate.op, gate.gate)}`);
103
+ write(` approve : ${approveCommand(gate.op, gate.gate, undefined, gate.planDigest)}`);
78
104
  if (gate.url) write(` approve at: ${gate.url}`);
79
105
  write(` expires : ${gate.expiresAt}`);
80
106
  // #2310: this run's own append reached only the local chant/lifecycle
@@ -102,7 +128,7 @@ export function renderHuman(result: OpRunResult, write: Writer = stderr): void {
102
128
  * the gate doesn't have to reassemble it from the op and gate names.
103
129
  */
104
130
  export function renderJson(result: OpRunResult, write: Writer = stdout): void {
105
- const approve = result.gate ? { approve: approveCommand(result.gate.op, result.gate.gate) } : {};
131
+ const approve = result.gate ? { approve: approveCommand(result.gate.op, result.gate.gate, undefined, result.gate.planDigest) } : {};
106
132
  // #2310: whether this run's own append reached the remote — not part of
107
133
  // the persisted ledger record (a replay has nothing new to report), but a
108
134
  // live run's JSON consumer needs it exactly where the human render shows it.
@@ -113,3 +139,8 @@ export function renderJson(result: OpRunResult, write: Writer = stdout): void {
113
139
  }
114
140
 
115
141
  export type { OpRunResult, StepRecord };
142
+
143
+ /** The command a person answers an open decision point with (#2749). */
144
+ export function pointAnswerCommand(id: string): string {
145
+ return `chant workspace points answer ${id} --answer <answer> --by <your name>`;
146
+ }
@@ -112,6 +112,16 @@ describe("op.json IR", () => {
112
112
  expect(serializeOpIR(reconstructed)).toBe(text);
113
113
  });
114
114
 
115
+ it("carries a step's own timeout, and round-trips it (#2787)", () => {
116
+ const original: OpConfig = { name: "dispatch", overview: "o", phases: [phase("Build", [shell("./build.sh", { timeout: "45m" }), shell("echo hi")])] };
117
+ const ir = buildOpIR(original);
118
+ const steps = ir.phases[0].steps as { profile: string; timeout?: string }[];
119
+ expect(steps[0]).toMatchObject({ profile: "atMostOnce", timeout: "45m" });
120
+ expect(steps[1]).not.toHaveProperty("timeout");
121
+ const text = serializeOpIR(original);
122
+ expect(serializeOpIR(opConfigFromIR(JSON.parse(text) as OpIR))).toBe(text);
123
+ });
124
+
115
125
  it("two serializations of the same config are byte-identical (determinism)", () => {
116
126
  const config = representativeOp();
117
127
  expect(serializeOpIR(config)).toBe(serializeOpIR(config));
package/src/op/op-ir.ts CHANGED
@@ -86,6 +86,7 @@ import type {
86
86
  OutcomeAttribute,
87
87
  OpConfig,
88
88
  OpSchedule,
89
+ OpWorkLease,
89
90
  PhaseDefinition,
90
91
  StepDefinition,
91
92
  ActivityStep,
@@ -117,6 +118,8 @@ export interface OpIRActivityStep {
117
118
  args: Record<string, unknown>;
118
119
  /** Resolved to its effective value — `ActivityStep.profile ?? "fastIdempotent"`. */
119
120
  profile: string;
121
+ /** The step's own timeout, replacing its profile's (#2787). Absent when the profile's applies. */
122
+ timeout?: string;
120
123
  /**
121
124
  * What this step's effect touches in the estate (#2022): the string values
122
125
  * of the args its contract declares as entity-identifying
@@ -187,6 +190,15 @@ export interface OpIR {
187
190
  * additive optional key, so it needs no `formatVersion` bump.
188
191
  */
189
192
  schedule?: OpSchedule;
193
+ /**
194
+ * The work item the Op runs under (#2748) — `OpConfig.workLease` verbatim,
195
+ * a step-output reference in `item` or `outcome` in the same
196
+ * `{ kind: "step-output-ref", step, path }` shape as in args. Absent when it
197
+ * declares none. A runtime that can't take the lease must not run the Op.
198
+ */
199
+ workLease?: OpWorkLease;
200
+ /** `OpConfig.changesCheckout` (#2748), present only when true. */
201
+ changesCheckout?: true;
190
202
  phases: OpIRPhase[];
191
203
  onFailure: OpIRPhase[];
192
204
  /** Every activity profile referenced by a step in this Op, keyed by profile name — from the registry passed to {@link buildOpIR}, empty when none was. */
@@ -230,6 +242,7 @@ function irActivityStep(step: ActivityStep, contracts: ReadonlyMap<string, Activ
230
242
  fn: step.fn,
231
243
  args: step.args ?? {},
232
244
  profile: effectiveProfile(step),
245
+ ...(step.timeout !== undefined ? { timeout: step.timeout } : {}),
233
246
  ...(entities ? { entities } : {}),
234
247
  ...(step.outcomeAttribute ? { outcomeAttribute: step.outcomeAttribute } : {}),
235
248
  };
@@ -350,6 +363,8 @@ export function buildOpIR(
350
363
  depends: config.depends ?? [],
351
364
  labels: config.labels ?? {},
352
365
  ...(config.schedule ? { schedule: config.schedule } : {}),
366
+ ...(config.workLease ? { workLease: config.workLease } : {}),
367
+ ...(config.changesCheckout ? { changesCheckout: true as const } : {}),
353
368
  phases: config.phases.map((p) => irPhase(p, contractRegistry)),
354
369
  onFailure: (config.onFailure ?? []).map((p) => irPhase(p, contractRegistry)),
355
370
  activityProfiles: sortedEntries(profiles),
@@ -382,6 +397,7 @@ function opStepFromIR(step: OpIRStep): StepDefinition {
382
397
  fn: step.fn,
383
398
  args: step.args,
384
399
  profile: step.profile as ActivityStep["profile"],
400
+ ...(step.timeout !== undefined ? { timeout: step.timeout } : {}),
385
401
  ...(step.outcomeAttribute ? { outcomeAttribute: step.outcomeAttribute } : {}),
386
402
  };
387
403
  }
@@ -428,5 +444,7 @@ export function opConfigFromIR(ir: OpIR): OpConfig {
428
444
  ...(ir.onFailure.length > 0 ? { onFailure: ir.onFailure.map(opPhaseFromIR) } : {}),
429
445
  ...(Object.keys(ir.labels).length > 0 ? { labels: ir.labels } : {}),
430
446
  ...(ir.schedule ? { schedule: ir.schedule } : {}),
447
+ ...(ir.workLease ? { workLease: ir.workLease } : {}),
448
+ ...(ir.changesCheckout ? { changesCheckout: true } : {}),
431
449
  };
432
450
  }
@@ -51,6 +51,7 @@ const READ_ONLY_ACTIVITY_FNS: ReadonlySet<string> = new Set([
51
51
  "spriteServiceGet",
52
52
  "spriteServiceList",
53
53
  "spriteServiceLogs",
54
+ "spriteServicesObserve",
54
55
  ]);
55
56
 
56
57
  /** Activity function names that always delete/destroy, regardless of args. */
@@ -38,7 +38,13 @@
38
38
  import type { ActivityFn, ActivityProfile } from "./activity-registry";
39
39
  import { discoverOps, type DiscoveredOp } from "./discover";
40
40
  import { runOpLocally, OpRunFailure, type OpRunResult } from "./local-executor";
41
- import { acquireLease, stillHoldsLease, currentHolderId, DEFAULT_LEASE_TTL_MS, type LeaseRecord, type AcquireLeaseResult } from "../lifecycle/lease";
41
+ import { acquireLease, releaseLease, stillHoldsLease, currentHolderId, DEFAULT_LEASE_TTL_MS, type LeaseRecord, type AcquireLeaseResult } from "../lifecycle/lease";
42
+ import { randomUUID } from "node:crypto";
43
+ import { readRunLedger, runEnvOf } from "../lifecycle/run-ledger";
44
+ import { readGateLedger, latestResolutionSince } from "../lifecycle/gate-ledger";
45
+ import { enterStewardTurn } from "./steward-turn";
46
+ import { stewardLeaseName, stewardTurnLeaseName, type StewardDeclaration } from "./steward";
47
+ import { stewardWorkHolder } from "./work-lease-run";
42
48
  import { StaleLockError } from "../lifecycle/git";
43
49
  import { cronMatches, cronDueBetween } from "./cron";
44
50
  import { createChangeSignalGate, DEFAULT_SIGNAL_FLOOR_MS, type WakeReason } from "./change-signal";
@@ -76,7 +82,36 @@ export async function discoverConvergeOps(
76
82
 
77
83
  /** One discovered ConvergeOp's outcome for one round — what `chant operator`'s one-line-per-tick log (and its tests) key off of. */
78
84
  export type OperatorTickEvent =
79
- | { kind: "ticked"; op: string; env: string; result: OpRunResult }
85
+ /**
86
+ * The Op ran. `resumed` names the decision point question it was waiting on
87
+ * (#2749) when a steward ran it because that question is now answered,
88
+ * rather than because its cron fired.
89
+ */
90
+ | {
91
+ kind: "ticked";
92
+ op: string;
93
+ env: string;
94
+ result: OpRunResult;
95
+ resumed?: string;
96
+ /**
97
+ * The gate the Op's last run stopped at (#2779), when a steward ran it
98
+ * because that gate is now approved rather than because its cron fired.
99
+ */
100
+ approved?: { op: string; gate: string };
101
+ }
102
+ /**
103
+ * A steward's Op whose last run stopped on an open decision point (#2749),
104
+ * and the question is still open, so the Op is not run this round. A person
105
+ * answers it through hud or `points answer`; the steward never does.
106
+ */
107
+ | { kind: "waiting-on-point"; op: string; env: string; point: string; question: string; state: string }
108
+ /**
109
+ * A steward's Op whose last run stopped at a gate (#2779), and nobody has
110
+ * approved it since, so the Op is not run this round. `gateOp` is the op
111
+ * the gate is recorded under: the Op's own, or a command's such as
112
+ * `workspace-upgrade`.
113
+ */
114
+ | { kind: "waiting-on-gate"; op: string; env: string; gateOp: string; gate: string }
80
115
  | { kind: "skipped-lease-held"; op: string; env: string; heldBy?: string }
81
116
  /** The Op declares its own `schedule.cron` (#2120) and this round did not land on a firing minute — the lease was never touched. An Op without a cron is never reported this way: it ticks every round, on `--interval`. */
82
117
  | { kind: "skipped-not-due"; op: string; env: string; cron: string }
@@ -94,7 +129,24 @@ export type OperatorTickEvent =
94
129
  * back off silently forever instead of surfacing the fix. One op's lease
95
130
  * error never aborts the round for every other op.
96
131
  */
97
- | { kind: "lease-error"; op: string; env: string; error: string };
132
+ | { kind: "lease-error"; op: string; env: string; error: string }
133
+ /**
134
+ * A local steward's round (#2731) found the steward's own lease held by
135
+ * another live holder: another `chant operator --steward` for the same
136
+ * steward. Nothing ticked. `op` is the steward's lease name and `env` is
137
+ * `-`, so a log line keyed on either still reads.
138
+ */
139
+ | { kind: "steward-busy"; op: string; env: string; steward: string; heldBy?: string }
140
+ /**
141
+ * A local steward's round (#2750) found its turn lease
142
+ * (`stewardTurnLeaseName`) held by someone else already mid-run — a `chant
143
+ * run <op>` typed by hand, or another round somehow still finishing one.
144
+ * Nothing ticked: the round stops here rather than trying its remaining
145
+ * ops, since every one of them would fail the same check (the turn is one
146
+ * lease for the whole steward, not one per op). The next round tries
147
+ * again.
148
+ */
149
+ | { kind: "turn-busy"; op: string; env: string; steward: string; heldBy?: string };
98
150
 
99
151
  export interface OperatorRoundOptions {
100
152
  cwd?: string;
@@ -119,6 +171,179 @@ export interface OperatorRoundOptions {
119
171
  * round makes up exactly one tick rather than a backlog.
120
172
  */
121
173
  scheduleState?: Map<string, Date>;
174
+ /**
175
+ * Run a local steward's Ops instead of every discovered ConvergeOp (#2731).
176
+ * Only its scheduled Ops tick, each on its own cron, because that is what
177
+ * the same steward does on Fountain: an Op with no schedule runs when
178
+ * someone asks for it. The round first takes (or renews) the steward's own
179
+ * lease, and ticks nothing when another holder has it.
180
+ */
181
+ steward?: StewardDeclaration;
182
+ /**
183
+ * How a steward's round reads the state of the questions its waiting runs
184
+ * stopped on (#2749): question id to state, or null when they can't be
185
+ * read. Defaults to the workspace's `points` read; a test injects one.
186
+ */
187
+ readQuestions?: (cwd: string) => Promise<Map<string, string> | null>;
188
+ }
189
+
190
+ /** A steward's Op whose last run waits on a decision point (#2749). */
191
+ interface WaitState {
192
+ /** The answer record's id. */
193
+ question: string;
194
+ point: string;
195
+ /** The question's state now, or the one the run recorded when it can't be read. */
196
+ state: string;
197
+ /** The question is answered, or its record is gone, so the Op runs again. */
198
+ answered: boolean;
199
+ }
200
+
201
+ /**
202
+ * The steward's Ops whose newest run is the steward's own and stopped on a
203
+ * decision point, with that question's state now. The questions are read
204
+ * from the workspace the way `points` reads them; a workspace that can't be
205
+ * read leaves every run waiting.
206
+ */
207
+ async function stewardWaits(steward: StewardDeclaration, opts: OperatorRoundOptions): Promise<Map<string, WaitState>> {
208
+ const out = new Map<string, WaitState>();
209
+ const runs: { op: string; question: string; point: string; state: string }[] = [];
210
+ for (const op of steward.ops) {
211
+ try {
212
+ const newest = (await readRunLedger(runEnvOf(op), op.name, { cwd: opts.cwd })).records.at(-1);
213
+ if (newest?.status === "waiting" && newest.point && newest.steward === steward.name) {
214
+ runs.push({ op: op.name, question: newest.point.id, point: newest.point.point, state: newest.point.state });
215
+ }
216
+ } catch {
217
+ // A ledger that can't be read has no waiting run to resume.
218
+ }
219
+ }
220
+ if (runs.length === 0) return out;
221
+ const states = await (opts.readQuestions ?? readQuestionStates)(opts.cwd ?? process.cwd());
222
+ for (const r of runs) {
223
+ const now = states?.get(r.question);
224
+ const answered = states !== null && (now === undefined || now === "answered");
225
+ out.set(r.op, { question: r.question, point: r.point, state: now ?? r.state, answered });
226
+ }
227
+ return out;
228
+ }
229
+
230
+ /** A steward's Op whose last run stopped at a gate (#2779). */
231
+ interface GateWaitState {
232
+ /** The op the gate is recorded under. */
233
+ gateOp: string;
234
+ gate: string;
235
+ /** Someone approved the gate since the run stopped there, so the Op runs again. */
236
+ approved: boolean;
237
+ }
238
+
239
+ /**
240
+ * The steward's Ops whose newest run is the steward's own and stopped at a
241
+ * gate, and whether the gate has been approved since. A gate ledger that
242
+ * can't be read leaves the run where it stopped.
243
+ */
244
+ async function stewardGates(steward: StewardDeclaration, opts: OperatorRoundOptions): Promise<Map<string, GateWaitState>> {
245
+ const out = new Map<string, GateWaitState>();
246
+ for (const op of steward.ops) {
247
+ try {
248
+ const newest = (await readRunLedger(runEnvOf(op), op.name, { cwd: opts.cwd })).records.at(-1);
249
+ if (newest?.status !== "gated" || !newest.gate || newest.steward !== steward.name) continue;
250
+ const gateOp = newest.gate.op ?? op.name;
251
+ const { resolutions } = await readGateLedger(gateOp, { cwd: opts.cwd });
252
+ const approved = latestResolutionSince(resolutions, newest.gate.name, newest.gate.since) !== undefined;
253
+ out.set(op.name, { gateOp, gate: newest.gate.name, approved });
254
+ } catch {
255
+ // A ledger that can't be read has no gated run to resume.
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+
261
+ /** Every question's state in the workspace, by id, or null when the workspace's points can't be read. */
262
+ async function readQuestionStates(cwd: string): Promise<Map<string, string> | null> {
263
+ try {
264
+ const { workspacePoints } = await import("../workspace/points-cli");
265
+ const doc = await workspacePoints({ cwd });
266
+ if (!("questions" in doc)) return null;
267
+ // An answer kind that could not be read might hold the question: say
268
+ // nothing rather than resume a run whose question may still be open.
269
+ if (doc.sources.some((s) => s.reason !== null)) return null;
270
+ return new Map(doc.questions.map((q) => [q.id, q.state]));
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ /** Take or renew a local steward's own lease (#2731). */
277
+ export async function acquireStewardLease(
278
+ steward: string,
279
+ holder: string,
280
+ opts: { cwd?: string; ttlMs?: number; now?: () => Date } = {},
281
+ ): Promise<AcquireLeaseResult> {
282
+ return acquireLease(stewardLeaseName(steward), holder, { ...opts, ttlMs: opts.ttlMs ?? DEFAULT_LEASE_TTL_MS });
283
+ }
284
+
285
+ /**
286
+ * How long a caller contending for a steward's turn lease
287
+ * (`acquireStewardTurn`) retries before giving up (#2750). Long enough to
288
+ * ride out a race with a turn that is just ending; short enough that a
289
+ * genuinely busy steward is reported back promptly. A round never waits
290
+ * (`waitMs` omitted there): it already runs on its own timer and simply
291
+ * tries the turn again next round, exactly as it already does for a busy
292
+ * per-op lease. `chant run <op>` (`../cli/handlers/run.ts`) is the caller
293
+ * that waits, since a human is standing at the terminal for the answer.
294
+ */
295
+ export const STEWARD_TURN_WAIT_MS = 2_000;
296
+
297
+ /** How often {@link acquireStewardTurn} retries while it waits. */
298
+ const STEWARD_TURN_POLL_MS = 150;
299
+
300
+ /**
301
+ * Take the steward's turn lease (#2750): the lease every run of one of its
302
+ * Ops — a round's scheduled tick, or `chant run <op>` typed by hand — holds
303
+ * for exactly as long as that one run takes, so the two are never beside each
304
+ * other. Distinct from {@link acquireStewardLease}, which one `chant operator
305
+ * --steward` process holds for its whole life: this one is held per-run, so a
306
+ * hand run succeeds the moment a round's tick ends, not only once the daemon
307
+ * itself stops.
308
+ *
309
+ * Contention never queues past `waitMs` (0 by default): consistent with a
310
+ * round's own leases, which skip and report rather than wait (see this
311
+ * module's doc), and with the fountain form, whose busy teammate is likewise
312
+ * refused rather than retried. A caller that wants to ride out a race with a
313
+ * turn about to end passes `waitMs`, and this polls every
314
+ * {@link STEWARD_TURN_POLL_MS} until it acquires or the wait runs out.
315
+ */
316
+ export async function acquireStewardTurn(
317
+ steward: string,
318
+ holder: string,
319
+ opts: { cwd?: string; ttlMs?: number; now?: () => Date; waitMs?: number } = {},
320
+ ): Promise<AcquireLeaseResult> {
321
+ const deadline = Date.now() + (opts.waitMs ?? 0);
322
+ for (;;) {
323
+ const result = await acquireLease(stewardTurnLeaseName(steward), holder, {
324
+ cwd: opts.cwd,
325
+ ttlMs: opts.ttlMs ?? DEFAULT_LEASE_TTL_MS,
326
+ now: opts.now,
327
+ });
328
+ if (result.acquired || result.reason !== "held" || Date.now() >= deadline) return result;
329
+ await new Promise((r) => setTimeout(r, STEWARD_TURN_POLL_MS));
330
+ }
331
+ }
332
+
333
+ /** Release a steward's turn lease, best-effort — a run whose turn was never actually acquired (or already lost) has nothing to release. */
334
+ async function releaseStewardTurn(steward: string, holder: string, token: string, opts: { cwd?: string } = {}): Promise<void> {
335
+ await releaseLease(stewardTurnLeaseName(steward), holder, token, opts).catch(() => false);
336
+ }
337
+
338
+ /**
339
+ * The Ops one round considers: a steward's Ops, or every ConvergeOp. A
340
+ * steward's Op with no schedule is considered only to resume a run of the
341
+ * steward's that waits on a decision point.
342
+ */
343
+ async function roundOps(opts: OperatorRoundOptions): Promise<DiscoveredOp["config"][]> {
344
+ if (opts.steward) return [...opts.steward.ops];
345
+ const { ops } = await discoverConvergeOps({ cwd: opts.cwd, env: opts.env });
346
+ return ops.map((d) => d.config);
122
347
  }
123
348
 
124
349
  /**
@@ -129,11 +354,66 @@ export interface OperatorRoundOptions {
129
354
  */
130
355
  export async function runOperatorRound(opts: OperatorRoundOptions): Promise<OperatorTickEvent[]> {
131
356
  const holder = opts.holder ?? currentHolderId();
132
- const { ops } = await discoverConvergeOps({ cwd: opts.cwd, env: opts.env });
133
357
  const events: OperatorTickEvent[] = [];
358
+ const steward = opts.steward;
359
+ const leaseOpts = { cwd: opts.cwd, ttlMs: opts.leaseTtlMs, now: opts.now };
360
+
361
+ // A local steward is one writer (#2731): its round runs only while it holds
362
+ // its own lease, renewed here and again after every tick, so a turn longer
363
+ // than the lease's TTL still keeps it.
364
+ const holdSteward = async (): Promise<boolean> => {
365
+ if (!steward) return true;
366
+ let acquired: AcquireLeaseResult;
367
+ try {
368
+ acquired = await acquireStewardLease(steward.name, holder, leaseOpts);
369
+ } catch (err) {
370
+ events.push({
371
+ kind: "lease-error",
372
+ op: stewardLeaseName(steward.name),
373
+ env: "-",
374
+ error: err instanceof Error ? err.message : String(err),
375
+ });
376
+ return false;
377
+ }
378
+ if (acquired.acquired) return true;
379
+ events.push({
380
+ kind: "steward-busy",
381
+ op: stewardLeaseName(steward.name),
382
+ env: "-",
383
+ steward: steward.name,
384
+ ...(acquired.heldBy?.holder ? { heldBy: acquired.heldBy.holder } : {}),
385
+ });
386
+ return false;
387
+ };
388
+
389
+ if (!(await holdSteward())) return events;
390
+
391
+ // A steward's Ops whose last run waits on a decision point (#2749), read
392
+ // once per round, and the state of those questions now.
393
+ const waiting = steward ? await stewardWaits(steward, opts) : new Map<string, WaitState>();
394
+ // And those whose last run stopped at a gate (#2779), with whether it has
395
+ // been approved since.
396
+ const gated = steward ? await stewardGates(steward, opts) : new Map<string, GateWaitState>();
134
397
 
135
- for (const { config } of ops) {
136
- const env = envOf(config) ?? "unknown";
398
+ for (const config of await roundOps(opts)) {
399
+ const env = steward ? runEnvOf(config) : (envOf(config) ?? "unknown");
400
+ const wait = waiting.get(config.name);
401
+ const stopped = gated.get(config.name);
402
+ let resumed: string | undefined;
403
+ let approved: { op: string; gate: string } | undefined;
404
+
405
+ // A run waiting on a question that is now answered is resumed on this
406
+ // round, whatever its cron says. One whose question is still open is left
407
+ // until a person answers it, unless its cron fires, which asks again.
408
+ if (wait?.answered) resumed = wait.question;
409
+ // The same for a run stopped at a gate that is now approved (#2779).
410
+ if (stopped?.approved) approved = { op: stopped.gateOp, gate: stopped.gate };
411
+ const standing: OperatorTickEvent | undefined = wait
412
+ ? { kind: "waiting-on-point", op: config.name, env, point: wait.point, question: wait.question, state: wait.state }
413
+ : stopped
414
+ ? { kind: "waiting-on-gate", op: config.name, env, gateOp: stopped.gateOp, gate: stopped.gate }
415
+ : undefined;
416
+ const resuming = resumed !== undefined || approved !== undefined;
137
417
 
138
418
  // An Op that declares its own cadence (#2120) is ticked on that cron
139
419
  // rather than on every round. Level-triggered: the question is whether a
@@ -147,10 +427,15 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
147
427
  const lastSeen = opts.scheduleState?.get(config.name);
148
428
  const due = lastSeen === undefined ? cronMatches(cron, now) : cronDueBetween(cron, lastSeen, now);
149
429
  opts.scheduleState?.set(config.name, now);
150
- if (!due) {
151
- events.push({ kind: "skipped-not-due", op: config.name, env, cron });
430
+ if (!due && !resuming) {
431
+ events.push(standing ?? { kind: "skipped-not-due", op: config.name, env, cron });
152
432
  continue;
153
433
  }
434
+ } else if (steward && !resuming) {
435
+ // An unscheduled Op runs when someone asks for it; the steward runs it
436
+ // only to resume its own waiting or gated run.
437
+ if (standing) events.push(standing);
438
+ continue;
154
439
  }
155
440
 
156
441
  let acquired: AcquireLeaseResult;
@@ -177,9 +462,48 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
177
462
  }
178
463
 
179
464
  const lease = acquired.lease as LeaseRecord;
465
+
466
+ // A local steward is one turn at a time (#2750): a hand run (`chant run
467
+ // <op>`) holds this same lease for as long as it runs, so a round never
468
+ // ticks beside one. It is one lease for the whole steward, not one per
469
+ // op, so finding it held stops the round outright — every op still ahead
470
+ // of it in `roundOps` would fail the identical check.
471
+ let turn: AcquireLeaseResult | undefined;
472
+ if (steward) {
473
+ try {
474
+ turn = await acquireStewardTurn(steward.name, holder, { cwd: opts.cwd, ttlMs: opts.leaseTtlMs, now: opts.now });
475
+ } catch (err) {
476
+ // Not turn contention — the acquire attempt itself failed (most
477
+ // likely a StaleLockError). Same shape as the per-op acquire's own
478
+ // catch above: report it and stop, rather than let it propagate and
479
+ // leak the per-op lease this round just took.
480
+ await releaseLease(config.name, holder, lease.token, { cwd: opts.cwd }).catch(() => false);
481
+ const message = err instanceof StaleLockError ? err.message : err instanceof Error ? err.message : String(err);
482
+ events.push({ kind: "lease-error", op: stewardTurnLeaseName(steward.name), env, error: message });
483
+ break;
484
+ }
485
+ if (!turn.acquired) {
486
+ await releaseLease(config.name, holder, lease.token, { cwd: opts.cwd }).catch(() => false);
487
+ events.push({ kind: "turn-busy", op: config.name, env, steward: steward.name, heldBy: turn.heldBy?.holder });
488
+ break;
489
+ }
490
+ }
491
+
492
+ // The run is the steward's turn (#2749): a decision point it asks names the
493
+ // steward and makes its model call through the steward's broker, and an
494
+ // answer from inside the turn is refused.
495
+ const runId = randomUUID();
496
+ const restoreTurn = steward
497
+ ? enterStewardTurn({ steward: steward.name, capabilities: steward.capabilities, vault: steward.vault, run: runId })
498
+ : undefined;
180
499
  try {
181
500
  const result = await runOpLocally(config, opts.activities, opts.profiles, opts.signal, {
501
+ runId,
502
+ ...(steward ? { steward: steward.name } : {}),
182
503
  ledger: { cwd: opts.cwd },
504
+ // A steward's turn claims work leases as `<steward>/<op>@<holder>`
505
+ // (#2748), which is how `workspace status` finds the lease it holds.
506
+ ...(steward ? { work: { holder: stewardWorkHolder(steward.name, config.name, holder) } } : {}),
183
507
  // #2301: without a sink, `settle` catches a failed ledger append and
184
508
  // drops it. That is the failure this tick can least afford to lose —
185
509
  // the message below points the reader at the ledger record, which is
@@ -195,7 +519,18 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
195
519
  }),
196
520
  });
197
521
  const held = await stillHoldsLease(config.name, holder, lease.token, { cwd: opts.cwd });
198
- events.push(held ? { kind: "ticked", op: config.name, env, result } : { kind: "fenced", op: config.name, env });
522
+ events.push(
523
+ held
524
+ ? {
525
+ kind: "ticked",
526
+ op: config.name,
527
+ env,
528
+ result,
529
+ ...(resumed !== undefined ? { resumed } : {}),
530
+ ...(approved !== undefined ? { approved } : {}),
531
+ }
532
+ : { kind: "fenced", op: config.name, env },
533
+ );
199
534
  } catch (err) {
200
535
  // #2301: "see its ledger record" was the whole message, and it sent the
201
536
  // reader to an artifact that a failed ledger append means is missing —
@@ -209,7 +544,11 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
209
544
  ? err.message
210
545
  : String(err);
211
546
  events.push({ kind: "tick-failed", op: config.name, env, error: message });
547
+ } finally {
548
+ restoreTurn?.();
549
+ if (steward && turn?.acquired) await releaseStewardTurn(steward.name, holder, turn.lease!.token, { cwd: opts.cwd });
212
550
  }
551
+ if (!(await holdSteward())) break;
213
552
  }
214
553
 
215
554
  return events;
@@ -419,7 +758,14 @@ export function formatRoundLine(event: OperatorTickEvent): string {
419
758
  switch (event.kind) {
420
759
  case "ticked":
421
760
  return `operator: ${event.op}@${event.env} ticked=1 status=${event.result.status}` +
422
- (event.result.gate ? ` gate="${event.result.gate.gate}"` : "");
761
+ (event.result.gate ? ` gate="${event.result.gate.gate}"` : "") +
762
+ (event.result.point ? ` point="${event.result.point.id}"` : "") +
763
+ (event.resumed ? ` resumed="${event.resumed}"` : "") +
764
+ (event.approved ? ` approved="${event.approved.op}/${event.approved.gate}"` : "");
765
+ case "waiting-on-point":
766
+ return `operator: ${event.op}@${event.env} waiting=1(point:${event.question}:${event.state})`;
767
+ case "waiting-on-gate":
768
+ return `operator: ${event.op}@${event.env} gated=1(gate:${event.gateOp}/${event.gate})`;
423
769
  case "skipped-lease-held":
424
770
  return `operator: ${event.op}@${event.env} skipped=1(lease-held${event.heldBy ? `:${event.heldBy}` : ""})`;
425
771
  case "skipped-not-due":
@@ -430,5 +776,9 @@ export function formatRoundLine(event: OperatorTickEvent): string {
430
776
  return `operator: ${event.op}@${event.env} fenced=1(lease lost mid-tick — ledger record still written)`;
431
777
  case "lease-error":
432
778
  return `operator: ${event.op}@${event.env} error=1(lease acquire failed — ${event.error})`;
779
+ case "steward-busy":
780
+ return `operator: steward ${event.steward} skipped=1(steward-lease-held${event.heldBy ? `:${event.heldBy}` : ""})`;
781
+ case "turn-busy":
782
+ return `operator: ${event.op}@${event.env} skipped=1(turn-held${event.heldBy ? `:${event.heldBy}` : ""}, steward "${event.steward}" is already mid-turn)`;
433
783
  }
434
784
  }