@intentius/chant 0.101.0 → 0.102.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 (427) hide show
  1. package/dist/agents/checks.d.ts.map +1 -1
  2. package/dist/attrref.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/nginx.d.ts.map +1 -1
  5. package/dist/audit/prior-art.d.ts +48 -0
  6. package/dist/audit/prior-art.d.ts.map +1 -1
  7. package/dist/behaviour-kinds.d.ts.map +1 -1
  8. package/dist/builder.d.ts.map +1 -1
  9. package/dist/cli/commands/carve-emit.d.ts.map +1 -1
  10. package/dist/cli/commands/check-lexicon.d.ts +16 -0
  11. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  12. package/dist/cli/commands/import.d.ts +8 -0
  13. package/dist/cli/commands/import.d.ts.map +1 -1
  14. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts +19 -0
  16. package/dist/cli/commands/init.d.ts.map +1 -1
  17. package/dist/cli/commands/onboard.d.ts +9 -0
  18. package/dist/cli/commands/onboard.d.ts.map +1 -1
  19. package/dist/cli/commands/pinned-upgrade.d.ts +19 -5
  20. package/dist/cli/commands/pinned-upgrade.d.ts.map +1 -1
  21. package/dist/cli/handlers/dev.d.ts.map +1 -1
  22. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  23. package/dist/cli/handlers/misc.d.ts.map +1 -1
  24. package/dist/cli/handlers/operator.d.ts +9 -0
  25. package/dist/cli/handlers/operator.d.ts.map +1 -1
  26. package/dist/cli/handlers/run.d.ts.map +1 -1
  27. package/dist/cli/lsp/server.d.ts.map +1 -1
  28. package/dist/cli/main.d.ts.map +1 -1
  29. package/dist/cli/mcp/server.d.ts.map +1 -1
  30. package/dist/cli/mcp/tools/build.d.ts.map +1 -1
  31. package/dist/cli/mcp/tools/composites.d.ts.map +1 -1
  32. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  33. package/dist/cli/mcp/tools/import.d.ts.map +1 -1
  34. package/dist/cli/mcp/tools/lint.d.ts.map +1 -1
  35. package/dist/cli/mcp/tools/scaffold.d.ts.map +1 -1
  36. package/dist/cli/mcp/tools/search.d.ts.map +1 -1
  37. package/dist/cli/mcp/workspace-tools.d.ts +1 -1
  38. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +11 -2
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts.map +1 -1
  42. package/dist/codegen/naming.d.ts.map +1 -1
  43. package/dist/codegen/pinned-upgrade.d.ts +67 -3
  44. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  45. package/dist/codegen/registry.d.ts.map +1 -1
  46. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  47. package/dist/components/capability.d.ts.map +1 -1
  48. package/dist/components/driver.d.ts.map +1 -1
  49. package/dist/components/fan-out.d.ts.map +1 -1
  50. package/dist/components/starter-plugin.d.ts.map +1 -1
  51. package/dist/components/verbs/process-runner.d.ts.map +1 -1
  52. package/dist/components/verbs/r2-sync.d.ts.map +1 -1
  53. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  54. package/dist/components/verbs/sbom-generator.d.ts.map +1 -1
  55. package/dist/components/verbs/sign.d.ts.map +1 -1
  56. package/dist/components/verbs/verify.d.ts.map +1 -1
  57. package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
  58. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  59. package/dist/components/verbs/wrangler.d.ts.map +1 -1
  60. package/dist/composite.d.ts.map +1 -1
  61. package/dist/config.d.ts +19 -7
  62. package/dist/config.d.ts.map +1 -1
  63. package/dist/declarable.d.ts +16 -0
  64. package/dist/declarable.d.ts.map +1 -1
  65. package/dist/discovery/cache.d.ts.map +1 -1
  66. package/dist/discovery/fold-import.d.ts +15 -5
  67. package/dist/discovery/fold-import.d.ts.map +1 -1
  68. package/dist/discovery/param-deps.d.ts +16 -0
  69. package/dist/discovery/param-deps.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -1
  71. package/dist/discovery/specifier-resolve.d.ts +37 -0
  72. package/dist/discovery/specifier-resolve.d.ts.map +1 -0
  73. package/dist/errors.d.ts.map +1 -1
  74. package/dist/fold/fold.d.ts +3 -1
  75. package/dist/fold/fold.d.ts.map +1 -1
  76. package/dist/graph-detail.d.ts.map +1 -1
  77. package/dist/graph-ir.d.ts.map +1 -1
  78. package/dist/graph-layout.d.ts.map +1 -1
  79. package/dist/import/base-parser.d.ts.map +1 -1
  80. package/dist/import/embedded.d.ts +7 -0
  81. package/dist/import/embedded.d.ts.map +1 -1
  82. package/dist/import/parser-options.d.ts +14 -0
  83. package/dist/import/parser-options.d.ts.map +1 -0
  84. package/dist/import/parser.d.ts +17 -0
  85. package/dist/import/parser.d.ts.map +1 -1
  86. package/dist/kubectl-context.d.ts.map +1 -1
  87. package/dist/lexicon-output.d.ts.map +1 -1
  88. package/dist/lexicon-schema.d.ts +3 -3
  89. package/dist/lexicon.d.ts +55 -3
  90. package/dist/lexicon.d.ts.map +1 -1
  91. package/dist/lifecycle/assert-live.d.ts.map +1 -1
  92. package/dist/lifecycle/gate-ledger.d.ts +15 -0
  93. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  94. package/dist/lifecycle/git.d.ts.map +1 -1
  95. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  96. package/dist/lifecycle/observe.d.ts.map +1 -1
  97. package/dist/lifecycle/plan-ledger.d.ts.map +1 -1
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/scenario.d.ts.map +1 -1
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/lifecycle/work-lease.d.ts +61 -2
  102. package/dist/lifecycle/work-lease.d.ts.map +1 -1
  103. package/dist/lint/config.d.ts +8 -8
  104. package/dist/lint/engine.d.ts +6 -0
  105. package/dist/lint/engine.d.ts.map +1 -1
  106. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  107. package/dist/op/activities/activity-contracts.d.ts +11 -11
  108. package/dist/op/activities/apply.d.ts +79 -9
  109. package/dist/op/activities/apply.d.ts.map +1 -1
  110. package/dist/op/activities/decide.d.ts.map +1 -1
  111. package/dist/op/activities/index.d.ts +1 -1
  112. package/dist/op/activities/index.d.ts.map +1 -1
  113. package/dist/op/activities/lexicon-upgrade.d.ts +44 -12
  114. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
  115. package/dist/op/activity-contract.d.ts +1 -1
  116. package/dist/op/activity-failure.d.ts.map +1 -1
  117. package/dist/op/activity-profiles.d.ts.map +1 -1
  118. package/dist/op/builders.d.ts.map +1 -1
  119. package/dist/op/composites/apply-op.d.ts +12 -5
  120. package/dist/op/composites/apply-op.d.ts.map +1 -1
  121. package/dist/op/composites/lexicon-upgrade-op.d.ts +22 -6
  122. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -1
  123. package/dist/op/decide-backend.d.ts.map +1 -1
  124. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  125. package/dist/op/emulator-lifecycle.d.ts +21 -2
  126. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  127. package/dist/op/gate-approval.d.ts +5 -1
  128. package/dist/op/gate-approval.d.ts.map +1 -1
  129. package/dist/op/gate-wait.d.ts.map +1 -1
  130. package/dist/op/gate.d.ts +19 -3
  131. package/dist/op/gate.d.ts.map +1 -1
  132. package/dist/op/index.d.ts +2 -0
  133. package/dist/op/index.d.ts.map +1 -1
  134. package/dist/op/local-executor.d.ts +6 -0
  135. package/dist/op/local-executor.d.ts.map +1 -1
  136. package/dist/op/operator.d.ts.map +1 -1
  137. package/dist/op/run-context.d.ts +39 -0
  138. package/dist/op/run-context.d.ts.map +1 -0
  139. package/dist/op/run-live.d.ts.map +1 -1
  140. package/dist/op/runtimes/local.d.ts.map +1 -1
  141. package/dist/op/source-archive.d.ts.map +1 -1
  142. package/dist/op/steward-beside.d.ts.map +1 -1
  143. package/dist/op/steward-points.d.ts.map +1 -1
  144. package/dist/op/steward.d.ts.map +1 -1
  145. package/dist/op/types.d.ts +8 -3
  146. package/dist/op/types.d.ts.map +1 -1
  147. package/dist/op/work-lease-run.d.ts +24 -4
  148. package/dist/op/work-lease-run.d.ts.map +1 -1
  149. package/dist/project-root.d.ts.map +1 -1
  150. package/dist/provenance.d.ts +21 -0
  151. package/dist/provenance.d.ts.map +1 -1
  152. package/dist/pseudo-parameter.d.ts.map +1 -1
  153. package/dist/reconcile.d.ts.map +1 -1
  154. package/dist/secret-materialization.d.ts.map +1 -1
  155. package/dist/terraform/emit-source.d.ts.map +1 -1
  156. package/dist/terraform/parse.d.ts.map +1 -1
  157. package/dist/testing.d.ts.map +1 -1
  158. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  159. package/dist/workspace/__fixtures__/decide-workspace.d.ts +1 -1
  160. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -1
  161. package/dist/workspace/agent-cli.d.ts +5 -0
  162. package/dist/workspace/agent-cli.d.ts.map +1 -1
  163. package/dist/workspace/box-factory.d.ts +73 -0
  164. package/dist/workspace/box-factory.d.ts.map +1 -0
  165. package/dist/workspace/changes.d.ts.map +1 -1
  166. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -1
  167. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  168. package/dist/workspace/checks.d.ts +5 -0
  169. package/dist/workspace/checks.d.ts.map +1 -1
  170. package/dist/workspace/compose-graph.d.ts +10 -0
  171. package/dist/workspace/compose-graph.d.ts.map +1 -1
  172. package/dist/workspace/conformance/index.d.ts +67 -6
  173. package/dist/workspace/conformance/index.d.ts.map +1 -1
  174. package/dist/workspace/conformance/vitest.d.ts +12 -3
  175. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  176. package/dist/workspace/conformance/writer.d.ts +368 -0
  177. package/dist/workspace/conformance/writer.d.ts.map +1 -0
  178. package/dist/workspace/decide.d.ts +1 -1
  179. package/dist/workspace/decide.d.ts.map +1 -1
  180. package/dist/workspace/declaration.d.ts +91 -4
  181. package/dist/workspace/declaration.d.ts.map +1 -1
  182. package/dist/workspace/declaration.schema.json +234 -5
  183. package/dist/workspace/generated-files.d.ts.map +1 -1
  184. package/dist/workspace/graph-cache.d.ts.map +1 -1
  185. package/dist/workspace/graph-cli.d.ts +2 -0
  186. package/dist/workspace/graph-cli.d.ts.map +1 -1
  187. package/dist/workspace/identity.d.ts +111 -0
  188. package/dist/workspace/identity.d.ts.map +1 -0
  189. package/dist/workspace/intent-cli.d.ts +7 -4
  190. package/dist/workspace/intent-cli.d.ts.map +1 -1
  191. package/dist/workspace/intent-record.d.ts +8 -1
  192. package/dist/workspace/intent-record.d.ts.map +1 -1
  193. package/dist/workspace/intent.d.ts +149 -5
  194. package/dist/workspace/intent.d.ts.map +1 -1
  195. package/dist/workspace/kinds.d.ts +18 -4
  196. package/dist/workspace/kinds.d.ts.map +1 -1
  197. package/dist/workspace/lineage-hash-index.d.ts.map +1 -1
  198. package/dist/workspace/lineage-lock.d.ts +2 -2
  199. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  200. package/dist/workspace/links.d.ts.map +1 -1
  201. package/dist/workspace/points.d.ts.map +1 -1
  202. package/dist/workspace/principal-classes.d.ts +114 -0
  203. package/dist/workspace/principal-classes.d.ts.map +1 -0
  204. package/dist/workspace/reason-codes.d.ts +21 -1
  205. package/dist/workspace/reason-codes.d.ts.map +1 -1
  206. package/dist/workspace/record-assets.d.ts.map +1 -1
  207. package/dist/workspace/records-checkout.d.ts +54 -0
  208. package/dist/workspace/records-checkout.d.ts.map +1 -0
  209. package/dist/workspace/records-cli.d.ts +27 -1
  210. package/dist/workspace/records-cli.d.ts.map +1 -1
  211. package/dist/workspace/records-close.d.ts +1 -1
  212. package/dist/workspace/records-close.d.ts.map +1 -1
  213. package/dist/workspace/records-since.d.ts.map +1 -1
  214. package/dist/workspace/records-write.d.ts +5 -4
  215. package/dist/workspace/records-write.d.ts.map +1 -1
  216. package/dist/workspace/records.d.ts +15 -0
  217. package/dist/workspace/records.d.ts.map +1 -1
  218. package/dist/workspace/runs-cli.d.ts +164 -0
  219. package/dist/workspace/runs-cli.d.ts.map +1 -0
  220. package/dist/workspace/runs.d.ts +423 -0
  221. package/dist/workspace/runs.d.ts.map +1 -0
  222. package/dist/workspace/source-block.d.ts +1 -1
  223. package/dist/workspace/source-block.d.ts.map +1 -1
  224. package/dist/workspace/status-gates.d.ts +15 -3
  225. package/dist/workspace/status-gates.d.ts.map +1 -1
  226. package/dist/workspace/status.d.ts +10 -0
  227. package/dist/workspace/status.d.ts.map +1 -1
  228. package/dist/workspace/symbols.d.ts +52 -0
  229. package/dist/workspace/symbols.d.ts.map +1 -0
  230. package/dist/workspace/trailer-joins.d.ts +46 -0
  231. package/dist/workspace/trailer-joins.d.ts.map +1 -0
  232. package/dist/workspace/trailers.d.ts +89 -0
  233. package/dist/workspace/trailers.d.ts.map +1 -0
  234. package/dist/workspace/trust/attestor.d.ts.map +1 -1
  235. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  236. package/dist/workspace/trust/seal.d.ts +33 -0
  237. package/dist/workspace/trust/seal.d.ts.map +1 -1
  238. package/dist/workspace/trust/test-repo.d.ts.map +1 -1
  239. package/dist/workspace/work-cli.d.ts +39 -3
  240. package/dist/workspace/work-cli.d.ts.map +1 -1
  241. package/dist/workspace/work-evidence.d.ts +1 -1
  242. package/dist/workspace/work-evidence.d.ts.map +1 -1
  243. package/dist/workspace/work.d.ts +26 -1
  244. package/dist/workspace/work.d.ts.map +1 -1
  245. package/dist/workspace/workspace-principals.schema.json +60 -0
  246. package/dist/workspace/write-scope.d.ts +79 -19
  247. package/dist/workspace/write-scope.d.ts.map +1 -1
  248. package/dist/yaml.d.ts +21 -39
  249. package/dist/yaml.d.ts.map +1 -1
  250. package/package.json +2 -2
  251. package/src/audit/prior-art.ts +15 -0
  252. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +1 -1
  253. package/src/cli/commands/carve-emit-state.test.ts +4 -0
  254. package/src/cli/commands/carve-emit.ts +5 -19
  255. package/src/cli/commands/check-lexicon.test.ts +62 -1
  256. package/src/cli/commands/check-lexicon.ts +36 -6
  257. package/src/cli/commands/import-live.test.ts +18 -0
  258. package/src/cli/commands/import-parser-options.test.ts +133 -0
  259. package/src/cli/commands/import.ts +21 -3
  260. package/src/cli/commands/init-lexicon/templates/project.ts +1 -1
  261. package/src/cli/commands/init.test.ts +8 -1
  262. package/src/cli/commands/init.ts +23 -8
  263. package/src/cli/commands/lint.test.ts +37 -0
  264. package/src/cli/commands/onboard.test.ts +53 -3
  265. package/src/cli/commands/onboard.ts +52 -4
  266. package/src/cli/commands/pinned-upgrade.ts +73 -33
  267. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +2 -2
  268. package/src/cli/handlers/components.ts +2 -2
  269. package/src/cli/handlers/dev.ts +8 -6
  270. package/src/cli/handlers/lifecycle.test.ts +67 -0
  271. package/src/cli/handlers/lifecycle.ts +6 -5
  272. package/src/cli/handlers/misc.ts +2 -0
  273. package/src/cli/handlers/operator.ts +87 -3
  274. package/src/cli/handlers/run.test.ts +111 -0
  275. package/src/cli/handlers/run.ts +85 -24
  276. package/src/cli/handlers/scenario.ts +2 -2
  277. package/src/cli/main.test.ts +1 -0
  278. package/src/cli/main.ts +65 -11
  279. package/src/cli/mcp/workspace-tools.test.ts +5 -2
  280. package/src/cli/mcp/workspace-tools.ts +29 -2
  281. package/src/cli/registry.ts +11 -2
  282. package/src/codegen/pinned-upgrade-multi.test.ts +157 -0
  283. package/src/codegen/pinned-upgrade-tags.test.ts +112 -0
  284. package/src/codegen/pinned-upgrade.test.ts +104 -0
  285. package/src/codegen/pinned-upgrade.ts +250 -56
  286. package/src/codegen/testdata/postgres-tags.json +242 -0
  287. package/src/config.test.ts +21 -0
  288. package/src/config.ts +15 -0
  289. package/src/declarable.test.ts +21 -0
  290. package/src/declarable.ts +19 -0
  291. package/src/discovery/fold-composite.test.ts +41 -5
  292. package/src/discovery/fold-import.test.ts +129 -0
  293. package/src/discovery/fold-import.ts +643 -131
  294. package/src/discovery/fold-package-composite.test.ts +264 -0
  295. package/src/discovery/fold-specifiers.test.ts +191 -0
  296. package/src/discovery/param-deps.test.ts +66 -1
  297. package/src/discovery/param-deps.ts +45 -0
  298. package/src/discovery/specifier-resolve.ts +344 -0
  299. package/src/fold/fold.test.ts +39 -1
  300. package/src/fold/fold.ts +83 -9
  301. package/src/fold-provenance.test.ts +140 -0
  302. package/src/graph-ir.test.ts +83 -0
  303. package/src/graph-ir.ts +29 -0
  304. package/src/import/embedded.test.ts +20 -0
  305. package/src/import/embedded.ts +13 -0
  306. package/src/import/parser-options.ts +46 -0
  307. package/src/import/parser.ts +20 -0
  308. package/src/lexicon.ts +55 -3
  309. package/src/lifecycle/gate-ledger.ts +16 -0
  310. package/src/lifecycle/observe.test.ts +18 -0
  311. package/src/lifecycle/observe.ts +6 -4
  312. package/src/lifecycle/snapshot.ts +2 -2
  313. package/src/lifecycle/work-lease.test.ts +1 -1
  314. package/src/lifecycle/work-lease.ts +93 -2
  315. package/src/lint/engine.test.ts +55 -0
  316. package/src/lint/engine.ts +30 -4
  317. package/src/lint/output-docs.test.ts +59 -56
  318. package/src/op/activities/apply.test.ts +189 -5
  319. package/src/op/activities/apply.ts +230 -8
  320. package/src/op/activities/index.ts +3 -0
  321. package/src/op/activities/lexicon-upgrade.test.ts +160 -6
  322. package/src/op/activities/lexicon-upgrade.ts +154 -51
  323. package/src/op/composites/apply-op.ts +12 -5
  324. package/src/op/composites/composites.test.ts +30 -3
  325. package/src/op/composites/lexicon-upgrade-op.test.ts +7 -2
  326. package/src/op/composites/lexicon-upgrade-op.ts +29 -8
  327. package/src/op/emulator-lifecycle.test.ts +23 -0
  328. package/src/op/emulator-lifecycle.ts +52 -14
  329. package/src/op/gate-approval.test.ts +74 -0
  330. package/src/op/gate-approval.ts +5 -1
  331. package/src/op/gate.ts +31 -5
  332. package/src/op/index.ts +2 -0
  333. package/src/op/local-executor.test.ts +15 -0
  334. package/src/op/local-executor.ts +40 -12
  335. package/src/op/local-output.ts +1 -1
  336. package/src/op/operator.ts +4 -0
  337. package/src/op/run-context.test.ts +141 -0
  338. package/src/op/run-context.ts +51 -0
  339. package/src/op/runtimes/local.test.ts +14 -0
  340. package/src/op/runtimes/local.ts +2 -0
  341. package/src/op/steward-beside.ts +2 -0
  342. package/src/op/types.ts +8 -3
  343. package/src/op/work-lease-run.test.ts +47 -0
  344. package/src/op/work-lease-run.ts +73 -11
  345. package/src/provenance.ts +35 -0
  346. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  347. package/src/workspace/agent-cli.ts +7 -3
  348. package/src/workspace/agent.schema.json +22 -0
  349. package/src/workspace/box-factory.test.ts +320 -0
  350. package/src/workspace/box-factory.ts +103 -0
  351. package/src/workspace/changes.schema.json +12 -20
  352. package/src/workspace/changes.ts +4 -1
  353. package/src/workspace/checks/boxes.test.ts +2 -0
  354. package/src/workspace/checks.ts +29 -5
  355. package/src/workspace/compose-graph.ts +7 -0
  356. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +27 -0
  357. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +95 -0
  358. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +132 -0
  359. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +48 -0
  360. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +108 -0
  361. package/src/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
  362. package/src/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
  363. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +578 -0
  364. package/src/workspace/conformance/conformance.e2e.test.ts +35 -3
  365. package/src/workspace/conformance/index.mjs +30 -3
  366. package/src/workspace/conformance/index.ts +103 -15
  367. package/src/workspace/conformance/vitest.ts +44 -2
  368. package/src/workspace/conformance/writer-conformance.e2e.test.ts +131 -0
  369. package/src/workspace/conformance/writer.test.ts +156 -0
  370. package/src/workspace/conformance/writer.ts +880 -0
  371. package/src/workspace/decide.ts +7 -1
  372. package/src/workspace/declaration.schema.json +234 -5
  373. package/src/workspace/declaration.ts +174 -10
  374. package/src/workspace/graph-cli.ts +5 -2
  375. package/src/workspace/graph.schema.json +247 -0
  376. package/src/workspace/identity.test.ts +196 -0
  377. package/src/workspace/identity.ts +186 -0
  378. package/src/workspace/intent-cli.ts +64 -10
  379. package/src/workspace/intent-record.schema.json +208 -0
  380. package/src/workspace/intent-record.ts +35 -1
  381. package/src/workspace/intent-why.test.ts +274 -0
  382. package/src/workspace/intent.schema.json +718 -7
  383. package/src/workspace/intent.test.ts +11 -2
  384. package/src/workspace/intent.ts +495 -32
  385. package/src/workspace/kinds.ts +44 -37
  386. package/src/workspace/points-write.schema.json +1 -0
  387. package/src/workspace/principal-classes.test.ts +175 -0
  388. package/src/workspace/principal-classes.ts +307 -0
  389. package/src/workspace/reason-codes.test.ts +11 -3
  390. package/src/workspace/reason-codes.ts +24 -1
  391. package/src/workspace/records-amend.schema.json +2 -0
  392. package/src/workspace/records-checkout.ts +106 -0
  393. package/src/workspace/records-cli.ts +103 -11
  394. package/src/workspace/records-close.schema.json +1 -0
  395. package/src/workspace/records-new.schema.json +2 -0
  396. package/src/workspace/records-review.schema.json +2 -0
  397. package/src/workspace/records-uncommitted.test.ts +171 -0
  398. package/src/workspace/records-write.ts +28 -6
  399. package/src/workspace/records.schema.json +72 -1
  400. package/src/workspace/records.ts +27 -0
  401. package/src/workspace/runs-cli.ts +381 -0
  402. package/src/workspace/runs-write.schema.json +583 -0
  403. package/src/workspace/runs.schema.json +810 -0
  404. package/src/workspace/runs.test.ts +242 -0
  405. package/src/workspace/runs.ts +709 -0
  406. package/src/workspace/status-contract.test.ts +5 -0
  407. package/src/workspace/status-gates.ts +22 -8
  408. package/src/workspace/status.schema.json +127 -2
  409. package/src/workspace/status.ts +21 -1
  410. package/src/workspace/symbols.ts +180 -0
  411. package/src/workspace/trailer-joins.test.ts +171 -0
  412. package/src/workspace/trailer-joins.ts +82 -0
  413. package/src/workspace/trailers.ts +148 -0
  414. package/src/workspace/trust/seal.ts +52 -1
  415. package/src/workspace/work-cli.e2e.test.ts +28 -1
  416. package/src/workspace/work-cli.ts +133 -8
  417. package/src/workspace/work-evidence.schema.json +2 -0
  418. package/src/workspace/work-history.schema.json +73 -1
  419. package/src/workspace/work-history.test.ts +48 -5
  420. package/src/workspace/work-kind-fields.test.ts +154 -0
  421. package/src/workspace/work-lease.schema.json +14 -1
  422. package/src/workspace/work.ts +82 -0
  423. package/src/workspace/workspace-principals.schema.json +60 -0
  424. package/src/workspace/write-scope.test.ts +128 -2
  425. package/src/workspace/write-scope.ts +170 -45
  426. package/src/yaml.test.ts +111 -1
  427. package/src/yaml.ts +141 -549
