@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
@@ -22,10 +22,15 @@ import {
22
22
  STATUS_GATE_REASON_CODES,
23
23
  STATUS_OUTPUT_SCHEMA_ID,
24
24
  STATUS_REASON_CODES,
25
+ STATUS_STEWARD_REASON_CODES,
25
26
  workspaceStatus,
26
27
  type StatusDocument,
27
28
  } from "./status";
28
29
  import schema from "./status.schema.json";
30
+ import { appendRunRecord } from "../lifecycle/run-ledger";
31
+ import { acquireStewardLease } from "../op/operator";
32
+ import { stewardWorkHolder } from "../op/work-lease-run";
33
+ import { claimWorkLease } from "../lifecycle/work-lease";
29
34
 
30
35
  const REPO = join(import.meta.dirname, "..", "..", "..", "..");
31
36
 
@@ -148,6 +153,7 @@ describe("status output schema", () => {
148
153
  expect(schema.$defs.environment.properties.reason.oneOf[1].properties!.code.enum).toEqual([...STATUS_REASON_CODES]);
149
154
  expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual([...STATUS_ERROR_CODES]);
150
155
  expect(schema.$defs.gateLedger.properties.reason.oneOf[1].properties!.code.enum).toEqual([...STATUS_GATE_REASON_CODES]);
156
+ expect(schema.$defs.member.properties.stewardReasons.items.properties.code.enum).toEqual([...STATUS_STEWARD_REASON_CODES]);
151
157
  });
152
158
  });
153
159
 
