@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,108 @@
1
+ /**
2
+ * The diagram checks, WSP131 to WSP133 (#2764).
3
+ *
4
+ * A declared diagram names a source and a render, pinned to the renderer
5
+ * its render was made with. chant never runs a renderer (ws-052), on a read
6
+ * path or here: these checks read the declaration and the tree checked
7
+ * only, and a render's drift is caught by comparing a recorded content hash,
8
+ * never by re-rendering the source.
9
+ *
10
+ * | Id | Code | Fails when |
11
+ * |---|---|---|
12
+ * | WSP131 | `diagram-source-missing` | a diagram names a source, and it does not exist in the tree read |
13
+ * | WSP132 | `diagram-render-missing` | a diagram's render does not exist in the tree read |
14
+ * | WSP133 | `diagram-render-drift` | a diagram records a `sourceHash`, and the source's bytes now hash to something else |
15
+ *
16
+ * WSP133 is opt-in per diagram: without a recorded `sourceHash` there is
17
+ * nothing to compare, so the entry is silently not checked for drift. A
18
+ * declaration records one by hashing the source when it commits a fresh
19
+ * render, the way a decision's evidence pins a file by hash
20
+ * (`../record-assets.ts`). Comparing that one recorded hash against the
21
+ * source in the tree read is cheap and deterministic, unlike shelling out to
22
+ * the pinned renderer and diffing its output, which chant does nowhere.
23
+ */
24
+
25
+ import { sha256Hex } from "../../content-digest";
26
+ import type { WorkspaceCheck, WorkspaceCheckContext, WorkspaceDiagnostic } from "../checks";
27
+ import { declaredDiagrams, type DiagramDeclaration } from "../declaration";
28
+ import type { ReasonCode } from "../reason-codes";
29
+
30
+ /** The read contract's codes for the diagram findings, carried as `code` on each. */
31
+ export const DIAGRAM_FINDING_CODES = ["diagram-source-missing", "diagram-render-missing", "diagram-render-drift"] as const satisfies readonly ReasonCode[];
32
+
33
+ export const WSP_DIAGRAM_SOURCE_MISSING = "WSP131";
34
+ export const WSP_DIAGRAM_RENDER_MISSING = "WSP132";
35
+ export const WSP_DIAGRAM_RENDER_DRIFT = "WSP133";
36
+
37
+ const where = (d: DiagramDeclaration) => (d.member === null ? "the workspace's own" : `member ${d.member}'s`);
38
+
39
+ function diagramFinding(check: WorkspaceCheck, d: DiagramDeclaration, field: "source" | "render" | "sourceHash", code: (typeof DIAGRAM_FINDING_CODES)[number], message: string): WorkspaceDiagnostic {
40
+ return {
41
+ checkId: check.id,
42
+ severity: check.severity,
43
+ code,
44
+ message: `${code}: ${message}`,
45
+ entity: d.member ?? undefined,
46
+ pointer: `${d.pointer}/${field}`,
47
+ };
48
+ }
49
+
50
+ export const DIAGRAM_CHECKS: readonly WorkspaceCheck[] = [
51
+ {
52
+ id: WSP_DIAGRAM_SOURCE_MISSING,
53
+ name: "diagram-source-missing",
54
+ description: "A diagram's source file, when it names one, exists in the tree read.",
55
+ severity: "error",
56
+ configurable: true,
57
+ check(ctx: WorkspaceCheckContext) {
58
+ const out: WorkspaceDiagnostic[] = [];
59
+ for (const d of declaredDiagrams(ctx.declaration)) {
60
+ if (d.source === null || ctx.tree.stat(d.source) === "file") continue;
61
+ out.push(diagramFinding(this, d, "source", "diagram-source-missing", `${where(d)} diagram ${d.name} names the source ${d.source}, which does not exist${ctx.tree.label}`));
62
+ }
63
+ return out;
64
+ },
65
+ },
66
+ {
67
+ id: WSP_DIAGRAM_RENDER_MISSING,
68
+ name: "diagram-render-missing",
69
+ description: "A diagram's render exists in the tree read.",
70
+ severity: "error",
71
+ configurable: true,
72
+ check(ctx: WorkspaceCheckContext) {
73
+ const out: WorkspaceDiagnostic[] = [];
74
+ for (const d of declaredDiagrams(ctx.declaration)) {
75
+ if (ctx.tree.stat(d.render) === "file") continue;
76
+ out.push(diagramFinding(this, d, "render", "diagram-render-missing", `${where(d)} diagram ${d.name} names the render ${d.render}, which does not exist${ctx.tree.label}`));
77
+ }
78
+ return out;
79
+ },
80
+ },
81
+ {
82
+ id: WSP_DIAGRAM_RENDER_DRIFT,
83
+ name: "diagram-render-drift",
84
+ description: "A diagram that records a sourceHash is unchanged since its render was made: the source's bytes still hash to it. Runs no renderer.",
85
+ severity: "error",
86
+ configurable: true,
87
+ check(ctx: WorkspaceCheckContext) {
88
+ const out: WorkspaceDiagnostic[] = [];
89
+ for (const d of declaredDiagrams(ctx.declaration)) {
90
+ if (d.sourceHash === null || d.source === null) continue;
91
+ if (ctx.tree.stat(d.source) !== "file") continue; // diagram-source-missing already reports it
92
+ const bytes = ctx.tree.bytes ? ctx.tree.bytes(d.source) : Buffer.from(ctx.tree.read(d.source), "utf-8");
93
+ const actual = sha256Hex(bytes);
94
+ if (actual === d.sourceHash) continue;
95
+ out.push(
96
+ diagramFinding(
97
+ this,
98
+ d,
99
+ "sourceHash",
100
+ "diagram-render-drift",
101
+ `${where(d)} diagram ${d.name}'s source ${d.source} changed since ${d.render} was rendered from it: recorded sourceHash ${d.sourceHash.slice(0, 12)} does not match the source's current hash ${actual.slice(0, 12)}${ctx.tree.label}; rerun the renderer and commit the result, or update sourceHash`,
102
+ ),
103
+ );
104
+ }
105
+ return out;
106
+ },
107
+ },
108
+ ];
@@ -15,11 +15,20 @@
15
15
  * | WSP114 | the records of `--kind` can't be read (fixed) |
