@intentius/chant 0.91.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 (344) 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/graph.d.ts.map +1 -1
  4. package/dist/cli/handlers/operator.d.ts +6 -0
  5. package/dist/cli/handlers/operator.d.ts.map +1 -1
  6. package/dist/cli/handlers/run.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  10. package/dist/cli/plugins.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +12 -2
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  14. package/dist/config.d.ts +22 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/lexicon-module.d.ts +22 -0
  17. package/dist/lexicon-module.d.ts.map +1 -1
  18. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  19. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  20. package/dist/lifecycle/run-ledger.d.ts +2 -0
  21. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  22. package/dist/lifecycle/symptoms.d.ts +51 -0
  23. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  24. package/dist/lint/rules/index.d.ts +4 -2
  25. package/dist/lint/rules/index.d.ts.map +1 -1
  26. package/dist/lint/rules/op/index.d.ts +14 -3
  27. package/dist/lint/rules/op/index.d.ts.map +1 -1
  28. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  29. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  31. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  33. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  35. package/dist/op/activities/activity-contracts.d.ts +201 -0
  36. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  37. package/dist/op/activities/apply.d.ts.map +1 -1
  38. package/dist/op/activities/change-coverage.d.ts +44 -0
  39. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  40. package/dist/op/activities/converge.d.ts +24 -1
  41. package/dist/op/activities/converge.d.ts.map +1 -1
  42. package/dist/op/activities/decide.d.ts +107 -0
  43. package/dist/op/activities/decide.d.ts.map +1 -0
  44. package/dist/op/activities/index.d.ts +10 -0
  45. package/dist/op/activities/index.d.ts.map +1 -1
  46. package/dist/op/activities/shell.d.ts +32 -0
  47. package/dist/op/activities/shell.d.ts.map +1 -1
  48. package/dist/op/activities/source-release.d.ts +86 -0
  49. package/dist/op/activities/source-release.d.ts.map +1 -0
  50. package/dist/op/activities/source-rollback.d.ts +100 -0
  51. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  52. package/dist/op/activities/work-evidence.d.ts +42 -0
  53. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  54. package/dist/op/activity-contract.d.ts +1 -0
  55. package/dist/op/activity-contract.d.ts.map +1 -1
  56. package/dist/op/activity-profiles.d.ts +16 -0
  57. package/dist/op/activity-profiles.d.ts.map +1 -1
  58. package/dist/op/builders.d.ts +84 -2
  59. package/dist/op/builders.d.ts.map +1 -1
  60. package/dist/op/composites/converge-op.d.ts +31 -4
  61. package/dist/op/composites/converge-op.d.ts.map +1 -1
  62. package/dist/op/decide-backend.d.ts +116 -0
  63. package/dist/op/decide-backend.d.ts.map +1 -0
  64. package/dist/op/decide-config.d.ts +120 -0
  65. package/dist/op/decide-config.d.ts.map +1 -0
  66. package/dist/op/decide-read-inputs.d.ts +40 -0
  67. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  68. package/dist/op/gate-wait.d.ts +43 -0
  69. package/dist/op/gate-wait.d.ts.map +1 -0
  70. package/dist/op/gate.d.ts +5 -2
  71. package/dist/op/gate.d.ts.map +1 -1
  72. package/dist/op/index.d.ts +11 -4
  73. package/dist/op/index.d.ts.map +1 -1
  74. package/dist/op/local-executor.d.ts +29 -4
  75. package/dist/op/local-executor.d.ts.map +1 -1
  76. package/dist/op/local-output.d.ts +2 -0
  77. package/dist/op/local-output.d.ts.map +1 -1
  78. package/dist/op/op-ir.d.ts +2 -0
  79. package/dist/op/op-ir.d.ts.map +1 -1
  80. package/dist/op/op-verb-class.d.ts.map +1 -1
  81. package/dist/op/operator.d.ts +97 -1
  82. package/dist/op/operator.d.ts.map +1 -1
  83. package/dist/op/runtime.d.ts +37 -5
  84. package/dist/op/runtime.d.ts.map +1 -1
  85. package/dist/op/runtimes/local.d.ts.map +1 -1
  86. package/dist/op/source-archive.d.ts +63 -0
  87. package/dist/op/source-archive.d.ts.map +1 -0
  88. package/dist/op/steward-points.d.ts +136 -0
  89. package/dist/op/steward-points.d.ts.map +1 -0
  90. package/dist/op/steward-turn.d.ts +53 -0
  91. package/dist/op/steward-turn.d.ts.map +1 -0
  92. package/dist/op/steward.d.ts +22 -1
  93. package/dist/op/steward.d.ts.map +1 -1
  94. package/dist/op/types.d.ts +7 -0
  95. package/dist/op/types.d.ts.map +1 -1
  96. package/dist/op/work-lease-run.d.ts +1 -1
  97. package/dist/op/work-lease-run.d.ts.map +1 -1
  98. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  100. package/dist/workspace/answers-ledger.d.ts +76 -0
  101. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  102. package/dist/workspace/changes-cli.d.ts +16 -0
  103. package/dist/workspace/changes-cli.d.ts.map +1 -0
  104. package/dist/workspace/changes.d.ts +200 -0
  105. package/dist/workspace/changes.d.ts.map +1 -0
  106. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  107. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  108. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  118. package/dist/workspace/chant-migrations.d.ts +99 -0
  119. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  120. package/dist/workspace/checks/boxes.d.ts +34 -1
  121. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  122. package/dist/workspace/checks/diagrams.d.ts +31 -0
  123. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  124. package/dist/workspace/checks/records.d.ts +7 -0
  125. package/dist/workspace/checks/records.d.ts.map +1 -1
  126. package/dist/workspace/checks.d.ts +3 -0
  127. package/dist/workspace/checks.d.ts.map +1 -1
  128. package/dist/workspace/compose-graph.d.ts +2 -0
  129. package/dist/workspace/compose-graph.d.ts.map +1 -1
  130. package/dist/workspace/conformance/index.d.ts.map +1 -1
  131. package/dist/workspace/decide.d.ts +49 -4
  132. package/dist/workspace/decide.d.ts.map +1 -1
  133. package/dist/workspace/declaration.d.ts +52 -0
  134. package/dist/workspace/declaration.d.ts.map +1 -1
  135. package/dist/workspace/declaration.schema.json +114 -1
  136. package/dist/workspace/declared-kinds.d.ts +20 -0
  137. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  138. package/dist/workspace/generated-files.d.ts +7 -0
  139. package/dist/workspace/generated-files.d.ts.map +1 -1
  140. package/dist/workspace/intent.d.ts.map +1 -1
  141. package/dist/workspace/lineage-check.d.ts +3 -0
  142. package/dist/workspace/lineage-check.d.ts.map +1 -1
  143. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  144. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  145. package/dist/workspace/ls.d.ts +39 -1
  146. package/dist/workspace/ls.d.ts.map +1 -1
  147. package/dist/workspace/point-answer.schema.json +95 -0
  148. package/dist/workspace/points-cli.d.ts.map +1 -1
  149. package/dist/workspace/points.d.ts +1 -1
  150. package/dist/workspace/reason-codes.d.ts +14 -2
  151. package/dist/workspace/reason-codes.d.ts.map +1 -1
  152. package/dist/workspace/records-cli.d.ts +11 -1
  153. package/dist/workspace/records-cli.d.ts.map +1 -1
  154. package/dist/workspace/records-close.d.ts +1 -1
  155. package/dist/workspace/records-close.d.ts.map +1 -1
  156. package/dist/workspace/records-write.d.ts +11 -5
  157. package/dist/workspace/records-write.d.ts.map +1 -1
  158. package/dist/workspace/records.d.ts +34 -4
  159. package/dist/workspace/records.d.ts.map +1 -1
  160. package/dist/workspace/status-gates.d.ts +7 -2
  161. package/dist/workspace/status-gates.d.ts.map +1 -1
  162. package/dist/workspace/status-stewards.d.ts +59 -1
  163. package/dist/workspace/status-stewards.d.ts.map +1 -1
  164. package/dist/workspace/status.d.ts +16 -0
  165. package/dist/workspace/status.d.ts.map +1 -1
  166. package/dist/workspace/work-cli.d.ts +113 -2
  167. package/dist/workspace/work-cli.d.ts.map +1 -1
  168. package/dist/workspace/work-evidence.d.ts +74 -0
  169. package/dist/workspace/work-evidence.d.ts.map +1 -0
  170. package/dist/workspace/work.d.ts +43 -1
  171. package/dist/workspace/work.d.ts.map +1 -1
  172. package/package.json +1 -1
  173. package/src/cli/commands/audit.test.ts +9 -1
  174. package/src/cli/commands/build.ts +3 -2
  175. package/src/cli/commands/lint.ts +7 -1
  176. package/src/cli/handlers/graph.test.ts +20 -0
  177. package/src/cli/handlers/graph.ts +16 -2
  178. package/src/cli/handlers/operator.ts +34 -2
  179. package/src/cli/handlers/run.test.ts +154 -2
  180. package/src/cli/handlers/run.ts +121 -2
  181. package/src/cli/main.test.ts +10 -0
  182. package/src/cli/main.ts +19 -0
  183. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  184. package/src/cli/mcp/workspace-tools.ts +62 -4
  185. package/src/cli/plugins.ts +11 -3
  186. package/src/cli/registry.ts +12 -2
  187. package/src/codegen/pinned-upgrade.ts +2 -1
  188. package/src/codegen/release-wiring.test.ts +49 -2
  189. package/src/components/capability-plugin-loader.ts +3 -3
  190. package/src/components/cli-support.ts +2 -2
  191. package/src/config.ts +11 -0
  192. package/src/lexicon-module.ts +56 -1
  193. package/src/lexicon-package.test.ts +96 -0
  194. package/src/lifecycle/converge-ledger.ts +20 -0
  195. package/src/lifecycle/run-ledger.ts +6 -0
  196. package/src/lifecycle/symptoms.ts +95 -0
  197. package/src/lint/rules/index.ts +6 -2
  198. package/src/lint/rules/op/index.ts +17 -4
  199. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  200. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  201. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  202. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  203. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  204. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  205. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  206. package/src/op/activities/activity-contracts.ts +190 -1
  207. package/src/op/activities/apply.ts +7 -6
  208. package/src/op/activities/change-coverage.ts +60 -0
  209. package/src/op/activities/converge.test.ts +55 -1
  210. package/src/op/activities/converge.ts +208 -29
  211. package/src/op/activities/decide.live.test.ts +78 -0
  212. package/src/op/activities/decide.test.ts +300 -0
  213. package/src/op/activities/decide.ts +267 -0
  214. package/src/op/activities/index.ts +21 -0
  215. package/src/op/activities/shell.test.ts +91 -1
  216. package/src/op/activities/shell.ts +91 -2
  217. package/src/op/activities/source-release.test.ts +118 -0
  218. package/src/op/activities/source-release.ts +168 -0
  219. package/src/op/activities/source-rollback.test.ts +131 -0
  220. package/src/op/activities/source-rollback.ts +239 -0
  221. package/src/op/activities/work-evidence.ts +62 -0
  222. package/src/op/activity-contract-registry.test.ts +2 -0
  223. package/src/op/activity-contract-registry.ts +2 -2
  224. package/src/op/activity-contract.test.ts +12 -0
  225. package/src/op/activity-contract.ts +6 -1
  226. package/src/op/activity-profiles.ts +29 -0
  227. package/src/op/activity-registry.test.ts +1 -1
  228. package/src/op/activity-registry.ts +2 -2
  229. package/src/op/builders.ts +119 -5
  230. package/src/op/command-gate.test.ts +192 -0
  231. package/src/op/composites/converge-op.test.ts +27 -1
  232. package/src/op/composites/converge-op.ts +69 -15
  233. package/src/op/decide-backend.test.ts +44 -0
  234. package/src/op/decide-backend.ts +212 -0
  235. package/src/op/decide-config.test.ts +52 -0
  236. package/src/op/decide-config.ts +80 -0
  237. package/src/op/decide-read-inputs.ts +117 -0
  238. package/src/op/gate-approval.ts +2 -2
  239. package/src/op/gate-wait.ts +57 -0
  240. package/src/op/gate.ts +8 -3
  241. package/src/op/generate-pipeline.ts +2 -2
  242. package/src/op/index.ts +18 -3
  243. package/src/op/local-executor.ts +151 -15
  244. package/src/op/local-output.test.ts +33 -0
  245. package/src/op/local-output.ts +20 -2
  246. package/src/op/op-ir.test.ts +10 -0
  247. package/src/op/op-ir.ts +4 -0
  248. package/src/op/op-verb-class.ts +1 -0
  249. package/src/op/operator.ts +290 -11
  250. package/src/op/runtime.ts +31 -8
  251. package/src/op/runtimes/local.ts +3 -1
  252. package/src/op/source-archive.ts +161 -0
  253. package/src/op/steward-points.test.ts +313 -0
  254. package/src/op/steward-points.ts +194 -0
  255. package/src/op/steward-turn.ts +79 -0
  256. package/src/op/steward.test.ts +87 -3
  257. package/src/op/steward.ts +25 -1
  258. package/src/op/types.ts +7 -0
  259. package/src/op/work-lease-run.test.ts +20 -1
  260. package/src/op/work-lease-run.ts +2 -2
  261. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  262. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  263. package/src/workspace/answers-ledger.ts +118 -0
  264. package/src/workspace/changes-cli.ts +56 -0
  265. package/src/workspace/changes.schema.json +472 -0
  266. package/src/workspace/changes.test.ts +315 -0
  267. package/src/workspace/changes.ts +512 -0
  268. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  269. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  270. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  271. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  272. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  273. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  274. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  275. package/src/workspace/chant-migrations.ts +164 -0
  276. package/src/workspace/check-contract.test.ts +4 -2
  277. package/src/workspace/check.schema.json +15 -5
  278. package/src/workspace/checks/boxes.test.ts +80 -1
  279. package/src/workspace/checks/boxes.ts +136 -1
  280. package/src/workspace/checks/diagrams.test.ts +134 -0
  281. package/src/workspace/checks/diagrams.ts +108 -0
  282. package/src/workspace/checks/records.ts +47 -0
  283. package/src/workspace/checks.ts +8 -1
  284. package/src/workspace/compose-graph.ts +2 -0
  285. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  286. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  287. package/src/workspace/conformance/index.ts +5 -0
  288. package/src/workspace/decide.test.ts +1 -1
  289. package/src/workspace/decide.ts +156 -23
  290. package/src/workspace/declaration.schema.json +114 -1
  291. package/src/workspace/declaration.test.ts +58 -0
  292. package/src/workspace/declaration.ts +104 -0
  293. package/src/workspace/declared-kinds.test.ts +5 -5
  294. package/src/workspace/declared-kinds.ts +46 -1
  295. package/src/workspace/generated-files.ts +14 -0
  296. package/src/workspace/graph-cli.ts +1 -1
  297. package/src/workspace/graph.schema.json +9 -1
  298. package/src/workspace/intent.schema.json +8 -2
  299. package/src/workspace/intent.ts +3 -10
  300. package/src/workspace/lineage-check.ts +16 -1
  301. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  302. package/src/workspace/lineage-upgrade.test.ts +70 -0
  303. package/src/workspace/lineage-upgrade.ts +81 -10
  304. package/src/workspace/ls-contract.test.ts +57 -0
  305. package/src/workspace/ls.schema.json +46 -1
  306. package/src/workspace/ls.ts +62 -3
  307. package/src/workspace/points-cli.ts +14 -3
  308. package/src/workspace/points-write.schema.json +11 -1
  309. package/src/workspace/points.schema.json +33 -0
  310. package/src/workspace/points.test.ts +1 -1
  311. package/src/workspace/points.ts +1 -1
  312. package/src/workspace/read-contract.test.ts +1 -1
  313. package/src/workspace/reason-codes.test.ts +11 -1
  314. package/src/workspace/reason-codes.ts +18 -2
  315. package/src/workspace/records-amend.schema.json +2 -0
  316. package/src/workspace/records-cli.ts +13 -3
  317. package/src/workspace/records-close.schema.json +2 -0
  318. package/src/workspace/records-new.schema.json +2 -0
  319. package/src/workspace/records-review.schema.json +2 -0
  320. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  321. package/src/workspace/records-sessions-write.test.ts +2 -34
  322. package/src/workspace/records-write.e2e.test.ts +108 -0
  323. package/src/workspace/records-write.test.ts +32 -41
  324. package/src/workspace/records-write.ts +33 -15
  325. package/src/workspace/records.schema.json +36 -2
  326. package/src/workspace/records.test.ts +48 -3
  327. package/src/workspace/records.ts +109 -3
  328. package/src/workspace/status-contract.test.ts +13 -8
  329. package/src/workspace/status-gates.ts +9 -4
  330. package/src/workspace/status-stewards.ts +108 -4
  331. package/src/workspace/status.schema.json +111 -7
  332. package/src/workspace/status.ts +36 -1
  333. package/src/workspace/work-acceptance.test.ts +274 -0
  334. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  335. package/src/workspace/work-cli.ts +193 -10
  336. package/src/workspace/work-evidence.schema.json +212 -0
  337. package/src/workspace/work-evidence.ts +169 -0
  338. package/src/workspace/work-history.schema.json +349 -0
  339. package/src/workspace/work-history.test.ts +156 -0
  340. package/src/workspace/work.test.ts +3 -2
  341. package/src/workspace/work.ts +100 -1
  342. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  343. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  344. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Decision points reached in a steward's turn (#2749): the model call goes