@@ -163,6 +169,8 @@ describe("chant workspace status on built workspaces", () => {
163
169
  expect(web.environments[0].ledger).toEqual({ layout: "members", path: "_members/web/staging/releases.jsonl", shared: false });
164
170
  // The later of the two records wins.
165
171
  expect(web.environments[0].releases.map((r) => [r.component, r.digest, r.gitSha])).toEqual([["web", D("a"), "a".repeat(40)]]);
172
+ // No plan was ever written for these digests (ws-055, #2733): the field is present, and null.
173
+ expect(web.environments[0].releases.map((r) => r.plan)).toEqual([null]);
166
174
  // api has no _members/api/ yet, so it falls back to the flat ledger, which the root member reads too.
167
175
  expect(api.environments[0].ledger).toEqual({ layout: "flat", path: "staging/releases.jsonl", shared: true });
168
176
  expect(site.environments[0].ledger).toEqual({ layout: "flat", path: "staging/releases.jsonl", shared: true });
@@ -199,6 +207,33 @@ describe("chant workspace status on built workspaces", () => {
199
207
  expect(text).toContain("3 members, 3 with a release in staging, 2 differ from prod, 0 unreadable");
200
208
  });
201
209
 
210
+ test("resolves a release's plan from _plans/<digest>.json, member-scoped per #2524 D7 (ws-055, #2733)", async () => {
211
+ const root = repo({
212
+ "chant.workspace.json": declaration([
213
+ { name: "site", dir: ".", kind: "other", because: "the root project" },
214
+ { name: "web", dir: "apps/web", kind: "chant" },
215
+ ]),
216
+ "apps/web/chant.config.ts": "",
217
+ });
218
+ const webPlan = { digest: D("a"), release: "r-web", units: [{ id: "w-1" }] };
219
+ const apiPlan = { digest: D("b"), release: "r-api", units: [] };
220
+ lifecycle(root, {
221
+ "_members/web/staging/releases.jsonl": jsonl(release("web", "staging", D("a"), "a".repeat(40))),
222
+ [`_members/web/_plans/sha256_${"a".repeat(64)}.json`]: JSON.stringify(webPlan),
223
+ "staging/releases.jsonl": jsonl(release("api", "staging", D("b"), "b".repeat(40)), release("worker", "staging", D("c"), "c".repeat(40))),
224
+ [`_plans/sha256_${"b".repeat(64)}.json`]: JSON.stringify(apiPlan),
225
+ });
226
+ const doc = result(await workspaceStatus({ cwd: root, env: "staging" }));
227
+ expectValid(doc);
228
+ const web = doc.members.find((m) => m.name === "web")!;
229
+ const site = doc.members.find((m) => m.name === "site")!;
230
+ expect(web.environments[0].releases[0].plan).toEqual(webPlan);
231
+ const byComponent = new Map(site.environments[0].releases.map((r) => [r.component, r]));
232
+ expect(byComponent.get("api")?.plan).toEqual(apiPlan);
233
+ // worker has no plan stored under its digest: reads null, not an error.
234
+ expect(byComponent.get("worker")?.plan).toBeNull();
235
+ });
236
+
202
237
  test("compares on the input digest when a release has one", async () => {
203
238
  const root = repo({ "chant.workspace.json": declaration([{ name: "chart", dir: "chart", kind: "other", because: "helm" }]) });
204
239
  lifecycle(root, {
@@ -253,6 +288,33 @@ describe("chant workspace status on built workspaces", () => {
253
288
  expect(formatStatus(doc)).toContain("web - no release");
254
289
  });
255
290
 
291
+ test("each member lists its box block: the capability, its broker and its scope (#2726)", async () => {
292
+ const root = repo({
293
+ "chant.workspace.json": declaration([
294
+ { name: "web", dir: "web", kind: "chant" },
295
+ {
296
+ name: "box",
297
+ dir: "box",
298
+ kind: "other",
299
+ because: "the box's declarations",
300
+ box: { capabilities: [{ name: "fountain", broker: "lobby", scope: ["agent", "vault", "conversations", "sandboxes"] }, { name: "inference" }] },
301
+ },
302
+ ]),
303
+ });
304
+ const doc = result(await workspaceStatus({ cwd: root, env: "prod" }));
305
+ expectValid(doc);
306
+ expect(doc.members.map((m) => m.box)).toEqual([
307
+ null,
308
+ {
309
+ capabilities: [
310
+ { name: "fountain", broker: "lobby", scope: ["agent", "vault", "conversations", "sandboxes"] },
311
+ { name: "inference", broker: null, scope: [] },
312
+ ],
313
+ isolation: null,
314
+ },
315
+ ]);
316
+ });
317
+
256
318
  test("every failure validates with its code", async () => {
257
319
  const empty = repo({});
258
320
  const outside = realpathSync(mkdtempSync(join(tmpdir(), "chant-status-nogit-")));
@@ -357,7 +419,7 @@ describe("gate state in chant workspace status --json (#2674)", () => {
357
419
  expiresAt: "2026-12-31T00:00:00.000Z",
358
420
  approvals: [{ principal: "alice", channel: "cli", at: "2026-09-20T01:00:00.000Z" }],
359
421
  needed: 1,
360
- approve: "chant approve web deploy --env staging",
422
+ approve: `chant approve web deploy --env staging --plan ${D("1")}`,
361
423
  },
362
424
  {
363
425
  component: "web",
@@ -369,7 +431,7 @@ describe("gate state in chant workspace status --json (#2674)", () => {
369
431
  expiresAt: "2026-12-31T00:00:00.000Z",
370
432
  approvals: [],
371
433
  needed: 1,
372
- approve: "chant approve web deploy --env prod",
434
+ approve: `chant approve web deploy --env prod --plan ${D("2")}`,
373
435
  },
374
436
  {
375
437
  component: "web",
@@ -382,15 +444,15 @@ describe("gate state in chant workspace status --json (#2674)", () => {
382
444
  // The agent's approval doesn't count toward the quorum.
383
445
  approvals: [{ principal: "bob", channel: "cli", at: "2026-09-20T02:00:00.000Z" }],
384
446
  needed: 2,
385
- approve: "chant approve web review --env staging",
447
+ approve: `chant approve web review --env staging --plan ${D("4")}`,
386
448
  },
387
449
  ]);
388
450
  // api and the root member both read the flat _gates/.
389
451
  expect(api.gateLedger).toEqual({ layout: "flat", path: "_gates", shared: true, malformed: 1, reason: null });
390
452
  expect(site.gates).toEqual(api.gates);
391
453
  expect(api.gates.map((g) => [g.component, g.name, g.env, g.state, g.approve])).toEqual([
392
- ["api", "deploy", "staging", "expired", "chant approve api deploy --env staging"],
393
- ["api", "smoke", "staging", "superseded", "chant approve api smoke --env staging"],
454
+ ["api", "deploy", "staging", "expired", `chant approve api deploy --env staging --plan ${D("5")}`],
455
+ ["api", "smoke", "staging", "superseded", `chant approve api smoke --env staging --plan ${D("6")}`],
394
456
  ["nightly", "confirm", null, "approved", "chant approve nightly confirm"],
395
457
  ]);
396
458
  // The superseding approval named another plan, so it isn't listed as one that counts.
@@ -452,13 +514,134 @@ describe("gate state in chant workspace status --json (#2674)", () => {
452
514
  expect(await runWorkspaceStatus({ args: { extraPositional: "staging", extraPositional2: root, json: true } } as unknown as CommandContext)).toBe(0);
453
515
  const printed = JSON.parse(String(log.mock.calls.at(-1)?.[0])) as StatusDocument;
454
516
  expectValid(printed);
455
- expect(result(printed).members.find((m) => m.name === "web")!.gates.map((g) => g.approve)).toContain("chant approve web deploy --env staging");
517
+ expect(result(printed).members.find((m) => m.name === "web")!.gates.map((g) => g.approve)).toContain(`chant approve web deploy --env staging --plan ${D("1")}`);
456
518
  } finally {
457
519
  log.mockRestore();
458
520
  }
459
521
  });
460
522
  });
461
523
 
524
+ describe("stewards in chant workspace status --json (#2731)", () => {
525
+ const op = (name: string, cron?: string) => ({
526
+ name,
527
+ overview: name,
528
+ phases: [{ name: "Run", steps: [{ kind: "activity", fn: "noop", args: {} }] }],
529
+ ...(cron ? { schedule: { cron, overlap: "skip" } } : {}),
530
+ });
531
+
532
+ /** A box member declaring one steward, local by default and fountain on fountain-k3d. */
533
+ function stewarded(): string {
534
+ const steward = {
535
+ kind: "Chant::Steward",
536
+ name: "box-steward",
537
+ ops: [
538
+ op("box-converge", "* * * * *"),
539
+ op("box-release"),
540
+ { ...op("box-dispatch", "*/5 * * * *"), workLease: { item: ["W-1", "W-2"] }, changesCheckout: true },
541
+ ],
542
+ form: { default: "local", environments: { "fountain-k3d": "fountain" } },
543
+ capabilities: ["fountain", "inference"],
544
+ vault: null,
545
+ };
546
+ const root = repo({
547
+ "chant.workspace.json": declaration([
548
+ { name: "box", dir: "box", kind: "chant", box: { capabilities: [{ name: "fountain", broker: "lobby", scope: ["agent", "vault"] }] } },
549
+ { name: "notes", dir: "notes", kind: "other", because: "prose" },
550
+ ]),
551
+ "box/chant.config.ts": "export default {};\n",
552
+ "box/ops/steward.op.ts": `export const steward = ${JSON.stringify(steward)};\n`,
553
+ "notes/README.md": "notes\n",
554
+ });
555
+ lifecycle(root, { "README": "ledger\n" });
556
+ return root;
557
+ }
558
+
559
+ test("names the steward, its form in the environment asked for, its Ops, schedules, last runs and lease", async () => {
560
+ const root = stewarded();
561
+ const box = join(root, "box");
562
+ await appendRunRecord(
563
+ { op: "box-converge", env: "local", started: "2026-09-25T10:00:00.000Z", ended: "2026-09-25T10:00:05.000Z", status: "ok", labels: {}, outcomes: {}, phases: [] },
564
+ { cwd: box },
565
+ );
566
+ expect((await acquireStewardLease("box-steward", "box-host:1:abc", { cwd: box, now: () => new Date("2026-09-25T10:00:00.000Z") })).acquired).toBe(true);
567
+ // #2748: the dispatch turn holds W-1; someone else holds W-2.
568
+ const turn = await claimWorkLease("W-1", stewardWorkHolder("box-steward", "box-dispatch", "box-host:1:abc"), { cwd: box, ttlMs: 600_000, now: () => new Date("2026-09-25T10:00:30.000Z") });
569
+ expect(turn.ok).toBe(true);
570
+ expect((await claimWorkLease("W-2", "someone-else", { cwd: box, ttlMs: 600_000, now: () => new Date("2026-09-25T10:00:30.000Z") })).ok).toBe(true);
571
+
572
+ const doc = result(await workspaceStatus({ cwd: root, env: "minimal", now: "2026-09-25T10:01:00.000Z" }));
573
+ expectValid(doc);
574
+ const [boxMember, notes] = doc.members;
575
+ expect(notes.stewards).toEqual([]);
576
+ expect(boxMember.stewardReasons).toEqual([]);
577
+ expect(boxMember.stewards).toHaveLength(1);
578
+ const s = boxMember.stewards[0];
579
+ expect(s).toMatchObject({
580
+ name: "box-steward",
581
+ file: "ops/steward.op.ts",
582
+ form: "local",
583
+ forms: { default: "local", environments: { "fountain-k3d": "fountain" } },
584
+ // #2726: the steward holds no credential; its capabilities join the box block.
585
+ vault: null,
586
+ capabilities: [
587
+ { name: "fountain", broker: "lobby", declared: true },
588
+ { name: "inference", broker: null, declared: false },
589
+ ],
590
+ lease: { holder: "box-host:1:abc", acquiredAt: "2026-09-25T10:00:00.000Z", live: true },
591
+ });
592
+ expect(s.ops).toEqual([
593
+ {
594
+ name: "box-converge",
595
+ schedule: { cron: "* * * * *", overlap: "skip" },
596
+ env: "local",
597
+ lastRun: { id: expect.any(String), status: "ok", started: "2026-09-25T10:00:00.000Z", ended: "2026-09-25T10:00:05.000Z", gate: null, point: null },
598
+ // #2778: not a ConvergeOp (no Converge label), so no tick.
599
+ lastTick: null,
600
+ changesCheckout: false,
601
+ workLease: null,
602
+ },
603
+ { name: "box-release", schedule: null, env: "local", lastRun: null, lastTick: null, changesCheckout: false, workLease: null },
604
+ {
605
+ name: "box-dispatch",
606
+ schedule: { cron: "*/5 * * * *", overlap: "skip" },
607
+ env: "local",
608
+ lastRun: null,
609
+ lastTick: null,
610
+ changesCheckout: true,
611
+ workLease: {
612
+ kind: null,
613
+ held: [
614
+ {
615
+ item: "W-1",
616
+ holder: "box-steward/box-dispatch@box-host:1:abc",
617
+ token: turn.ok ? turn.lease.token : "",
618
+ acquiredAt: "2026-09-25T10:00:30.000Z",
619
+ expiresAt: "2026-09-25T10:10:30.000Z",
620
+ state: "active",
621
+ },
622
+ ],
623
+ },
624
+ },
625
+ ]);
626
+ // #2749: nothing the steward runs waits on a decision point.
627
+ expect(s.waiting).toEqual([]);
628
+
629
+ const k3d = result(await workspaceStatus({ cwd: root, env: "fountain-k3d", now: "2026-09-25T11:00:00.000Z" }));
630
+ expectValid(k3d);
631
+ expect(k3d.members[0].stewards[0].form).toBe("fountain");
632
+ expect(k3d.members[0].stewards[0].lease!.live).toBe(false);
633
+ });
634
+
635
+ test("an op file that can't be imported is a reason, and the read still succeeds", async () => {
636
+ const root = stewarded();
637
+ writeFileSync(join(root, "box", "ops", "broken.op.ts"), "export const x = ;\n");
638
+ const doc = result(await workspaceStatus({ cwd: root, env: "minimal" }));
639
+ expectValid(doc);
640
+ expect(doc.members[0].stewards.map((s) => s.name)).toEqual(["box-steward"]);
641
+ expect(doc.members[0].stewardReasons.map((r) => r.code)).toEqual(["stewards-unreadable"]);
642
+ });
643
+ });
644
+
462
645
  describe("chant workspace status on the chant repo (#2557)", () => {
463
646
  test("validates and lists every declared member", async () => {
464
647
  const doc = result(await workspaceStatus({ cwd: join(REPO, "packages", "core"), env: "prod", compareTo: "staging" }));
@@ -103,9 +103,14 @@ export const defaultGateLedgerReader: GateLedgerReader = async (dir, commit, cwd
103
103
  return files;
104
104
  };
105
105
 
106
- /** The exact `chant approve` line for a gate. */
107
- export function approveCommand(component: string, gate: string, env: string | null): string {
108
- return `chant approve ${component} ${gate}${env !== null ? ` --env ${env}` : ""}`;
106
+ /**
107
+ * The exact `chant approve` line for a gate. A gate bound to a plan (#2300)
108
+ * gets `--plan <digest>` too (#2832), so running the line back approves the
109
+ * plan this read showed and not whatever happens to be pending by the time
110
+ * someone runs it.
111
+ */
112
+ export function approveCommand(component: string, gate: string, env: string | null, planDigest?: string | null): string {
113
+ return `chant approve ${component} ${gate}${env !== null ? ` --env ${env}` : ""}${planDigest ? ` --plan ${planDigest}` : ""}`;
109
114
  }
110
115
 
111
116
  const approvalOf = (r: GateResolutionRecord): StatusGateApproval => ({ principal: r.resolvedBy, channel: r.origin ?? null, at: r.timestamp });
@@ -150,7 +155,7 @@ function decide(component: string, standing: PendingGateRecord, resolutions: Gat
150
155
  expiresAt: standing.expiresAt,
151
156
  approvals: approvals.map(approvalOf),
152
157
  needed,
153
- approve: approveCommand(component, gate, env),
158
+ approve: approveCommand(component, gate, env, planDigest),
154
159
  };
155
160
  }
156
161
 
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Stewards in `chant workspace status --json` (#2731): which steward runs a
3
+ * member's operational work, the form it takes in the environment asked for,
4
+ * its Ops with their schedules, and each Op's last run.
5
+ *
6
+ * The steward itself is read from its declaration, the same `*.op.ts` files
7
+ * `chant operator --steward` reads (`../op/discover.ts`), and only for a member
8
+ * of kind `chant` that is a chant project of its own (a `chant.config.ts` or
9
+ * `.json` in its directory), so a member never lists a parent project's
10
+ * steward. Everything
11
+ * else is read from the local `chant/lifecycle` branch and refs, never
12
+ * fetched, as the rest of status is:
13
+ *
14
+ * - each Op's last run is the newest record in its run ledger,
15
+ * `<env>/runs__<op>.jsonl` under the member's ledger prefix, where `<env>`
16
+ * is the Op's own `labels.Env` or `local`;
17
+ * - `waiting` lists the open decision points the steward waits on (#2749):
18
+ * each Op whose newest run is the steward's own and stopped on a question,
19
+ * as the run ledger records it. The question's state now is `points`'s;
20
+ * - `lease` is the local steward's own lease ref
21
+ * (`refs/chant/lease/[<prefix>]_stewards/<name>`), present while a
22
+ * `chant operator --steward` holds it or until it expires;
23
+ * - an Op's `workLease.held` is the work leases its turns hold (#2748): those
24
+ * whose holder is `<steward>/<op>@...` (`stewardWorkHolder`), read from the
25
+ * ledger of the Op's work kind, or the member's own.
26
+ */
27
+
28
+ import { existsSync, realpathSync } from "node:fs";
29
+ import { dirname, join, relative, resolve } from "node:path";
30
+ import { discoverStewards } from "../op/discover";
31
+ import { stewardFormFor, stewardLeaseName, type StewardForm } from "../op/steward";
32
+ import { readRunLedger, runEnvOf } from "../lifecycle/run-ledger";
33
+ import { readConvergeLedger, type ConvergeTickRecord } from "../lifecycle/converge-ledger";
34
+ import { leaseRef } from "../lifecycle/lease";
35
+ import { readBlobBySha, readRefSha } from "../lifecycle/git";
36
+ import { resolveMemberLedger } from "../lifecycle/member-ledger";
37
+ import { listWorkLeases } from "../lifecycle/work-lease";
38
+ import { stewardWorkHolder } from "../op/work-lease-run";
39
+ import type { OpConfig } from "../op/types";
40
+ import type { OpRunRecord } from "../op/runtime";
41
+ import { approveCommand } from "./status-gates";
42
+ import type { ReasonCode } from "./reason-codes";
43
+
44
+ /** Why a member's stewards can't be fully listed. Closed: a new code is a contract change. */
45
+ export const STEWARD_REASON_CODES = [
46
+ /** An `*.op.ts` file could not be imported, so a steward it declares may be missing. */
47
+ "stewards-unreadable",
48
+ /** A steward was dropped: its name, or an Op it lists, belongs to another steward. */
49
+ "stewards-conflict",
50
+ /** Reading an Op's run ledger, or a ConvergeOp's converge ledger, failed, so its last run or last tick is null. */
51
+ "steward-runs-unreadable",
52
+ ] as const satisfies readonly ReasonCode[];
53
+ export type StewardReasonCode = (typeof STEWARD_REASON_CODES)[number];
54
+
55
+ export interface StatusStewardRun {
56
+ id: string;
57
+ status: OpRunRecord["status"];
58
+ started: string;
59
+ ended: string;
60
+ /**
61
+ * The gate the run stopped at, for a `gated` run: its name, when the run
62
+ * stopped there, the op it is recorded under (the Op's own, or the
63
+ * command's for a step whose command stopped at its own gate, #2779, such
64
+ * as `workspace-upgrade`), and the command that approves it.
65
+ */
66
+ gate: { name: string; since: string; op: string; approve: string } | null;
67
+ /** The open decision point the run stopped on, for a `waiting` run (#2749). */
68
+ point: StatusStewardWait | null;
69
+ }
70
+
71
+ /** An open decision point a steward's run stopped on (#2749), as the run ledger records it. */
72
+ export interface StatusStewardWait {
73
+ /** The answer record's id: what `points --open` lists and `points answer` names. */
74
+ id: string;
75
+ point: string;
76
+ /** The question's state when the run stopped. */
77
+ state: "escalated" | "proposed";
78
+ path: string;
79
+ subject: string | null;
80
+ since: string;
81
+ }
82
+
83
+ /** A ConvergeOp's newest tick on the converge ledger (#2778). */
84
+ export interface StatusStewardTick {
85
+ /** The tick's id, or null for a tick recorded before ticks had ids. */
86
+ id: string | null;
87
+ timestamp: string;
88
+ /** The tick's one log line. */
89
+ log: string;
90
+ /** Every rule that fired. */
91
+ firedRuleIds: string[];
92
+ /** What each fired rule did, and for which resource on a ConvergeOp with an observer step. */
93
+ outcomes: {
94
+ ruleId: string;
95
+ action: ConvergeTickRecord["outcomes"][number]["action"];
96
+ op: string | null;
97
+ resource: string | null;
98
+ reason: string | null;
99
+ }[];
100
+ /** What the observer step reported, or null for a ConvergeOp that observes a lexicon environment. */
101
+ resources: { name: string; status: "in-sync" | "drifted" | "unknown"; detail: string | null }[] | null;
102
+ }
103
+
104
+ export interface StatusStewardOp {
105
+ name: string;
106
+ /** The Op's cadence, or null for an Op the steward runs only when asked. */
107
+ schedule: { cron: string; overlap: "skip" } | null;
108
+ /** The environment its runs are recorded under: its `labels.Env`, or `local`. */
109
+ env: string;
110
+ /** The newest run in its run ledger, or null when it has none. */
111
+ lastRun: StatusStewardRun | null;
112
+ /** For a ConvergeOp, its newest tick on the converge ledger (#2778); null for any other Op, or before its first tick. */
113
+ lastTick: StatusStewardTick | null;
114
+ /** Whether the Op changes the checkout, and so runs under a work lease on a branch of its own (#2748). */
115
+ changesCheckout: boolean;
116
+ /**
117
+ * The Op's work lease (#2748), or null when it declares none: the work kind
118
+ * file whose ledger holds it (null for the member's own), and the leases
119
+ * the steward's turns of this Op hold, a live one while a turn runs.
120
+ */
121
+ workLease: { kind: string | null; held: StatusStewardWorkLease[] } | null;
122
+ }
123
+
124
+ /** A work lease a steward's turn holds (#2748). */
125
+ export interface StatusStewardWorkLease {
126
+ item: string;
127
+ holder: string;
128
+ token: string;
129
+ acquiredAt: string;
130
+ expiresAt: string;
131
+ state: "active" | "expired";
132
+ }
133
+
134
+ export interface StatusSteward {
135
+ name: string;
136
+ /** The `*.op.ts` file declaring it, relative to the member's directory. */
137
+ file: string;
138
+ /** Its form in the environment status was asked for. */
139
+ form: StewardForm;
140
+ /** The declared form: a default and the environments that differ from it. */
141
+ forms: { default: StewardForm; environments: Record<string, StewardForm> };
142
+ /** The vault the steward holds on Fountain, by name, or null. */
143
+ vault: string | null;
144
+ /**
145
+ * The box capabilities the steward reaches through a broker (#2726), joined
146
+ * with the member's box block: `broker` is the block's, and `declared` is
147
+ * false when the block doesn't list the capability (or there is no block).
148
+ */
149
+ capabilities: { name: string; broker: string | null; declared: boolean }[];
150
+ /** The local steward's own lease, or null when no local operator has held it. */
151
+ lease: { holder: string; acquiredAt: string; expiresAt: string; live: boolean } | null;
152
+ ops: StatusStewardOp[];
153
+ /**
154
+ * The open decision points the steward waits on (#2749): each Op whose
155
+ * newest run is the steward's own and stopped on a question.
156
+ */
157
+ waiting: (StatusStewardWait & { op: string; run: string })[];
158
+ }
159
+
160
+ export interface MemberStewards {
161
+ stewards: StatusSteward[];
162
+ reasons: { code: StewardReasonCode; message: string }[];
163
+ }
164
+
165
+ function waitOf(p: NonNullable<OpRunRecord["point"]>): StatusStewardWait {
166
+ return {
167
+ id: p.id,
168
+ point: p.point,
169
+ state: p.state === "proposed" ? "proposed" : "escalated",
170
+ path: p.path,
171
+ subject: p.subject ?? null,
172
+ since: p.since,
173
+ };
174
+ }
175
+
176
+ function gateOf(g: NonNullable<OpRunRecord["gate"]>, opName: string): NonNullable<StatusStewardRun["gate"]> {
177
+ const op = g.op ?? opName;
178
+ return { name: g.name, since: g.since, op, approve: approveCommand(op, g.name, null) };
179
+ }
180
+
181
+ function tickOf(t: ConvergeTickRecord): StatusStewardTick {
182
+ return {
183
+ id: t.id ?? null,
184
+ timestamp: t.timestamp,
185
+ log: t.log,
186
+ firedRuleIds: [...t.firedRuleIds],
187
+ outcomes: t.outcomes.map((o) => ({
188
+ ruleId: o.ruleId,
189
+ action: o.action,
190
+ op: o.op ?? null,
191
+ resource: o.resource ?? null,
192
+ reason: o.reason ?? null,
193
+ })),
194
+ resources: t.resources ? t.resources.map((r) => ({ name: r.name, status: r.status, detail: r.detail ?? null })) : null,
195
+ };
196
+ }
197
+
198
+ /** Whether a directory is a chant project of its own. */
199
+ function isChantProject(dir: string): boolean {
200
+ return existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"));
201
+ }
202
+
203
+ async function readStewardLease(name: string, memberDir: string, now: string): Promise<StatusSteward["lease"]> {
204
+ try {
205
+ const { prefix } = await resolveMemberLedger(memberDir);
206
+ const sha = await readRefSha(leaseRef(stewardLeaseName(name), prefix), { cwd: memberDir });
207
+ if (!sha) return null;
208
+ const record = JSON.parse((await readBlobBySha(sha, { cwd: memberDir })) ?? "") as Record<string, unknown>;
209
+ if (typeof record.holder !== "string" || typeof record.expiresAt !== "string" || typeof record.acquiredAt !== "string") return null;
210
+ return {
211
+ holder: record.holder,
212
+ acquiredAt: record.acquiredAt,
213
+ expiresAt: record.expiresAt,
214
+ live: new Date(record.expiresAt).getTime() > new Date(now).getTime(),
215
+ };
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ /** The work leases `steward`'s turns of `op` hold, from the ledger of the Op's kind or the member's. */
222
+ async function readHeldWorkLeases(steward: string, op: OpConfig, memberDir: string, now: string): Promise<StatusStewardWorkLease[]> {
223
+ try {
224
+ const kind = op.workLease?.kind;
225
+ const cwd = kind ? dirname(resolve(memberDir, kind)) : memberDir;
226
+ const { prefix } = await resolveMemberLedger(cwd);
227
+ const mine = stewardWorkHolder(steward, op.name, "");
228
+ return (await listWorkLeases({ cwd, memberPrefix: prefix, now: new Date(now) }))
229
+ .filter((l) => l.holder.startsWith(mine))
230
+ .map((l) => ({ item: l.item, holder: l.holder, token: l.token, acquiredAt: l.acquiredAt, expiresAt: l.expiresAt, state: l.state }));
231
+ } catch {
232
+ return [];
233
+ }
234
+ }
235
+
236
+ /**
237
+ * The stewards declared in one member, for `env`. `memberDir` is absolute.
238
+ * Only a member of kind `chant` with a config of its own is read.
239
+ */
240
+ export async function readMemberStewards(
241
+ memberDir: string,
242
+ env: string,
243
+ now: string,
244
+ kind = "chant",
245
+ box: { capabilities: { name: string; broker: string | null }[] } | null = null,
246
+ ): Promise<MemberStewards> {
247
+ const reasons: MemberStewards["reasons"] = [];
248
+ if (kind !== "chant" || !isChantProject(memberDir)) return { stewards: [], reasons };
249
+
250
+ let discovered: Awaited<ReturnType<typeof discoverStewards>>;
251
+ try {
252
+ discovered = await discoverStewards({ cwd: memberDir });
253
+ } catch (err) {
254
+ reasons.push({ code: "stewards-unreadable", message: err instanceof Error ? err.message.split("\n")[0] : String(err) });
255
+ return { stewards: [], reasons };
256
+ }
257
+ const { stewards: found, errors, conflicts } = discovered;
258
+ for (const message of errors) reasons.push({ code: "stewards-unreadable", message });
259
+ for (const message of conflicts) reasons.push({ code: "stewards-conflict", message });
260
+
261
+ const stewards: StatusSteward[] = [];
262
+ for (const { declaration, filePath } of [...found.values()].sort((a, b) => a.declaration.name.localeCompare(b.declaration.name))) {
263
+ const ops: StatusStewardOp[] = [];
264
+ const waiting: StatusSteward["waiting"] = [];
265
+ for (const op of declaration.ops) {
266
+ const opEnv = runEnvOf(op);
267
+ let lastRun: StatusStewardRun | null = null;
268
+ try {
269
+ const newest = (await readRunLedger(opEnv, op.name, { cwd: memberDir })).records.at(-1);
270
+ if (newest) {
271
+ lastRun = {
272
+ id: newest.id,
273
+ status: newest.status,
274
+ started: newest.started,
275
+ ended: newest.ended,
276
+ gate: newest.gate ? gateOf(newest.gate, op.name) : null,
277
+ point: newest.point ? waitOf(newest.point) : null,
278
+ };
279
+ if (newest.status === "waiting" && lastRun.point && newest.steward === declaration.name) {
280
+ waiting.push({ op: op.name, run: newest.id, ...lastRun.point });
281
+ }
282
+ }
283
+ } catch (err) {
284
+ reasons.push({
285
+ code: "steward-runs-unreadable",
286
+ message: `${opEnv}/runs__${op.name}.jsonl: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
287
+ });
288
+ }
289
+ let lastTick: StatusStewardTick | null = null;
290
+ if (op.labels?.Converge === "true") {
291
+ try {
292
+ const newest = (await readConvergeLedger(opEnv, { cwd: memberDir })).records.filter((r) => r.op === op.name).at(-1);
293
+ if (newest) lastTick = tickOf(newest);
294
+ } catch (err) {
295
+ reasons.push({
296
+ code: "steward-runs-unreadable",
297
+ message: `${opEnv}/converge.jsonl: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
298
+ });
299
+ }
300
+ }
301
+ ops.push({
302
+ name: op.name,
303
+ schedule: op.schedule ? { cron: op.schedule.cron, overlap: "skip" } : null,
304
+ env: opEnv,
305
+ lastRun,
306
+ lastTick,
307
+ changesCheckout: op.changesCheckout === true,
308
+ workLease: op.workLease
309
+ ? { kind: op.workLease.kind ?? null, held: await readHeldWorkLeases(declaration.name, op, memberDir, now) }
310
+ : null,
311
+ });
312
+ }
313
+ stewards.push({
314
+ name: declaration.name,
315
+ file: relative(realpathSync(memberDir), realpathSync(filePath)).split("\\").join("/"),
316
+ form: stewardFormFor(declaration, env),
317
+ forms: { default: declaration.form.default, environments: { ...declaration.form.environments } },
318
+ vault: typeof declaration.vault === "string" ? declaration.vault : null,
319
+ capabilities: (Array.isArray(declaration.capabilities) ? declaration.capabilities : []).map((name) => {
320
+ const declared = box?.capabilities.find((c) => c.name === name);
321
+ return { name, broker: declared?.broker ?? null, declared: declared !== undefined };
322
+ }),
323
+ lease: await readStewardLease(declaration.name, memberDir, now),
324
+ ops,
325
+ waiting,
326
+ });
327
+ }
328
+ return { stewards, reasons };
329
+ }