16
16
  * | WSP115 | a record kind the declaration names is missing or does not load as one (fixed, #2680) |
17
17
  * | WSP116 | an answer kind's decision points file is missing or not valid (fixed, ws-058, #2738) |
18
+ * | WSP117 | a done work item has an acceptance criterion no passing evidence meets (#2772) |
19
+ *
20
+ * WSP117 reads the records of every declared work kind with acceptance
21
+ * criteria, in the working tree, and the records of `--kind`. It carries the
22
+ * reason code `work-acceptance-unmet`.
18
23
  */
19
24
 
20
25
  import type { WorkspaceCheck, WorkspaceDiagnostic } from "../checks";
26
+ import type { ReasonCode } from "../reason-codes";
21
27
  import type { ReadErrorCode, RecordEntry } from "../records";
22
28
 
29
+ /** The read contract's codes the record checks carry as `code`: WSP117's (#2772). */
30
+ export const RECORD_FINDING_CODES = ["work-acceptance-unmet"] as const satisfies readonly ReasonCode[];
31
+
23
32
  /** The records `check --kind` read, or why they could not be read. */
24
33
  export type RecordFacts =
25
34
  | { kind: string; records: readonly (RecordEntry & { file: string })[] }
@@ -124,4 +133,42 @@ export const RECORD_CHECKS: readonly WorkspaceCheck[] = [
124
133
  return out;
125
134
  },
126
135
  },
136
+ {
137
+ id: "WSP117",
138
+ name: "work-acceptance-unmet",
139
+ description:
140
+ "Every done work item meets its acceptance criteria: each has evidence that names it, passed, and has the verification the criterion expects, and a manual verdict is from someone other than the implementer (#2772).",
141
+ severity: "error",
142
+ configurable: true,
143
+ check(ctx) {
144
+ const out: WorkspaceDiagnostic[] = [];
145
+ const seen = new Set<string>();
146
+ const facts = ctx.facts?.records;
147
+ if (facts && !("error" in facts)) {
148
+ for (const r of facts.records) {
149
+ if (r.supersededBy !== null) continue;
150
+ const w = r.warnings.find((x) => x.code === "work-acceptance-unmet");
151
+ if (!w || seen.has(r.file)) continue;
152
+ seen.add(r.file);
153
+ out.push({ checkId: this.id, severity: this.severity, message: w.message, pointer: "", file: r.file, code: "work-acceptance-unmet" });
154
+ }
155
+ }
156
+ for (const k of ctx.facts?.declaredKinds ?? []) {
157
+ for (const a of k.acceptance ?? []) {
158
+ if (a.unmet === null || seen.has(a.file)) continue;
159
+ seen.add(a.file);
160
+ out.push({
161
+ checkId: this.id,
162
+ severity: this.severity,
163
+ message: a.unmet,
164
+ ...(k.declared.member !== null ? { entity: k.declared.member } : {}),
165
+ pointer: "",
166
+ file: a.file,
167
+ code: "work-acceptance-unmet",
168
+ });
169
+ }
170
+ }
171
+ return out;
172
+ },
173
+ },
127
174
  ];