@@ -0,0 +1,171 @@
1
+ /**
2
+ * chant's commit trailers (#3149): the vocabulary, and the joins `graph
3
+ * --intent` and `graph --intent --record` make through it.
4
+ *
5
+ * - c0 adds the workspace and s-001, decided, which constrains path:app.
6
+ * - c1 adds W-001, open, implementing s-001.
7
+ * - c2 edits app/main.js with Chant-Record: work:W-001, Chant-Run and Chant-Agent.
8
+ * - c3 edits app/main.js with only a Chant-Lease, whose token the lease
9
+ * history of W-001 on chant/lifecycle names.
10
+ * - c4 edits app/main.js with Chant-Record: decision:s-001.
11
+ * - c5 edits app/main.js with nothing to account for it.
12
+ * - c6 edits app/main.js with a Chant-Record no kind has, and the apply trailers.
13
+ */
14
+
15
+ import { readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
18
+ import { writeBlobToPath } from "../lifecycle/git";
19
+ import { cleanScratch, contract, git, REPO, repo, writeFiles } from "./__fixtures__/contract-repo";
20
+ import { intentGraph, type CommitNode, type IntentDocument } from "./intent";
21
+ import { intentRecord, type IntentRecordDocument } from "./intent-record";
22
+ import intentRecordSchema from "./intent-record.schema.json";
23
+ import intentSchema from "./intent.schema.json";
24
+ import { parseFrontMatter } from "./records";
25
+ import { formatChantTrailers, parseRecordRef, readChantTrailers } from "./trailers";
26
+
27
+ const REF = join(REPO, "reference-workspace");
28
+ const BASE = (() => {
29
+ const fm = parseFrontMatter(readFileSync(join(REF, "decisions", "ref-001-how-the-app-is-deployed.md"), "utf-8"));
30
+ if (!fm.ok) throw new Error(fm.message);
31
+ return fm.value;
32
+ })();
33
+
34
+ function decision(id: string, fields: Record<string, unknown>): string {
35
+ const data = { ...BASE, id, title: `Decision ${id}`, state: "decided", supersedes: [], evidence: [], reviews: [], ...fields };
36
+ return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
37
+ }
38
+
39
+ function work(id: string, fields: Record<string, unknown>): string {
40
+ const data = { schema: 1, id, title: `Work ${id}`, state: "open", implements: [], needs: [], constrains: [], evidence: [], opened_on: "2026-10-01", supersedes: [], ...fields };
41
+ return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n\nWhat the work is.\n`;
42
+ }
43
+
44
+ const TOKEN = "3f0c7d2e-lease-token";
45
+ const TIP = "a".repeat(40);
46
+ let root: string;
47
+ const sha: Record<string, string> = {};
48
+ let tick = 0;
49
+
50
+ function commit(message: string[]): string {
51
+ const date = `2026-10-01T12:00:${String(tick++).padStart(2, "0")}Z`;
52
+ process.env.GIT_AUTHOR_DATE = date;
53
+ process.env.GIT_COMMITTER_DATE = date;
54
+ try {
55
+ git(root, "add", "-A");
56
+ git(root, "commit", "-q", ...message.flatMap((m) => ["-m", m]));
57
+ } finally {
58
+ delete process.env.GIT_AUTHOR_DATE;
59
+ delete process.env.GIT_COMMITTER_DATE;
60
+ }
61
+ return git(root, "rev-parse", "HEAD");
62
+ }
63
+
64
+ const KINDS = ["decisions/decision.kind.mjs", "work/work.kind.mjs"];
65
+
66
+ beforeAll(async () => {
67
+ root = repo({
68
+ "chant.workspace.json": JSON.stringify({ name: "trailers", schema: 1, members: [{ name: "app", dir: "app", kind: "other", because: "the app" }] }),
69
+ "decisions/decision.kind.mjs": readFileSync(join(REF, "decisions", "decision.kind.mjs"), "utf-8"),
70
+ "decisions/decision.schema.json": readFileSync(join(REF, "decisions", "decision.schema.json"), "utf-8"),
71
+ "work/work.kind.mjs": readFileSync(join(REF, "work", "work.kind.mjs"), "utf-8"),
72
+ "work/work.schema.json": readFileSync(join(REF, "work", "work.schema.json"), "utf-8"),
73
+ "app/main.js": "export const n = 0;\n",
74
+ "decisions/s-001-app.md": decision("s-001", { constrains: ["path:app"] }),
75
+ });
76
+ sha.c0 = commit(["the workspace, and s-001"]);
77
+ writeFiles(root, { "work/W-001-app.md": work("W-001", { implements: ["s-001"] }) });
78
+ sha.c1 = commit(["W-001"]);
79
+ writeFiles(root, { "app/main.js": "export const n = 1;\n" });
80
+ sha.c2 = commit(["one", formatChantTrailers({ agent: "factory", run: "r-0001", records: ["work:W-001"] }).join("\n")]);
81
+ writeFiles(root, { "app/main.js": "export const n = 2;\n" });
82
+ sha.c3 = commit(["two", `Chant-Lease: ${TOKEN}`]);
83
+ writeFiles(root, { "app/main.js": "export const n = 3;\n" });
84
+ sha.c4 = commit(["three", "Chant-Record: decision:s-001"]);
85
+ writeFiles(root, { "app/main.js": "export const n = 4;\n" });
86
+ sha.c5 = commit(["four"]);
87
+ writeFiles(root, { "app/main.js": "export const n = 5;\n" });
88
+ sha.c6 = commit(["five", formatChantTrailers({ records: [{ kind: "contract", id: "C-001" }], applied: { by: "alice", at: "2026-10-01T12:00:06Z", commit: TIP } }).join("\n")]);
89
+ const line = { version: 1, event: "claim", item: "W-001", holder: "factory", by: "factory", token: TOKEN, acquiredAt: "2026-10-01T12:00:00Z", expiresAt: "2026-10-01T12:10:00Z", timestamp: "2026-10-01T12:00:00Z" };
90
+ await writeBlobToPath("_leases", "W-001.jsonl", JSON.stringify(line), "Work lease claim: W-001", { cwd: root });
91
+ });
92
+ afterAll(cleanScratch);
93
+
94
+ describe("the trailer vocabulary", () => {
95
+ test("formats in a fixed order and reads back what it wrote", () => {
96
+ const lines = formatChantTrailers({ applied: { by: "bob", commit: TIP }, records: ["work:W-1", { kind: "decision", id: "ws-075" }], run: "r-1", lease: "tok", agent: "factory" });
97
+ expect(lines).toEqual(["Chant-Agent: factory", "Chant-Lease: tok", "Chant-Run: r-1", "Chant-Record: work:W-1", "Chant-Record: decision:ws-075", "Chant-Applied-By: bob", `Chant-Applied-Commit: ${TIP}`]);
98
+ const trailers: Record<string, string[]> = {};
99
+ for (const l of lines) (trailers[l.slice(0, l.indexOf(":"))] ??= []).push(l.slice(l.indexOf(":") + 1).trim());
100
+ expect(readChantTrailers(trailers)).toEqual({
101
+ agent: "factory",
102
+ lease: "tok",
103
+ run: "r-1",
104
+ records: [
105
+ { kind: "work", id: "W-1" },
106
+ { kind: "decision", id: "ws-075" },
107
+ ],
108
+ applied: { by: "bob", at: null, commit: TIP },
109
+ });
110
+ });
111
+
112
+ test("refuses a value git would read differently", () => {
113
+ expect(() => formatChantTrailers({ run: "a\nb" })).toThrow(/one line/);
114
+ expect(() => formatChantTrailers({ records: ["W-1"] })).toThrow(/<kind>:<id>/);
115
+ expect(() => formatChantTrailers({ applied: { by: "x", commit: "abc" } })).toThrow(/full commit id/);
116
+ });
117
+
118
+ test("reads keys without case and skips malformed record refs", () => {
119
+ expect(parseRecordRef("Work:W-1")).toBeUndefined();
120
+ const t = readChantTrailers({ "chant-record": ["work:W-1", "nonsense", "work:W-1"], "CHANT-RUN": ["r-9"] });
121
+ expect(t.records).toEqual([{ kind: "work", id: "W-1" }]);
122
+ expect(t.run).toBe("r-9");
123
+ expect(t.applied).toBeNull();
124
+ });
125
+ });
126
+
127
+ const intentContract = contract(intentSchema);
128
+ const recordContract = contract(intentRecordSchema);
129
+
130
+ async function region(): Promise<Exclude<IntentDocument, { error: unknown }>> {
131
+ const { doc } = await intentGraph({ cwd: root, region: "app/main.js", kinds: KINDS.map((k) => join(root, k)) });
132
+ intentContract.expectValid(doc);
133
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
134
+ return doc;
135
+ }
136
+
137
+ describe("graph --intent joins a commit through chant's trailers", () => {
138
+ test("each commit node reports what its trailers name", async () => {
139
+ const doc = await region();
140
+ const commit = (k: string) => doc.nodes.find((n) => n.id === `commit:${sha[k]}`) as CommitNode;
141
+ expect(commit("c2").joins).toEqual({ agent: "factory", lease: null, run: "r-0001", records: [{ kind: "work", id: "W-001", node: "record:work/W-001" }], applied: null });
142
+ expect(commit("c3").joins.lease).toEqual({ token: TOKEN, item: "W-001" });
143
+ expect(commit("c4").joins.records).toEqual([{ kind: "decision", id: "s-001", node: "record:decision/s-001" }]);
144
+ expect(commit("c5").joins).toEqual({ agent: null, lease: null, run: null, records: [], applied: null });
145
+ expect(commit("c6").joins.records).toEqual([{ kind: "contract", id: "C-001", node: null }]);
146
+ expect(commit("c6").joins.applied).toEqual({ by: "alice", at: "2026-10-01T12:00:06Z", commit: TIP });
147
+ });
148
+
149
+ test("a carried record makes the commit the decision's own work", async () => {
150
+ const doc = await region();
151
+ const state = (k: string) => (doc.nodes.find((n) => n.id === `commit:${sha[k]}`) as CommitNode).state;
152
+ expect(state("c2")).toBe("decided");
153
+ expect(state("c3")).toBe("decided");
154
+ expect(state("c4")).toBe("decided");
155
+ expect(state("c5")).toBe("decided-by-window");
156
+ expect(state("c6")).toBe("decided-by-window");
157
+ const carries = doc.edges.filter((e) => e.kind === "carries").map((e) => `${e.from} ${e.to}`);
158
+ // Newest first, as the history lists the commits.
159
+ expect(carries).toEqual([`commit:${sha.c4} record:decision/s-001`, `commit:${sha.c3} record:work/W-001`, `commit:${sha.c2} record:work/W-001`]);
160
+ expect(doc.nodes.some((n) => n.id === "record:work/W-001" && n.kind === "work")).toBe(true);
161
+ });
162
+
163
+ test("graph --intent --record buckets the same commits as own", async () => {
164
+ const { doc } = await intentRecord({ cwd: root, record: "s-001", kinds: KINDS.map((k) => join(root, k)) });
165
+ recordContract.expectValid(doc);
166
+ if ("error" in doc) throw new Error(doc.error.message);
167
+ const bucket = (k: string) => (doc as Exclude<IntentRecordDocument, { error: unknown }>).commits.find((c) => c.sha === sha[k])?.bucket;
168
+ expect([bucket("c2"), bucket("c3"), bucket("c4"), bucket("c5")]).toEqual(["own", "own", "own", "worked"]);
169
+ expect(doc.commits.find((c) => c.sha === sha.c3)?.joins.lease).toEqual({ token: TOKEN, item: "W-001" });
170
+ });
171
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Joining a commit through chant's own trailers (#3149, ws-075), for `graph
3
+ * --intent` and `graph --intent --record`.
4
+ *
5
+ * A plugin's `commitJoins` reads a kind's own trailers (`intent-joins.ts`).
6
+ * These are core's: a `Chant-Record` names a record by kind and id, and a
7
+ * `Chant-Lease` names a work lease by its fencing token, which the lease
8
+ * histories on `chant/lifecycle` turn into the work item it was taken on.
9
+ * Both read the local branch and never fetch.
10
+ */
11
+
12
+ import { execFileSync } from "node:child_process";
13
+ import { parseLeaseHistory } from "../lifecycle/work-lease";
14
+ import { readChantTrailers, type AppliedTrailers, type ChantTrailers } from "./trailers";
15
+
16
+ /** The branch the lease histories are on. */
17
+ const LEDGER_BRANCH = "refs/heads/chant/lifecycle";
18
+
19
+ /** What chant's trailers on one commit join it to, as a commit in `graph --intent` reports it. */
20
+ export interface CommitTrailerJoins {
21
+ /** `Chant-Agent`: the agent session that wrote the commit. */
22
+ agent: string | null;
23
+ /** `Chant-Lease`: the work lease's token, and the work item a lease history on `chant/lifecycle` names for it, or null when none does. */
24
+ lease: { token: string; item: string | null } | null;
25
+ /** `Chant-Run`: the agent run that made the commit. */
26
+ run: string | null;
27
+ /** Each `Chant-Record`, with the id of its node in the graph, or null when no kind read has the record. */
28
+ records: { kind: string; id: string; node: string | null }[];
29
+ /** The `Chant-Applied-*` trailers of a commit that applied a leased branch. */
30
+ applied: AppliedTrailers | null;
31
+ }
32
+
33
+ function git(top: string, args: string[]): string | undefined {
34
+ try {
35
+ return execFileSync("git", args, { cwd: top, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * The work item each lease token names, from the lease histories
43
+ * (`_leases/<item>.jsonl`, any member's) on the local `chant/lifecycle`
44
+ * branch. A token no history names is left out. Never fetches.
45
+ */
46
+ export function leaseItemsByToken(top: string, tokens: string[]): Map<string, string> {
47
+ const out = new Map<string, string>();
48
+ const wanted = [...new Set(tokens.filter((t) => t !== ""))];
49
+ if (wanted.length === 0 || git(top, ["rev-parse", "--verify", "--quiet", LEDGER_BRANCH]) === undefined) return out;
50
+ const listed = git(top, ["grep", "-l", "-F", ...wanted.flatMap((t) => ["-e", t]), LEDGER_BRANCH, "--", ":(glob)**/_leases/*.jsonl"]);
51
+ for (const line of (listed ?? "").split("\n")) {
52
+ const path = line.startsWith(`${LEDGER_BRANCH}:`) ? line.slice(LEDGER_BRANCH.length + 1) : "";
53
+ const m = path.match(/(?:^|\/)_leases\/([^/]+)\.jsonl$/);
54
+ if (!m) continue;
55
+ const text = git(top, ["show", `${LEDGER_BRANCH}:${path}`]);
56
+ for (const r of parseLeaseHistory(text ?? "").records) if (wanted.includes(r.token) && r.item === m[1] && !out.has(r.token)) out.set(r.token, r.item);
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /** Read chant's trailers from each commit, and the work items their lease tokens name. */
62
+ export function readTrailerJoins(top: string, commits: { sha: string; trailers: Record<string, string[]> }[]): Map<string, { trailers: ChantTrailers; joins: CommitTrailerJoins }> {
63
+ const read = commits.map((c) => ({ sha: c.sha, trailers: readChantTrailers(c.trailers) }));
64
+ const items = leaseItemsByToken(
65
+ top,
66
+ read.map((r) => r.trailers.lease).filter((t): t is string => t !== null),
67
+ );
68
+ const out = new Map<string, { trailers: ChantTrailers; joins: CommitTrailerJoins }>();
69
+ for (const { sha, trailers } of read) {
70
+ out.set(sha, {
71
+ trailers,
72
+ joins: {
73
+ agent: trailers.agent,
74
+ lease: trailers.lease === null ? null : { token: trailers.lease, item: items.get(trailers.lease) ?? null },
75
+ run: trailers.run,
76
+ records: trailers.records.map((r) => ({ kind: r.kind, id: r.id, node: null })),
77
+ applied: trailers.applied,
78
+ },
79
+ });
80
+ }
81
+ return out;
82
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * chant's commit trailers (#3149, ws-075): the vocabulary a commit uses to
3
+ * name the lease, the agent run, the records and the apply behind it.
4
+ *
5
+ * A trailer is how a commit points at a fact kept elsewhere: a work lease on
6
+ * `chant/lifecycle`, an agent run in the run ledger (#3033), a record in the
7
+ * working tree. Provenance is keyed to those ids, never to commit SHAs, so a
8
+ * commit that keeps its message through a rebase or a cherry-pick keeps its
9
+ * joins (#3037). chant never writes a commit itself (ws-074): whoever makes
10
+ * the commit adds the trailers, and chant reads them back in `graph
11
+ * --intent`, `graph --intent --record` and `workspace runs`.
12
+ *
13
+ * | Trailer | Value | Names |
14
+ * |---|---|---|
15
+ * | `Chant-Agent` | an agent session | the session that wrote the commit (ws-067) |
16
+ * | `Chant-Lease` | a fencing token | the work lease the commit was made under |
17
+ * | `Chant-Run` | a run id | the agent run that made the commit (#3033) |
18
+ * | `Chant-Record` | `<kind>:<id>` | a record the commit carries out or changes; repeatable |
19
+ * | `Chant-Applied-By` | a principal | who applied a leased branch, on the apply commit |
20
+ * | `Chant-Applied-At` | ISO 8601 | when it was applied |
21
+ * | `Chant-Applied-Commit` | a commit id | the tip of the branch that was applied |
22
+ *
23
+ * Every other trailer is a plugin's, and core gives it no meaning; a kind's
24
+ * `commitJoins` may (`intent-joins.ts`).
25
+ */
26
+
27
+ import { trailerValue, trailerValues } from "./intent-joins";
28
+
29
+ /** The agent session that wrote a commit (ws-067). */
30
+ export const AGENT_TRAILER = "Chant-Agent";
31
+ /** The fencing token of the work lease a commit was made under. */
32
+ export const LEASE_TRAILER = "Chant-Lease";
33
+ /** The agent run that made a commit (#3033). */
34
+ export const RUN_TRAILER = "Chant-Run";
35
+ /** A record a commit carries out or changes, as `<kind>:<id>`. Repeatable. */
36
+ export const RECORD_TRAILER = "Chant-Record";
37
+ /** Who applied a leased branch, on the commit that applied it. */
38
+ export const APPLIED_BY_TRAILER = "Chant-Applied-By";
39
+ /** When the branch was applied, ISO 8601. */
40
+ export const APPLIED_AT_TRAILER = "Chant-Applied-At";
41
+ /** The tip of the branch that was applied. */
42
+ export const APPLIED_COMMIT_TRAILER = "Chant-Applied-Commit";
43
+
44
+ /** Every trailer key core reads, in the order a writer should add them. */
45
+ export const CHANT_TRAILERS = [AGENT_TRAILER, LEASE_TRAILER, RUN_TRAILER, RECORD_TRAILER, APPLIED_BY_TRAILER, APPLIED_AT_TRAILER, APPLIED_COMMIT_TRAILER] as const;
46
+
47
+ /** A record kind's name, as a kind file's `recordKind.name` gives it. */
48
+ const KIND_NAME = /^[a-z][a-z0-9-]*$/;
49
+ /** A record id, as the record kinds allocate them and as a work lease keys them. */
50
+ const RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
51
+ const COMMIT_ID = /^[0-9a-f]{40,64}$/;
52
+
53
+ /** One `Chant-Record` value. */
54
+ export interface RecordRef {
55
+ /** The record kind's name, such as `decision` or `work`. */
56
+ kind: string;
57
+ id: string;
58
+ }
59
+
60
+ /** An apply, from the `Chant-Applied-*` trailers. */
61
+ export interface AppliedTrailers {
62
+ by: string;
63
+ /** ISO 8601, or null when the trailer is missing. */
64
+ at: string | null;
65
+ /** The applied branch's tip, or null when the trailer is missing or is not a commit id. */
66
+ commit: string | null;
67
+ }
68
+
69
+ /** What chant's own trailers on one commit say. */
70
+ export interface ChantTrailers {
71
+ agent: string | null;
72
+ lease: string | null;
73
+ run: string | null;
74
+ /** Each well-formed `Chant-Record` value, in order, without repeats. */
75
+ records: RecordRef[];
76
+ applied: AppliedTrailers | null;
77
+ }
78
+
79
+ /** Parse one `Chant-Record` value, `<kind>:<id>`, or undefined when it is not one. */
80
+ export function parseRecordRef(value: string): RecordRef | undefined {
81
+ const colon = value.indexOf(":");
82
+ if (colon <= 0) return undefined;
83
+ const kind = value.slice(0, colon).trim();
84
+ const id = value.slice(colon + 1).trim();
85
+ return KIND_NAME.test(kind) && RECORD_ID.test(id) ? { kind, id } : undefined;
86
+ }
87
+
88
+ /** Read chant's own trailers from a commit's parsed trailers. Keys compare without case, as git's do. */
89
+ export function readChantTrailers(trailers: Record<string, string[]>): ChantTrailers {
90
+ const records: RecordRef[] = [];
91
+ for (const v of trailerValues(trailers, RECORD_TRAILER)) {
92
+ const ref = parseRecordRef(v);
93
+ if (ref && !records.some((r) => r.kind === ref.kind && r.id === ref.id)) records.push(ref);
94
+ }
95
+ const by = trailerValue(trailers, APPLIED_BY_TRAILER) ?? null;
96
+ const commit = trailerValue(trailers, APPLIED_COMMIT_TRAILER) ?? null;
97
+ return {
98
+ agent: trailerValue(trailers, AGENT_TRAILER) ?? null,
99
+ lease: trailerValue(trailers, LEASE_TRAILER) ?? null,
100
+ run: trailerValue(trailers, RUN_TRAILER) ?? null,
101
+ records,
102
+ applied: by === null ? null : { by, at: trailerValue(trailers, APPLIED_AT_TRAILER) ?? null, commit: commit !== null && COMMIT_ID.test(commit) ? commit : null },
103
+ };
104
+ }
105
+
106
+ /** Whether a commit carries any of chant's own trailers. */
107
+ export function hasChantTrailers(t: ChantTrailers): boolean {
108
+ return t.agent !== null || t.lease !== null || t.run !== null || t.records.length > 0 || t.applied !== null;
109
+ }
110
+
111
+ /** What a writer passes to {@link formatChantTrailers}. */
112
+ export interface ChantTrailerInput {
113
+ agent?: string;
114
+ lease?: string;
115
+ run?: string;
116
+ records?: (RecordRef | string)[];
117
+ applied?: { by: string; at?: string; commit?: string };
118
+ }
119
+
120
+ /**
121
+ * The trailer lines for a commit message, in {@link CHANT_TRAILERS} order.
122
+ * Throws when a value holds a line break or a record ref is malformed, since
123
+ * either would write a trailer git parses differently.
124
+ */
125
+ export function formatChantTrailers(input: ChantTrailerInput): string[] {
126
+ const lines: string[] = [];
127
+ const add = (key: string, value: string | undefined) => {
128
+ if (value === undefined) return;
129
+ const v = value.trim();
130
+ if (v === "" || /[\r\n]/.test(v)) throw new Error(`${key} takes one line of text, not ${JSON.stringify(value)}`);
131
+ lines.push(`${key}: ${v}`);
132
+ };
133
+ add(AGENT_TRAILER, input.agent);
134
+ add(LEASE_TRAILER, input.lease);
135
+ add(RUN_TRAILER, input.run);
136
+ for (const r of input.records ?? []) {
137
+ const ref = typeof r === "string" ? parseRecordRef(r) : r;
138
+ if (!ref || !KIND_NAME.test(ref.kind) || !RECORD_ID.test(ref.id)) throw new Error(`${RECORD_TRAILER} takes <kind>:<id>, not ${JSON.stringify(r)}`);
139
+ add(RECORD_TRAILER, `${ref.kind}:${ref.id}`);
140
+ }
141
+ if (input.applied) {
142
+ add(APPLIED_BY_TRAILER, input.applied.by);
143
+ add(APPLIED_AT_TRAILER, input.applied.at);
144
+ if (input.applied.commit !== undefined && !COMMIT_ID.test(input.applied.commit)) throw new Error(`${APPLIED_COMMIT_TRAILER} takes a full commit id, not ${JSON.stringify(input.applied.commit)}`);
145
+ add(APPLIED_COMMIT_TRAILER, input.applied.commit);
146
+ }
147
+ return lines;
148
+ }
@@ -36,6 +36,12 @@ export const REVIEW_SEAL_NAMESPACE = "chant-review";
36
36
  /** The ssh signature namespace of a record's author seal (#2688). */
37
37
  export const RECORD_SEAL_NAMESPACE = "chant-record";
38
38
 
39
+ /**
40
+ * The ssh signature namespace of a gate approval's seal (#3163, ws-080), so a
41
+ * verdict, record or commit signature can't stand in for one.
42
+ */
43
+ export const GATE_SEAL_NAMESPACE = "chant-gate";
44
+
39
45
  /** A seal as a review entry holds it. */
40
46
  export interface VerdictSeal {
41
47
  /** The principal who signed: the reviewer, as the signers file names them. */
@@ -81,6 +87,27 @@ export function recordSealPayload(record: string, digest: string, author: string
81
87
  return Buffer.from(`${record}\n${digest}\n${author}\n${state ?? ""}`, "utf-8");
82
88
  }
83
89
 
90
+ /** The gate approval a seal covers (#3163): the fields of a gate resolution line that say what was approved, by whom and when. */
91
+ export interface SealedGateApproval {
92
+ op: string;
93
+ gate: string;
94
+ environment?: string;
95
+ planDigest?: string;
96
+ resolvedBy: string;
97
+ timestamp: string;
98
+ seal?: unknown;
99
+ }
100
+
101
+ /**
102
+ * The bytes a gate approval's seal signs (#3163): the op (or component), the
103
+ * gate, the environment, the plan digest, the approver and the time, joined
104
+ * by LF with no final newline. An approval that binds no environment or plan
105
+ * signs an empty line for it.
106
+ */
107
+ export function gateSealPayload(a: SealedGateApproval): Buffer {
108
+ return Buffer.from(`${a.op}\n${a.gate}\n${a.environment ?? ""}\n${a.planDigest ?? ""}\n${a.resolvedBy}\n${a.timestamp}`, "utf-8");
109
+ }
110
+
84
111
  /** The record an author seal covers (#2688). */
85
112
  export interface SealedRecord {
86
113
  record: string | null;
@@ -155,6 +182,22 @@ export function checkRecordSeal(policy: TrustPolicy, r: SealedRecord): SealCheck
155
182
  });
156
183
  }
157
184
 
185
+ /**
186
+ * Check a gate approval's seal (#3163), as a verdict's is checked, over
187
+ * {@link gateSealPayload} in the `chant-gate` namespace: it verifies only
188
+ * when the signers file at base lists a key for the approver.
189
+ */
190
+ export function checkGateSeal(policy: TrustPolicy, a: SealedGateApproval): SealCheck {
191
+ return checkSeal(policy, {
192
+ principal: a.resolvedBy,
193
+ what: `the approval of gate ${a.gate} by ${a.resolvedBy}`,
194
+ role: "approver",
195
+ namespace: GATE_SEAL_NAMESPACE,
196
+ seal: a.seal,
197
+ payload: gateSealPayload(a),
198
+ });
199
+ }
200
+
158
201
  function checkSeal(policy: TrustPolicy, s: SealSubject): SealCheck {
159
202
  const where = `${policy.signersPath} at base`;
160
203
  if (s.seal === undefined || s.seal === null) {
@@ -205,7 +248,7 @@ function checkIntegrity(principal: string, payload: Buffer, signature: string, n
205
248
  const r = sshKeygen(["-Y", "check-novalidate", "-n", namespace, "-s", sigFile], payload);
206
249
  if (r.missing) return { attested: null, code: "seal-unverifiable", message: `the seal by ${principal} can't be checked here: ssh-keygen is not installed` };
207
250
  if (r.status !== 0) {
208
- return { attested: false, code: "seal-signature-invalid", message: `the seal by ${principal} does not verify over ${namespace === RECORD_SEAL_NAMESPACE ? "this record" : "this verdict"}, even without a signers file` };
251
+ return { attested: false, code: "seal-signature-invalid", message: `the seal by ${principal} does not verify over ${namespace === RECORD_SEAL_NAMESPACE ? "this record" : namespace === GATE_SEAL_NAMESPACE ? "this approval" : "this verdict"}, even without a signers file` };
209
252
  }
210
253
  const key = FINGERPRINT.exec(r.stdout + r.stderr)?.[1];
211
254
  return {
@@ -279,6 +322,14 @@ export function sealRecord(keyFile: string, r: { record: string; digest: string;
279
322
  return sign(keyFile, recordSealPayload(r.record, r.digest, r.author, r.state), RECORD_SEAL_NAMESPACE, r.author);
280
323
  }
281
324
 
325
+ /**
326
+ * Seal a gate approval (#3163) with the key in `keyFile`, over
327
+ * {@link gateSealPayload}. Throws a {@link SealError}.
328
+ */
329
+ export function sealGateApproval(keyFile: string, a: SealedGateApproval): VerdictSeal {
330
+ return sign(keyFile, gateSealPayload(a), GATE_SEAL_NAMESPACE, a.resolvedBy);
331
+ }
332
+
282
333
  function sign(keyFile: string, payload: Buffer, namespace: string, signer: string): VerdictSeal {
283
334
  const signed = sshKeygen(["-q", "-Y", "sign", "-n", namespace, "-f", keyFile], payload);
284
335
  if (signed.missing) throw new SealError("--sign needs ssh-keygen, and it is not installed here");
@@ -158,12 +158,39 @@ 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, release or history");
161
+ expect(verb.stderr).toContain("claim, renew, release, history or evidence");
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");
165
165
  }, 120_000);
166
166
 
167
+ test("evidence (#3159): needs the holder, the token and the entry, runs only under the live lease, and names only a criterion the item lists", async () => {
168
+ const entry = JSON.stringify({ criterion: "AC-1", result: "pass", title: "The test", url: "https://example.com/ci/1" });
169
+ const usage = await chant(root, ["workspace", "work", "evidence", "W-002", "--holder", "a", "--from", "-"]);
170
+ expect(usage.code).toBe(1);
171
+ expect(JSON.parse(usage.stdout).error).toMatchObject({ code: "write-usage-invalid", message: expect.stringContaining("--token") });
172
+ const evidence = (holder: string, token: string) =>
173
+ new Promise<Run>((resolve) => {
174
+ const child = spawn(CHANT[0], [...CHANT.slice(1), "workspace", "work", "evidence", "W-002", "--holder", holder, "--token", token, "--from", "-"], { cwd: root, env: { ...process.env, NO_COLOR: "1" } });
175
+ let stdout = "";
176
+ let stderr = "";
177
+ child.stdout.on("data", (d) => (stdout += d));
178
+ child.stderr.on("data", (d) => (stderr += d));
179
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
180
+ child.stdin.end(entry);
181
+ });
182
+ // worker-d holds W-002 since the records test above: anyone else is refused.
183
+ const held = json(await chant(root, ["workspace", "work", "claim", "W-002", "--holder", "ev", "--json"])).refused.heldBy as { holder: string; token: string };
184
+ const other = await evidence("ev", "none");
185
+ expect(other.code).toBe(1);
186
+ expect(JSON.parse(other.stdout).error.code).toBe("lease-held");
187
+ const stale = await evidence(held.holder, "not-the-token");
188
+ expect(JSON.parse(stale.stdout).error.code).toBe("lease-token-mismatch");
189
+ const unlisted = await evidence(held.holder, held.token);
190
+ expect(unlisted.code).toBe(1);
191
+ expect(JSON.parse(unlisted.stdout).error.code).toBe("work-criterion-unknown");
192
+ }, 120_000);
193
+
167
194
  test("the lease fields reach an MCP client through workspace-records and workspace-status", async () => {
168
195
  const claim = await chant(root, ["workspace", "work", "claim", "W-001", "--holder", "mcp-worker", "--json"]);
169
196
  expect(claim.code, claim.stderr).toBe(0);