@intentius/chant 0.102.0 → 0.104.1

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 (471) hide show
  1. package/dist/audit/core.d.ts +2 -0
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/change-set.d.ts +232 -0
  5. package/dist/change-set.d.ts.map +1 -0
  6. package/dist/cli/commands/audit.d.ts +7 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/build.d.ts.map +1 -1
  10. package/dist/cli/handlers/change-set.d.ts +11 -0
  11. package/dist/cli/handlers/change-set.d.ts.map +1 -0
  12. package/dist/cli/handlers/fan-out.d.ts +33 -0
  13. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  14. package/dist/cli/handlers/operator.d.ts +7 -0
  15. package/dist/cli/handlers/operator.d.ts.map +1 -1
  16. package/dist/cli/handlers/pr.d.ts +31 -0
  17. package/dist/cli/handlers/pr.d.ts.map +1 -0
  18. package/dist/cli/handlers/run.d.ts.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  21. package/dist/cli/mcp/workspace-tools.d.ts +3 -2
  22. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  23. package/dist/cli/registry.d.ts +63 -1
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/components/auto-release.d.ts +16 -0
  26. package/dist/components/auto-release.d.ts.map +1 -1
  27. package/dist/components/capability.d.ts +53 -0
  28. package/dist/components/capability.d.ts.map +1 -1
  29. package/dist/components/changed-files.d.ts +31 -0
  30. package/dist/components/changed-files.d.ts.map +1 -0
  31. package/dist/components/cli-support.d.ts +9 -1
  32. package/dist/components/cli-support.d.ts.map +1 -1
  33. package/dist/components/component.d.ts +24 -0
  34. package/dist/components/component.d.ts.map +1 -1
  35. package/dist/components/deploy-units.d.ts.map +1 -1
  36. package/dist/components/driver.d.ts +13 -0
  37. package/dist/components/driver.d.ts.map +1 -1
  38. package/dist/components/fan-out-output.d.ts +6 -0
  39. package/dist/components/fan-out-output.d.ts.map +1 -1
  40. package/dist/components/fan-out-record.d.ts +41 -0
  41. package/dist/components/fan-out-record.d.ts.map +1 -0
  42. package/dist/components/fan-out-run.d.ts +65 -0
  43. package/dist/components/fan-out-run.d.ts.map +1 -1
  44. package/dist/components/fan-out-support.d.ts +2 -0
  45. package/dist/components/fan-out-support.d.ts.map +1 -1
  46. package/dist/components/fan-out.d.ts +6 -0
  47. package/dist/components/fan-out.d.ts.map +1 -1
  48. package/dist/components/gated-wave-pipeline.d.ts +50 -0
  49. package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/layers.d.ts +25 -0
  53. package/dist/components/layers.d.ts.map +1 -0
  54. package/dist/components/pr-pipeline.d.ts +61 -0
  55. package/dist/components/pr-pipeline.d.ts.map +1 -0
  56. package/dist/components/pr-run.d.ts +119 -0
  57. package/dist/components/pr-run.d.ts.map +1 -0
  58. package/dist/components/promote.d.ts.map +1 -1
  59. package/dist/components/verbs/shell.d.ts.map +1 -1
  60. package/dist/components/wave-plan.d.ts +49 -0
  61. package/dist/components/wave-plan.d.ts.map +1 -0
  62. package/dist/gated-waves.d.ts +147 -0
  63. package/dist/gated-waves.d.ts.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/lexicon.d.ts +48 -0
  67. package/dist/lexicon.d.ts.map +1 -1
  68. package/dist/lifecycle/gate-ledger.d.ts +7 -0
  69. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  70. package/dist/lifecycle/git.d.ts +46 -1
  71. package/dist/lifecycle/git.d.ts.map +1 -1
  72. package/dist/lint/post-synth.d.ts +18 -0
  73. package/dist/lint/post-synth.d.ts.map +1 -1
  74. package/dist/op/activities/activity-contracts.d.ts +126 -0
  75. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  76. package/dist/op/activities/change-set.d.ts +62 -0
  77. package/dist/op/activities/change-set.d.ts.map +1 -0
  78. package/dist/op/activities/decide.d.ts +2 -0
  79. package/dist/op/activities/decide.d.ts.map +1 -1
  80. package/dist/op/activities/factory.d.ts +137 -0
  81. package/dist/op/activities/factory.d.ts.map +1 -0
  82. package/dist/op/activities/index.d.ts +4 -0
  83. package/dist/op/activities/index.d.ts.map +1 -1
  84. package/dist/op/activity-contract.d.ts.map +1 -1
  85. package/dist/op/builders.d.ts +11 -0
  86. package/dist/op/builders.d.ts.map +1 -1
  87. package/dist/op/decide-backend.d.ts +5 -1
  88. package/dist/op/decide-backend.d.ts.map +1 -1
  89. package/dist/op/decide-read-inputs.d.ts +4 -2
  90. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  91. package/dist/op/factory.d.ts +83 -0
  92. package/dist/op/factory.d.ts.map +1 -0
  93. package/dist/op/index.d.ts +4 -1
  94. package/dist/op/index.d.ts.map +1 -1
  95. package/dist/op/op-ir.d.ts +2 -0
  96. package/dist/op/op-ir.d.ts.map +1 -1
  97. package/dist/op/op-verb-class.d.ts.map +1 -1
  98. package/dist/op/types.d.ts +8 -0
  99. package/dist/op/types.d.ts.map +1 -1
  100. package/dist/op/work-lease-run.d.ts.map +1 -1
  101. package/dist/plan-summary.d.ts +207 -0
  102. package/dist/plan-summary.d.ts.map +1 -0
  103. package/dist/pr-forge.d.ts +105 -0
  104. package/dist/pr-forge.d.ts.map +1 -0
  105. package/dist/pr-loop.d.ts +178 -0
  106. package/dist/pr-loop.d.ts.map +1 -0
  107. package/dist/telemetry-attribution.d.ts +31 -0
  108. package/dist/telemetry-attribution.d.ts.map +1 -1
  109. package/dist/terraform/hcl2json.d.ts +45 -0
  110. package/dist/terraform/hcl2json.d.ts.map +1 -0
  111. package/dist/terraform/parse.d.ts +4 -40
  112. package/dist/terraform/parse.d.ts.map +1 -1
  113. package/dist/vendor/js-yaml-load.d.ts +56 -0
  114. package/dist/vendor/js-yaml-load.d.ts.map +1 -0
  115. package/dist/workspace/box-cli.d.ts +12 -0
  116. package/dist/workspace/box-cli.d.ts.map +1 -0
  117. package/dist/workspace/box-factory.d.ts +27 -3
  118. package/dist/workspace/box-factory.d.ts.map +1 -1
  119. package/dist/workspace/box-listing.d.ts +85 -0
  120. package/dist/workspace/box-listing.d.ts.map +1 -0
  121. package/dist/workspace/box-publish.d.ts +147 -0
  122. package/dist/workspace/box-publish.d.ts.map +1 -0
  123. package/dist/workspace/broker-protocol.d.ts +190 -0
  124. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  125. package/dist/workspace/checks.d.ts.map +1 -1
  126. package/dist/workspace/composites.d.ts +2 -2
  127. package/dist/workspace/composites.d.ts.map +1 -1
  128. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  129. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/broker.d.ts +202 -0
  131. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  132. package/dist/workspace/conformance/index.d.ts +2 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/conformance/vitest.d.ts +9 -2
  135. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  136. package/dist/workspace/conformance/writer.d.ts +108 -14
  137. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  138. package/dist/workspace/decide.d.ts +126 -12
  139. package/dist/workspace/decide.d.ts.map +1 -1
  140. package/dist/workspace/decision-points.schema.json +11 -4
  141. package/dist/workspace/declaration.d.ts +65 -0
  142. package/dist/workspace/declaration.d.ts.map +1 -1
  143. package/dist/workspace/declaration.schema.json +153 -4
  144. package/dist/workspace/environments.d.ts +36 -10
  145. package/dist/workspace/environments.d.ts.map +1 -1
  146. package/dist/workspace/factory-rules.d.ts +180 -0
  147. package/dist/workspace/factory-rules.d.ts.map +1 -0
  148. package/dist/workspace/init.d.ts +21 -0
  149. package/dist/workspace/init.d.ts.map +1 -1
  150. package/dist/workspace/intent-cli.d.ts +1 -1
  151. package/dist/workspace/intent-cli.d.ts.map +1 -1
  152. package/dist/workspace/intent-record.d.ts +28 -1
  153. package/dist/workspace/intent-record.d.ts.map +1 -1
  154. package/dist/workspace/intent.d.ts +102 -5
  155. package/dist/workspace/intent.d.ts.map +1 -1
  156. package/dist/workspace/json-edit.d.ts +24 -0
  157. package/dist/workspace/json-edit.d.ts.map +1 -0
  158. package/dist/workspace/jsonc.d.ts +11 -0
  159. package/dist/workspace/jsonc.d.ts.map +1 -1
  160. package/dist/workspace/kinds.d.ts +48 -0
  161. package/dist/workspace/kinds.d.ts.map +1 -1
  162. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  163. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  164. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  165. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  166. package/dist/workspace/point-answer.schema.json +42 -2
  167. package/dist/workspace/points-cli.d.ts +10 -3
  168. package/dist/workspace/points-cli.d.ts.map +1 -1
  169. package/dist/workspace/points.d.ts +66 -5
  170. package/dist/workspace/points.d.ts.map +1 -1
  171. package/dist/workspace/reason-codes.d.ts +33 -4
  172. package/dist/workspace/reason-codes.d.ts.map +1 -1
  173. package/dist/workspace/record-sessions.d.ts +5 -7
  174. package/dist/workspace/record-sessions.d.ts.map +1 -1
  175. package/dist/workspace/records-cli.d.ts +7 -0
  176. package/dist/workspace/records-cli.d.ts.map +1 -1
  177. package/dist/workspace/records-close.d.ts +7 -3
  178. package/dist/workspace/records-close.d.ts.map +1 -1
  179. package/dist/workspace/records-write.d.ts +66 -6
  180. package/dist/workspace/records-write.d.ts.map +1 -1
  181. package/dist/workspace/records.d.ts +28 -9
  182. package/dist/workspace/records.d.ts.map +1 -1
  183. package/dist/workspace/runs-cli.d.ts +138 -6
  184. package/dist/workspace/runs-cli.d.ts.map +1 -1
  185. package/dist/workspace/runs.d.ts +132 -11
  186. package/dist/workspace/runs.d.ts.map +1 -1
  187. package/dist/workspace/session-kinds.d.ts +1 -1
  188. package/dist/workspace/session-kinds.d.ts.map +1 -1
  189. package/dist/workspace/squash.d.ts +80 -0
  190. package/dist/workspace/squash.d.ts.map +1 -0
  191. package/dist/workspace/status-gates.d.ts +2 -0
  192. package/dist/workspace/status-gates.d.ts.map +1 -1
  193. package/dist/workspace/status.d.ts +54 -0
  194. package/dist/workspace/status.d.ts.map +1 -1
  195. package/dist/workspace/symbols.d.ts +16 -5
  196. package/dist/workspace/symbols.d.ts.map +1 -1
  197. package/dist/workspace/trust/run-commits.d.ts +46 -0
  198. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  199. package/dist/workspace/trust/run-statement.d.ts +128 -0
  200. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  201. package/dist/workspace/trust/seal.d.ts +6 -1
  202. package/dist/workspace/trust/seal.d.ts.map +1 -1
  203. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  204. package/dist/workspace/trust/verify.d.ts +16 -2
  205. package/dist/workspace/trust/verify.d.ts.map +1 -1
  206. package/dist/workspace/wip-cli.d.ts +21 -0
  207. package/dist/workspace/wip-cli.d.ts.map +1 -0
  208. package/dist/workspace/wip.d.ts +268 -0
  209. package/dist/workspace/wip.d.ts.map +1 -0
  210. package/dist/workspace/work-cli.d.ts.map +1 -1
  211. package/dist/workspace/work-evidence.d.ts +1 -1
  212. package/dist/workspace/work-evidence.d.ts.map +1 -1
  213. package/dist/workspace/work-size.d.ts +55 -0
  214. package/dist/workspace/work-size.d.ts.map +1 -0
  215. package/dist/workspace/workspace-kinds.schema.json +122 -0
  216. package/dist/workspace/write-lock-cli.d.ts +60 -0
  217. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  218. package/dist/workspace/write-lock.d.ts +152 -0
  219. package/dist/workspace/write-lock.d.ts.map +1 -0
  220. package/dist/workspace/write-scope.d.ts +6 -2
  221. package/dist/workspace/write-scope.d.ts.map +1 -1
  222. package/dist/yaml.d.ts +1 -1
  223. package/dist/yaml.d.ts.map +1 -1
  224. package/package.json +6 -1
  225. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  226. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  227. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  228. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  229. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  230. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  231. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  232. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  233. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  234. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  235. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  236. package/src/audit/core.test.ts +15 -0
  237. package/src/audit/core.ts +8 -2
  238. package/src/audit/discover.test.ts +11 -0
  239. package/src/audit/discover.ts +7 -2
  240. package/src/change-set-bundle.test.ts +57 -0
  241. package/src/change-set.test.ts +215 -0
  242. package/src/change-set.ts +445 -0
  243. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  244. package/src/cli/commands/audit.ts +29 -2
  245. package/src/cli/commands/build.test.ts +19 -0
  246. package/src/cli/commands/build.ts +3 -2
  247. package/src/cli/handlers/build.ts +13 -1
  248. package/src/cli/handlers/change-set.test.ts +56 -0
  249. package/src/cli/handlers/change-set.ts +44 -0
  250. package/src/cli/handlers/fan-out.test.ts +221 -2
  251. package/src/cli/handlers/fan-out.ts +165 -75
  252. package/src/cli/handlers/graph.ts +3 -0
  253. package/src/cli/handlers/operator.test.ts +21 -0
  254. package/src/cli/handlers/operator.ts +34 -3
  255. package/src/cli/handlers/pr.ts +373 -0
  256. package/src/cli/handlers/run.ts +1 -0
  257. package/src/cli/main.test.ts +20 -0
  258. package/src/cli/main.ts +185 -23
  259. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  260. package/src/cli/mcp/op-tools.ts +24 -2
  261. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  262. package/src/cli/mcp/workspace-tools.ts +34 -6
  263. package/src/cli/registry.ts +63 -1
  264. package/src/components/auto-release.ts +1 -1
  265. package/src/components/capability.ts +55 -0
  266. package/src/components/changed-files.ts +66 -0
  267. package/src/components/cli-support.ts +28 -5
  268. package/src/components/component.schema.json +17 -0
  269. package/src/components/component.ts +25 -0
  270. package/src/components/deploy-units.ts +3 -0
  271. package/src/components/driver.test.ts +52 -0
  272. package/src/components/driver.ts +31 -13
  273. package/src/components/fan-out-output.ts +42 -1
  274. package/src/components/fan-out-record.ts +69 -0
  275. package/src/components/fan-out-run.test.ts +17 -0
  276. package/src/components/fan-out-run.ts +304 -1
  277. package/src/components/fan-out-support.ts +3 -0
  278. package/src/components/fan-out-waves.test.ts +375 -0
  279. package/src/components/fan-out.ts +18 -17
  280. package/src/components/gated-wave-pipeline.ts +82 -0
  281. package/src/components/index.ts +1 -0
  282. package/src/components/layers.ts +30 -0
  283. package/src/components/pr-pipeline.ts +91 -0
  284. package/src/components/pr-run.test.ts +205 -0
  285. package/src/components/pr-run.ts +280 -0
  286. package/src/components/promote.ts +5 -0
  287. package/src/components/verbs/shell.test.ts +11 -0
  288. package/src/components/verbs/shell.ts +7 -3
  289. package/src/components/wave-plan.ts +124 -0
  290. package/src/discovery/sandbox/bundle.ts +1 -1
  291. package/src/gated-waves-bundle.test.ts +62 -0
  292. package/src/gated-waves.test.ts +118 -0
  293. package/src/gated-waves.ts +240 -0
  294. package/src/index.ts +1 -0
  295. package/src/lexicon.ts +51 -0
  296. package/src/lifecycle/gate-ledger.ts +7 -0
  297. package/src/lifecycle/git.ts +102 -16
  298. package/src/lint/post-synth.test.ts +19 -0
  299. package/src/lint/post-synth.ts +19 -1
  300. package/src/meta/declared-imports.test.ts +1 -1
  301. package/src/op/activities/activity-contracts.ts +84 -0
  302. package/src/op/activities/change-set.ts +114 -0
  303. package/src/op/activities/decide.ts +3 -0
  304. package/src/op/activities/factory.ts +675 -0
  305. package/src/op/activities/index.ts +6 -0
  306. package/src/op/activity-contract.test.ts +21 -1
  307. package/src/op/activity-contract.ts +19 -9
  308. package/src/op/builders.ts +23 -0
  309. package/src/op/decide-backend.ts +5 -1
  310. package/src/op/decide-read-inputs.ts +33 -5
  311. package/src/op/factory.e2e.test.ts +232 -0
  312. package/src/op/factory.ts +118 -0
  313. package/src/op/index.ts +9 -1
  314. package/src/op/op-ir.test.ts +9 -0
  315. package/src/op/op-ir.ts +4 -0
  316. package/src/op/op-verb-class.ts +3 -0
  317. package/src/op/types.ts +8 -0
  318. package/src/op/work-lease-run.ts +3 -0
  319. package/src/plan-summary.test.ts +263 -0
  320. package/src/plan-summary.ts +845 -0
  321. package/src/pr-forge.test.ts +237 -0
  322. package/src/pr-forge.ts +332 -0
  323. package/src/pr-loop-bundle.test.ts +46 -0
  324. package/src/pr-loop.test.ts +171 -0
  325. package/src/pr-loop.ts +324 -0
  326. package/src/telemetry-attribution.test.ts +23 -1
  327. package/src/telemetry-attribution.ts +51 -0
  328. package/src/terraform/hcl2json.test.ts +21 -0
  329. package/src/terraform/hcl2json.ts +81 -0
  330. package/src/terraform/parse.ts +11 -74
  331. package/src/vendor/js-yaml-load.test.ts +115 -0
  332. package/src/vendor/js-yaml-load.ts +1939 -0
  333. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  334. package/src/workspace/app-kind.test.ts +196 -0
  335. package/src/workspace/box-cli.ts +113 -0
  336. package/src/workspace/box-factory.test.ts +4 -1
  337. package/src/workspace/box-factory.ts +37 -4
  338. package/src/workspace/box-listing-write.schema.json +204 -0
  339. package/src/workspace/box-listing.test.ts +234 -0
  340. package/src/workspace/box-listing.ts +324 -0
  341. package/src/workspace/box-publish.schema.json +498 -0
  342. package/src/workspace/box-publish.test.ts +204 -0
  343. package/src/workspace/box-publish.ts +420 -0
  344. package/src/workspace/box-ship.test.ts +96 -0
  345. package/src/workspace/broker-protocol.schema.json +186 -0
  346. package/src/workspace/broker-protocol.test.ts +122 -0
  347. package/src/workspace/broker-protocol.ts +317 -0
  348. package/src/workspace/builder-tiers.test.ts +146 -0
  349. package/src/workspace/change-set.schema.json +460 -0
  350. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  351. package/src/workspace/checks/boxes.test.ts +3 -0
  352. package/src/workspace/checks.ts +10 -3
  353. package/src/workspace/component-environments.test.ts +105 -0
  354. package/src/workspace/composites.schema.json +23 -4
  355. package/src/workspace/composites.test.ts +8 -8
  356. package/src/workspace/composites.ts +13 -2
  357. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  358. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  359. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  360. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  361. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  362. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  363. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  364. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  365. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  366. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  367. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  368. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  369. package/src/workspace/conformance/broker.test.ts +122 -0
  370. package/src/workspace/conformance/broker.ts +792 -0
  371. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  372. package/src/workspace/conformance/index.mjs +22 -1
  373. package/src/workspace/conformance/index.ts +5 -1
  374. package/src/workspace/conformance/vitest.ts +33 -1
  375. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  376. package/src/workspace/conformance/writer.test.ts +32 -2
  377. package/src/workspace/conformance/writer.ts +339 -28
  378. package/src/workspace/decide.test.ts +208 -4
  379. package/src/workspace/decide.ts +427 -73
  380. package/src/workspace/decision-points.schema.json +11 -4
  381. package/src/workspace/declaration.schema.json +153 -4
  382. package/src/workspace/declaration.ts +141 -0
  383. package/src/workspace/environments.ts +66 -14
  384. package/src/workspace/factory-rules.test.ts +62 -0
  385. package/src/workspace/factory-rules.ts +218 -0
  386. package/src/workspace/graph-cli.ts +2 -2
  387. package/src/workspace/graph.schema.json +67 -0
  388. package/src/workspace/identity.test.ts +23 -0
  389. package/src/workspace/init.ts +82 -2
  390. package/src/workspace/intent-cli.ts +26 -9
  391. package/src/workspace/intent-patch-id.test.ts +162 -0
  392. package/src/workspace/intent-record.schema.json +130 -3
  393. package/src/workspace/intent-record.ts +74 -28
  394. package/src/workspace/intent-reviews.test.ts +76 -0
  395. package/src/workspace/intent-squash.test.ts +213 -0
  396. package/src/workspace/intent-why.test.ts +5 -5
  397. package/src/workspace/intent.schema.json +234 -4
  398. package/src/workspace/intent.ts +260 -66
  399. package/src/workspace/json-edit.test.ts +121 -0
  400. package/src/workspace/json-edit.ts +142 -0
  401. package/src/workspace/jsonc.ts +20 -0
  402. package/src/workspace/kinds.ts +157 -2
  403. package/src/workspace/lexicon-symbols.test.ts +148 -0
  404. package/src/workspace/lexicon-symbols.ts +125 -0
  405. package/src/workspace/lineage-upgrade.test.ts +63 -0
  406. package/src/workspace/lineage-upgrade.ts +146 -5
  407. package/src/workspace/plan-summary.schema.json +155 -0
  408. package/src/workspace/point-answer.schema.json +42 -2
  409. package/src/workspace/points-cli.ts +42 -16
  410. package/src/workspace/points-write.schema.json +162 -4
  411. package/src/workspace/points.schema.json +157 -2
  412. package/src/workspace/points.test.ts +13 -1
  413. package/src/workspace/points.ts +118 -12
  414. package/src/workspace/pr-report.schema.json +95 -0
  415. package/src/workspace/profiles.test.ts +159 -0
  416. package/src/workspace/read-contract.test.ts +2 -2
  417. package/src/workspace/reason-codes.test.ts +13 -1
  418. package/src/workspace/reason-codes.ts +38 -4
  419. package/src/workspace/record-sessions.ts +9 -8
  420. package/src/workspace/records-amend.schema.json +78 -0
  421. package/src/workspace/records-cli.ts +20 -6
  422. package/src/workspace/records-close.schema.json +78 -0
  423. package/src/workspace/records-close.ts +31 -7
  424. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  425. package/src/workspace/records-concurrent.test.ts +145 -0
  426. package/src/workspace/records-formats.test.ts +46 -10
  427. package/src/workspace/records-new.schema.json +7 -0
  428. package/src/workspace/records-review.schema.json +78 -0
  429. package/src/workspace/records-sessions-write.test.ts +69 -0
  430. package/src/workspace/records-sessions.test.ts +22 -0
  431. package/src/workspace/records-write.ts +192 -25
  432. package/src/workspace/records.schema.json +16 -0
  433. package/src/workspace/records.ts +50 -9
  434. package/src/workspace/reference-kinds/app/package.json +10 -0
  435. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  436. package/src/workspace/run-statement.schema.json +622 -0
  437. package/src/workspace/runs-cli.ts +343 -14
  438. package/src/workspace/runs-statement.test.ts +239 -0
  439. package/src/workspace/runs-write.schema.json +416 -5
  440. package/src/workspace/runs.schema.json +451 -4
  441. package/src/workspace/runs.test.ts +39 -0
  442. package/src/workspace/runs.ts +296 -35
  443. package/src/workspace/session-kinds.ts +3 -3
  444. package/src/workspace/squash.ts +198 -0
  445. package/src/workspace/status-contract.test.ts +5 -2
  446. package/src/workspace/status-gates.ts +3 -1
  447. package/src/workspace/status.schema.json +126 -3
  448. package/src/workspace/status.ts +90 -1
  449. package/src/workspace/symbols.ts +20 -9
  450. package/src/workspace/trust/run-commits.ts +115 -0
  451. package/src/workspace/trust/run-statement.ts +226 -0
  452. package/src/workspace/trust/seal.ts +8 -2
  453. package/src/workspace/trust/verify-cli.ts +6 -5
  454. package/src/workspace/trust/verify.ts +39 -6
  455. package/src/workspace/wip-cli.ts +97 -0
  456. package/src/workspace/wip-write.schema.json +504 -0
  457. package/src/workspace/wip.schema.json +334 -0
  458. package/src/workspace/wip.test.ts +294 -0
  459. package/src/workspace/wip.ts +753 -0
  460. package/src/workspace/work-cli.ts +3 -0
  461. package/src/workspace/work-evidence.schema.json +3 -0
  462. package/src/workspace/work-size.test.ts +77 -0
  463. package/src/workspace/work-size.ts +93 -0
  464. package/src/workspace/workspace-kinds.schema.json +122 -0
  465. package/src/workspace/write-lock-cli.ts +131 -0
  466. package/src/workspace/write-lock.schema.json +172 -0
  467. package/src/workspace/write-lock.test.ts +189 -0
  468. package/src/workspace/write-lock.ts +474 -0
  469. package/src/workspace/write-scope.test.ts +16 -1
  470. package/src/workspace/write-scope.ts +33 -11
  471. package/src/yaml.ts +31 -21