@@ -29,8 +29,11 @@
29
29
  * | WSP111 to WSP114 | records read with `--kind` (#2549) |
30
30
  * | WSP115 | the record kinds the declaration names (#2680) |
31
31
  * | WSP116 | the decision points file of each declared answer kind (ws-058) |
32
+ * | WSP117 | a done work item's acceptance criteria all met (#2772) |
32
33
  * | WSP121, WSP122 | boxes: no literal credential, every capability brokered (#2726) |
33
34
  * | WSP123, WSP124 | box isolation: no shared port, state path or cookie, no literal machine path (#2727) |
35
+ * | WSP125 | boxes: a fountain Box declares the callback token fountain gives its sandbox (#2780) |
36
+ * | WSP131 to WSP133 | diagram artifacts: source and render exist, a recorded source hash still matches (#2764) |
34
37
  */
35
38
 
36
39
  import { realpathSync } from "node:fs";
@@ -49,6 +52,7 @@ import { gatherLedgerFacts, LEDGER_CHECKS, type MemberLedgerFacts } from "./chec
49
52
  import { gatherPipelineFacts, PIPELINE_CHECKS, type MemberPipelineFacts } from "./checks/pipelines";
50
53
  import { RECORD_CHECKS, type RecordFacts } from "./checks/records";
51
54
  import { BOX_CHECKS } from "./checks/boxes";
55
+ import { DIAGRAM_CHECKS } from "./checks/diagrams";
52
56
  import { loadDeclaredKinds, type DeclaredKind } from "./declared-kinds";
53
57
 
54
58
  /**
@@ -319,6 +323,8 @@ export const WORKSPACE_CHECKS: readonly WorkspaceCheck[] = [
319
323
  ...RECORD_CHECKS,
320
324
  // Boxes and their brokered capabilities (#2726).
321
325
  ...BOX_CHECKS,
326
+ // Diagram artifacts (#2764).
327
+ ...DIAGRAM_CHECKS,
322
328
  ];
323
329
 
324
330
  const BY_ID = new Map(WORKSPACE_CHECKS.map((c) => [c.id, c]));
@@ -461,7 +467,8 @@ export async function runDeclarationChecks(
461
467
  const { registry, problems } = loadKindRegistry(declaration.pins, root);
462
468
  const gathered = options.gather === false || options.tree ? {} : await gatherWorkspaceFacts(root, declaration, options);
463
469
  // The declared record kinds load from the working tree; under --at only whether each exists at the revision is checked (#2680).
464
- const declaredKinds = options.gather === false ? undefined : await loadDeclaredKinds(declaration, tree, root, { load: !options.tree });
470
+ // A declared work kind's acceptance criteria are counted in the working tree too (#2772).
471
+ const declaredKinds = options.gather === false ? undefined : await loadDeclaredKinds(declaration, tree, root, { load: !options.tree, acceptance: !options.tree });
465
472
  const facts: WorkspaceFacts = { ...gathered, ...(options.records ? { records: options.records } : {}), ...(declaredKinds ? { declaredKinds } : {}) };
466
473
  const ctx: WorkspaceCheckContext = { declaration, tree, groups, kinds: registry, kindProblems: problems, facts };
467
474
  const findings = runWorkspaceChecks(ctx);
@@ -113,6 +113,8 @@ export interface GraphRecord {
113
113
  state: string | null;
114
114
  valid: boolean;
115
115
  supersededBy: string | null;
116
+ /** The ids of records whose remediates link names this one (#2774). */
117
+ remediatedBy: string[];
116
118
  }
117
119
 
118
120
  /** Read one member's `chant graph --format ir` output, upgrading an unversioned (v1) IR in place. */
@@ -17,6 +17,9 @@ export const recordKind = {
17
17
  // Sealed once reached (#2555).
18
18
  closedStates: ["ratified", "superseded"],
19
19
  supersedes: { field: "supersedes", key: "decision" },
20
+ // A remediates link names a closed decision this one fixes the consequence
21
+ // of, without replacing it: the target's state never changes (#2774).
22
+ remediates: { field: "remediates", key: "decision" },
20
23
  // A supersedes link takes effect under an equal or stricter approval rule
21
24
  // (#2524 D4): from a record ranked above 0 and at least as high as the one it
22
25
  // names. A decided record supersedes a decided or proposed one, a ratified
@@ -26,9 +29,16 @@ export const recordKind = {
26
29
  pins: { field: "evidence" },
27
30
  // member:<name> and path:<path> entries are the record's links in workspace graph (#2549).
28
31
  constrains: { field: "constrains" },
32
+ // Workspace paths a change carrying the decision out must not touch
33
+ // (#2773): chant workspace check --changes reports change-out-of-scope.
34
+ outOfScope: { field: "out_of_scope" },
29
35
  // Verdicts, and the field naming the decider, for each record's digest and
30
36
  // quorum (#2671, #2672).
31
37
  reviews: { field: "reviews", decider: "decided_by" },
38
+ // records new --by and the MCP records-new tool's by name a proposal's
39
+ // proposer here, apart from decided_by, which stays null until the
40
+ // decision is decided (#2756).
41
+ proposedBy: { field: "proposed_by" },
32
42
  // source is where a decision came from, and says where its proposal came
33
43
  // from too: via, client, harness, model, session, turns and a transcript
34
44
  // pinned by hash (#2708).
@@ -116,6 +116,19 @@
116
116
  ]
117
117
  }
118
118
  },
119
+ "remediates": {
120
+ "description": "Closed decisions this one fixes the consequence of, without replacing them: each names the decision by id and may say why (#2774). The named decision's status never changes because of a remediates link; a decision still open is amended instead, never remediated. Optional: most decisions remediate nothing.",
121
+ "type": "array",
122
+ "items": {
123
+ "type": "object",
124
+ "additionalProperties": false,
125
+ "required": ["decision"],
126
+ "properties": {
127
+ "decision": { "$ref": "#/definitions/decisionId" },
128
+ "reason": { "type": ["string", "null"] }
129
+ }
130
+ }
131
+ },
119
132
  "evidence": {
120
133
  "description": "The research, audits, design sections and workspace files behind the decision. A public link carries a url; a file in the workspace carries a path and the hash of its bytes (#2549). May be empty when the decision cites nothing; chant workspace records then warns decision-no-evidence (#2654).",
121
134
  "type": "array",
@@ -127,6 +140,10 @@
127
140
  "description": "Who chose. A forge login. Null in state proposed or withdrawn.",
128
141
  "type": ["string", "null"]
129
142
  },
143
+ "proposed_by": {
144
+ "description": "Who proposed this decision: the name chant workspace records new --by or the MCP records-new tool's by wrote, for a decision that opened proposed (#2756). Null or absent when nobody named a proposer, or the decision opened straight into a later state. Never cleared by a later decide, ratify or supersede.",
145
+ "type": ["string", "null"]
146
+ },
130
147
  "decided_on": {
131
148
  "description": "When the choice was made, as YYYY-MM-DD. Quoted, so YAML reads it as a string.",
132
149
  "type": ["string", "null"],
@@ -218,6 +235,15 @@
218
235
  "type": "string",
219
236
  "pattern": "^([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+|[a-z][a-z0-9]{0,15}-[0-9]{3,}|member:[a-z0-9][a-z0-9-]{0,39}|path:(?!/)(?!(?:[^/]*/)*\\.{1,2}(?:/|$))(?!.*//)[^\\\\\\u0000-\\u001f]*[^/\\\\\\u0000-\\u001f])$"
220
237
  }
238
+ },
239
+ "out_of_scope": {
240
+ "description": "Workspace paths this decision puts out of scope (#2773): files or directories, from the workspace root, that a change carrying out the decision must not touch. chant workspace check --changes reports a change to one as change-out-of-scope while the decision is in hand, and the decision does not cover it even where its constrains would. Optional.",
241
+ "type": "array",
242
+ "items": {
243
+ "type": "string",
244
+ "pattern": "^(?!/)(?!(?:[^/]*/)*\\.{1,2}(?:/|$))(?!.*//)[^\\\\\\u0000-\\u001f]*[^/\\\\\\u0000-\\u001f]$"
245
+ },
246
+ "uniqueItems": true
221
247
  }
222
248
  },
223
249
  "allOf": [
@@ -346,6 +346,9 @@ export function mcpToolCall(argv: readonly string[]): { name: string; arguments:
346
346
  else if (a === "--since") args.since = value();
347
347
  else if (a === "--compare-to") args.compareTo = value();
348
348
  else if (a === "--intent") args.intent = value();
349
+ else if (a === "--changes") args.range = value();
350
+ else if (a === "--work") args.work = value();
351
+ else if (a === "--severity") args.severity = value();
349
352
  else if (a === "--current") args.current = true;
350
353
  else if (a === "--composites") args.composites = true;
351
354
  else if (a.startsWith("-")) return undefined;
@@ -358,6 +361,8 @@ export function mcpToolCall(argv: readonly string[]): { name: string; arguments:
358
361
  return positional.length === 1 && kinds.length === 0 ? { name: "workspace-status", arguments: { ...args, env: positional[0] } } : undefined;
359
362
  case "records":
360
363
  return positional.length === 0 && kinds.length <= 1 ? { name: "workspace-records", arguments: { ...args, ...(kinds.length ? { kind: kinds[0] } : {}) } } : undefined;
364
+ case "check":
365
+ return positional.length === 0 && args.range !== undefined ? { name: "workspace-changes", arguments: { ...args, ...(kinds.length ? { kind: kinds } : {}) } } : undefined;
361
366
  case "graph":
362
367
  return positional.length === 0 ? { name: "workspace-graph", arguments: { ...args, ...(kinds.length ? { kind: kinds } : {}) } } : undefined;
363
368
  default:
@@ -134,7 +134,7 @@ describe("points ask (#2739)", () => {
134
134
  expect(q).toMatchObject({ state: "escalated", open: true, current: true, model: { answer: "medium", confidence: 0.175, threshold: 0.8, model: "bosun-v3.1-1.7b", observed: false } });
135
135
  expect(q.escalations.map((e) => e.reason)).toEqual(["no row matches these inputs", "not observed: confidence 0.175 is below the threshold 0.8"]);
136
136
  expect(listed.questions.every((x) => x.open)).toBe(true);
137
- expect(listed.points.map((p) => p.name)).toEqual(["slice-tier", "ship-skip", "needs-decision"]);
137
+ expect(listed.points.map((p) => p.name)).toEqual(["slice-tier", "ship-skip", "finding-triage", "needs-a-decision", "needs-decision"]);
138
138
  expect(listed.points[0].inputs[0]).toEqual({ name: "work-item.criteria", output: "work-item", description: "acceptance criteria in the work item" });
139
139
 
140
140
  // A later ask still escalating keeps the standing record; one a model answers rewrites it, as proposed.
@@ -66,6 +66,10 @@ import { policyAtBase, resolveBase } from "./trust/provenance";
66
66
  import { emptyPolicy, type TrustPolicy } from "./trust/policy";
67
67
  import { AGENT_ROLE } from "./records-cli";
68
68
  import type { SourceVia } from "./source-block";
69
+ import type { RecordSource } from "./record-source";
70
+ import { currentStewardTurn } from "../op/steward-turn";
71
+ import { RefCASConflictError } from "../lifecycle/git";
72
+ import { readLedgerAnswers, refreshLedger, withLedgerAnswers, writeLedgerAnswer, type LedgerAnswers } from "./answers-ledger";
69
73
 
70
74
  /** The version of the write documents `points ask` and `points answer` print. */
71
75
  export const POINTS_WRITE_CONTRACT_VERSION = 1;
@@ -93,6 +97,8 @@ export const POINTS_WRITE_ERROR_CODES = [
93
97
  "record-not-found",
94
98
  "record-closed",
95
99
  "record-id-taken",
100
+ /** The answer was given during a steward's turn (#2749): a steward never answers a question, its own or another's. */
101
+ "answer-in-steward-turn",
96
102
  ...RECORD_REASON_CODES,
97
103
  ] as const satisfies readonly ReasonCode[];
98
104
  export type PointsWriteErrorCode = (typeof POINTS_WRITE_ERROR_CODES)[number];
@@ -141,12 +147,47 @@ export interface QuestionView {
141
147
  answeredBy: string[];
142
148
  askedOn: string | null;
143
149
  answeredOn: string | null;
150
+ /**
151
+ * The steward whose turn asked the question (#2749), and the run it was in,
152
+ * or null when no steward asked it. The steward waits on the question and
153
+ * never answers it: a person does, through hud or `points answer`.
154
+ */
155
+ askedBy: { steward: string; run: string | null } | null;
156
+ /**
157
+ * Where the record is held when it is on the lifecycle ledger rather than
158
+ * in the working tree (#2786): `chant/lifecycle:<path>`, which `git show`
159
+ * reads. A question asked in a steward's turn is held there, since a
160
+ * steward never writes the checkout. null for a record in the tree; `path`
161
+ * is where the record reads as being either way.
162
+ */
163
+ ledger: string | null;
144
164
  valid: boolean;
145
165
  warnings: RecordWarning[];
146
166
  }
147
167
 
148
- /** One answer record as a {@link QuestionView}, or null when its front matter can't be read. `point` is its point as declared now, when there is one. */
149
- export function questionView(entry: RecordEntry, point: Point | undefined): QuestionView | null {
168
+ /** The harness a steward's question names in its source block (#2749). */
169
+ export const STEWARD_HARNESS = "chant-steward";
170
+
171
+ /** The steward that asked, from a record's source block, or null. */
172
+ export function askedByOf(data: Record<string, unknown>): QuestionView["askedBy"] {
173
+ const source = data.source;
174
+ if (source === null || typeof source !== "object" || Array.isArray(source)) return null;
175
+ const s = source as Record<string, unknown>;
176
+ if (s.harness !== STEWARD_HARNESS) return null;
177
+ const client = s.client as Record<string, unknown> | undefined;
178
+ const steward = client && typeof client.name === "string" ? client.name : null;
179
+ if (steward === null) return null;
180
+ const session = s.session as Record<string, unknown> | string | undefined;
181
+ const run = typeof session === "string" ? session : session && typeof session === "object" && typeof session.id === "string" ? session.id : null;
182
+ return { steward, run };
183
+ }
184
+
185
+ /**
186
+ * One answer record as a {@link QuestionView}, or null when its front matter
187
+ * can't be read. `point` is its point as declared now, when there is one, and
188
+ * `ledger` where the record is held when it is on the lifecycle ledger.
189
+ */
190
+ export function questionView(entry: RecordEntry, point: Point | undefined, ledger: string | null = null): QuestionView | null {
150
191
  const d = entry.data;
151
192
  if (d === null || entry.id === null) return null;
152
193
  const str = (v: unknown): string | null => (typeof v === "string" ? v : null);
@@ -187,6 +228,8 @@ export function questionView(entry: RecordEntry, point: Point | undefined): Ques
187
228
  answeredBy: Array.isArray(d.answered_by) ? d.answered_by.filter((b): b is string => typeof b === "string") : [],
188
229
  askedOn: str(d.asked_on),
189
230
  answeredOn: str(d.answered_on),
231
+ askedBy: askedByOf(d),
232
+ ledger,
190
233
  valid: entry.valid,
191
234
  warnings: entry.warnings,
192
235
  };
@@ -276,6 +319,21 @@ export interface AskPointOptions {
276
319
  via?: SourceVia;
277
320
  /** The client that asked, for its source block. */
278
321
  client?: { name: string; version?: string };
322
+ /**
323
+ * The steward whose turn asks (#2749), and its run. Recorded in the source
324
+ * block (harness `chant-steward`, the steward as the client, the run as the
325
+ * session), so the question says who is waiting on it and `points answer`
326
+ * never counts the steward toward its quorum. Takes the place of `client`.
327
+ */
328
+ steward?: { name: string; run?: string };
329
+ /**
330
+ * Where a new record goes: the answer kind's directory in the working tree,
331
+ * or the lifecycle ledger (#2786). Defaults to the ledger in a steward's
332
+ * turn (`steward` given, or this process a steward's turn), since a steward
333
+ * never writes the checkout, and to the tree otherwise. A question already
334
+ * held on the ledger is rewritten there whatever this says.
335
+ */
336
+ store?: "tree" | "ledger";
279
337
  /** The date written as asked_on, and answered_on for a table's answer, YYYY-MM-DD. Defaults to today, in UTC. */
280
338
  on?: string;
281
339
  dryRun?: boolean;
@@ -319,9 +377,34 @@ function checkInputs(name: string, point: Point, inputs: unknown): Record<string
319
377
  return inputs as Record<string, unknown>;
320
378
  }
321
379
 
322
- /** Write `data` as the record at `path`, after reading it back with every other record. */
323
- async function write(o: Opened, before: RecordEntry[], path: string, text: string, create: boolean, dryRun: boolean): Promise<RecordWarning[]> {
324
- const warnings = await validateWrite(o, before, path, text);
380
+ /** Where a write goes: the working tree, or the lifecycle ledger (#2786). */
381
+ interface WriteTarget {
382
+ /** The records as read: the tree with the ledger's answers laid over it. */
383
+ source: RecordSource;
384
+ ledger: LedgerAnswers;
385
+ /** Write to the ledger rather than the tree. */
386
+ onLedger: boolean;
387
+ message: string;
388
+ }
389
+
390
+ /**
391
+ * Write `text` as the record at `path`, after reading it back with every
392
+ * other record: into the working tree, or onto the ledger. Returns the
393
+ * record's warnings and, for a ledger write, where it went.
394
+ */
395
+ async function write(o: Opened, before: RecordEntry[], path: string, text: string, create: boolean, dryRun: boolean, target: WriteTarget): Promise<{ warnings: RecordWarning[]; ledger: string | null }> {
396
+ const warnings = await validateWrite(o, before, path, text, target.source);
397
+ const held = target.ledger.byPath.get(path);
398
+ if (target.onLedger) {
399
+ if (dryRun) return { warnings, ledger: held?.ledger ?? null };
400
+ try {
401
+ const { ledger } = await writeLedgerAnswer(target.ledger, path, text, target.message, held?.sha ?? null);
402
+ return { warnings, ledger };
403
+ } catch (err) {
404
+ if (err instanceof RefCASConflictError) throw new PointsWriteError("record-id-taken", `${path} was written on chant/lifecycle by another ask or answer at the same time: ask again to read it`);
405
+ throw err;
406
+ }
407
+ }
325
408
  if (!dryRun) {
326
409
  try {
327
410
  writeFileSync(abs(o, path), text, create ? { flag: "wx" } : undefined);
@@ -330,7 +413,14 @@ async function write(o: Opened, before: RecordEntry[], path: string, text: strin
330
413
  throw err;
331
414
  }
332
415
  }
333
- return warnings;
416
+ return { warnings, ledger: null };
417
+ }
418
+
419
+ /** The kind's answers on the ledger, and its records read with them laid over the tree. */
420
+ async function readWithLedger(o: Opened): Promise<{ ledger: LedgerAnswers; source: RecordSource; before: RecordEntry[] }> {
421
+ const ledger = await readLedgerAnswers({ file: o.loaded.file, name: o.loaded.kind.name, dirRel: o.dirRel });
422
+ const source = withLedgerAnswers(o.source, ledger);
423
+ return { ledger, source, before: await readAll(o, source) };
334
424
  }
335
425
 
336
426
  function recordData(o: Opened, fields: Record<string, unknown>): Record<string, unknown> {
@@ -355,15 +445,21 @@ function resultFields(result: ChainResult): Record<string, unknown> {
355
445
  */
356
446
  export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocument> {
357
447
  try {
448
+ // A steward's turn (#2749): named by the caller, or this process's, as when an Op shells out to `points ask`.
449
+ const turn = opts.steward ? undefined : currentStewardTurn();
450
+ const steward = opts.steward ?? (turn ? { name: turn.steward, ...(turn.run ? { run: turn.run } : {}) } : undefined);
451
+ const toLedger = opts.store !== undefined ? opts.store === "ledger" : steward !== undefined;
358
452
  const kinds = await answerKindFiles(opts.cwd, opts.kind);
359
453
  const { o, point } = await findPoint(kinds, opts.point, opts.cwd);
360
454
  const inputs = checkInputs(opts.point, point, opts.inputs);
361
455
  const version = pointVersion(point);
362
456
  const hash = inputsHash(opts.point, version, inputs);
363
457
  const id = answerId(opts.point, hash);
364
- const before = await readAll(o, o.source);
458
+ if (toLedger && !opts.dryRun) await refreshLedger(o.loaded.file);
459
+ const { ledger, source: read, before } = await readWithLedger(o);
365
460
  const existing = before.find((e) => e.id === id);
366
- const done = (entry: RecordEntry, extra: { reused: boolean; written: boolean; text?: string }): PointsWriteDocument => ({
461
+ const heldAt = (entry: RecordEntry): string | null => ledger.byPath.get(entry.path)?.ledger ?? null;
462
+ const done = (entry: RecordEntry, extra: { reused: boolean; written: boolean; text?: string; ledger?: string | null }): PointsWriteDocument => ({
367
463
  $schema: POINTS_WRITE_SCHEMA_ID,
368
464
  contract: POINTS_WRITE_CONTRACT_VERSION,
369
465
  verb: "ask",
@@ -373,7 +469,7 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
373
469
  reused: extra.reused,
374
470
  written: extra.written,
375
471
  dryRun: !!opts.dryRun,
376
- question: questionView(entry, point)!,
472
+ question: questionView(entry, point, extra.ledger !== undefined ? extra.ledger : heldAt(entry))!,
377
473
  ...(extra.text !== undefined ? { text: extra.text } : {}),
378
474
  });
379
475
  if (existing && existing.data !== null && (existing.state === "answered" || existing.state === "proposed")) return done(existing, { reused: true, written: false });
@@ -393,7 +489,14 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
393
489
  const answer = result.status === "escalated" ? undefined : result.answer;
394
490
  const title = titleFor(point, opts.subject, state, answer);
395
491
  const modelId = result.status === "proposed" ? result.decider.model : undefined;
396
- const source = { via: opts.via ?? "cli", ...(opts.client ? { client: opts.client } : {}), ...(modelId ? { model: modelId } : {}) };
492
+ const client = steward ? { name: steward.name } : opts.client;
493
+ const source = {
494
+ via: opts.via ?? "cli",
495
+ ...(steward ? { harness: STEWARD_HARNESS } : {}),
496
+ ...(client ? { client } : {}),
497
+ ...(modelId ? { model: modelId } : {}),
498
+ ...(steward?.run ? { session: { id: steward.run } } : {}),
499
+ };
397
500
  const data = recordData(o, {
398
501
  id,
399
502
  title,
@@ -411,16 +514,19 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
411
514
  });
412
515
  const path = existing ? existing.path : o.dirRel === "." ? `${id}.md` : `${o.dirRel}/${id}.md`;
413
516
  const text = renderRecord(data, body(point, title));
414
- const warnings = await write(o, before, path, text, !existing, !!opts.dryRun);
415
- const entry: RecordEntry = { ...(existing ?? emptyEntry(path)), id, path, state, data, valid: true, reasons: [], warnings };
416
- return done(entry, { reused: false, written: !opts.dryRun, ...(opts.dryRun ? { text } : {}) });
517
+ // A steward's question goes on the ledger, and one already held there stays there (#2786).
518
+ const onLedger = toLedger || ledger.byPath.has(path);
519
+ const message = `Decision point ${opts.point}: ${state}${opts.subject ? ` (${opts.subject})` : ""}${steward ? `, asked by ${steward.name}` : ""}`;
520
+ const wrote = await write(o, before, path, text, !existing, !!opts.dryRun, { source: read, ledger, onLedger, message });
521
+ const entry: RecordEntry = { ...(existing ?? emptyEntry(path)), id, path, state, data, valid: true, reasons: [], warnings: wrote.warnings };
522
+ return done(entry, { reused: false, written: !opts.dryRun, ledger: wrote.ledger, ...(opts.dryRun ? { text } : {}) });
417
523
  } catch (err) {
418
524
  return failure("ask", err);
419
525
  }
420
526
  }
421
527
 
422
528
  function emptyEntry(path: string): RecordEntry {
423
- return { id: null, path, state: null, valid: true, reasons: [], supersededBy: null, data: null, assets: [], warnings: [], digest: "" };
529
+ return { id: null, path, state: null, valid: true, reasons: [], supersededBy: null, remediatedBy: [], data: null, assets: [], warnings: [], digest: "" };
424
530
  }
425
531
 
426
532
  // ── points answer ────────────────────────────────────────────────────────────
@@ -453,9 +559,16 @@ function policyFor(root: string): TrustPolicy {
453
559
  * leaving out anyone holding the agent role in the trust policy at base, and,
454
560
  * when the quorum names roles, anyone holding none of them.
455
561
  */
456
- export function tallyQuorum(by: string[], quorum: { count: number; roles?: string[] }, policy: TrustPolicy): { counted: string[]; left: { name: string; why: string }[]; met: boolean } {
562
+ export function tallyQuorum(
563
+ by: string[],
564
+ quorum: { count: number; roles?: string[] },
565
+ policy: TrustPolicy,
566
+ /** The steward that asked the question (#2749): it never counts toward the answer. */
567
+ steward?: string,
568
+ ): { counted: string[]; left: { name: string; why: string }[]; met: boolean } {
457
569
  const holders = (role: string) => new Set((policy.roles[role] ?? []).map(normalisePrincipal));
458
570
  const agents = holders(AGENT_ROLE);
571
+ const asker = steward !== undefined ? normalisePrincipal(steward) : undefined;
459
572
  const counted: string[] = [];
460
573
  const seen = new Set<string>();
461
574
  const left: { name: string; why: string }[] = [];
@@ -463,6 +576,10 @@ export function tallyQuorum(by: string[], quorum: { count: number; roles?: strin
463
576
  const p = normalisePrincipal(name);
464
577
  if (p === "" || seen.has(p)) continue;
465
578
  seen.add(p);
579
+ if (asker !== undefined && p === asker) {
580
+ left.push({ name, why: "is the steward that asked" });
581
+ continue;
582
+ }
466
583
  if (agents.has(p)) {
467
584
  left.push({ name, why: "holds the agent role" });
468
585
  continue;
@@ -484,20 +601,34 @@ export function tallyQuorum(by: string[], quorum: { count: number; roles?: strin
484
601
  */
485
602
  export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWriteDocument> {
486
603
  try {
604
+ // A steward waits on a question and never answers one (#2749), as it never
605
+ // clears a gate: the answer has to come from a person, through hud or a
606
+ // shell, not from the steward's own turn or a process it started.
607
+ const turn = currentStewardTurn();
608
+ if (turn) {
609
+ throw new PointsWriteError(
610
+ "answer-in-steward-turn",
611
+ `this is the steward ${turn.steward}'s turn, and a steward never answers a decision point: a person answers ${opts.id} through hud or \`chant workspace points answer\` at a shell`,
612
+ );
613
+ }
487
614
  const kinds = await answerKindFiles(opts.cwd, opts.kind);
488
615
  if (kinds.length === 0) throw new PointsWriteError("points-undeclared", "no record kind with an answers block is declared: name one with --kind, or declare one in chant.workspace.json");
489
- let found: { opened: OpenedPoints; before: RecordEntry[]; target: RecordEntry & { data: Record<string, unknown> } } | undefined;
616
+ let found:
617
+ | { opened: OpenedPoints; before: RecordEntry[]; read: Awaited<ReturnType<typeof readWithLedger>>; target: RecordEntry & { data: Record<string, unknown> } }
618
+ | undefined;
490
619
  for (const k of kinds) {
491
620
  const opened = await openAnswers(k, opts.cwd);
492
- const before = await readAll(opened.o, opened.o.source);
493
- const target = before.find((e) => e.id === opts.id);
621
+ // A question a steward asked is on the ledger (#2786), and people's answer goes back there.
622
+ const read = await readWithLedger(opened.o);
623
+ const target = read.before.find((e) => e.id === opts.id);
494
624
  if (target && target.data !== null) {
495
- found = { opened, before, target: target as RecordEntry & { data: Record<string, unknown> } };
625
+ found = { opened, before: read.before, read, target: target as RecordEntry & { data: Record<string, unknown> } };
496
626
  break;
497
627
  }
498
628
  }
499
629
  if (!found) throw new PointsWriteError("record-not-found", `no answer record has id ${opts.id}`);
500
630
  const { opened, before, target } = found;
631
+ const ledger = found.read.ledger;
501
632
  const { o } = opened;
502
633
  const d = target.data;
503
634
  if (target.state === "answered") throw new PointsWriteError("record-closed", `${opts.id} is answered, and an answer never changes: ask again with other inputs, or change the point, which asks the question anew`);
@@ -511,7 +642,7 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
511
642
  throw new PointsWriteError("answer-not-candidate", `${opts.id} takes one of ${allowed.map((c) => JSON.stringify(c)).join(", ")}, not ${JSON.stringify(opts.answer)}`);
512
643
  }
513
644
  const quorum = quorumOf(point);
514
- const tally = tallyQuorum(opts.by, quorum, policyFor(o.root));
645
+ const tally = tallyQuorum(opts.by, quorum, policyFor(o.root), askedByOf(d)?.steward);
515
646
  if (!tally.met) {
516
647
  const got = tally.counted.length;
517
648
  const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
@@ -555,8 +686,10 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
555
686
  fields.title = titleFor(point, subject, "answered", value);
556
687
  const data = recordData(o, fields);
557
688
  const text = renderRecord(data, body(point, String(fields.title)));
558
- const warnings = await write(o, before, target.path, text, false, !!opts.dryRun);
559
- const entry: RecordEntry = { ...target, state: "answered", data, valid: true, reasons: [], warnings };
689
+ const onLedger = ledger.byPath.has(target.path);
690
+ const message = `Decision point ${name}: answered ${show(value, type)} by ${tally.counted.join(", ")}`;
691
+ const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message });
692
+ const entry: RecordEntry = { ...target, state: "answered", data, valid: true, reasons: [], warnings: wrote.warnings };
560
693
  return {
561
694
  $schema: POINTS_WRITE_SCHEMA_ID,
562
695
  contract: POINTS_WRITE_CONTRACT_VERSION,
@@ -567,7 +700,7 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
567
700
  reused: false,
568
701
  written: !opts.dryRun,
569
702
  dryRun: !!opts.dryRun,
570
- question: questionView(entry, point)!,
703
+ question: questionView(entry, point, onLedger ? (wrote.ledger ?? ledger.byPath.get(target.path)?.ledger ?? null) : null)!,
571
704
  ...(opts.dryRun ? { text } : {}),
572
705
  };
573
706
  } catch (err) {