@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,274 @@
1
+ /**
2
+ * Acceptance criteria on work records (#2772), on a workspace built from the
3
+ * reference workspace's decision and work kinds in a throwaway git repository.
4
+ *
5
+ * - W-001 is done, and passing evidence of each criterion's verification
6
+ * meets both of them, the manual one by bob while alice owns the item: met.
7
+ * - W-002 is done, and its only evidence is a unit test for an e2e
8
+ * criterion: unmet.
9
+ * - W-003 is done, and its manual criterion's verdict is by alice, its owner:
10
+ * the verdict does not count, so it is unmet as well.
11
+ * - W-004 is done and lists no criteria: a legacy item, read as before.
12
+ * - W-005 is in progress. A steward's Op attaches evidence to it under its
13
+ * work lease, and is refused on the manual criterion.
14
+ */
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
19
+ import { cleanScratch, contract, git, REPO, repo } from "./__fixtures__/contract-repo";
20
+ import { runDeclarationChecks } from "./checks";
21
+ import { listWorkspaceWithKinds } from "./ls";
22
+ import lsSchema from "./ls.schema.json";
23
+ import { queryRecords } from "./records-cli";
24
+ import recordsSchema from "./records.schema.json";
25
+ import { workspaceStatus } from "./status";
26
+ import statusSchema from "./status.schema.json";
27
+ import { attachWorkEvidence } from "./work-evidence";
28
+ import workEvidenceSchema from "./work-evidence.schema.json";
29
+ import { workEvidence } from "../op/builders";
30
+ import { workEvidence as workEvidenceActivity } from "../op/activities";
31
+ import { ACTIVITY_PROFILES } from "../op/activity-profiles";
32
+ import { OpRunFailure, runOpLocally, type OpRunResult } from "../op/local-executor";
33
+ import type { ActivityFn } from "../op/activity-registry";
34
+ import type { OpConfig } from "../op/types";
35
+ import { declareSteward } from "../op/steward";
36
+ import { claimWorkLease, releaseWorkLease } from "../lifecycle/work-lease";
37
+
38
+ const REF = join(REPO, "reference-workspace");
39
+ const ref = (path: string) => readFileSync(join(REF, path), "utf-8");
40
+
41
+ function work(id: string, fields: Record<string, unknown>): string {
42
+ const data = {
43
+ schema: 1,
44
+ id,
45
+ title: `Work ${id}`,
46
+ state: "done",
47
+ implements: [],
48
+ needs: [],
49
+ constrains: ["member:app"],
50
+ evidence: [],
51
+ owner: "alice",
52
+ opened_on: "2026-09-25",
53
+ closed_on: "2026-09-25",
54
+ source: { kind: "workspace", member: "app" },
55
+ supersedes: [],
56
+ ...fields,
57
+ };
58
+ return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
59
+ }
60
+
61
+ const link = (criterion: string, verification: string, result: string, extra: Record<string, unknown> = {}) => ({
62
+ title: `${criterion} ${verification}`,
63
+ url: `https://example.com/${criterion}`,
64
+ criterion,
65
+ verification,
66
+ result,
67
+ ...extra,
68
+ });
69
+
70
+ const KIND = "work/work.kind.mjs";
71
+ let root: string;
72
+
73
+ beforeAll(() => {
74
+ root = repo({
75
+ "chant.workspace.json": JSON.stringify(
76
+ {
77
+ name: "studio",
78
+ schema: 1,
79
+ members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }],
80
+ records: [{ kind: "decisions/decision.kind.mjs" }, { kind: KIND }],
81
+ },
82
+ null,
83
+ 2,
84
+ ),
85
+ "app/server.mjs": "export const port = 8080;\n",
86
+ "decisions/decision.kind.mjs": ref("decisions/decision.kind.mjs"),
87
+ "decisions/decision.schema.json": ref("decisions/decision.schema.json"),
88
+ "decisions/ref-001-how-the-app-is-deployed.md": ref("decisions/ref-001-how-the-app-is-deployed.md"),
89
+ [KIND]: ref(KIND),
90
+ "work/work.schema.json": ref("work/work.schema.json"),
91
+ "work/W-001-met.md": work("W-001", {
92
+ acceptance: [
93
+ { id: "AC-1", text: "The server answers on its port", verification: "unit" },
94
+ { id: "AC-2", text: "A designer finds the page matches the spec", verification: "manual" },
95
+ ],
96
+ evidence: [link("AC-1", "unit", "fail"), link("AC-1", "unit", "pass"), link("AC-2", "manual", "pass", { by: "bob" })],
97
+ }),
98
+ "work/W-002-unmet.md": work("W-002", {
99
+ acceptance: [{ id: "AC-1", text: "The page loads in a browser", verification: "e2e" }],
100
+ evidence: [link("AC-1", "unit", "pass")],
101
+ }),
102
+ "work/W-003-self.md": work("W-003", {
103
+ acceptance: [{ id: "AC-1", text: "Someone else signs off", verification: "manual" }],
104
+ evidence: [link("AC-1", "manual", "pass", { by: "Alice" })],
105
+ }),
106
+ "work/W-004-legacy.md": work("W-004", { evidence: [{ title: "The review", url: "https://example.com/review" }] }),
107
+ "work/W-005-leased.md": work("W-005", {
108
+ state: "in-progress",
109
+ closed_on: null,
110
+ acceptance: [
111
+ { id: "AC-1", text: "The server's test passes", verification: "unit" },
112
+ { id: "AC-2", text: "A person tries it", verification: "manual" },
113
+ ],
114
+ }),
115
+ });
116
+ git(root, "config", "user.name", "t");
117
+ git(root, "config", "user.email", "t@example.com");
118
+ git(root, "config", "commit.gpgsign", "false");
119
+ git(root, "add", "-A");
120
+ git(root, "commit", "-q", "-m", "the work");
121
+ });
122
+ afterAll(cleanScratch);
123
+
124
+ const records = contract(recordsSchema);
125
+
126
+ async function read() {
127
+ const doc = await queryRecords({ kind: join(root, KIND), cwd: root, workGaps: false });
128
+ records.expectValid(doc);
129
+ if ("error" in doc || !("records" in doc)) throw new Error(JSON.stringify(doc));
130
+ return doc.records;
131
+ }
132
+
133
+ const codes = (r: { warnings: { code: string }[] }) => r.warnings.map((w) => w.code);
134
+
135
+ describe("acceptance criteria on read (#2772)", () => {
136
+ test("met: each criterion has passing evidence of its verification, and the manual verdict is someone else's", async () => {
137
+ const w1 = (await read()).find((r) => r.id === "W-001")!;
138
+ expect(w1.valid).toBe(true);
139
+ expect(w1.acceptance).toEqual({
140
+ met: 2,
141
+ total: 2,
142
+ criteria: [
143
+ { id: "AC-1", verification: "unit", met: true },
144
+ { id: "AC-2", verification: "manual", met: true },
145
+ ],
146
+ });
147
+ expect(codes(w1)).toEqual([]);
148
+ });
149
+
150
+ test("unmet: evidence of another verification does not count, and the done item is warned", async () => {
151
+ const w2 = (await read()).find((r) => r.id === "W-002")!;
152
+ expect(w2.acceptance).toMatchObject({ met: 0, total: 1 });
153
+ expect(codes(w2)).toEqual(["work-acceptance-unmet"]);
154
+ expect(w2.warnings[0].message).toMatch(/AC-1 \(e2e\)/);
155
+ });
156
+
157
+ test("manual by the implementer: the verdict is refused, warned, and the criterion stays unmet", async () => {
158
+ const w3 = (await read()).find((r) => r.id === "W-003")!;
159
+ expect(w3.acceptance).toMatchObject({ met: 0, total: 1 });
160
+ expect(codes(w3)).toEqual(["work-acceptance-self-verified", "work-acceptance-unmet"]);
161
+ });
162
+
163
+ test("a legacy item without criteria is valid, reads acceptance null and gets no acceptance warning", async () => {
164
+ const w4 = (await read()).find((r) => r.id === "W-004")!;
165
+ expect(w4.valid).toBe(true);
166
+ expect(w4.acceptance).toBeNull();
167
+ expect(codes(w4)).toEqual([]);
168
+ });
169
+
170
+ test("an open item with criteria unmet is not warned: only done needs them met", async () => {
171
+ const w5 = (await read()).find((r) => r.id === "W-005")!;
172
+ expect(w5.acceptance).toMatchObject({ met: 0, total: 2 });
173
+ expect(codes(w5)).toEqual([]);
174
+ });
175
+
176
+ test("check fails with WSP117 and the reason code on each done item with a criterion unmet", async () => {
177
+ const report = await runDeclarationChecks(root);
178
+ const wsp117 = report.diagnostics.filter((d) => d.ruleId === "WSP117");
179
+ expect(wsp117.map((d) => [d.file, d.code, d.severity])).toEqual([
180
+ ["work/W-002-unmet.md", "work-acceptance-unmet", "error"],
181
+ ["work/W-003-self.md", "work-acceptance-unmet", "error"],
182
+ ]);
183
+ expect(report.ok).toBe(false);
184
+ });
185
+
186
+ test("ls --json and status --json count each item's criteria, met of total, in the read contract", async () => {
187
+ const ls = await listWorkspaceWithKinds({ cwd: root });
188
+ contract(lsSchema).expectValid(ls);
189
+ if ("error" in ls) throw new Error(ls.error.message);
190
+ const kinds = Object.fromEntries(ls.workspace.records.map((r) => [r.path, r.acceptance]));
191
+ expect(kinds["decisions/decision.kind.mjs"]).toBeNull();
192
+ expect(kinds[KIND]).toEqual([
193
+ { item: "W-001", state: "done", met: 2, total: 2 },
194
+ { item: "W-002", state: "done", met: 0, total: 1 },
195
+ { item: "W-003", state: "done", met: 0, total: 1 },
196
+ { item: "W-005", state: "in-progress", met: 0, total: 2 },
197
+ ]);
198
+
199
+ const status = await workspaceStatus({ cwd: root, env: "dev" });
200
+ contract(statusSchema).expectValid(status);
201
+ if ("error" in status) throw new Error(status.error.message);
202
+ expect(status.acceptance.map((a) => [a.member, a.kind, a.item, `${a.met}/${a.total}`])).toEqual([
203
+ [null, KIND, "W-001", "2/2"],
204
+ [null, KIND, "W-002", "0/1"],
205
+ [null, KIND, "W-003", "0/1"],
206
+ [null, KIND, "W-005", "0/2"],
207
+ ]);
208
+ });
209
+ });
210
+
211
+ describe("a steward Op under workLease attaches evidence to a criterion (#2772)", () => {
212
+ const evidenceDoc = contract(workEvidenceSchema);
213
+
214
+ async function run(config: OpConfig, holder: string): Promise<OpRunResult> {
215
+ const activities = new Map<string, ActivityFn>([["workEvidence", workEvidenceActivity as unknown as ActivityFn]]);
216
+ try {
217
+ return await runOpLocally(config, activities, ACTIVITY_PROFILES, undefined, { cwd: root, ledger: { cwd: root }, work: { holder } });
218
+ } catch (err) {
219
+ if (err instanceof OpRunFailure) return err.result;
220
+ throw err;
221
+ }
222
+ }
223
+
224
+ const op = (step: ReturnType<typeof workEvidence>): OpConfig => ({
225
+ name: "verify",
226
+ overview: "attach the test run to its criterion",
227
+ workLease: { item: "W-005", kind: KIND },
228
+ phases: [{ name: "Verify", steps: [step] }],
229
+ });
230
+
231
+ test("the run holding the lease appends passing evidence, and the criterion is met", async () => {
232
+ const config = op(workEvidence("AC-1", { result: "pass", title: "the server test", url: "https://example.com/ci/1", cwd: root }));
233
+ expect(declareSteward({ name: "box", ops: [config] }).ops).toHaveLength(1);
234
+ const result = await run(config, "box/verify@h1");
235
+ expect(result.status).toBe("ok");
236
+ expect(result.workLease).toMatchObject({ item: "W-005", released: true });
237
+ const w5 = (await read()).find((r) => r.id === "W-005")!;
238
+ expect(w5.acceptance).toMatchObject({ met: 1, total: 2 });
239
+ expect(w5.data!.evidence).toEqual([
240
+ expect.objectContaining({ url: "https://example.com/ci/1", criterion: "AC-1", verification: "unit", result: "pass", by: "box/verify@h1" }),
241
+ ]);
242
+ });
243
+
244
+ test("a manual criterion is refused: the run holding the lease is the implementer", async () => {
245
+ const result = await run(op(workEvidence("AC-2", { result: "pass", title: "tried it", url: "https://example.com/tried", cwd: root })), "box/verify@h2");
246
+ expect(result.status).toBe("fail");
247
+ expect(JSON.stringify(result.records)).toMatch(/work-acceptance-self-verified/);
248
+ expect((await read()).find((r) => r.id === "W-005")!.acceptance).toMatchObject({ met: 1, total: 2 });
249
+ });
250
+
251
+ test("without the live lease, or with another token or criterion, nothing is written", async () => {
252
+ const ask = (over: Partial<Parameters<typeof attachWorkEvidence>[0]>) =>
253
+ attachWorkEvidence({ cwd: root, item: "W-005", holder: "me", token: "t", criterion: "AC-1", result: "pass", title: "x", url: "https://example.com/x", ...over });
254
+ const unleased = await ask({});
255
+ evidenceDoc.expectValid(unleased);
256
+ expect("error" in unleased && unleased.error.code).toBe("lease-not-held");
257
+
258
+ const claimed = await claimWorkLease("W-005", "me", { cwd: join(root, "work") });
259
+ if (!claimed.ok) throw new Error(claimed.message);
260
+ try {
261
+ expect(await ask({ token: "other" })).toMatchObject({ error: { code: "lease-token-mismatch" } });
262
+ expect(await ask({ holder: "you", token: claimed.lease.token })).toMatchObject({ error: { code: "lease-held" } });
263
+ expect(await ask({ token: claimed.lease.token, criterion: "AC-9" })).toMatchObject({ error: { code: "work-criterion-unknown" } });
264
+ const ok = await ask({ token: claimed.lease.token, result: "fail" });
265
+ evidenceDoc.expectValid(ok);
266
+ expect(ok).toMatchObject({ item: "W-005", kind: KIND, path: "work/W-005-leased.md", acceptance: { met: 1, total: 2 } });
267
+ } finally {
268
+ await releaseWorkLease("W-005", "me", { cwd: join(root, "work"), token: claimed.lease.token });
269
+ }
270
+ expect(await attachWorkEvidence({ cwd: root, item: "W-004", holder: "me", token: "t", criterion: "AC-1", result: "pass", title: "x", url: "https://example.com/x" })).toMatchObject({
271
+ error: { code: "work-item-closed" },
272
+ });
273
+ });
274
+ });
@@ -158,7 +158,7 @@ describe("chant workspace work (#2732)", () => {
158
158
  expect(json(closed).error.code).toBe("work-item-closed");
159
159
  const verb = await chant(root, ["workspace", "work", "grab", "W-001", "--holder", "a"]);
160
160
  expect(verb.code).toBe(1);
161
- expect(verb.stderr).toContain("claim, renew or release");
161
+ expect(verb.stderr).toContain("claim, renew, release or history");
162
162
  const noHolder = await chant(root, ["workspace", "work", "claim", "W-001"]);
163
163
  expect(noHolder.code).toBe(1);
164
164
  expect(noHolder.stderr).toContain("--holder");
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * `chant workspace work claim|renew|release <id> --holder <name>` (#2732,
3
- * ws-055): the work lease, who is working on which work item.
3
+ * ws-055): the work lease, who is working on which work item. And `chant
4
+ * workspace work history <id>` (#2785): the item's lease history, each claim
5
+ * and how it ended, a read-contract output (`work-history.schema.json`).
4
6
  *
5
7
  * The lease itself is `../lifecycle/work-lease.ts`, the operator lease under
6
8
  * the key `work/<id>`. This file finds the work item first: in the work kind
@@ -24,6 +26,9 @@ import type { CommandContext } from "../cli/registry";
24
26
  import { parseDuration } from "../op/duration";
25
27
  import {
26
28
  claimWorkLease,
29
+ leaseHistoryPath,
30
+ listWorkLeases,
31
+ readLeaseHistory,
27
32
  releaseWorkLease,
28
33
  renewWorkLease,
29
34
  workLeaseRef,
@@ -32,11 +37,13 @@ import {
32
37
  type WorkLeaseRefusal,
33
38
  type WorkLeaseResult,
34
39
  type LeaseHistoryWrite,
40
+ type LeaseHistoryRecord,
41
+ type WorkLeaseState,
35
42
  } from "../lifecycle/work-lease";
36
43
  import { resolveMemberLedger } from "../lifecycle/member-ledger";
37
- import { WorkspaceReadError } from "./declaration";
44
+ import { readerVersion, WorkspaceReadError } from "./declaration";
38
45
  import { declaredKindFiles, readRecordsFor } from "./records-cli";
39
- import { loadRecordKind, RecordReadError, type LoadedRecordKind } from "./records";
46
+ import { loadRecordKind, RecordReadError, type LoadedRecordKind, type RecordEntry } from "./records";
40
47
  import { gitRoot } from "./record-source";
41
48
  import type { ReasonCode } from "./reason-codes";
42
49
 
@@ -48,7 +55,10 @@ export const WORK_LEASE_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/w
48
55
 
49
56
  export { WORK_LEASE_REFUSALS } from "../lifecycle/work-lease";
50
57
 
51
- const USAGE = "chant workspace work claim|renew|release <id> --holder <name> [--kind <kind file>] [--ttl <seconds|duration>] [--token <token>] [--outcome <text>] [--note <text>] [--json]";
58
+ const USAGE = [
59
+ "chant workspace work claim|renew|release <id> --holder <name> [--kind <kind file>] [--ttl <seconds|duration>] [--token <token>] [--outcome <text>] [--note <text>] [--json]",
60
+ "chant workspace work history <id> [--kind <kind file>] [--json]",
61
+ ].join("\n");
52
62
 
53
63
  /** Exit code when the lease was refused rather than the command failing. */
54
64
  export const EXIT_LEASE_REFUSED = 2;
@@ -94,7 +104,8 @@ export type WorkLeaseDocument = DocHead &
94
104
  | { error: { code: WorkErrorCode; message: string } }
95
105
  );
96
106
 
97
- class WorkError extends Error {
107
+ /** Why a work command could not run, with its code. */
108
+ export class WorkError extends Error {
98
109
  constructor(
99
110
  readonly code: WorkErrorCode,
100
111
  message: string,
@@ -103,8 +114,19 @@ class WorkError extends Error {
103
114
  }
104
115
  }
105
116
 
106
- /** The work kind holding `id`: the one `kind` names, or the one declared work kind with a record `id`. */
107
- async function findWorkItem(id: string, cwd: string, kind: string | undefined): Promise<{ loaded: LoadedRecordKind; state: string | null; root: string }> {
117
+ /** A work item found by id: its kind, its state, its record, and the repository root its path is from. */
118
+ export interface FoundWorkItem {
119
+ loaded: LoadedRecordKind;
120
+ state: string | null;
121
+ record: RecordEntry;
122
+ root: string;
123
+ }
124
+
125
+ /**
126
+ * The work kind holding `id`: the one `kind` names, or the one declared work
127
+ * kind with a record `id`. Throws a {@link WorkError}.
128
+ */
129
+ export async function findWorkItem(id: string, cwd: string, kind: string | undefined): Promise<FoundWorkItem> {
108
130
  let candidates: string[];
109
131
  if (kind !== undefined) {
110
132
  candidates = [kind];
@@ -116,7 +138,7 @@ async function findWorkItem(id: string, cwd: string, kind: string | undefined):
116
138
  throw new WorkError("work-kind-missing", `${err.code}: ${err.describe()}; without --kind, the declaration names the work kind`);
117
139
  }
118
140
  }
119
- const found: { loaded: LoadedRecordKind; state: string | null; root: string }[] = [];
141
+ const found: FoundWorkItem[] = [];
120
142
  let workKinds = 0;
121
143
  for (const file of candidates) {
122
144
  let loaded: LoadedRecordKind;
@@ -140,7 +162,7 @@ async function findWorkItem(id: string, cwd: string, kind: string | undefined):
140
162
  throw err;
141
163
  }
142
164
  const record = read.result.records.find((r) => r.id === id);
143
- if (record) found.push({ loaded, state: record.state, root: read.root });
165
+ if (record) found.push({ loaded, state: record.state, record, root: read.root });
144
166
  }
145
167
  if (workKinds === 0) throw new WorkError("work-kind-missing", "the declaration names no work kind; pass --kind <kind file>");
146
168
  if (found.length === 0) throw new WorkError("work-item-unknown", `no work record has the id ${id}`);
@@ -199,16 +221,177 @@ function strip(l: WorkLease): Omit<WorkLease, "item"> {
199
221
  return { holder: l.holder, token: l.token, acquiredAt: l.acquiredAt, expiresAt: l.expiresAt };
200
222
  }
201
223
 
224
+ // ── work history (#2785) ────────────────────────────────────────────────────
225
+
226
+ /** The version of the `work history` output this chant writes: read contract 1. */
227
+ export const WORK_HISTORY_CONTRACT_VERSION = 1;
228
+
229
+ /** `$id` of the JSON Schema for `work history --json`, shipped beside this file. */
230
+ export const WORK_HISTORY_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json";
231
+
232
+ /** Why `work history` read nothing. Closed. */
233
+ export const WORK_HISTORY_ERROR_CODES = [
234
+ "kind-unreadable",
235
+ "work-kind-missing",
236
+ "work-kind-ambiguous",
237
+ "work-item-unknown",
238
+ "not-a-git-repository",
239
+ ] as const satisfies readonly ReasonCode[];
240
+ export type WorkHistoryErrorCode = (typeof WORK_HISTORY_ERROR_CODES)[number];
241
+
242
+ /**
243
+ * How a claim ended. `released`: its holder, or someone closing out an
244
+ * expired lease, gave it back, with an outcome when one was given. `held`: it
245
+ * is the live lease now. `expired`: it ran out and nobody released it or
246
+ * claimed the item since. `lost`: it ran out unreleased and another claim
247
+ * took the item.
248
+ */
249
+ export type ClaimEnd = "released" | "held" | "expired" | "lost";
250
+
251
+ /** One claim of a work item: one fencing token, from its claim to how it ended. */
252
+ export interface LeaseClaim {
253
+ token: string;
254
+ holder: string;
255
+ acquiredAt: string;
256
+ /** The expiry its last claim or renew set. */
257
+ expiresAt: string;
258
+ renewals: number;
259
+ ended: ClaimEnd;
260
+ /** The release, when it was released. */
261
+ release: { by: string; at: string; outcome: string | null; note: string | null } | null;
262
+ }
263
+
264
+ /** What `work history --json` prints. */
265
+ export type WorkHistoryDocument =
266
+ | {
267
+ $schema: string;
268
+ contract: number;
269
+ chant: string;
270
+ item: string;
271
+ /** The work kind file, from the repository root. */
272
+ kind: string;
273
+ /** The lease ref. */
274
+ ref: string;
275
+ /** The history file on the branch, under the member's prefix when the kind's member has one. */
276
+ ledger: { branch: string; path: string };
277
+ /** The lease ref's record now, live or expired, or null when there is none. Read from local refs, without fetching. */
278
+ lease: (Omit<WorkLease, "item"> & { state: "active" | "expired" }) | null;
279
+ /** Each claim, oldest first. */
280
+ claims: LeaseClaim[];
281
+ /** Every line of the history, oldest first. */
282
+ events: LeaseHistoryRecord[];
283
+ /** Lines of the history that are not lease events, left out. */
284
+ malformed: number;
285
+ summary: { claims: number; released: number; held: number; expired: number; lost: number; outcomes: Record<string, number> };
286
+ }
287
+ | { $schema: string; contract: number; chant: string; error: { code: WorkHistoryErrorCode; message: string } };
288
+
289
+ /** Fold a history into its claims: one per token, in the order they were first seen. */
290
+ export function claimsOf(events: LeaseHistoryRecord[], live: Pick<WorkLeaseState, "token" | "state"> | null): LeaseClaim[] {
291
+ const byToken = new Map<string, LeaseClaim>();
292
+ const order: string[] = [];
293
+ for (const e of events) {
294
+ let c = byToken.get(e.token);
295
+ if (!c) {
296
+ c = { token: e.token, holder: e.holder, acquiredAt: e.acquiredAt, expiresAt: e.expiresAt, renewals: 0, ended: "expired", release: null };
297
+ byToken.set(e.token, c);
298
+ order.push(e.token);
299
+ }
300
+ if (e.event === "renew") c.renewals++;
301
+ if (e.event !== "release") c.expiresAt = e.expiresAt;
302
+ if (e.event === "release") c.release = { by: e.by, at: e.timestamp, outcome: e.outcome ?? null, note: e.note ?? null };
303
+ }
304
+ return order.map((token, i) => {
305
+ const c = byToken.get(token)!;
306
+ if (c.release) c.ended = "released";
307
+ else if (live && live.token === token) c.ended = live.state === "active" ? "held" : "expired";
308
+ else if (i < order.length - 1) c.ended = "lost";
309
+ else c.ended = "expired";
310
+ return c;
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Read work item `id`'s lease history (#2785): every claim, renew and release
316
+ * on `_leases/<id>.jsonl` in the ledger of the member owning the work kind,
317
+ * folded into claims. Read from the local `chant/lifecycle` branch and lease
318
+ * refs; it never fetches, like the other read-contract reads.
319
+ */
320
+ export async function workHistory(req: { id: string; cwd: string; kind?: string }): Promise<WorkHistoryDocument> {
321
+ const head = { $schema: WORK_HISTORY_OUTPUT_SCHEMA_ID, contract: WORK_HISTORY_CONTRACT_VERSION, chant: readerVersion() };
322
+ try {
323
+ if (!WORK_ITEM_ID_PATTERN.test(req.id) || req.id.includes("..")) throw new WorkError("work-item-unknown", `${JSON.stringify(req.id)} is not a work item id`);
324
+ if (!gitRoot(req.cwd)) throw new WorkError("not-a-git-repository", "a work lease's history is on the chant/lifecycle branch, and this directory is not in a git repository");
325
+ const { loaded, root } = await findWorkItem(req.id, req.cwd, req.kind);
326
+ const leaseCwd = dirname(loaded.file);
327
+ const { prefix } = await resolveMemberLedger(leaseCwd);
328
+ const { records: events, malformed } = await readLeaseHistory(req.id, { cwd: leaseCwd });
329
+ const live = (await listWorkLeases({ cwd: leaseCwd, memberPrefix: prefix })).find((l) => l.item === req.id) ?? null;
330
+ const claims = claimsOf(events, live);
331
+ const count = (end: ClaimEnd) => claims.filter((c) => c.ended === end).length;
332
+ const outcomes: Record<string, number> = {};
333
+ for (const c of claims) if (c.release?.outcome) outcomes[c.release.outcome] = (outcomes[c.release.outcome] ?? 0) + 1;
334
+ return {
335
+ ...head,
336
+ item: req.id,
337
+ kind: relative(root, loaded.file).split("\\").join("/"),
338
+ ref: workLeaseRef(req.id, prefix),
339
+ ledger: { branch: "chant/lifecycle", path: leaseHistoryPath(req.id, prefix) },
340
+ lease: live ? { holder: live.holder, token: live.token, acquiredAt: live.acquiredAt, expiresAt: live.expiresAt, state: live.state } : null,
341
+ claims,
342
+ events,
343
+ malformed,
344
+ summary: { claims: claims.length, released: count("released"), held: count("held"), expired: count("expired"), lost: count("lost"), outcomes },
345
+ };
346
+ } catch (err) {
347
+ if (err instanceof WorkError) return { ...head, error: { code: err.code as WorkHistoryErrorCode, message: err.message } };
348
+ throw err;
349
+ }
350
+ }
351
+
352
+ /** The history as lines for a person. */
353
+ export function formatWorkHistory(doc: Extract<WorkHistoryDocument, { claims: unknown }>): string {
354
+ const lines = [`${doc.item}: ${doc.ledger.branch}:${doc.ledger.path}`];
355
+ for (const c of doc.claims) {
356
+ const how = c.release ? `released${c.release.outcome ? ` ${c.release.outcome}` : ""} by ${c.release.by} at ${c.release.at}` : c.ended === "held" ? `held until ${c.expiresAt}` : `${c.ended}, never released (expired ${c.expiresAt})`;
357
+ lines.push(` ${c.acquiredAt} ${c.holder} token ${c.token} ${c.renewals} renewal${c.renewals === 1 ? "" : "s"} ${how}`);
358
+ }
359
+ if (doc.claims.length === 0) lines.push(" never claimed");
360
+ if (doc.malformed > 0) lines.push(` ${doc.malformed} malformed line${doc.malformed === 1 ? "" : "s"} left out`);
361
+ return lines.join("\n");
362
+ }
363
+
364
+ async function runWorkHistory(ctx: CommandContext): Promise<number> {
365
+ const { args } = ctx;
366
+ const id = args.extraPositional2;
367
+ if (!id) {
368
+ console.error(formatError({ message: "history needs a work item id", hint: USAGE }));
369
+ return 1;
370
+ }
371
+ for (const [flag, v] of [["--holder", args.holder], ["--ttl", args.ttl], ["--token", args.token], ["--outcome", args.outcome], ["--note", args.note]] as const) {
372
+ if (v !== undefined) {
373
+ console.error(formatError({ message: `history is a read and takes no ${flag}`, hint: USAGE }));
374
+ return 1;
375
+ }
376
+ }
377
+ const doc = await workHistory({ id, cwd: process.cwd(), kind: args.kind });
378
+ if (args.json) console.log(JSON.stringify(doc, null, 2));
379
+ else if ("error" in doc) console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
380
+ else console.log(formatWorkHistory(doc));
381
+ return "error" in doc ? 1 : 0;
382
+ }
383
+
202
384
  export async function runWorkspaceWork(ctx: CommandContext): Promise<number> {
203
385
  const { args } = ctx;
204
386
  const verb = args.extraPositional;
387
+ if (verb === "history") return runWorkHistory(ctx);
205
388
  const fail = (message: string): number => {
206
389
  if (args.json) console.log(JSON.stringify({ $schema: WORK_LEASE_OUTPUT_SCHEMA_ID, contract: WORK_LEASE_CONTRACT_VERSION, error: { code: "write-usage-invalid", message } }, null, 2));
207
390
  else console.error(formatError({ message, hint: USAGE }));
208
391
  return 1;
209
392
  };
210
393
  if (verb !== "claim" && verb !== "renew" && verb !== "release") {
211
- return fail(verb ? `chant workspace work takes claim, renew or release, not ${verb}` : "chant workspace work needs claim, renew or release");
394
+ return fail(verb ? `chant workspace work takes claim, renew, release or history, not ${verb}` : "chant workspace work needs claim, renew, release or history");
212
395
  }
213
396
  const id = args.extraPositional2;
214
397
  if (!id) return fail(`${verb} needs a work item id`);