@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
@@ -0,0 +1,395 @@
1
+ /**
2
+ * An Op run under a work item's lease (#2748, the Steward's gap 4 in #2731).
3
+ *
4
+ * An Op that declares `workLease` (`./types.ts`) runs its steps under the work
5
+ * lease of one work item (`../lifecycle/work-lease.ts`, ws-055). The local
6
+ * executor (`./local-executor.ts`) drives the {@link RunWorkLease} here:
7
+ *
8
+ * - Claim. Before the first step when the item is a literal or named by the
9
+ * run (`chant run <op> --work <id>`); right after the producing step when
10
+ * the item is a reference to a step's output. A list of candidates is tried
11
+ * in order and the first one nobody holds is taken. When nothing can be
12
+ * claimed the run ends `ok` with its remaining steps skipped, and the claim's
13
+ * record carries the refusal, which names who holds the item.
14
+ * - Heartbeat. While the steps run, the lease is renewed every third of its
15
+ * time to live, keeping its fencing token. A renewal the lease refuses (it
16
+ * ran out and someone else claimed it, or it was released) means the lease
17
+ * is lost: the in-flight step's abort signal fires, no further step starts,
18
+ * and the run fails with `lease-lost`. A renewal that fails for any other
19
+ * reason (the remote didn't answer) is retried at the next beat; if that
20
+ * goes on past the expiry, the next renewal is refused and the lease is lost
21
+ * then. These are the semantics of chud's dispatcher
22
+ * (packages/runtime/src/dispatch.mjs), which this replaces.
23
+ * - Fence. Before the run's record is written, one last renewal checks that
24
+ * the lease is still this run's, with the same token. Refused, the run is
25
+ * recorded as failed with `lease-lost`, never as done.
26
+ * - Release. When the run ends, the lease is given back with the run's
27
+ * outcome (`done`, `not_done`, `gated`, or the Op's own `outcome`), which
28
+ * lands on `_leases/<id>.jsonl`. A lost lease is not released: it isn't
29
+ * this run's to give back.
30
+ *
31
+ * Steps read the lease through {@link workLeaseOutput}: its item, holder,
32
+ * token and expiry, and for an Op that changes the checkout, the worktree and
33
+ * branch it works in. An activity that writes somewhere a token can fence
34
+ * (another ledger, a remote) passes the token along.
35
+ *
36
+ * ## Changing the checkout
37
+ *
38
+ * An Op with `changesCheckout` (applying a build, `chant workspace upgrade`)
39
+ * must declare `workLease`, and its leased steps get a git worktree of their
40
+ * own: `chant/work/<item>` checked out under the repository's git directory,
41
+ * from the checkout's HEAD, or from the branch as an earlier run left it. The
42
+ * worktree is removed when the run ends and the branch keeps whatever the
43
+ * steps committed, for the runner to deliver. The checkout a coding agent is
44
+ * editing, its index and its uncommitted files, are never touched.
45
+ */
46
+
47
+ import { existsSync, rmSync } from "node:fs";
48
+ import { isAbsolute, join, resolve } from "node:path";
49
+ import { getRuntime } from "../runtime-adapter";
50
+ import {
51
+ claimWorkLease,
52
+ DEFAULT_WORK_LEASE_TTL_MS,
53
+ releaseWorkLease,
54
+ renewWorkLease,
55
+ workLeaseRef,
56
+ WORK_ITEM_ID_PATTERN,
57
+ type WorkLease,
58
+ } from "../lifecycle/work-lease";
59
+ import { resolveMemberLedger } from "../lifecycle/member-ledger";
60
+ import { currentHolderId } from "../lifecycle/lease";
61
+ import { parseDuration } from "./duration";
62
+ import { isStepOutputRef, stepOutput, type StepOutputRef } from "./step-output-ref";
63
+ import { WORK_LEASE_STEP_ID, type OpConfig } from "./types";
64
+
65
+ export { WORK_LEASE_STEP_ID } from "./types";
66
+ export { workLeaseProblems, workLeaseNeedsRunItem } from "./work-lease-decl";
67
+
68
+ /** Why a run under a work lease failed when the lease was lost: chud's reason, kept. */
69
+ export const LEASE_LOST = "lease-lost";
70
+
71
+ /** The branch prefix an Op that changes the checkout works on. */
72
+ export const WORK_BRANCH_PREFIX = "chant/work/";
73
+
74
+ /** What a run's steps read about its work lease, via {@link workLeaseOutput}. */
75
+ export interface WorkLeaseOutput {
76
+ item: string;
77
+ holder: string;
78
+ /** The fencing token: the same for the whole run unless the lease is lost. */
79
+ token: string;
80
+ acquiredAt: string;
81
+ expiresAt: string;
82
+ /** The lease ref. */
83
+ ref: string;
84
+ /** For an Op that changes the checkout: the worktree its steps work in. Otherwise null. */
85
+ worktree: string | null;
86
+ /** For an Op that changes the checkout: the worktree's branch. Otherwise null. */
87
+ branch: string | null;
88
+ }
89
+
90
+ /** How a run's work lease ended, on `OpRunResult.workLease`. */
91
+ export interface WorkLeaseRunResult {
92
+ /** The item claimed, or null when the run claimed nothing. */
93
+ item: string | null;
94
+ holder: string;
95
+ token: string | null;
96
+ /** The branch an Op that changes the checkout worked on, or null. */
97
+ branch: string | null;
98
+ /** Whether the lease was given back. False when it was lost, or never claimed. */
99
+ released: boolean;
100
+ /** The outcome the release recorded, or null when there was no release. */
101
+ outcome: string | null;
102
+ /** Why the lease was lost, or null when it wasn't. */
103
+ lost: string | null;
104
+ /** Why nothing was claimed, or null when something was. */
105
+ refusal: string | null;
106
+ }
107
+
108
+ /**
109
+ * A reference to the run's work lease, for a step's args:
110
+ * `workLeaseOutput("token")`, `workLeaseOutput("worktree")`, or the whole
111
+ * {@link WorkLeaseOutput} with no path.
112
+ */
113
+ export function workLeaseOutput(path?: keyof WorkLeaseOutput): StepOutputRef {
114
+ return stepOutput(WORK_LEASE_STEP_ID, path);
115
+ }
116
+
117
+ /**
118
+ * The holder a steward's turn claims work leases as:
119
+ * `<steward>/<op>@<process>`. `chant workspace status --json` reads it back
120
+ * to show the lease a steward's current turn holds beside its Ops.
121
+ */
122
+ export function stewardWorkHolder(steward: string, op: string, process: string = currentHolderId()): string {
123
+ return `${steward}/${op}@${process}`;
124
+ }
125
+
126
+ function resolvePath(value: unknown, path?: string): unknown {
127
+ if (!path) return value;
128
+ return path.split(".").reduce<unknown>((acc, key) => (acc == null ? acc : (acc as Record<string, unknown>)[key]), value);
129
+ }
130
+
131
+ async function git(args: string[], cwd: string): Promise<{ ok: boolean; stdout: string; stderr: string }> {
132
+ const out = await getRuntime().spawn(["git", ...args], { cwd });
133
+ return { ok: out.exitCode === 0, stdout: out.stdout.trim(), stderr: out.stderr.trim() };
134
+ }
135
+
136
+ /** What a claim attempt came to, for the executor to record. */
137
+ export type WorkClaimOutcome =
138
+ | { kind: "claimed"; lease: WorkLeaseOutput; tried: string[] }
139
+ /** Nothing to claim, or every candidate is held. `refusal` says which. */
140
+ | { kind: "unclaimed"; refusal: string; tried: string[] };
141
+
142
+ export interface RunWorkLeaseOptions {
143
+ /** The project directory: where the run's ledger is, and what `workLease.kind` is relative to. */
144
+ cwd: string;
145
+ holder?: string;
146
+ /** The item the run names (`--work`), which takes the place of the Op's own. */
147
+ item?: string;
148
+ /** Called with a renewal's error that didn't lose the lease, which the next beat retries. */
149
+ onRenewError?: (message: string) => void;
150
+ }
151
+
152
+ /** One run's hold on one work item's lease. Created by the executor for an Op with `workLease`. */
153
+ export class RunWorkLease {
154
+ readonly holder: string;
155
+ readonly ttlMs: number;
156
+ /** Aborts when the lease is lost, so the step in flight stops. */
157
+ readonly lostSignal: AbortSignal;
158
+ /** Set once the claim was made or given up on. */
159
+ attempted = false;
160
+ lease: WorkLeaseOutput | undefined;
161
+ refusal: string | undefined;
162
+ lost: string | undefined;
163
+
164
+ private readonly spec: NonNullable<OpConfig["workLease"]>;
165
+ private readonly abort = new AbortController();
166
+ private timer: ReturnType<typeof setInterval> | undefined;
167
+ private beat: Promise<void> | undefined;
168
+
169
+ constructor(
170
+ private readonly config: Pick<OpConfig, "name" | "workLease" | "changesCheckout">,
171
+ private readonly opts: RunWorkLeaseOptions,
172
+ ) {
173
+ if (!config.workLease) throw new Error(`Op "${config.name}" declares no workLease`);
174
+ this.spec = config.workLease;
175
+ this.holder = opts.holder ?? currentHolderId();
176
+ this.ttlMs = this.spec.ttl ? parseDuration(this.spec.ttl) : DEFAULT_WORK_LEASE_TTL_MS;
177
+ this.lostSignal = this.abort.signal;
178
+ }
179
+
180
+ /** The directory whose ledger holds the lease: the kind file's, or the project's. */
181
+ private get ledgerCwd(): string {
182
+ const kind = this.spec.kind;
183
+ if (!kind) return this.opts.cwd;
184
+ const file = isAbsolute(kind) ? kind : resolve(this.opts.cwd, kind);
185
+ return resolve(file, "..");
186
+ }
187
+
188
+ /** Whether the claim is due at a step boundary, given the results so far. */
189
+ due(resultsById: ReadonlyMap<string, unknown>): boolean {
190
+ if (this.attempted) return false;
191
+ if (this.opts.item !== undefined) return true;
192
+ const item = this.spec.item;
193
+ return !isStepOutputRef(item) || resultsById.has(item.step);
194
+ }
195
+
196
+ /** The candidate ids, in order. Empty when the reference resolved to nothing. */
197
+ private candidates(resultsById: ReadonlyMap<string, unknown>): string[] {
198
+ if (this.opts.item !== undefined) return [this.opts.item];
199
+ const item = this.spec.item;
200
+ const value = isStepOutputRef(item) ? resolvePath(resultsById.get(item.step), item.path) : item;
201
+ if (value === undefined || value === null || value === "") return [];
202
+ const list = Array.isArray(value) ? value : [value];
203
+ return list.filter((v) => v !== undefined && v !== null && v !== "").map((v) => {
204
+ if (typeof v !== "string" || !WORK_ITEM_ID_PATTERN.test(v) || v.includes("..")) {
205
+ throw new Error(`Op "${this.config.name}": workLease.item resolved to ${JSON.stringify(v)}, which can't key a work lease`);
206
+ }
207
+ return v;
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Claim the first candidate nobody holds. On a claim, the heartbeat starts,
213
+ * and for an Op that changes the checkout its worktree is made. Throws when
214
+ * the claim could not be made for a reason other than someone holding it
215
+ * (git failed): the run fails, as for any failing step.
216
+ */
217
+ async claim(resultsById: ReadonlyMap<string, unknown>): Promise<WorkClaimOutcome> {
218
+ this.attempted = true;
219
+ const tried = this.candidates(resultsById);
220
+ if (tried.length === 0) {
221
+ this.refusal = "nothing to claim: the work item resolved to no id";
222
+ return { kind: "unclaimed", refusal: this.refusal, tried };
223
+ }
224
+ const refused: string[] = [];
225
+ for (const id of tried) {
226
+ const result = await claimWorkLease(id, this.holder, { cwd: this.ledgerCwd, ttlMs: this.ttlMs, note: `op ${this.config.name}` });
227
+ if (!result.ok) {
228
+ refused.push(`${result.reason}: ${result.message}`);
229
+ continue;
230
+ }
231
+ const { prefix, members } = await resolveMemberLedger(this.ledgerCwd);
232
+ const lease: WorkLeaseOutput = {
233
+ ...pick(result.lease),
234
+ ref: workLeaseRef(id, prefix),
235
+ worktree: null,
236
+ branch: null,
237
+ };
238
+ this.lease = lease;
239
+ if (this.config.changesCheckout) {
240
+ try {
241
+ const made = await this.makeWorktree(id, members);
242
+ lease.worktree = made.worktree;
243
+ lease.branch = made.branch;
244
+ } catch (err) {
245
+ await releaseWorkLease(id, this.holder, { cwd: this.ledgerCwd, token: lease.token, outcome: "not_done", note: "the worktree could not be made" }).catch(() => undefined);
246
+ this.lease = undefined;
247
+ throw err;
248
+ }
249
+ }
250
+ this.startHeartbeat();
251
+ return { kind: "claimed", lease, tried };
252
+ }
253
+ this.refusal = refused.join("; ");
254
+ return { kind: "unclaimed", refusal: this.refusal, tried };
255
+ }
256
+
257
+ /**
258
+ * A worktree for `id` on `chant/work/[<member>/]<id>`, under the git
259
+ * directory so the checkout's own `git status` never lists it. Starts from
260
+ * the branch when an earlier run left it, from HEAD otherwise.
261
+ */
262
+ private async makeWorktree(id: string, members: readonly string[]): Promise<{ worktree: string; branch: string }> {
263
+ const cwd = this.opts.cwd;
264
+ const common = await git(["rev-parse", "--git-common-dir"], cwd);
265
+ if (!common.ok) throw new Error(`Op "${this.config.name}" changes the checkout, but ${cwd} is not a git checkout: ${common.stderr}`);
266
+ const commonDir = isAbsolute(common.stdout) ? common.stdout : resolve(cwd, common.stdout);
267
+ const scope = members.join("/");
268
+ const branch = `${WORK_BRANCH_PREFIX}${scope ? `${scope}/` : ""}${id}`;
269
+ const worktree = join(commonDir, "chant-work", ...members, id);
270
+ if (existsSync(worktree)) {
271
+ await git(["worktree", "remove", "--force", worktree], cwd);
272
+ rmSync(worktree, { recursive: true, force: true });
273
+ }
274
+ await git(["worktree", "prune"], cwd);
275
+ const exists = (await git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], cwd)).ok;
276
+ const add = exists
277
+ ? await git(["worktree", "add", "--quiet", worktree, branch], cwd)
278
+ : await git(["worktree", "add", "--quiet", "-b", branch, worktree, "HEAD"], cwd);
279
+ if (!add.ok) throw new Error(`Op "${this.config.name}": could not make the worktree for ${id} on ${branch}: ${add.stderr}`);
280
+ return { worktree, branch };
281
+ }
282
+
283
+ private lose(why: string): void {
284
+ if (this.lost !== undefined) return;
285
+ this.lost = why;
286
+ this.stopHeartbeat();
287
+ this.abort.abort(new Error(`${LEASE_LOST}: ${why}`));
288
+ }
289
+
290
+ /** One renewal. Refused, the lease is lost; failed otherwise, the next beat retries. */
291
+ private async renew(): Promise<void> {
292
+ const lease = this.lease;
293
+ if (!lease || this.lost !== undefined) return;
294
+ try {
295
+ const result = await renewWorkLease(lease.item, this.holder, { cwd: this.ledgerCwd, ttlMs: this.ttlMs, token: lease.token });
296
+ if (result.ok) {
297
+ lease.expiresAt = result.lease.expiresAt;
298
+ return;
299
+ }
300
+ // The remote refusing the push is the lease held elsewhere only when it
301
+ // says who holds it; unreachable, the next beat tries again.
302
+ if (result.reason === "lease-push-rejected" && !result.heldBy) {
303
+ this.opts.onRenewError?.(result.message);
304
+ return;
305
+ }
306
+ if (result.reason === "lease-race") {
307
+ this.opts.onRenewError?.(result.message);
308
+ return;
309
+ }
310
+ this.lose(result.message);
311
+ } catch (err) {
312
+ this.opts.onRenewError?.(err instanceof Error ? err.message : String(err));
313
+ }
314
+ }
315
+
316
+ private startHeartbeat(): void {
317
+ const every = Math.max(1_000, Math.floor(this.ttlMs / 3));
318
+ this.timer = setInterval(() => {
319
+ if (this.beat) return;
320
+ this.beat = this.renew().finally(() => {
321
+ this.beat = undefined;
322
+ });
323
+ }, every);
324
+ (this.timer as { unref?: () => void }).unref?.();
325
+ }
326
+
327
+ private stopHeartbeat(): void {
328
+ if (this.timer) clearInterval(this.timer);
329
+ this.timer = undefined;
330
+ }
331
+
332
+ /**
333
+ * The last renewal before the run's record is written, after the heartbeat
334
+ * stops. Returns whether the lease is still this run's.
335
+ */
336
+ async fence(): Promise<boolean> {
337
+ if (!this.lease) return this.lost === undefined;
338
+ this.stopHeartbeat();
339
+ await this.beat;
340
+ if (this.lost !== undefined) return false;
341
+ const lease = this.lease;
342
+ try {
343
+ const result = await renewWorkLease(lease.item, this.holder, { cwd: this.ledgerCwd, ttlMs: this.ttlMs, token: lease.token });
344
+ if (result.ok) lease.expiresAt = result.lease.expiresAt;
345
+ else this.lose(result.message);
346
+ } catch (err) {
347
+ this.lose(`the lease could not be renewed before the run was recorded: ${err instanceof Error ? err.message : String(err)}`);
348
+ }
349
+ return this.lost === undefined;
350
+ }
351
+
352
+ /**
353
+ * End the run's hold: stop the heartbeat, remove the worktree (the branch
354
+ * keeps what was committed), and give the lease back with the outcome
355
+ * unless it was lost.
356
+ */
357
+ async finish(status: "ok" | "fail" | "gated" | "waiting", resultsById: ReadonlyMap<string, unknown>): Promise<WorkLeaseRunResult> {
358
+ this.stopHeartbeat();
359
+ await this.beat;
360
+ const lease = this.lease;
361
+ const base: WorkLeaseRunResult = {
362
+ item: lease?.item ?? null,
363
+ holder: this.holder,
364
+ token: lease?.token ?? null,
365
+ branch: lease?.branch ?? null,
366
+ released: false,
367
+ outcome: null,
368
+ lost: this.lost ?? null,
369
+ refusal: this.refusal ?? null,
370
+ };
371
+ if (!lease) return base;
372
+ if (lease.worktree) {
373
+ await git(["worktree", "remove", "--force", lease.worktree], this.opts.cwd).catch(() => undefined);
374
+ rmSync(lease.worktree, { recursive: true, force: true });
375
+ await git(["worktree", "prune"], this.opts.cwd).catch(() => undefined);
376
+ }
377
+ if (this.lost !== undefined) return base;
378
+ let outcome = status === "ok" ? "done" : status === "gated" || status === "waiting" ? status : "not_done";
379
+ if (status === "ok" && this.spec.outcome && isStepOutputRef(this.spec.outcome)) {
380
+ const declared = resolvePath(resultsById.get(this.spec.outcome.step), this.spec.outcome.path);
381
+ if (typeof declared === "string" && declared.trim() !== "") outcome = declared.trim();
382
+ }
383
+ const released = await releaseWorkLease(lease.item, this.holder, {
384
+ cwd: this.ledgerCwd,
385
+ token: lease.token,
386
+ outcome,
387
+ note: `op ${this.config.name}`,
388
+ }).catch(() => undefined);
389
+ return { ...base, released: released?.ok === true, outcome: released?.ok ? outcome : null };
390
+ }
391
+ }
392
+
393
+ function pick(lease: WorkLease): Omit<WorkLeaseOutput, "ref" | "worktree" | "branch"> {
394
+ return { item: lease.item, holder: lease.holder, token: lease.token, acquiredAt: lease.acquiredAt, expiresAt: lease.expiresAt };
395
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * A throwaway workspace for the decide tests: a git repository with an answer
3
+ * kind (the reference workspace's), a task kind the points read through the
4
+ * read contract, three points (a noul, a choice and a score), and a box member
5
+ * that declares one brokered capability and one without a broker.
6
+ */
7
+
8
+ import { execFileSync } from "node:child_process";
9
+ import { cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+
13
+ /** The chant checkout the tests run in. */
14
+ export const REPO = realpathSync(join(import.meta.dirname, "..", "..", "..", "..", ".."));
15
+ const REF = join(REPO, "reference-workspace");
16
+
17
+ export const MODEL = "jev-1.13.0";
18
+
19
+ const model = (extra: Record<string, unknown> = {}) => ({ kind: "model", backend: "systemone", model: MODEL, threshold: 0.8, ...extra });
20
+
21
+ export const POINTS = {
22
+ points: {
23
+ triage: {
24
+ title: "Does this task need a person now",
25
+ question: { type: "noul", instructions: "Does the task need a person to look at it today?", criteria: { true: "A person looks at it today.", false: "It can wait." } },
26
+ inputs: { "record.size": "the task's size in points", "record.risky": "whether the task touches production" },
27
+ deciders: [{ kind: "table", rows: [{ when: { "record.size": 0 }, answer: false }] }, model(), { kind: "quorum", count: 1 }],
28
+ },
29
+ route: {
30
+ title: "Which team takes this task",
31
+ question: { type: "choice", instructions: "Pick the team that takes the task.", criteria: { platform: "The platform team.", app: "The app team.", docs: "The docs team." } },
32
+ inputs: { "record.title": "the task's title", "record.size": "the task's size in points" },
33
+ deciders: [model({ unreachable: "fail" }), { kind: "quorum", count: 1 }],
34
+ },
35
+ effort: {
36
+ title: "How much effort this task takes",
37
+ question: { type: "score", instructions: "Rate the effort the task takes.", criteria: ["low", "medium", "high"] },
38
+ inputs: { "record.size": "the task's size in points", "member.kind": "the kind of the member the task is in" },
39
+ deciders: [model(), { kind: "quorum", count: 1 }],
40
+ },
41
+ },
42
+ };
43
+
44
+ const TASK_KIND = `export const recordKind = {
45
+ name: "task",
46
+ location: { dir: ".", match: "^T-[0-9]+\\\\.md$" },
47
+ format: "markdown-front-matter",
48
+ schema: { id: "urn:intentius:chant:test-task:1", path: "task.schema.json" },
49
+ idField: "id",
50
+ stateField: "state",
51
+ states: ["open", "done"],
52
+ closedStates: ["done"],
53
+ };
54
+ `;
55
+
56
+ const TASK_SCHEMA = {
57
+ $schema: "http://json-schema.org/draft-07/schema#",
58
+ $id: "urn:intentius:chant:test-task:1",
59
+ type: "object",
60
+ required: ["id", "title", "state"],
61
+ properties: {
62
+ id: { type: "string" },
63
+ title: { type: "string" },
64
+ state: { enum: ["open", "done"] },
65
+ size: { type: "integer" },
66
+ risky: { type: "boolean" },
67
+ },
68
+ additionalProperties: false,
69
+ };
70
+
71
+ const task = (id: string, title: string, size: number, risky: boolean) =>
72
+ `---\nid: "${id}"\ntitle: "${title}"\nstate: "open"\nsize: ${size}\nrisky: ${risky}\n---\n\n# ${title}\n`;
73
+
74
+ const scratch: string[] = [];
75
+
76
+ export function cleanScratch(): void {
77
+ for (const d of scratch.splice(0)) rmSync(d, { recursive: true, force: true });
78
+ }
79
+
80
+ export function git(cwd: string, ...args: string[]): string {
81
+ return execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
82
+ }
83
+
84
+ /** Make the workspace and commit it. Returns its root. */
85
+ export function workspace(points: unknown = POINTS): string {
86
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-decide-")));
87
+ scratch.push(root);
88
+ const files: Record<string, string> = {
89
+ "chant.workspace.json": JSON.stringify(
90
+ {
91
+ name: "decide-test",
92
+ schema: 1,
93
+ members: [
94
+ { name: "app", dir: "app", kind: "other", because: "a plain Node server" },
95
+ {
96
+ name: "box",
97
+ dir: "box",
98
+ kind: "other",
99
+ because: "the box the steward runs in",
100
+ box: { capabilities: [{ name: "inference", broker: "lobby", scope: ["agent"] }, { name: "raw" }] },
101
+ },
102
+ ],
103
+ records: [{ kind: "answers/answer.kind.mjs" }, { kind: "tasks/task.kind.mjs" }],
104
+ },
105
+ null,
106
+ 2,
107
+ ),
108
+ "app/server.mjs": "export const port = 8080;\n",
109
+ "box/README.md": "The box.\n",
110
+ "decisions/points.json": JSON.stringify(points, null, 2),
111
+ "tasks/task.kind.mjs": TASK_KIND,
112
+ "tasks/task.schema.json": JSON.stringify(TASK_SCHEMA, null, 2),
113
+ "tasks/T-1.md": task("T-1", "Rotate the deploy key", 5, true),
114
+ "tasks/T-2.md": task("T-2", "Fix a typo in the README", 1, false),
115
+ };
116
+ for (const [path, text] of Object.entries(files)) {
117
+ mkdirSync(dirname(join(root, path)), { recursive: true });
118
+ writeFileSync(join(root, path), text);
119
+ }
120
+ cpSync(join(REF, "answers", "answer.kind.mjs"), join(root, "answers", "answer.kind.mjs"));
121
+ cpSync(join(REF, "answers", "answer.schema.json"), join(root, "answers", "answer.schema.json"));
122
+ git(root, "init", "-q");
123
+ git(root, "add", "-A");
124
+ git(root, "commit", "-q", "-m", "one");
125
+ return root;
126
+ }
127
+
128
+ /** The front matter text of a written record. */
129
+ export const recordText = (root: string, path: string): string => readFileSync(join(root, path), "utf-8");