3
+ * through the steward's brokered capability, an open question stops the run
4
+ * as `waiting` and stays open in the workspace for a person, the steward's
5
+ * turn never answers it, and the local steward resumes the Op on the first
6
+ * round after a person answers.
7
+ */
8
+
9
+ import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
12
+ import { cleanScratch, commitAll, contract, git, REPO, repo } from "../workspace/__fixtures__/contract-repo";
13
+ import statusSchema from "../workspace/status.schema.json";
14
+ import pointsSchema from "../workspace/points.schema.json";
15
+ import { answerPoint, askPoint } from "../workspace/decide";
16
+ import { workspacePoints } from "../workspace/points-cli";
17
+ import type { WireAnswer } from "../workspace/points";
18
+ import { readMemberStewards } from "../workspace/status-stewards";
19
+ import { readRunLedger } from "../lifecycle/run-ledger";
20
+ import type { ActivityFn, ActivityProfile } from "./activity-registry";
21
+ import { runOpLocally } from "./local-executor";
22
+ import { formatRoundLine, runOperatorRound } from "./operator";
23
+ import { declareSteward } from "./steward";
24
+ import { askPointInRun, isPointWait, PointWait, type BrokeredModelAsk } from "./steward-points";
25
+ import { enterStewardTurn, resetStewardTurn, STEWARD_ENV } from "./steward-turn";
26
+ import type { OpConfig } from "./types";
27
+
28
+ const REF = join(REPO, "reference-workspace");
29
+ const PROFILES: Record<string, ActivityProfile> = {};
30
+ const on = "2026-09-25";
31
+
32
+ const points = {
33
+ points: {
34
+ "ship-now": {
35
+ title: "Ship this release now",
36
+ question: { type: "noul", instructions: "Is the release ready to ship?", criteria: { true: "Ship it.", false: "Hold it." } },
37
+ inputs: { release: "the release, as status lists it" },
38
+ deciders: [
39
+ { kind: "table", rows: [{ when: { release: "hotfix" }, answer: true }] },
40
+ { kind: "model", backend: "systemone", model: "jev-1.13.0", threshold: 0.8 },
41
+ { kind: "quorum", count: 1 },
42
+ ],
43
+ },
44
+ },
45
+ };
46
+
47
+ const confident: WireAnswer = { type: "noul", noul: 0.97 };
48
+
49
+ let root: string;
50
+
51
+ beforeAll(() => {
52
+ root = repo(
53
+ {
54
+ "chant.workspace.json": JSON.stringify(
55
+ { name: "studio", schema: 1, members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }], records: [{ kind: "answers/answer.kind.mjs" }] },
56
+ null,
57
+ 2,
58
+ ),
59
+ "app/server.mjs": "export const port = 8080;\n",
60
+ "decisions/points.json": JSON.stringify(points, null, 2),
61
+ ".chant/trust.json": JSON.stringify({ schema: 1, roles: { agent: ["bot"] } }),
62
+ },
63
+ false,
64
+ );
65
+ cpSync(join(REF, "answers", "answer.kind.mjs"), join(root, "answers", "answer.kind.mjs"));
66
+ cpSync(join(REF, "answers", "answer.schema.json"), join(root, "answers", "answer.schema.json"));
67
+ // The run ledger and leases are written with git plumbing, which needs an identity.
68
+ git(root, "config", "user.name", "Steward Test");
69
+ git(root, "config", "user.email", "steward@example.com");
70
+ commitAll(root, "c0");
71
+ });
72
+
73
+ afterEach(() => resetStewardTurn());
74
+ afterAll(cleanScratch);
75
+
76
+ /** A brokered model call that records what it was asked through. */
77
+ function brokered(answer: WireAnswer, calls: { capability: string }[]): BrokeredModelAsk {
78
+ return async (req, via) => {
79
+ calls.push({ capability: via.capability });
80
+ return { model: req.model, answer };
81
+ };
82
+ }
83
+
84
+ /** A steward's question, as held on the lifecycle ledger (#2786). */
85
+ function ledgerText(id: string): string {
86
+ return git(root, "show", `chant/lifecycle:_answers/answer/${id}.md`);
87
+ }
88
+
89
+ async function waitOf(p: Promise<unknown>): Promise<PointWait> {
90
+ try {
91
+ await p;
92
+ } catch (err) {
93
+ if (isPointWait(err)) return err;
94
+ throw err;
95
+ }
96
+ throw new Error("expected the ask to wait");
97
+ }
98
+
99
+ describe("askPointInRun (#2749)", () => {
100
+ test("an answered question returns its answer, outside any steward", async () => {
101
+ const got = await askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "hotfix" }, on });
102
+ expect(got.answer).toBe(true);
103
+ expect(got.question).toMatchObject({ state: "answered", askedBy: null });
104
+ });
105
+
106
+ test("in a steward's turn the model call goes through the steward's brokered capability, and a proposal waits for a person", async () => {
107
+ enterStewardTurn({ steward: "box-steward", capabilities: ["inference"], vault: null, run: "run-1" });
108
+ const calls: { capability: string }[] = [];
109
+ const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-1" }, subject: "r-1", ask: brokered(confident, calls), on }));
110
+ expect(calls).toEqual([{ capability: "inference" }]);
111
+ expect(wait.question).toMatchObject({ point: "ship-now", state: "proposed", subject: "r-1", steward: "box-steward" });
112
+ const text = ledgerText(wait.question.id);
113
+ expect(text).toContain('harness: "chant-steward"');
114
+ expect(text).toContain('name: "box-steward"');
115
+ expect(text).toContain('id: "run-1"');
116
+ // The question is open in the read contract, naming who waits on it.
117
+ const doc = await workspacePoints({ cwd: root, open: true });
118
+ if (!("questions" in doc)) throw new Error(doc.error.message);
119
+ expect(doc.questions.find((q) => q.id === wait.question.id)).toMatchObject({
120
+ open: true,
121
+ state: "proposed",
122
+ askedBy: { steward: "box-steward", run: "run-1" },
123
+ ledger: `chant/lifecycle:_answers/answer/${wait.question.id}.md`,
124
+ });
125
+ });
126
+
127
+ test("a steward that doesn't name the capability makes no model call, and the question goes to people", async () => {
128
+ enterStewardTurn({ steward: "box-steward", capabilities: ["fountain"], vault: null });
129
+ const calls: { capability: string }[] = [];
130
+ const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-2" }, ask: brokered(confident, calls), on }));
131
+ expect(calls).toEqual([]);
132
+ expect(wait.question.state).toBe("escalated");
133
+ const text = ledgerText(wait.question.id);
134
+ expect(text).toMatch(/does not name the brokered capability inference \(it names fountain\)/);
135
+ });
136
+
137
+ test("a steward holding a vault makes no model call either", async () => {
138
+ enterStewardTurn({ steward: "vault-steward", capabilities: [], vault: "creds" });
139
+ const calls: { capability: string }[] = [];
140
+ const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-3" }, ask: brokered(confident, calls), on }));
141
+ expect(calls).toEqual([]);
142
+ expect(ledgerText(wait.question.id)).toMatch(/holds the vault creds/);
143
+ });
144
+ });
145
+
146
+ describe("a steward's question stays out of the checkout (#2786)", () => {
147
+ const status = () => git(root, "status", "--porcelain", "--untracked-files=all");
148
+
149
+ test("asked in a steward's turn, it is written on chant/lifecycle, the checkout stays clean, and points --open lists it", async () => {
150
+ // Earlier tests asked outside a turn, into the tree; commit those so the checkout starts clean.
151
+ commitAll(root, "earlier answers");
152
+ expect(status()).toBe("");
153
+ enterStewardTurn({ steward: "box-steward", capabilities: [], vault: null, run: "run-9" });
154
+ const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-9" }, subject: "r-9", on }));
155
+ expect(status()).toBe("");
156
+ expect(existsSync(join(root, wait.question.path))).toBe(false);
157
+ expect(ledgerText(wait.question.id)).toContain(`id: "${wait.question.id}"`);
158
+
159
+ const doc = await workspacePoints({ cwd: root, open: true });
160
+ if (!("questions" in doc)) throw new Error(doc.error.message);
161
+ const listed = doc.questions.find((q) => q.id === wait.question.id);
162
+ expect(listed).toMatchObject({ state: "escalated", open: true, subject: "r-9", path: wait.question.path, ledger: `chant/lifecycle:_answers/answer/${wait.question.id}.md` });
163
+ const pointsShape = contract(pointsSchema);
164
+ pointsShape.expectValid(doc);
165
+
166
+ // Asked again in the same turn, the question is found on the ledger, not asked twice.
167
+ const again = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-9" }, subject: "r-9", on }));
168
+ expect(again.question.id).toBe(wait.question.id);
169
+
170
+ // A person answers it: the answer goes back on the ledger, and the checkout is still clean.
171
+ resetStewardTurn();
172
+ const answered = await answerPoint({ cwd: root, id: wait.question.id, answer: "no", by: ["alice"], on });
173
+ if ("error" in answered) throw new Error(answered.error.message);
174
+ expect(answered.question).toMatchObject({ state: "answered", answer: false, ledger: `chant/lifecycle:_answers/answer/${wait.question.id}.md` });
175
+ expect(status()).toBe("");
176
+ expect(ledgerText(wait.question.id)).toContain('state: "answered"');
177
+
178
+ // The steward's next turn reads the answer and goes on.
179
+ enterStewardTurn({ steward: "box-steward", capabilities: [], vault: null, run: "run-10" });
180
+ const got = await askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-9" }, subject: "r-9", on });
181
+ expect(got.answer).toBe(false);
182
+ expect(status()).toBe("");
183
+ });
184
+
185
+ test("points ask shelled out from a steward's turn (CHANT_STEWARD) goes on the ledger too, naming the steward", async () => {
186
+ expect(status()).toBe("");
187
+ process.env[STEWARD_ENV] = "box-steward";
188
+ const doc = await askPoint({ cwd: root, point: "ship-now", inputs: { release: "r-11" }, subject: "r-11", on });
189
+ if ("error" in doc) throw new Error(doc.error.message);
190
+ expect(doc.written).toBe(true);
191
+ expect(doc.question).toMatchObject({ state: "escalated", askedBy: { steward: "box-steward", run: null }, ledger: `chant/lifecycle:_answers/answer/${doc.id}.md` });
192
+ expect(status()).toBe("");
193
+ });
194
+
195
+ test("outside a steward's turn a question is still written into the answer kind's directory", async () => {
196
+ const doc = await askPoint({ cwd: root, point: "ship-now", inputs: { release: "r-12" }, on });
197
+ if ("error" in doc) throw new Error(doc.error.message);
198
+ expect(doc.question.ledger).toBeNull();
199
+ expect(existsSync(join(root, doc.path))).toBe(true);
200
+ rmSync(join(root, doc.path));
201
+ });
202
+ });
203
+
204
+ describe("a steward never answers a question (#2749)", () => {
205
+ test("points answer is refused inside a steward's turn, and in a process it started", async () => {
206
+ enterStewardTurn({ steward: "box-steward", capabilities: ["inference"], vault: null });
207
+ const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-4" }, on }));
208
+ const doc = await answerPoint({ cwd: root, id: wait.question.id, answer: "yes", by: ["alice"], on });
209
+ expect("error" in doc && doc.error.code).toBe("answer-in-steward-turn");
210
+
211
+ resetStewardTurn();
212
+ process.env[STEWARD_ENV] = "box-steward";
213
+ const child = await answerPoint({ cwd: root, id: wait.question.id, answer: "yes", by: ["alice"], on });
214
+ expect("error" in child && child.error.code).toBe("answer-in-steward-turn");
215
+ });
216
+
217
+ test("the steward that asked never counts toward the quorum", async () => {
218
+ enterStewardTurn({ steward: "box-steward", capabilities: [], vault: null });
219
+ const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-5" }, on }));
220
+ resetStewardTurn();
221
+ const doc = await answerPoint({ cwd: root, id: wait.question.id, answer: "yes", by: ["Box-Steward", "bot"], on });
222
+ if (!("error" in doc)) throw new Error("expected a refusal");
223
+ expect(doc.error.code).toBe("quorum-not-met");
224
+ expect(doc.error.message).toMatch(/Box-Steward, who is the steward that asked/);
225
+ expect(doc.error.message).toMatch(/bot, who holds the agent role/);
226
+ });
227
+ });
228
+
229
+ function shipOp(name: string, release: string, cron?: string): OpConfig {
230
+ return {
231
+ name,
232
+ overview: `${name} fixture`,
233
+ phases: [
234
+ { name: "Decide", steps: [{ kind: "activity", fn: "askShip", args: { release } }] },
235
+ { name: "Ship", steps: [{ kind: "activity", fn: "ship", args: {} }] },
236
+ ],
237
+ onFailure: [{ name: "Undo", steps: [{ kind: "activity", fn: "undo", args: {} }] }],
238
+ ...(cron ? { schedule: { cron, overlap: "skip" as const } } : {}),
239
+ };
240
+ }
241
+
242
+ function shipActivities(log: string[]): Map<string, ActivityFn> {
243
+ return new Map<string, ActivityFn>([
244
+ ["askShip", async (args) => (await askPointInRun({ cwd: root, point: "ship-now", inputs: { release: args.release }, on })).answer],
245
+ ["ship", async () => { log.push("ship"); return { shipped: true }; }],
246
+ ["undo", async () => { log.push("undo"); return {}; }],
247
+ ]);
248
+ }
249
+
250
+ describe("an Op waiting on a decision point (#2749)", () => {
251
+ test("stops as waiting, like a gate: no later phase, no compensation, and the ledger names the question", async () => {
252
+ const log: string[] = [];
253
+ const result = await runOpLocally(shipOp("ship-once", "r-6"), shipActivities(log), PROFILES, undefined, { ledger: { cwd: root } });
254
+ expect(result.status).toBe("waiting");
255
+ expect(log).toEqual([]);
256
+ expect(result.records.map((r) => [r.fn, r.status])).toEqual([["askShip", "skipped"], ["ship", "skipped"]]);
257
+ expect(result.point).toMatchObject({ point: "ship-now", state: "escalated", steward: null });
258
+ const newest = (await readRunLedger("local", "ship-once", { cwd: root })).records.at(-1)!;
259
+ expect(newest).toMatchObject({ status: "waiting", point: { id: result.point!.id, point: "ship-now", state: "escalated" } });
260
+ expect(newest.steward).toBeUndefined();
261
+ expect(newest.phases[0].steps[0]).toMatchObject({ fn: "askShip", status: "skipped", point: { id: result.point!.id } });
262
+ });
263
+
264
+ test("a local steward records the wait, lists it in status, leaves it while open, and resumes once a person answers", async () => {
265
+ const config = shipOp("box-ship", "r-7", "0 0 1 1 *");
266
+ const steward = declareSteward({ name: "box-steward", ops: [config], capabilities: ["inference"] });
267
+ const log: string[] = [];
268
+ const scheduleState = new Map<string, Date>();
269
+ // Cron minutes are local time, so the rounds are too.
270
+ const round = (minute: number) =>
271
+ runOperatorRound({ cwd: root, steward, activities: shipActivities(log), profiles: PROFILES, holder: "box", now: () => new Date(2027, 0, 1, 0, minute, 10), scheduleState });
272
+
273
+ // The cron fires: the Op asks, nobody has answered, the run waits.
274
+ const first = await round(0);
275
+ expect(first).toHaveLength(1);
276
+ expect(first[0]).toMatchObject({ kind: "ticked", op: "box-ship" });
277
+ const ticked = first[0] as Extract<(typeof first)[number], { kind: "ticked" }>;
278
+ expect(ticked.result.status).toBe("waiting");
279
+ const question = ticked.result.point!.id;
280
+ expect(formatRoundLine(first[0])).toContain(`status=waiting point="${question}"`);
281
+ const newest = (await readRunLedger("local", "box-ship", { cwd: root })).records.at(-1)!;
282
+ expect(newest).toMatchObject({ status: "waiting", steward: "box-steward", point: { id: question } });
283
+
284
+ // workspace status lists what the steward waits on, beside its Ops.
285
+ mkdirSync(join(root, "ops"), { recursive: true });
286
+ writeFileSync(join(root, "chant.config.json"), "{}\n");
287
+ writeFileSync(join(root, "ops", "steward.op.ts"), `export const steward = ${JSON.stringify(steward)};\n`);
288
+ const status = await readMemberStewards(root, "local", "2027-01-01T00:01:00Z");
289
+ expect(status.reasons).toEqual([]);
290
+ const entry = status.stewards.find((s) => s.name === "box-steward")!;
291
+ // The entry keeps status.schema.json's steward shape (#2731, #2749).
292
+ const stewardShape = contract({ $schema: statusSchema.$schema, $id: "urn:test:status-steward", $defs: statusSchema.$defs, $ref: "#/$defs/steward" });
293
+ stewardShape.expectValid(entry);
294
+ expect(entry.ops[0].lastRun).toMatchObject({ status: "waiting", point: { id: question, point: "ship-now", state: "escalated" } });
295
+ expect(entry.waiting).toEqual([
296
+ { op: "box-ship", run: newest.id, id: question, point: "ship-now", state: "escalated", path: newest.point!.path, subject: null, since: newest.point!.since },
297
+ ]);
298
+
299
+ // Still open: the steward does not run it, and says what it waits on.
300
+ const second = await round(5);
301
+ expect(second).toEqual([{ kind: "waiting-on-point", op: "box-ship", env: "local", point: "ship-now", question, state: "escalated" }]);
302
+ expect(formatRoundLine(second[0])).toBe(`operator: box-ship@local waiting=1(point:${question}:escalated)`);
303
+
304
+ // A person answers, through hud or at a shell; the next round resumes the Op.
305
+ const answered = await answerPoint({ cwd: root, id: question, answer: "yes", by: ["alice"], on });
306
+ expect("error" in answered).toBe(false);
307
+ const third = await round(10);
308
+ expect(third[0]).toMatchObject({ kind: "ticked", op: "box-ship", resumed: question });
309
+ expect((third[0] as { result: { status: string } }).result.status).toBe("ok");
310
+ expect(log).toEqual(["ship"]);
311
+ expect((await readMemberStewards(root, "local", "2027-01-01T00:11:00Z")).stewards[0].waiting).toEqual([]);
312
+ });
313
+ });
@@ -0,0 +1,194 @@
1
+ /**
2
+ * A decision point asked inside an Op run (#2749), and in a steward's turn in
3
+ * particular.
4
+ *
5
+ * An Op that needs a decision (the factory's builder tier, whether to ship)
6
+ * calls {@link askPointInRun} from one of its activities, such as the decide
7
+ * activity (#2740). It asks the point through `askPoint` (ws-058,
8
+ * `../workspace/decide.ts`), which records the question and its answer as a
9
+ * record, and then:
10
+ *
11
+ * - an `answered` question returns its answer, and the Op goes on;
12
+ * - an `escalated` or `proposed` question is open, so the activity throws
13
+ * {@link PointWait}. The executor ends the run there with status `waiting`,
14
+ * as a gate ends it with `gated`: no later step runs, no `onFailure` phase
15
+ * runs, and the run ledger records the question the run is waiting on.
16
+ *
17
+ * The question stays open in the workspace, where `chant workspace points
18
+ * --open`, MCP's `workspace-points` and hud read it, and a person answers it
19
+ * with `points answer`. Nothing waits in the meantime: the next run asks the
20
+ * same point with the same inputs, finds the answer record, and goes on. A
21
+ * local steward re-runs a waiting Op on the first round after its question is
22
+ * answered (`./operator.ts`).
23
+ *
24
+ * ## In a steward's turn
25
+ *
26
+ * A steward runs unattended, so two more rules hold when the process is a
27
+ * steward's turn (`./steward-turn.ts`):
28
+ *
29
+ * - The model call goes through the broker. The caller supplies the call as a
30
+ * {@link BrokeredModelAsk}, which is handed the box capability to reach the
31
+ * model through, and only a capability the steward declares
32
+ * (`declareSteward({ capabilities })`, #2726). A steward that names none,
33
+ * or holds a vault, makes no model call: the model decider is recorded as
34
+ * not reached and the question goes to people.
35
+ * - The question is written on the `chant/lifecycle` branch, not in the
36
+ * checkout (#2786): the working tree is the coding agent's, and an Op's
37
+ * leased worktree goes when the run ends. `points` reads it from there
38
+ * (`../workspace/answers-ledger.ts`), and a person's answer goes there too.
39
+ * - The question names the steward and its run in its source block, and
40
+ * neither the steward's turn nor the steward's name can answer it: `points
41
+ * answer` refuses inside a steward's turn (`answer-in-steward-turn`) and
42
+ * leaves the asking steward out of the quorum.
43
+ *
44
+ * The question never goes to the steward's own thread: on Fountain that thread
45
+ * is the steward's, and nobody reads it for questions.
46
+ */
47
+
48
+ import type { ModelAsk, ModelRequest, WireAnswer } from "../workspace/points";
49
+ import type { QuestionView } from "../workspace/decide";
50
+ import { currentStewardTurn, type StewardTurn } from "./steward-turn";
51
+
52
+ /** The box capability a model call goes through when the caller names none. */
53
+ export const DEFAULT_INFERENCE_CAPABILITY = "inference";
54
+
55
+ /** The open question a run stopped on, as the run ledger records it. */
56
+ export interface WaitingPoint {
57
+ /** The answer record's id: what `points answer` and hud name. */
58
+ id: string;
59
+ /** The point asked. */
60
+ point: string;
61
+ /** Its state when the run stopped: `escalated` to people, or `proposed` by a model and waiting for a person to confirm it. */
62
+ state: "escalated" | "proposed";
63
+ /** The answer record, from the repository root. */
64
+ path: string;
65
+ /** What the question is about, or null. */
66
+ subject: string | null;
67
+ /** The steward whose turn asked it, or null. */
68
+ steward: string | null;
69
+ }
70
+
71
+ /** The marker {@link PointWait} carries, so a copy of core linked twice still recognises it. */
72
+ const POINT_WAIT = Symbol.for("chant.op.PointWait");
73
+
74
+ /**
75
+ * Thrown by an activity whose decision point is open. Not a failure: the
76
+ * executor ends the run with status `waiting` (see the module doc).
77
+ */
78
+ export class PointWait extends Error {
79
+ readonly [POINT_WAIT] = true;
80
+ constructor(readonly question: WaitingPoint) {
81
+ super(
82
+ `waiting on decision point ${question.point} (${question.id}, ${question.state}): ` +
83
+ `a person answers it through hud or \`chant workspace points answer ${question.id}\``,
84
+ );
85
+ this.name = "PointWait";
86
+ }
87
+ }
88
+
89
+ /** Is this a {@link PointWait}? Duck-typed on the shared symbol. */
90
+ export function isPointWait(err: unknown): err is PointWait {
91
+ return !!err && typeof err === "object" && (err as Record<symbol, unknown>)[POINT_WAIT] === true && "question" in err;
92
+ }
93
+
94
+ /**
95
+ * The model call, supplied by the caller (the decide activity, #2740, or a
96
+ * test's stub), made through a box capability. `via.capability` is the
97
+ * capability to reach the model through and `via.broker` the broker the
98
+ * member's box block names for it, when that is known. The function holds no
99
+ * credential: the broker does.
100
+ */
101
+ export type BrokeredModelAsk = (
102
+ request: ModelRequest,
103
+ via: { capability: string; broker: string | null },
104
+ ) => Promise<{ model: string; answer: WireAnswer }>;
105
+
106
+ export interface AskPointInRunOptions {
107
+ /** Where the workspace and the answer kind are found. */
108
+ cwd: string;
109
+ point: string;
110
+ inputs: unknown;
111
+ subject?: string;
112
+ /** The answer kind file, or a declared kind's name. */
113
+ kind?: string;
114
+ /** The model call. Without it, a model decider is not asked. */
115
+ ask?: BrokeredModelAsk;
116
+ /** The box capability the model call goes through. Default `inference`. */
117
+ capability?: string;
118
+ /** The broker the member's box block names for the capability, when the caller has read it. */
119
+ broker?: string | null;
120
+ /** The date written as asked_on, YYYY-MM-DD. */
121
+ on?: string;
122
+ /** The steward turn, when the caller has it. Default: this process's (`currentStewardTurn`). */
123
+ turn?: StewardTurn | null;
124
+ }
125
+
126
+ /** What a steward reaches through its broker, read from its declaration when the turn named only the steward. */
127
+ async function stewardCapabilities(turn: StewardTurn, cwd: string): Promise<{ capabilities: readonly string[]; vault: string | null }> {
128
+ if (turn.capabilities !== undefined) return { capabilities: turn.capabilities, vault: turn.vault ?? null };
129
+ const { discoverStewards } = await import("./discover");
130
+ const found = (await discoverStewards({ cwd })).stewards.get(turn.steward)?.declaration;
131
+ return { capabilities: found?.capabilities ?? [], vault: found?.vault ?? null };
132
+ }
133
+
134
+ /**
135
+ * The {@link ModelAsk} a point is asked with. In a steward's turn the call is
136
+ * made only through a capability the steward declares; otherwise it throws,
137
+ * which the chain records as the model not being reached.
138
+ */
139
+ export async function brokeredModelAsk(
140
+ ask: BrokeredModelAsk,
141
+ opts: { capability?: string; broker?: string | null; turn?: StewardTurn | null; cwd: string },
142
+ ): Promise<ModelAsk> {
143
+ const capability = opts.capability ?? DEFAULT_INFERENCE_CAPABILITY;
144
+ const via = { capability, broker: opts.broker ?? null };
145
+ const turn = opts.turn === undefined ? currentStewardTurn() : opts.turn;
146
+ if (!turn) return (request) => ask(request, via);
147
+ const { capabilities, vault } = await stewardCapabilities(turn, opts.cwd);
148
+ if (!capabilities.includes(capability)) {
149
+ const why =
150
+ vault !== null
151
+ ? `the steward ${turn.steward} holds the vault ${vault} and names no brokered capabilities, and a steward's model call goes through the broker`
152
+ : `the steward ${turn.steward} does not name the brokered capability ${capability}` +
153
+ (capabilities.length ? ` (it names ${capabilities.join(", ")})` : "") +
154
+ `, and a steward holds no credential of its own`;
155
+ return async () => {
156
+ throw new Error(why);
157
+ };
158
+ }
159
+ return (request) => ask(request, via);
160
+ }
161
+
162
+ /**
163
+ * Ask a decision point from inside an Op run. Resolves with the answer when
164
+ * the question is answered, and throws {@link PointWait} when it is open.
165
+ * Any other problem (the points can't be read, the inputs are wrong, a model
166
+ * declared `unreachable: "fail"` could not answer) throws an ordinary error,
167
+ * which fails the step.
168
+ */
169
+ export async function askPointInRun(opts: AskPointInRunOptions): Promise<{ answer: string | boolean; question: QuestionView }> {
170
+ const turn = opts.turn === undefined ? currentStewardTurn() : opts.turn;
171
+ const ask = opts.ask ? await brokeredModelAsk(opts.ask, { capability: opts.capability, broker: opts.broker, turn, cwd: opts.cwd }) : undefined;
172
+ const { askPoint } = await import("../workspace/decide");
173
+ const doc = await askPoint({
174
+ cwd: opts.cwd,
175
+ point: opts.point,
176
+ inputs: opts.inputs,
177
+ ...(opts.subject !== undefined ? { subject: opts.subject } : {}),
178
+ ...(opts.kind !== undefined ? { kind: opts.kind } : {}),
179
+ ...(ask ? { ask } : {}),
180
+ ...(opts.on !== undefined ? { on: opts.on } : {}),
181
+ ...(turn ? { steward: { name: turn.steward, ...(turn.run ? { run: turn.run } : {}) } } : {}),
182
+ });
183
+ if ("error" in doc) throw new Error(`decision point ${opts.point}: ${doc.error.code}: ${doc.error.message}`);
184
+ const q = doc.question;
185
+ if (q.state === "answered" && q.answer !== null) return { answer: q.answer, question: q };
186
+ throw new PointWait({
187
+ id: q.id,
188
+ point: q.point,
189
+ state: q.state === "proposed" ? "proposed" : "escalated",
190
+ path: q.path,
191
+ subject: q.subject,
192
+ steward: q.askedBy?.steward ?? null,
193
+ });
194
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Whether this process is a steward's turn, and whose (#2749).
3
+ *
4
+ * A steward runs unattended. In the local form, `chant operator --steward`
5
+ * is the steward for as long as it runs; on Fountain, the Agent's `chant acp
6
+ * --steward <name>` session is. Two things read this: a decision point asked
7
+ * during the turn (`./steward-points.ts`), which records the steward on the
8
+ * question and makes its model call through the steward's brokered
9
+ * capability, and `points answer`, which refuses to record an answer from a
10
+ * steward's own turn, as the gate ledger's origin rule refuses a gate
11
+ * resolved on the channel that reached it.
12
+ *
13
+ * Like the gate origin (`../lifecycle/gate-origin.ts`) this is a property of
14
+ * the process, set once at its entry point, not a parameter threaded through
15
+ * every executor and activity. It is also exported to child processes as
16
+ * `CHANT_STEWARD`, so a `chant workspace points answer` an Op shells out to
17
+ * is refused the same way.
18
+ */
19
+
20
+ /** The environment variable a steward's turn carries into child processes. */
21
+ export const STEWARD_ENV = "CHANT_STEWARD";
22
+
23
+ /** A steward's turn, as the process knows it. */
24
+ export interface StewardTurn {
25
+ /** The steward's name. */
26
+ steward: string;
27
+ /**
28
+ * The box capabilities the steward reaches through the broker (#2726), or
29
+ * undefined when the process was told only the steward's name (a child
30
+ * process, or `chant acp --steward <name>`): the declaration is then read
31
+ * from the project's `*.op.ts` files when a model call needs it.
32
+ */
33
+ capabilities?: readonly string[];
34
+ /** The vault the steward holds, by name, when it is not behind a broker. */
35
+ vault?: string | null;
36
+ /** The run the steward is in, when an Op is running. */
37
+ run?: string;
38
+ }
39
+
40
+ let ambient: StewardTurn | undefined;
41
+
42
+ /**
43
+ * Declare that this process is a steward's turn. Called once by `chant
44
+ * operator --steward` and `chant acp --steward`; also exports
45
+ * {@link STEWARD_ENV} so a child process knows it too.
46
+ */
47
+ export function setStewardTurn(turn: StewardTurn): void {
48
+ ambient = { ...turn };
49
+ process.env[STEWARD_ENV] = turn.steward;
50
+ }
51
+
52
+ /**
53
+ * Be a steward's turn until the returned function is called, which puts back
54
+ * whatever was there before. `chant operator --steward` enters one per Op run,
55
+ * naming the run, so a question asked during it names the run too.
56
+ */
57
+ export function enterStewardTurn(turn: StewardTurn): () => void {
58
+ const before = ambient;
59
+ const envBefore = process.env[STEWARD_ENV];
60
+ setStewardTurn(turn);
61
+ return () => {
62
+ ambient = before;
63
+ if (envBefore === undefined) delete process.env[STEWARD_ENV];
64
+ else process.env[STEWARD_ENV] = envBefore;
65
+ };
66
+ }
67
+
68
+ /** The steward whose turn this process is, or undefined. */
69
+ export function currentStewardTurn(): StewardTurn | undefined {
70
+ if (ambient) return ambient;
71
+ const name = process.env[STEWARD_ENV];
72
+ return name && name.trim() !== "" ? { steward: name.trim() } : undefined;
73
+ }
74
+
75
+ /** Forget the turn. For tests, which must not leak a steward into each other. */
76
+ export function resetStewardTurn(): void {
77
+ ambient = undefined;
78
+ delete process.env[STEWARD_ENV];
79
+ }