@@ -0,0 +1,753 @@
1
+ /**
2
+ * Work in progress that survives the box (#3172, ws-085).
3
+ *
4
+ * ws-074 keeps work in progress as uncommitted files in a work branch's
5
+ * working tree, and #3147 keeps an unfinished attempt under a local ref. On a
6
+ * box (a sprite, a fountain sandbox) both sit on one disk until someone
7
+ * commits and pushes. This module owns one ref namespace for that work and
8
+ * replicates it under the policy a box block declares:
9
+ *
10
+ * refs/chant/wip/<branch> a chain of snapshots of the checkout on <branch>
11
+ *
12
+ * A snapshot is a commit whose tree is the whole working tree, staged,
13
+ * unstaged and untracked files alike (ignored files left out), taken through
14
+ * a temporary index so the checkout's own index is never touched. Its first
15
+ * parent is the snapshot before it on the same ref, when there is one, and
16
+ * its last parent is the commit `HEAD` named, so pushing the ref carries the
17
+ * branch's commits too. Its message carries `Chant-Wip-*` trailers: the
18
+ * branch, the head, the caller's label (hud's turn, studio's checkpoint), who
19
+ * took it, and whether it was a save or the checkpoint a restore takes first.
20
+ * The ref's first-parent chain is the branch's checkpoint history, newest
21
+ * first. The branch name already separates worktrees and tabs, since a
22
+ * branch is checked out in one worktree at a time; who took a snapshot is a
23
+ * trailer, not a path segment.
24
+ *
25
+ * Replication pushes the refs a policy names (work branches, kept attempts,
26
+ * the snapshots and the ledger branch) to a git remote under their own names,
27
+ * never forced, and records what reached the remote under
28
+ * `refs/chant/replica/<remote>/<ref without refs/>`, so `status` and `wip`
29
+ * report how far each ref is from the remote without fetching. `wip fetch`
30
+ * on a replacement box mirrors the remote into that namespace and creates or
31
+ * fast-forwards the local refs; `wip restore` then puts the working tree back
32
+ * exactly as it was saved.
33
+ *
34
+ * chant's record writes never run git (ws-074), so nothing here runs on a
35
+ * record write. A snapshot is taken when a caller asks (`wip save`), and
36
+ * chant pushes on its own only after a save or a work lease release, as the
37
+ * policy says.
38
+ */
39
+
40
+ import { spawnSync } from "node:child_process";
41
+ import { copyFileSync, existsSync, lstatSync, mkdtempSync, rmSync, unlinkSync } from "node:fs";
42
+ import { tmpdir } from "node:os";
43
+ import { join, resolve } from "node:path";
44
+ import { KEPT_REF_PREFIX } from "../lifecycle/work-lease";
45
+ import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type BoxReplicate, type ReplicateRefClass, type ReplicateTrigger } from "./declaration";
46
+ import { IDENTITY_CODES, IdentityError, refuseUnidentified } from "./identity";
47
+ import type { ReasonCode } from "./reason-codes";
48
+ import { gitTop } from "./tree";
49
+ import { locateWorkspace } from "./which-chant";
50
+ import { scopeSource } from "./write-scope";
51
+
52
+ /** Where a branch's work-in-progress snapshots live. */
53
+ export const WIP_REF_PREFIX = "refs/chant/wip/";
54
+ /** Where chant records what reached each remote: `refs/chant/replica/<remote>/<ref without refs/>`. */
55
+ export const REPLICA_REF_PREFIX = "refs/chant/replica/";
56
+ /** The work branches a lease's run works on (`chant/work/[<member>/]<item>`). */
57
+ export const WORK_BRANCH_REF_PREFIX = "refs/heads/chant/work/";
58
+ /** The ledger branch. */
59
+ export const LEDGER_REF = "refs/heads/chant/lifecycle";
60
+
61
+ /** The version of the documents `wip` and its writes print. */
62
+ export const WIP_CONTRACT_VERSION = 1;
63
+ export const WIP_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/wip/v1/wip.schema.json";
64
+ export const WIP_WRITE_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/wip-write/v1/wip-write.schema.json";
65
+
66
+ /** Why `wip` could not read. Closed. */
67
+ export const WIP_ERROR_CODES = [...WORKSPACE_ERROR_CODES] as const satisfies readonly ReasonCode[];
68
+ export type WipErrorCode = (typeof WIP_ERROR_CODES)[number];
69
+
70
+ /** Why a `wip save|restore|push|fetch` wrote nothing. Closed. */
71
+ export const WIP_WRITE_ERROR_CODES = [
72
+ ...WORKSPACE_ERROR_CODES,
73
+ "write-usage-invalid",
74
+ "wip-no-branch",
75
+ "wip-none",
76
+ "wip-snapshot-unknown",
77
+ "wip-branch-other",
78
+ "wip-race",
79
+ "wip-policy-none",
80
+ "wip-remote-unknown",
81
+ ...IDENTITY_CODES,
82
+ ] as const satisfies readonly ReasonCode[];
83
+ export type WipWriteErrorCode = (typeof WIP_WRITE_ERROR_CODES)[number];
84
+
85
+ /** What a snapshot was taken for: a caller's save, or the checkpoint a restore takes before it changes the working tree. */
86
+ export type WipSnapshotKind = "save" | "pre-restore";
87
+
88
+ /** One snapshot of a branch's working tree. */
89
+ export interface WipSnapshot {
90
+ commit: string;
91
+ /** The tree of the working tree as it was. */
92
+ tree: string;
93
+ /** The branch it was taken on. */
94
+ branch: string;
95
+ /** The commit `HEAD` named when it was taken. */
96
+ head: string;
97
+ kind: WipSnapshotKind;
98
+ /** What the caller called it, such as a turn id, or null. */
99
+ label: string | null;
100
+ /** Who took it, as the caller named them, or null. */
101
+ by: string | null;
102
+ /** When it was taken, ISO 8601. */
103
+ at: string;
104
+ }
105
+
106
+ /** A replicate policy as the read contract prints it: the box that declares it and its fields, defaults filled in. */
107
+ export interface ReplicatePolicyView {
108
+ box: string;
109
+ remote: string;
110
+ refs: ReplicateRefClass[];
111
+ on: ReplicateTrigger[];
112
+ every: string | null;
113
+ }
114
+
115
+ /** One ref a policy replicates and where it stands against the remote. */
116
+ export interface ReplicaRefState {
117
+ ref: string;
118
+ class: ReplicateRefClass;
119
+ commit: string;
120
+ /** What the remote held at the last push or fetch through chant (or, for a branch, git's remote-tracking ref), or null when unknown. */
121
+ replica: string | null;
122
+ replicated: boolean;
123
+ /** Commits reachable from `commit` that nothing known to be on the remote reaches: 0 when replicated. */
124
+ ahead: number;
125
+ }
126
+
127
+ /** Where every replicated ref stands (#3172). Read locally; never fetches. */
128
+ export interface ReplicationState {
129
+ policy: ReplicatePolicyView;
130
+ /** Whether the checkout has a git remote of the policy's name. */
131
+ remoteConfigured: boolean;
132
+ /** True when every ref the policy names is on the remote as it is locally. */
133
+ replicated: boolean;
134
+ refs: ReplicaRefState[];
135
+ }
136
+
137
+ /** One ref a push or fetch moved, or could not. */
138
+ export interface RefPush {
139
+ ref: string;
140
+ commit: string;
141
+ /** Why the remote refused it or could not be reached; absent when it went. */
142
+ reason?: string;
143
+ }
144
+
145
+ /** What one push did. */
146
+ export interface ReplicationPush {
147
+ remote: string;
148
+ pushed: RefPush[];
149
+ rejected: RefPush[];
150
+ /** Refs that were already on the remote as they are locally. */
151
+ upToDate: number;
152
+ }
153
+
154
+ type Head = { $schema: string; contract: number; chant: string };
155
+
156
+ /** What `chant workspace wip --json` prints. */
157
+ export type WipDocument =
158
+ | (Head & {
159
+ checkout: { branch: string | null; head: string | null };
160
+ branches: { branch: string; ref: string; tip: string; snapshots: WipSnapshot[] }[];
161
+ replication: ReplicationState | null;
162
+ })
163
+ | (Head & { error: { code: WipErrorCode; message: string } });
164
+
165
+ export type WipAction = "save" | "restore" | "push" | "fetch";
166
+
167
+ /** One ref `wip fetch` looked at, and what it did with the local ref. */
168
+ export interface FetchedRef {
169
+ ref: string;
170
+ class: ReplicateRefClass;
171
+ /** What the remote has. */
172
+ remote: string;
173
+ /** The local ref before the fetch, or null when there was none. */
174
+ local: string | null;
175
+ result: "created" | "fast-forward" | "up-to-date" | "ahead" | "diverged" | "checked-out";
176
+ }
177
+
178
+ /** What `chant workspace wip save|restore|push|fetch` prints. */
179
+ export type WipWriteDocument =
180
+ | (Head & { action: "save"; branch: string; ref: string; created: boolean; snapshot: WipSnapshot; replication: ReplicationPush | null })
181
+ | (Head & {
182
+ action: "restore";
183
+ branch: string;
184
+ ref: string;
185
+ head: string;
186
+ snapshot: WipSnapshot;
187
+ checkpoint: { created: boolean; snapshot: WipSnapshot };
188
+ headMoved: boolean;
189
+ paths: string[];
190
+ })
191
+ | (Head & { action: "push"; policy: ReplicatePolicyView; replication: ReplicationPush })
192
+ | (Head & { action: "fetch"; policy: ReplicatePolicyView; refs: FetchedRef[]; failed: string | null })
193
+ | (Head & { action: WipAction | null; error: { code: WipWriteErrorCode; message: string } });
194
+
195
+ class WipError extends Error {
196
+ constructor(
197
+ readonly code: WipWriteErrorCode,
198
+ message: string,
199
+ ) {
200
+ super(message);
201
+ }
202
+ }
203
+
204
+ // ── git ──────────────────────────────────────────────────────────────────────
205
+
206
+ interface GitRun {
207
+ ok: boolean;
208
+ stdout: string;
209
+ stderr: string;
210
+ }
211
+
212
+ /** How long a push or fetch may take before chant gives up on the remote. */
213
+ const NETWORK_TIMEOUT_MS = 120_000;
214
+
215
+ function git(top: string, args: readonly string[], opts: { env?: Record<string, string>; input?: string; timeout?: number } = {}): GitRun {
216
+ const r = spawnSync("git", ["--no-optional-locks", ...args], {
217
+ cwd: top,
218
+ encoding: "utf-8",
219
+ input: opts.input,
220
+ // Never wait on a person: a remote that wants a password is a refused push.
221
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", ...opts.env },
222
+ maxBuffer: 256 * 1024 * 1024,
223
+ timeout: opts.timeout,
224
+ });
225
+ return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? (r.error ? r.error.message : "") };
226
+ }
227
+
228
+ function out(top: string, args: readonly string[]): string | null {
229
+ const r = git(top, args);
230
+ return r.ok ? r.stdout.trim() : null;
231
+ }
232
+
233
+ /** The checkout's branch and head, or a refusal when there is no branch with a commit to keep work for. */
234
+ function checkoutOf(top: string): { branch: string; head: string } {
235
+ const branch = out(top, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
236
+ const head = out(top, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
237
+ if (!branch) throw new WipError("wip-no-branch", "HEAD is detached, so there is no branch to keep work in progress for; check out the work branch first");
238
+ if (!head) throw new WipError("wip-no-branch", `branch ${branch} has no commit yet, so there is nothing to keep work in progress against; commit once first`);
239
+ return { branch, head };
240
+ }
241
+
242
+ /** The ref a branch's snapshots live on. */
243
+ export function wipRef(branch: string): string {
244
+ return `${WIP_REF_PREFIX}${branch}`;
245
+ }
246
+
247
+ /** The ref recording what `remote` holds of `ref`. */
248
+ export function replicaRef(remote: string, ref: string): string {
249
+ return `${REPLICA_REF_PREFIX}${remote}/${ref.slice("refs/".length)}`;
250
+ }
251
+
252
+ /** The class a ref belongs to, or null when no policy replicates it. */
253
+ export function refClass(ref: string): ReplicateRefClass | null {
254
+ if (ref.startsWith(WORK_BRANCH_REF_PREFIX)) return "work";
255
+ if (ref.startsWith(KEPT_REF_PREFIX)) return "kept";
256
+ if (ref.startsWith(WIP_REF_PREFIX)) return "wip";
257
+ if (ref === LEDGER_REF) return "ledger";
258
+ return null;
259
+ }
260
+
261
+ /** Every local ref of the given classes and its commit, sorted by ref. */
262
+ function localRefs(top: string, classes: readonly ReplicateRefClass[]): { ref: string; class: ReplicateRefClass; commit: string }[] {
263
+ const patterns = classes.map((c) => (c === "work" ? WORK_BRANCH_REF_PREFIX : c === "kept" ? KEPT_REF_PREFIX : c === "wip" ? WIP_REF_PREFIX : LEDGER_REF));
264
+ if (patterns.length === 0) return [];
265
+ const text = out(top, ["for-each-ref", "--format=%(refname) %(objectname)", ...patterns]) ?? "";
266
+ const refs: { ref: string; class: ReplicateRefClass; commit: string }[] = [];
267
+ for (const line of text.split("\n")) {
268
+ const [ref, commit] = line.split(" ");
269
+ const c = ref ? refClass(ref) : null;
270
+ if (c && classes.includes(c) && commit) refs.push({ ref, class: c, commit });
271
+ }
272
+ return refs.sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
273
+ }
274
+
275
+ /** Every ref under `prefixes` and its commit. */
276
+ function refMap(top: string, prefixes: readonly string[]): Map<string, string> {
277
+ const text = out(top, ["for-each-ref", "--format=%(refname) %(objectname)", ...prefixes]) ?? "";
278
+ const map = new Map<string, string>();
279
+ for (const line of text.split("\n")) {
280
+ const [ref, commit] = line.split(" ");
281
+ if (ref && commit) map.set(ref, commit);
282
+ }
283
+ return map;
284
+ }
285
+
286
+ // ── snapshots ────────────────────────────────────────────────────────────────
287
+
288
+ const TRAILER = /^Chant-Wip-([A-Za-z]+): (.*)$/;
289
+
290
+ /** A snapshot from a commit's id, tree, committer time and message, or null when the commit is not one. */
291
+ function parseSnapshot(commit: string, tree: string, time: string, message: string): WipSnapshot | null {
292
+ const t: Record<string, string> = {};
293
+ for (const line of message.split("\n")) {
294
+ const m = TRAILER.exec(line);
295
+ if (m) t[m[1].toLowerCase()] = m[2];
296
+ }
297
+ if (!t.branch || !t.head) return null;
298
+ return {
299
+ commit,
300
+ tree,
301
+ branch: t.branch,
302
+ head: t.head,
303
+ kind: t.kind === "pre-restore" ? "pre-restore" : "save",
304
+ label: t.label ?? null,
305
+ by: t.by ?? null,
306
+ at: new Date(Number(time) * 1000).toISOString(),
307
+ };
308
+ }
309
+
310
+ const FIELD = "\x1f";
311
+ const RECORD = "\x1e";
312
+
313
+ /**
314
+ * The snapshots on `ref`, newest first: its first-parent chain, down to the
315
+ * first commit that is not a snapshot (the branch commit the oldest was taken
316
+ * on). History the branch itself reaches is never walked.
317
+ */
318
+ export function listSnapshots(top: string, ref: string, branch: string): WipSnapshot[] {
319
+ const tip = out(top, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
320
+ if (!tip) return [];
321
+ const branchRef = `refs/heads/${branch}`;
322
+ const stop = out(top, ["rev-parse", "--verify", "--quiet", `${branchRef}^{commit}`]) ? [`^${branchRef}`] : [];
323
+ const r = git(top, ["log", "--first-parent", "-n", "10000", `--format=%H${FIELD}%T${FIELD}%ct${FIELD}%B${RECORD}`, tip, ...stop, "--"]);
324
+ if (!r.ok) return [];
325
+ const snapshots: WipSnapshot[] = [];
326
+ for (const rec of r.stdout.split(RECORD)) {
327
+ const [commit, tree, time, message] = rec.replace(/^\n/, "").split(FIELD);
328
+ if (!commit || !tree) continue;
329
+ const s = parseSnapshot(commit, tree, time, message ?? "");
330
+ if (!s) break;
331
+ snapshots.push(s);
332
+ }
333
+ return snapshots;
334
+ }
335
+
336
+ /** The snapshot `rev` names, or null when it is not a commit or not a snapshot. */
337
+ function readSnapshot(top: string, rev: string): WipSnapshot | null {
338
+ const r = git(top, ["log", "-n", "1", `--format=%H${FIELD}%T${FIELD}%ct${FIELD}%B`, `${rev}^{commit}`, "--"]);
339
+ if (!r.ok) return null;
340
+ const [commit, tree, time, message] = r.stdout.split(FIELD);
341
+ return commit && tree ? parseSnapshot(commit, tree, time, message ?? "") : null;
342
+ }
343
+
344
+ /** The tree of the whole working tree, staged, unstaged and untracked alike, written through a temporary index. */
345
+ function worktreeTree(top: string): string {
346
+ const dir = mkdtempSync(join(tmpdir(), "chant-wip-"));
347
+ try {
348
+ const index = join(dir, "index");
349
+ // Start from the checkout's index so git reuses its stat data; the original is never written.
350
+ const own = out(top, ["rev-parse", "--git-path", "index"]);
351
+ if (own && existsSync(resolve(top, own))) copyFileSync(resolve(top, own), index);
352
+ const env = { GIT_INDEX_FILE: index };
353
+ const add = git(top, ["add", "--all", "--", ":/"], { env });
354
+ if (!add.ok) throw new Error(`wip: git add into a temporary index failed: ${add.stderr.trim()}`);
355
+ const tree = git(top, ["write-tree"], { env });
356
+ if (!tree.ok) throw new Error(`wip: git write-tree failed: ${tree.stderr.trim()}`);
357
+ return tree.stdout.trim();
358
+ } finally {
359
+ rmSync(dir, { recursive: true, force: true });
360
+ }
361
+ }
362
+
363
+ /** A label or principal as a trailer value takes it: one line, no control characters. */
364
+ function trailerValue(value: string | undefined, flag: string): string | null {
365
+ if (value === undefined) return null;
366
+ // eslint-disable-next-line no-control-regex
367
+ if (value === "" || value.length > 200 || /[\u0000-\u001f\u007f]/.test(value)) {
368
+ throw new WipError("write-usage-invalid", `${flag} is one line of at most 200 characters, with no control characters`);
369
+ }
370
+ return value;
371
+ }
372
+
373
+ interface Snapshotted {
374
+ created: boolean;
375
+ snapshot: WipSnapshot;
376
+ }
377
+
378
+ /** Take a snapshot of the checkout on `branch` at `head`, or return the tip when it already holds exactly this. */
379
+ function snapshot(top: string, branch: string, head: string, meta: { kind: WipSnapshotKind; label: string | null; by: string | null }): Snapshotted {
380
+ const ref = wipRef(branch);
381
+ const tree = worktreeTree(top);
382
+ const previous = out(top, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
383
+ const prev = previous ? readSnapshot(top, previous) : null;
384
+ if (prev && prev.tree === tree && prev.head === head && prev.label === meta.label && prev.by === meta.by && prev.kind === meta.kind) {
385
+ return { created: false, snapshot: prev };
386
+ }
387
+ // A pre-restore checkpoint of a tree the tip already holds adds nothing to restore from.
388
+ if (prev && meta.kind === "pre-restore" && prev.tree === tree && prev.head === head) return { created: false, snapshot: prev };
389
+ const lines = [
390
+ `chant wip: ${branch}${meta.label ? ` (${meta.label})` : ""}`,
391
+ "",
392
+ `Chant-Wip-Branch: ${branch}`,
393
+ `Chant-Wip-Head: ${head}`,
394
+ `Chant-Wip-Kind: ${meta.kind}`,
395
+ ...(meta.label ? [`Chant-Wip-Label: ${meta.label}`] : []),
396
+ ...(meta.by ? [`Chant-Wip-By: ${meta.by}`] : []),
397
+ "",
398
+ ];
399
+ // An identity only when git has none, so a configured one is kept.
400
+ const ident = git(top, ["var", "GIT_COMMITTER_IDENT"]).ok && git(top, ["var", "GIT_AUTHOR_IDENT"]).ok;
401
+ const who = ident ? [] : ["-c", "user.name=chant", "-c", "user.email=chant@localhost"];
402
+ const parents = prev ? ["-p", prev.commit, "-p", head] : ["-p", head];
403
+ const made = git(top, [...who, "commit-tree", "--no-gpg-sign", tree, ...parents, "-F", "-"], { input: lines.join("\n") });
404
+ if (!made.ok) throw new Error(`wip: git commit-tree failed: ${made.stderr.trim()}`);
405
+ const commit = made.stdout.trim();
406
+ const moved = git(top, ["update-ref", "-m", `chant wip ${meta.kind}`, ref, commit, previous ?? ""]);
407
+ if (!moved.ok) throw new WipError("wip-race", `another writer moved ${ref} while this snapshot was taken; take it again`);
408
+ const s = readSnapshot(top, commit);
409
+ if (!s) throw new Error(`wip: the snapshot ${commit} does not read back`);
410
+ return { created: true, snapshot: s };
411
+ }
412
+
413
+ // ── the policy ───────────────────────────────────────────────────────────────
414
+
415
+ /** A box's replicate policy as the read contract prints it. */
416
+ export function policyView(box: string, r: BoxReplicate): ReplicatePolicyView {
417
+ return { box, remote: r.remote, refs: [...r.refs], on: [...r.on], every: r.every };
418
+ }
419
+
420
+ /**
421
+ * The replicate policy the declaration in the working tree states, or null
422
+ * when no box block declares one or there is no declaration. A declaration
423
+ * that can't be read throws its WorkspaceReadError.
424
+ */
425
+ export function readReplicatePolicy(cwd: string): ReplicatePolicyView | null {
426
+ let located;
427
+ try {
428
+ located = locateWorkspace(cwd);
429
+ } catch (err) {
430
+ if (err instanceof WorkspaceReadError && err.code === "declaration-missing") return null;
431
+ throw err;
432
+ }
433
+ const declaration = readDeclaration(located.tree);
434
+ const m = declaration.members.find((x) => x.box?.replicate);
435
+ return m ? policyView(m.name, m.box!.replicate!) : null;
436
+ }
437
+
438
+ function remotes(top: string): string[] {
439
+ return (out(top, ["remote"]) ?? "").split("\n").filter(Boolean);
440
+ }
441
+
442
+ /** Where every ref the policy names stands against its remote, read locally (#3172). */
443
+ export function replicationState(top: string, policy: ReplicatePolicyView): ReplicationState {
444
+ const local = localRefs(top, policy.refs);
445
+ const replicas = refMap(top, [`${REPLICA_REF_PREFIX}${policy.remote}/`, `refs/remotes/${policy.remote}/`]);
446
+ const known = (ref: string): string | null =>
447
+ replicas.get(replicaRef(policy.remote, ref)) ?? (ref.startsWith("refs/heads/") ? (replicas.get(`refs/remotes/${policy.remote}/${ref.slice("refs/heads/".length)}`) ?? null) : null);
448
+ const refs = local.map(({ ref, class: c, commit }) => {
449
+ const replica = known(ref);
450
+ if (replica === commit) return { ref, class: c, commit, replica, replicated: true, ahead: 0 };
451
+ const not = replicas.size > 0 ? ["--not", `--glob=${REPLICA_REF_PREFIX}${policy.remote}/*`, `--remotes=${policy.remote}`] : [];
452
+ const n = Number(out(top, ["rev-list", "--count", commit, ...not, "--"]) ?? "0");
453
+ return { ref, class: c, commit, replica, replicated: false, ahead: Number.isFinite(n) ? n : 0 };
454
+ });
455
+ return { policy, remoteConfigured: remotes(top).includes(policy.remote), replicated: refs.every((r) => r.replicated), refs };
456
+ }
457
+
458
+ /** Push every ref the policy names that the remote doesn't hold as it is locally. Never forced. */
459
+ export function pushReplicas(top: string, policy: ReplicatePolicyView): ReplicationPush {
460
+ const state = replicationState(top, policy);
461
+ const todo = state.refs.filter((r) => !r.replicated);
462
+ const result: ReplicationPush = { remote: policy.remote, pushed: [], rejected: [], upToDate: state.refs.length - todo.length };
463
+ if (todo.length === 0) return result;
464
+ if (!state.remoteConfigured) {
465
+ result.rejected = todo.map((r) => ({ ref: r.ref, commit: r.commit, reason: `this checkout has no git remote named ${policy.remote}` }));
466
+ return result;
467
+ }
468
+ // A pre-push hook is the repository's gate for publishing work, not for keeping it safe.
469
+ for (let i = 0; i < todo.length; i += 200) {
470
+ const batch = todo.slice(i, i + 200);
471
+ const r = git(top, ["push", "--porcelain", "--no-verify", policy.remote, ...batch.map((x) => `${x.ref}:${x.ref}`)], { timeout: NETWORK_TIMEOUT_MS });
472
+ const status = new Map<string, { flag: string; summary: string }>();
473
+ for (const line of r.stdout.split("\n")) {
474
+ const m = /^(.)\t([^:]+):([^\t]+)\t(.*)$/.exec(line);
475
+ if (m) status.set(m[3], { flag: m[1], summary: m[4] });
476
+ }
477
+ const failure = r.stderr.trim().split("\n").filter(Boolean).pop() ?? "the push failed";
478
+ for (const x of batch) {
479
+ const s = status.get(x.ref);
480
+ if (s && s.flag !== "!") {
481
+ git(top, ["update-ref", replicaRef(policy.remote, x.ref), x.commit]);
482
+ if (s.flag === "=") result.upToDate++;
483
+ else result.pushed.push({ ref: x.ref, commit: x.commit });
484
+ } else {
485
+ result.rejected.push({ ref: x.ref, commit: x.commit, reason: s ? s.summary : failure });
486
+ }
487
+ }
488
+ }
489
+ return result;
490
+ }
491
+
492
+ /**
493
+ * Push what the policy names after `event`, when the policy says chant pushes
494
+ * then. Never throws: a box that can't reach its remote still saves and
495
+ * releases, and `status` shows what is behind.
496
+ */
497
+ export function replicateAfter(event: ReplicateTrigger, cwd: string): ReplicationPush | null {
498
+ try {
499
+ const policy = readReplicatePolicy(cwd);
500
+ const top = gitTop(cwd);
501
+ if (!policy || !top || !policy.on.includes(event)) return null;
502
+ return pushReplicas(top, policy);
503
+ } catch {
504
+ return null;
505
+ }
506
+ }
507
+
508
+ // ── the read ─────────────────────────────────────────────────────────────────
509
+
510
+ /** `chant workspace wip [--branch <branch>] --json`: every branch's snapshots, and where replication stands. */
511
+ export function workspaceWip(req: { cwd: string; branch?: string }): WipDocument {
512
+ const head: Head = { $schema: WIP_SCHEMA_ID, contract: WIP_CONTRACT_VERSION, chant: readerVersion() };
513
+ const top = gitTop(req.cwd);
514
+ if (!top) return { ...head, error: { code: "not-a-git-repository", message: "work in progress is kept in git refs, and this directory is not in a git repository" } };
515
+ let policy: ReplicatePolicyView | null;
516
+ try {
517
+ policy = readReplicatePolicy(req.cwd);
518
+ } catch (err) {
519
+ if (err instanceof WorkspaceReadError) return { ...head, error: { code: err.code as WipErrorCode, message: err.describe() } };
520
+ throw err;
521
+ }
522
+ const branch = out(top, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
523
+ const commit = out(top, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
524
+ const branches = [...refMap(top, [WIP_REF_PREFIX])]
525
+ .map(([ref, tip]) => ({ branch: ref.slice(WIP_REF_PREFIX.length), ref, tip }))
526
+ .filter((b) => req.branch === undefined || b.branch === req.branch)
527
+ .sort((a, b) => (a.branch < b.branch ? -1 : a.branch > b.branch ? 1 : 0))
528
+ .map((b) => ({ ...b, snapshots: listSnapshots(top, b.ref, b.branch) }));
529
+ return {
530
+ ...head,
531
+ checkout: { branch, head: commit },
532
+ branches,
533
+ replication: policy ? replicationState(top, policy) : null,
534
+ };
535
+ }
536
+
537
+ // ── the writes ───────────────────────────────────────────────────────────────
538
+
539
+ function fail(action: WipAction | null, err: unknown): WipWriteDocument {
540
+ const head: Head = { $schema: WIP_WRITE_SCHEMA_ID, contract: WIP_CONTRACT_VERSION, chant: readerVersion() };
541
+ if (err instanceof WipError) return { ...head, action, error: { code: err.code, message: err.message } };
542
+ if (err instanceof IdentityError) return { ...head, action, error: { code: err.code, message: err.message } };
543
+ if (err instanceof WorkspaceReadError) return { ...head, action, error: { code: err.code as WipWriteErrorCode, message: err.describe() } };
544
+ throw err;
545
+ }
546
+
547
+ function topOf(cwd: string): string {
548
+ const top = gitTop(cwd);
549
+ if (!top) throw new WorkspaceReadError("not-a-git-repository", "work in progress is kept in git refs, and this directory is not in a git repository");
550
+ return top;
551
+ }
552
+
553
+ export interface WipSaveRequest {
554
+ cwd: string;
555
+ /** What the caller calls this snapshot, such as `turn:12` (`--label`). */
556
+ label?: string;
557
+ /** Who took it (`--by`), held to the identity rule at base (ws-080). */
558
+ by?: string;
559
+ /** The agent session (`CHANT_AGENT`). */
560
+ agent?: string;
561
+ }
562
+
563
+ /** `chant workspace wip save`: snapshot the checkout's working tree onto `refs/chant/wip/<branch>`, then push when the policy says so. */
564
+ export function wipSave(req: WipSaveRequest): WipWriteDocument {
565
+ try {
566
+ const label = trailerValue(req.label, "--label");
567
+ const by = trailerValue(req.by, "--by");
568
+ const top = topOf(req.cwd);
569
+ const policy = readReplicatePolicy(req.cwd);
570
+ const { branch, head } = checkoutOf(top);
571
+ if (by !== null) refuseUnidentified(scopeSource(req.cwd), [by], "--by", { agent: req.agent });
572
+ const taken = snapshot(top, branch, head, { kind: "save", label, by });
573
+ const replication = policy && policy.on.includes("save") ? pushReplicas(top, policy) : null;
574
+ return {
575
+ $schema: WIP_WRITE_SCHEMA_ID,
576
+ contract: WIP_CONTRACT_VERSION,
577
+ chant: readerVersion(),
578
+ action: "save",
579
+ branch,
580
+ ref: wipRef(branch),
581
+ created: taken.created,
582
+ snapshot: taken.snapshot,
583
+ replication,
584
+ };
585
+ } catch (err) {
586
+ return fail("save", err);
587
+ }
588
+ }
589
+
590
+ export interface WipRestoreRequest {
591
+ cwd: string;
592
+ /** The snapshot to restore, as a commit id or anything git resolves to one; the branch's latest when undefined. */
593
+ snapshot?: string;
594
+ by?: string;
595
+ agent?: string;
596
+ }
597
+
598
+ /**
599
+ * `chant workspace wip restore [<snapshot>]`: put the working tree back as
600
+ * the snapshot holds it. It first takes a snapshot of the working tree as it
601
+ * is (kind `pre-restore`), so a restore can itself be undone. The branch and
602
+ * `HEAD` never move; the index is reset to `HEAD`, so everything restored
603
+ * reads as uncommitted, staged or not (#3160 counts them alike). Ignored
604
+ * files are left as they are.
605
+ */
606
+ export function wipRestore(req: WipRestoreRequest): WipWriteDocument {
607
+ try {
608
+ const by = trailerValue(req.by, "--by");
609
+ const top = topOf(req.cwd);
610
+ const { branch, head } = checkoutOf(top);
611
+ const ref = wipRef(branch);
612
+ let target: WipSnapshot | null;
613
+ if (req.snapshot === undefined) {
614
+ target = readSnapshot(top, ref);
615
+ if (!target) throw new WipError("wip-none", `branch ${branch} has no work-in-progress snapshot under ${ref}; chant workspace wip lists the ones there are` + "; wip fetch brings back one that was replicated");
616
+ } else {
617
+ target = readSnapshot(top, req.snapshot);
618
+ if (!target) throw new WipError("wip-snapshot-unknown", `${req.snapshot} is not a work-in-progress snapshot chant took; chant workspace wip lists them`);
619
+ }
620
+ if (target.branch !== branch) {
621
+ throw new WipError("wip-branch-other", `snapshot ${target.commit.slice(0, 12)} was taken on ${target.branch}, and the checkout is on ${branch}; check out ${target.branch} to restore it`);
622
+ }
623
+ if (by !== null) refuseUnidentified(scopeSource(req.cwd), [by], "--by", { agent: req.agent });
624
+ const checkpoint = snapshot(top, branch, head, { kind: "pre-restore", label: `before ${target.commit.slice(0, 12)}`, by });
625
+ const paths = restoreTree(top, checkpoint.snapshot.tree, target.tree);
626
+ return {
627
+ $schema: WIP_WRITE_SCHEMA_ID,
628
+ contract: WIP_CONTRACT_VERSION,
629
+ chant: readerVersion(),
630
+ action: "restore",
631
+ branch,
632
+ ref,
633
+ head,
634
+ snapshot: target,
635
+ checkpoint,
636
+ headMoved: target.head !== head,
637
+ paths,
638
+ };
639
+ } catch (err) {
640
+ return fail("restore", err);
641
+ }
642
+ }
643
+
644
+ /** Make the working tree, which holds `from`, hold `to`; reset the index to `HEAD`. Returns the paths changed, sorted. */
645
+ function restoreTree(top: string, from: string, to: string): string[] {
646
+ const diff = git(top, ["diff-tree", "-r", "-z", "--no-renames", "--name-status", from, to]);
647
+ if (!diff.ok) throw new Error(`wip: git diff-tree failed: ${diff.stderr.trim()}`);
648
+ const fields = diff.stdout.split("\0").filter((f) => f !== "");
649
+ const removed: string[] = [];
650
+ const written: string[] = [];
651
+ for (let i = 0; i + 1 < fields.length; i += 2) (fields[i] === "D" ? removed : written).push(fields[i + 1]);
652
+ for (const p of removed) {
653
+ const file = join(top, ...p.split("/"));
654
+ try {
655
+ if (lstatSync(file).isFile() || lstatSync(file).isSymbolicLink()) unlinkSync(file);
656
+ } catch {
657
+ // Already gone.
658
+ }
659
+ }
660
+ if (written.length > 0) {
661
+ const dir = mkdtempSync(join(tmpdir(), "chant-wip-"));
662
+ try {
663
+ const env = { GIT_INDEX_FILE: join(dir, "index") };
664
+ const read = git(top, ["read-tree", to], { env });
665
+ if (!read.ok) throw new Error(`wip: git read-tree failed: ${read.stderr.trim()}`);
666
+ const co = git(top, ["checkout-index", "--force", "-z", "--stdin"], { env, input: `${written.join("\0")}\0` });
667
+ if (!co.ok) throw new Error(`wip: git checkout-index failed: ${co.stderr.trim()}`);
668
+ } finally {
669
+ rmSync(dir, { recursive: true, force: true });
670
+ }
671
+ }
672
+ const reset = git(top, ["read-tree", "HEAD"]);
673
+ if (!reset.ok) throw new Error(`wip: resetting the index to HEAD failed: ${reset.stderr.trim()}`);
674
+ return [...removed, ...written].sort();
675
+ }
676
+
677
+ function requirePolicy(top: string, cwd: string, verb: string): ReplicatePolicyView {
678
+ const policy = readReplicatePolicy(cwd);
679
+ if (!policy) throw new WipError("wip-policy-none", `no box block declares replicate, so wip ${verb} has no remote; declare one on the box (box.replicate) in a reviewed change`);
680
+ if (!remotes(top).includes(policy.remote)) {
681
+ throw new WipError("wip-remote-unknown", `the replicate policy on ${policy.box}'s box names the remote ${policy.remote}, and this checkout has none; the host adds it, with its credential, before chant pushes`);
682
+ }
683
+ return policy;
684
+ }
685
+
686
+ /** `chant workspace wip push`: push every ref the policy names that the remote doesn't hold, for a host's schedule (`every`). */
687
+ export function wipPush(req: { cwd: string }): WipWriteDocument {
688
+ try {
689
+ const top = topOf(req.cwd);
690
+ const policy = requirePolicy(top, req.cwd, "push");
691
+ return { $schema: WIP_WRITE_SCHEMA_ID, contract: WIP_CONTRACT_VERSION, chant: readerVersion(), action: "push", policy, replication: pushReplicas(top, policy) };
692
+ } catch (err) {
693
+ return fail("push", err);
694
+ }
695
+ }
696
+
697
+ /**
698
+ * `chant workspace wip fetch`: on a replacement box, mirror what the remote
699
+ * holds of the policy's refs into `refs/chant/replica/<remote>/`, then create
700
+ * each local ref that is missing and fast-forward each that is behind. A
701
+ * branch checked out in any worktree, and a local ref that has moved on or
702
+ * forked, are left as they are and reported.
703
+ */
704
+ export function wipFetch(req: { cwd: string }): WipWriteDocument {
705
+ try {
706
+ const top = topOf(req.cwd);
707
+ const policy = requirePolicy(top, req.cwd, "fetch");
708
+ const remote = policy.remote;
709
+ const mirror = (ref: string) => replicaRef(remote, ref);
710
+ const specs = policy.refs
711
+ .filter((c) => c !== "ledger")
712
+ .map((c) => {
713
+ const prefix = c === "work" ? WORK_BRANCH_REF_PREFIX : c === "kept" ? KEPT_REF_PREFIX : WIP_REF_PREFIX;
714
+ return `+${prefix}*:${mirror(prefix)}*`;
715
+ });
716
+ let failed: string | null = null;
717
+ if (specs.length > 0) {
718
+ const r = git(top, ["fetch", "--no-tags", "--prune", "--no-write-fetch-head", remote, ...specs], { timeout: NETWORK_TIMEOUT_MS });
719
+ if (!r.ok) failed = r.stderr.trim().split("\n").filter(Boolean).pop() ?? "the fetch failed";
720
+ }
721
+ if (policy.refs.includes("ledger") && failed === null) {
722
+ // The ledger may not be on the remote yet; that is not a failure.
723
+ const r = git(top, ["fetch", "--no-tags", "--no-write-fetch-head", remote, `+${LEDGER_REF}:${mirror(LEDGER_REF)}`], { timeout: NETWORK_TIMEOUT_MS });
724
+ if (!r.ok && !/couldn't find remote ref|no such ref/i.test(r.stderr)) failed = r.stderr.trim().split("\n").filter(Boolean).pop() ?? "the fetch failed";
725
+ }
726
+ const checkedOut = new Set(
727
+ (out(top, ["worktree", "list", "--porcelain"]) ?? "")
728
+ .split("\n")
729
+ .filter((l) => l.startsWith("branch "))
730
+ .map((l) => l.slice("branch ".length)),
731
+ );
732
+ const base = `${REPLICA_REF_PREFIX}${remote}/`;
733
+ const refs: FetchedRef[] = [];
734
+ for (const [replica, sha] of refMap(top, [base])) {
735
+ const ref = `refs/${replica.slice(base.length)}`;
736
+ const c = refClass(ref);
737
+ if (!c || !policy.refs.includes(c)) continue;
738
+ const local = out(top, ["rev-parse", "--verify", "--quiet", ref]);
739
+ let result: FetchedRef["result"];
740
+ if (local === sha) result = "up-to-date";
741
+ else if (local === null) result = git(top, ["update-ref", ref, sha, ""]).ok ? "created" : "diverged";
742
+ else if (checkedOut.has(ref)) result = "checked-out";
743
+ else if (git(top, ["merge-base", "--is-ancestor", local, sha]).ok) result = git(top, ["update-ref", ref, sha, local]).ok ? "fast-forward" : "diverged";
744
+ else if (git(top, ["merge-base", "--is-ancestor", sha, local]).ok) result = "ahead";
745
+ else result = "diverged";
746
+ refs.push({ ref, class: c, remote: sha, local, result });
747
+ }
748
+ refs.sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
749
+ return { $schema: WIP_WRITE_SCHEMA_ID, contract: WIP_CONTRACT_VERSION, chant: readerVersion(), action: "fetch", policy, refs, failed };
750
+ } catch (err) {
751
+ return fail("fetch", err);
752
+ }
753
+ }