@intentius/chant 0.102.0 → 0.104.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 (464) 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/workspace/box-cli.d.ts +12 -0
  114. package/dist/workspace/box-cli.d.ts.map +1 -0
  115. package/dist/workspace/box-factory.d.ts +27 -3
  116. package/dist/workspace/box-factory.d.ts.map +1 -1
  117. package/dist/workspace/box-listing.d.ts +85 -0
  118. package/dist/workspace/box-listing.d.ts.map +1 -0
  119. package/dist/workspace/box-publish.d.ts +147 -0
  120. package/dist/workspace/box-publish.d.ts.map +1 -0
  121. package/dist/workspace/broker-protocol.d.ts +190 -0
  122. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  123. package/dist/workspace/checks.d.ts.map +1 -1
  124. package/dist/workspace/composites.d.ts +2 -2
  125. package/dist/workspace/composites.d.ts.map +1 -1
  126. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  127. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  128. package/dist/workspace/conformance/broker.d.ts +202 -0
  129. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/index.d.ts +2 -1
  131. package/dist/workspace/conformance/index.d.ts.map +1 -1
  132. package/dist/workspace/conformance/vitest.d.ts +9 -2
  133. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  134. package/dist/workspace/conformance/writer.d.ts +108 -14
  135. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  136. package/dist/workspace/decide.d.ts +126 -12
  137. package/dist/workspace/decide.d.ts.map +1 -1
  138. package/dist/workspace/decision-points.schema.json +11 -4
  139. package/dist/workspace/declaration.d.ts +65 -0
  140. package/dist/workspace/declaration.d.ts.map +1 -1
  141. package/dist/workspace/declaration.schema.json +153 -4
  142. package/dist/workspace/environments.d.ts +36 -10
  143. package/dist/workspace/environments.d.ts.map +1 -1
  144. package/dist/workspace/factory-rules.d.ts +180 -0
  145. package/dist/workspace/factory-rules.d.ts.map +1 -0
  146. package/dist/workspace/init.d.ts +21 -0
  147. package/dist/workspace/init.d.ts.map +1 -1
  148. package/dist/workspace/intent-cli.d.ts +1 -1
  149. package/dist/workspace/intent-cli.d.ts.map +1 -1
  150. package/dist/workspace/intent-record.d.ts +28 -1
  151. package/dist/workspace/intent-record.d.ts.map +1 -1
  152. package/dist/workspace/intent.d.ts +102 -5
  153. package/dist/workspace/intent.d.ts.map +1 -1
  154. package/dist/workspace/json-edit.d.ts +24 -0
  155. package/dist/workspace/json-edit.d.ts.map +1 -0
  156. package/dist/workspace/jsonc.d.ts +11 -0
  157. package/dist/workspace/jsonc.d.ts.map +1 -1
  158. package/dist/workspace/kinds.d.ts +48 -0
  159. package/dist/workspace/kinds.d.ts.map +1 -1
  160. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  161. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  162. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  163. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  164. package/dist/workspace/point-answer.schema.json +42 -2
  165. package/dist/workspace/points-cli.d.ts +10 -3
  166. package/dist/workspace/points-cli.d.ts.map +1 -1
  167. package/dist/workspace/points.d.ts +66 -5
  168. package/dist/workspace/points.d.ts.map +1 -1
  169. package/dist/workspace/reason-codes.d.ts +33 -4
  170. package/dist/workspace/reason-codes.d.ts.map +1 -1
  171. package/dist/workspace/record-sessions.d.ts +5 -7
  172. package/dist/workspace/record-sessions.d.ts.map +1 -1
  173. package/dist/workspace/records-cli.d.ts +7 -0
  174. package/dist/workspace/records-cli.d.ts.map +1 -1
  175. package/dist/workspace/records-close.d.ts +7 -3
  176. package/dist/workspace/records-close.d.ts.map +1 -1
  177. package/dist/workspace/records-write.d.ts +66 -6
  178. package/dist/workspace/records-write.d.ts.map +1 -1
  179. package/dist/workspace/records.d.ts +28 -9
  180. package/dist/workspace/records.d.ts.map +1 -1
  181. package/dist/workspace/runs-cli.d.ts +138 -6
  182. package/dist/workspace/runs-cli.d.ts.map +1 -1
  183. package/dist/workspace/runs.d.ts +132 -11
  184. package/dist/workspace/runs.d.ts.map +1 -1
  185. package/dist/workspace/session-kinds.d.ts +1 -1
  186. package/dist/workspace/session-kinds.d.ts.map +1 -1
  187. package/dist/workspace/squash.d.ts +80 -0
  188. package/dist/workspace/squash.d.ts.map +1 -0
  189. package/dist/workspace/status-gates.d.ts +2 -0
  190. package/dist/workspace/status-gates.d.ts.map +1 -1
  191. package/dist/workspace/status.d.ts +54 -0
  192. package/dist/workspace/status.d.ts.map +1 -1
  193. package/dist/workspace/symbols.d.ts +16 -5
  194. package/dist/workspace/symbols.d.ts.map +1 -1
  195. package/dist/workspace/trust/run-commits.d.ts +46 -0
  196. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  197. package/dist/workspace/trust/run-statement.d.ts +128 -0
  198. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  199. package/dist/workspace/trust/seal.d.ts +6 -1
  200. package/dist/workspace/trust/seal.d.ts.map +1 -1
  201. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  202. package/dist/workspace/trust/verify.d.ts +16 -2
  203. package/dist/workspace/trust/verify.d.ts.map +1 -1
  204. package/dist/workspace/wip-cli.d.ts +21 -0
  205. package/dist/workspace/wip-cli.d.ts.map +1 -0
  206. package/dist/workspace/wip.d.ts +268 -0
  207. package/dist/workspace/wip.d.ts.map +1 -0
  208. package/dist/workspace/work-cli.d.ts.map +1 -1
  209. package/dist/workspace/work-evidence.d.ts +1 -1
  210. package/dist/workspace/work-evidence.d.ts.map +1 -1
  211. package/dist/workspace/work-size.d.ts +55 -0
  212. package/dist/workspace/work-size.d.ts.map +1 -0
  213. package/dist/workspace/workspace-kinds.schema.json +122 -0
  214. package/dist/workspace/write-lock-cli.d.ts +60 -0
  215. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  216. package/dist/workspace/write-lock.d.ts +152 -0
  217. package/dist/workspace/write-lock.d.ts.map +1 -0
  218. package/dist/workspace/write-scope.d.ts +6 -2
  219. package/dist/workspace/write-scope.d.ts.map +1 -1
  220. package/package.json +6 -1
  221. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  222. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  223. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  224. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  225. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  226. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  227. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  228. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  229. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  230. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  231. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  232. package/src/audit/core.test.ts +15 -0
  233. package/src/audit/core.ts +8 -2
  234. package/src/audit/discover.test.ts +11 -0
  235. package/src/audit/discover.ts +7 -2
  236. package/src/change-set-bundle.test.ts +57 -0
  237. package/src/change-set.test.ts +215 -0
  238. package/src/change-set.ts +445 -0
  239. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  240. package/src/cli/commands/audit.ts +29 -2
  241. package/src/cli/commands/build.test.ts +19 -0
  242. package/src/cli/commands/build.ts +3 -2
  243. package/src/cli/handlers/build.ts +13 -1
  244. package/src/cli/handlers/change-set.test.ts +56 -0
  245. package/src/cli/handlers/change-set.ts +44 -0
  246. package/src/cli/handlers/fan-out.test.ts +221 -2
  247. package/src/cli/handlers/fan-out.ts +165 -75
  248. package/src/cli/handlers/graph.ts +3 -0
  249. package/src/cli/handlers/operator.test.ts +21 -0
  250. package/src/cli/handlers/operator.ts +34 -3
  251. package/src/cli/handlers/pr.ts +373 -0
  252. package/src/cli/handlers/run.ts +1 -0
  253. package/src/cli/main.test.ts +20 -0
  254. package/src/cli/main.ts +185 -23
  255. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  256. package/src/cli/mcp/op-tools.ts +24 -2
  257. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  258. package/src/cli/mcp/workspace-tools.ts +34 -6
  259. package/src/cli/registry.ts +63 -1
  260. package/src/components/auto-release.ts +1 -1
  261. package/src/components/capability.ts +55 -0
  262. package/src/components/changed-files.ts +66 -0
  263. package/src/components/cli-support.ts +28 -5
  264. package/src/components/component.schema.json +17 -0
  265. package/src/components/component.ts +25 -0
  266. package/src/components/deploy-units.ts +3 -0
  267. package/src/components/driver.test.ts +52 -0
  268. package/src/components/driver.ts +31 -13
  269. package/src/components/fan-out-output.ts +42 -1
  270. package/src/components/fan-out-record.ts +69 -0
  271. package/src/components/fan-out-run.test.ts +17 -0
  272. package/src/components/fan-out-run.ts +304 -1
  273. package/src/components/fan-out-support.ts +3 -0
  274. package/src/components/fan-out-waves.test.ts +375 -0
  275. package/src/components/fan-out.ts +18 -17
  276. package/src/components/gated-wave-pipeline.ts +82 -0
  277. package/src/components/index.ts +1 -0
  278. package/src/components/layers.ts +30 -0
  279. package/src/components/pr-pipeline.ts +91 -0
  280. package/src/components/pr-run.test.ts +205 -0
  281. package/src/components/pr-run.ts +280 -0
  282. package/src/components/promote.ts +5 -0
  283. package/src/components/verbs/shell.test.ts +11 -0
  284. package/src/components/verbs/shell.ts +7 -3
  285. package/src/components/wave-plan.ts +124 -0
  286. package/src/discovery/sandbox/bundle.ts +1 -1
  287. package/src/gated-waves-bundle.test.ts +62 -0
  288. package/src/gated-waves.test.ts +118 -0
  289. package/src/gated-waves.ts +240 -0
  290. package/src/index.ts +1 -0
  291. package/src/lexicon.ts +51 -0
  292. package/src/lifecycle/gate-ledger.ts +7 -0
  293. package/src/lifecycle/git.ts +102 -16
  294. package/src/lint/post-synth.test.ts +19 -0
  295. package/src/lint/post-synth.ts +19 -1
  296. package/src/meta/declared-imports.test.ts +1 -1
  297. package/src/op/activities/activity-contracts.ts +84 -0
  298. package/src/op/activities/change-set.ts +114 -0
  299. package/src/op/activities/decide.ts +3 -0
  300. package/src/op/activities/factory.ts +675 -0
  301. package/src/op/activities/index.ts +6 -0
  302. package/src/op/activity-contract.test.ts +21 -1
  303. package/src/op/activity-contract.ts +19 -9
  304. package/src/op/builders.ts +23 -0
  305. package/src/op/decide-backend.ts +5 -1
  306. package/src/op/decide-read-inputs.ts +33 -5
  307. package/src/op/factory.e2e.test.ts +232 -0
  308. package/src/op/factory.ts +118 -0
  309. package/src/op/index.ts +9 -1
  310. package/src/op/op-ir.test.ts +9 -0
  311. package/src/op/op-ir.ts +4 -0
  312. package/src/op/op-verb-class.ts +3 -0
  313. package/src/op/types.ts +8 -0
  314. package/src/op/work-lease-run.ts +3 -0
  315. package/src/plan-summary.test.ts +263 -0
  316. package/src/plan-summary.ts +845 -0
  317. package/src/pr-forge.test.ts +237 -0
  318. package/src/pr-forge.ts +332 -0
  319. package/src/pr-loop-bundle.test.ts +46 -0
  320. package/src/pr-loop.test.ts +171 -0
  321. package/src/pr-loop.ts +324 -0
  322. package/src/telemetry-attribution.test.ts +23 -1
  323. package/src/telemetry-attribution.ts +51 -0
  324. package/src/terraform/hcl2json.test.ts +21 -0
  325. package/src/terraform/hcl2json.ts +81 -0
  326. package/src/terraform/parse.ts +11 -74
  327. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  328. package/src/workspace/app-kind.test.ts +196 -0
  329. package/src/workspace/box-cli.ts +113 -0
  330. package/src/workspace/box-factory.test.ts +4 -1
  331. package/src/workspace/box-factory.ts +37 -4
  332. package/src/workspace/box-listing-write.schema.json +204 -0
  333. package/src/workspace/box-listing.test.ts +234 -0
  334. package/src/workspace/box-listing.ts +324 -0
  335. package/src/workspace/box-publish.schema.json +498 -0
  336. package/src/workspace/box-publish.test.ts +204 -0
  337. package/src/workspace/box-publish.ts +420 -0
  338. package/src/workspace/box-ship.test.ts +96 -0
  339. package/src/workspace/broker-protocol.schema.json +186 -0
  340. package/src/workspace/broker-protocol.test.ts +122 -0
  341. package/src/workspace/broker-protocol.ts +317 -0
  342. package/src/workspace/builder-tiers.test.ts +146 -0
  343. package/src/workspace/change-set.schema.json +460 -0
  344. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  345. package/src/workspace/checks/boxes.test.ts +3 -0
  346. package/src/workspace/checks.ts +10 -3
  347. package/src/workspace/component-environments.test.ts +105 -0
  348. package/src/workspace/composites.schema.json +23 -4
  349. package/src/workspace/composites.test.ts +8 -8
  350. package/src/workspace/composites.ts +13 -2
  351. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  352. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  353. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  354. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  355. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  356. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  357. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  358. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  359. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  360. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  361. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  362. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  363. package/src/workspace/conformance/broker.test.ts +122 -0
  364. package/src/workspace/conformance/broker.ts +792 -0
  365. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  366. package/src/workspace/conformance/index.mjs +22 -1
  367. package/src/workspace/conformance/index.ts +5 -1
  368. package/src/workspace/conformance/vitest.ts +33 -1
  369. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  370. package/src/workspace/conformance/writer.test.ts +32 -2
  371. package/src/workspace/conformance/writer.ts +339 -28
  372. package/src/workspace/decide.test.ts +208 -4
  373. package/src/workspace/decide.ts +427 -73
  374. package/src/workspace/decision-points.schema.json +11 -4
  375. package/src/workspace/declaration.schema.json +153 -4
  376. package/src/workspace/declaration.ts +141 -0
  377. package/src/workspace/environments.ts +66 -14
  378. package/src/workspace/factory-rules.test.ts +62 -0
  379. package/src/workspace/factory-rules.ts +218 -0
  380. package/src/workspace/graph-cli.ts +2 -2
  381. package/src/workspace/graph.schema.json +67 -0
  382. package/src/workspace/identity.test.ts +23 -0
  383. package/src/workspace/init.ts +82 -2
  384. package/src/workspace/intent-cli.ts +26 -9
  385. package/src/workspace/intent-patch-id.test.ts +162 -0
  386. package/src/workspace/intent-record.schema.json +130 -3
  387. package/src/workspace/intent-record.ts +74 -28
  388. package/src/workspace/intent-reviews.test.ts +76 -0
  389. package/src/workspace/intent-squash.test.ts +213 -0
  390. package/src/workspace/intent-why.test.ts +5 -5
  391. package/src/workspace/intent.schema.json +234 -4
  392. package/src/workspace/intent.ts +260 -66
  393. package/src/workspace/json-edit.test.ts +121 -0
  394. package/src/workspace/json-edit.ts +142 -0
  395. package/src/workspace/jsonc.ts +20 -0
  396. package/src/workspace/kinds.ts +157 -2
  397. package/src/workspace/lexicon-symbols.test.ts +148 -0
  398. package/src/workspace/lexicon-symbols.ts +125 -0
  399. package/src/workspace/lineage-upgrade.test.ts +63 -0
  400. package/src/workspace/lineage-upgrade.ts +146 -5
  401. package/src/workspace/plan-summary.schema.json +155 -0
  402. package/src/workspace/point-answer.schema.json +42 -2
  403. package/src/workspace/points-cli.ts +42 -16
  404. package/src/workspace/points-write.schema.json +162 -4
  405. package/src/workspace/points.schema.json +157 -2
  406. package/src/workspace/points.test.ts +13 -1
  407. package/src/workspace/points.ts +118 -12
  408. package/src/workspace/pr-report.schema.json +95 -0
  409. package/src/workspace/profiles.test.ts +159 -0
  410. package/src/workspace/read-contract.test.ts +2 -2
  411. package/src/workspace/reason-codes.test.ts +13 -1
  412. package/src/workspace/reason-codes.ts +38 -4
  413. package/src/workspace/record-sessions.ts +9 -8
  414. package/src/workspace/records-amend.schema.json +78 -0
  415. package/src/workspace/records-cli.ts +20 -6
  416. package/src/workspace/records-close.schema.json +78 -0
  417. package/src/workspace/records-close.ts +31 -7
  418. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  419. package/src/workspace/records-concurrent.test.ts +145 -0
  420. package/src/workspace/records-formats.test.ts +46 -10
  421. package/src/workspace/records-new.schema.json +7 -0
  422. package/src/workspace/records-review.schema.json +78 -0
  423. package/src/workspace/records-sessions-write.test.ts +69 -0
  424. package/src/workspace/records-sessions.test.ts +22 -0
  425. package/src/workspace/records-write.ts +192 -25
  426. package/src/workspace/records.schema.json +16 -0
  427. package/src/workspace/records.ts +50 -9
  428. package/src/workspace/reference-kinds/app/package.json +10 -0
  429. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  430. package/src/workspace/run-statement.schema.json +622 -0
  431. package/src/workspace/runs-cli.ts +343 -14
  432. package/src/workspace/runs-statement.test.ts +239 -0
  433. package/src/workspace/runs-write.schema.json +416 -5
  434. package/src/workspace/runs.schema.json +451 -4
  435. package/src/workspace/runs.test.ts +39 -0
  436. package/src/workspace/runs.ts +296 -35
  437. package/src/workspace/session-kinds.ts +3 -3
  438. package/src/workspace/squash.ts +198 -0
  439. package/src/workspace/status-contract.test.ts +5 -2
  440. package/src/workspace/status-gates.ts +3 -1
  441. package/src/workspace/status.schema.json +126 -3
  442. package/src/workspace/status.ts +90 -1
  443. package/src/workspace/symbols.ts +20 -9
  444. package/src/workspace/trust/run-commits.ts +115 -0
  445. package/src/workspace/trust/run-statement.ts +226 -0
  446. package/src/workspace/trust/seal.ts +8 -2
  447. package/src/workspace/trust/verify-cli.ts +6 -5
  448. package/src/workspace/trust/verify.ts +39 -6
  449. package/src/workspace/wip-cli.ts +97 -0
  450. package/src/workspace/wip-write.schema.json +504 -0
  451. package/src/workspace/wip.schema.json +334 -0
  452. package/src/workspace/wip.test.ts +294 -0
  453. package/src/workspace/wip.ts +753 -0
  454. package/src/workspace/work-cli.ts +3 -0
  455. package/src/workspace/work-evidence.schema.json +3 -0
  456. package/src/workspace/work-size.test.ts +77 -0
  457. package/src/workspace/work-size.ts +93 -0
  458. package/src/workspace/workspace-kinds.schema.json +122 -0
  459. package/src/workspace/write-lock-cli.ts +131 -0
  460. package/src/workspace/write-lock.schema.json +172 -0
  461. package/src/workspace/write-lock.test.ts +189 -0
  462. package/src/workspace/write-lock.ts +474 -0
  463. package/src/workspace/write-scope.test.ts +16 -1
  464. package/src/workspace/write-scope.ts +33 -11
@@ -25,6 +25,8 @@ export type { ReleaseRollbackPlanArgs, ReleaseRollbackPlanResult, ReleaseRollbac
25
25
  export type { HttpCheckArgs, HttpFetch } from "./http-check";
26
26
 
27
27
  export { lifecycleSnapshot, lifecycleDiff } from "./lifecycle";
28
+ export { composeChangeSet, lifecyclePlanChangeSet, readChangeSetPart } from "./change-set";
29
+ export type { ComposeChangeSetArgs, ComposeChangeSetResult, LifecyclePlanChangeSetArgs, ReadChangeSetPartArgs, ChangeSetPartResult } from "./change-set";
28
30
  export type { LifecycleSnapshotArgs, LifecycleDiffArgs, LifecycleDiffResult } from "./lifecycle";
29
31
 
30
32
  export { chantTeardown } from "./teardown";
@@ -181,3 +183,7 @@ export type { ChangeCoverageArgs, ChangeCoverageResult } from "./change-coverage
181
183
  // loads the workspace code on first call.
182
184
  export { decide } from "./decide";
183
185
  export type { DecideArgs, DecideResult } from "./decide";
186
+
187
+ // The factory reference Op's rules and hooks (#3406, ws-087): `factoryOp` in ../factory.ts declares them.
188
+ export { factoryPick, factoryReady, factoryAsk, factoryBuild, factoryCheck, factoryRecord } from "./factory";
189
+ export type { FactoryAskResult, FactoryBuildResult, FactoryCheckResult, FactoryRecordResult, FactoryPickResult, FactoryLease } from "./factory";
@@ -7,7 +7,8 @@
7
7
 
8
8
  import { describe, expect, it } from "vitest";
9
9
  import { z } from "zod";
10
- import { activity, phase, effect } from "./builders";
10
+ import { activity, decide, phase, effect } from "./builders";
11
+ import { decideContract } from "./activities/activity-contracts";
11
12
  import type { OpConfig } from "./types";
12
13
  import {
13
14
  activityContract,
@@ -118,6 +119,25 @@ describe("validateActivitySteps() — passing Ops", () => {
118
119
  });
119
120
  });
120
121
 
122
+ describe("validateActivitySteps() — a whole-value reference in a record-typed arg (#3299)", () => {
123
+ const contracts = new Map([["decide", decideContract]]);
124
+
125
+ it("accepts decide's `inputs: step.out.json` without throwing", () => {
126
+ const fetch = activity("fetchInputs", {}, { id: "fetch" });
127
+ const ask = decide("dispatch-tier", { inputs: fetch.out.json as unknown as Record<string, unknown> });
128
+ const config = opWith({ phases: [phase("Fetch", [fetch]), phase("Decide", [ask])] });
129
+ expect(validateActivitySteps(config, contracts)).toEqual([]);
130
+ });
131
+
132
+ it("reports a symbol key in a record-typed arg as a finding instead of throwing", () => {
133
+ const step = activity("decide", { point: "dispatch-tier", inputs: { [Symbol("odd")]: 1 } });
134
+ const config = opWith({ phases: [phase("Decide", [step])] });
135
+ const issues = validateActivitySteps(config, contracts);
136
+ expect(issues.length).toBeGreaterThan(0);
137
+ expect(issues[0].message).toContain("args.inputs.Symbol(odd)");
138
+ });
139
+ });
140
+
121
141
  describe("validateActivitySteps() — the four failure classes from #1288", () => {
122
142
  it("flags an unknown profile — e.g. kubectlApply(..., { profile: \"longInfa\" })", () => {
123
143
  const step = activity("kubectlApply", { manifest: "dist/k8s.yaml" }, "longInfa" as never);
@@ -150,14 +150,22 @@ function isStepOutputRefValue(value: unknown): boolean {
150
150
  return typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[STEP_OUTPUT_REF_BRAND] === true;
151
151
  }
152
152
 
153
- /** The value at `path` (a zod issue's `.path`) inside `obj`, or `undefined` if any segment doesn't resolve. */
154
- function valueAtPath(obj: unknown, path: ReadonlyArray<PropertyKey>): unknown {
153
+ /**
154
+ * Does any prefix of `path` (a zod issue's `.path`) inside `obj` land on a
155
+ * step-output reference? A reference stands in for the whole value at its
156
+ * position, so an issue at or below it describes the placeholder, not the
157
+ * value it resolves to. "Below" matters for a whole-value reference in a
158
+ * record-typed arg (`inputs: step.out.json`, #3299): zod walks the
159
+ * reference's own keys, brand symbol included, and reports issues inside it.
160
+ */
161
+ function pathCrossesStepOutputRef(obj: unknown, path: ReadonlyArray<PropertyKey>): boolean {
155
162
  let current = obj;
156
163
  for (const segment of path) {
157
- if (current === null || typeof current !== "object") return undefined;
164
+ if (current === null || typeof current !== "object") return false;
158
165
  current = (current as Record<PropertyKey, unknown>)[segment];
166
+ if (isStepOutputRefValue(current)) return true;
159
167
  }
160
- return current;
168
+ return false;
161
169
  }
162
170
 
163
171
  /** Unwrap `ZodOptional`/`ZodNullable`/`ZodDefault` (and similar) down to the schema they wrap. */
@@ -270,16 +278,18 @@ export function validateActivitySteps(
270
278
  const parsed = contract.args.safeParse(step.args ?? {});
271
279
  if (!parsed.success) {
272
280
  for (const issue of parsed.error.issues) {
273
- // A step-output reference (#1290) sitting at this path is a
274
- // placeholder object at build time, not the value it will
275
- // resolve to — so an args-schema type mismatch here is a false
281
+ // A step-output reference (#1290) sitting at this path, or above
282
+ // it, is a placeholder object at build time, not the value it
283
+ // will resolve to — so an args-schema issue there is a false
276
284
  // positive; OPS013 (`validateStepOutputRefs`) is what validates
277
285
  // a reference, against the *producer's* declared return schema.
278
286
  // An unrecognized-key issue's path is the parent object (`[]`
279
287
  // for a top-level extra key), which is never itself a reference,
280
288
  // so a genuinely misspelled key is still caught either way.
281
- if (isStepOutputRefValue(valueAtPath(step.args, issue.path))) continue;
282
- const path = issue.path.length > 0 ? issue.path.join(".") : "(args)";
289
+ if (pathCrossesStepOutputRef(step.args, issue.path)) continue;
290
+ // String(), not join(): a zod path can hold a symbol key, and
291
+ // join() throws converting one (#3299).
292
+ const path = issue.path.length > 0 ? issue.path.map((segment) => String(segment)).join(".") : "(args)";
283
293
  issues.push({ opName: config.name, phase: phase.name, fn: step.fn, message: `args.${path}: ${issue.message}` });
284
294
  }
285
295
  }
@@ -18,6 +18,7 @@ import type { PolicyGateArgs } from "./activities/policy";
18
18
  import type { GuardValidateArgs } from "./activities/guard-validate";
19
19
  import type { WorkEvidenceArgs } from "./activities/work-evidence";
20
20
  import type { DecideArgs } from "./activities/decide";
21
+ import type { ComposeChangeSetArgs, LifecyclePlanChangeSetArgs, ReadChangeSetPartArgs } from "./activities/change-set";
21
22
  import { isValidCronExpression, cronSyntaxMessage } from "./cron";
22
23
 
23
24
  /** An `activity()` result — the plain `ActivityStep` shape plus the `.out` reference sugar (#1290). */
@@ -398,6 +399,28 @@ export const releasePlan = (args: WithStepRefs<ReleasePlanArgs> & StepOpts): Nam
398
399
  return activity("releasePlan", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
399
400
  };
400
401
 
402
+ /**
403
+ * Join members' change-set parts into one document (#3181). Its `digest` is
404
+ * what a combined run's gate binds: `gate("approve", { plan:
405
+ * changeSet.out.digest })`. Defaults to the `fastIdempotent` profile.
406
+ */
407
+ export const composeChangeSet = (args: WithStepRefs<ComposeChangeSetArgs> & StepOpts): NamedActivityStep => {
408
+ const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
409
+ return activity("composeChangeSet", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
410
+ };
411
+
412
+ /** A chant member's change-set part, from `chant lifecycle plan <env> --json` (#3181). Its `part` feeds {@link composeChangeSet}. */
413
+ export const lifecyclePlanChangeSet = (args: WithStepRefs<LifecyclePlanChangeSetArgs> & StepOpts): NamedActivityStep => {
414
+ const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
415
+ return activity("lifecyclePlanChangeSet", rest, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
416
+ };
417
+
418
+ /** A member's change-set part from a plan file a chant or warden run wrote (#3181). Its `part` feeds {@link composeChangeSet}. */
419
+ export const readChangeSetPart = (args: WithStepRefs<ReadChangeSetPartArgs> & StepOpts): NamedActivityStep => {
420
+ const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
421
+ return activity("readChangeSetPart", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
422
+ };
423
+
401
424
  /**
402
425
  * Record a release in the release ledger with its plan (#2782, ws-055), once:
403
426
  * a retry whose release the ledger already names records nothing. Defaults to
@@ -6,9 +6,13 @@
6
6
  * POST <url>/v1/systemone Authorization: Bearer <key>
7
7
  * { "model": "jev-1.13.0", "state": {...}, "questions": { "<name>": { "type": "noul" | "choice" | "score", "instructions": ..., "criteria": ... } } }
8
8
  *
9
- * { "model": "jev-1.13.0", "answers": { "<name>": { "type": "noul", "noul": 0.22 } | { "type": "choice", "choice": ..., "probabilities": {...}, "confidence": ... } | { "type": "score", ... } } }
9
+ * { "model": "jev-1.13.0", "answers": { "<name>": { "type": "noul", "noul": 0.22, "reason"?: ... } | { "type": "choice", "choice": ..., "probabilities": {...}, "confidence": ..., "reason"?: ... } | { "type": "score", ..., "reason"?: ... } } }
10
10
  * ```
11
11
  *
12
+ * `reason` is optional: the model's own explanation of its answer (#3345).
13
+ * Core keeps it in the answer record beside a proposal, or as `model_reason`
14
+ * in the escalation entry of an answer below the threshold.
15
+ *
12
16
  * {@link backendAsk} turns a set of backends into core's `ModelAsk`, the
13
17
  * model call `askPoint` takes (`../workspace/points.ts`). This file
14
18
  * only speaks the wire format: whether an answer is observed, proposed or
@@ -10,10 +10,12 @@
10
10
  *
11
11
  * Read by id or name here:
12
12
  *
13
- * record, decision, work-item a record, by id, from the declared record kinds (`records --json`)
13
+ * record, decision, work-item a record, by id, from the declared record kinds (`records --json`);
14
+ * a work item also has its size, `criteria`, `files`, `words` and
15
+ * `fits_<tier>`, for the slice-tier point (`../workspace/work-size.ts`, #3150)
14
16
  * member a member, by name (`ls --json`)
15
17
  *
16
- * The other outputs a point may name (finding, region, commit, gate, release,
18
+ * The other outputs a point may name (finding, region, commit, node, gate, release,
17
19
  * environment, component) are passed in `inputs`, as the output printed them.
18
20
  * These reads never call a model, and never write. The module loads code
19
21
  * under `workspace/`, so the `decide` activity imports it on first call.
@@ -22,6 +24,11 @@
22
24
  import { declaredKindFiles, queryDeclaredRecords } from "../workspace/records-cli";
23
25
  import { listWorkspace } from "../workspace/ls";
24
26
  import { inputOutput } from "../workspace/points";
27
+ import { readFileSync } from "node:fs";
28
+ import { join } from "node:path";
29
+ import { bodyText, loadRecordKind } from "../workspace/records";
30
+ import { locateWorkspace } from "../workspace/which-chant";
31
+ import { DEFAULT_TIER_LIMITS, measureWorkItem, sizeFields } from "../workspace/work-size";
25
32
 
26
33
  /** The outputs {@link readInputs} reads by id or name. */
27
34
  export const READABLE_OUTPUTS = ["record", "decision", "work-item", "member"] as const;
@@ -45,19 +52,40 @@ export function fieldOf(value: unknown, path: string): unknown {
45
52
  }
46
53
 
47
54
  async function readRecord(id: string, cwd: string, output: string): Promise<Record<string, unknown>> {
48
- const set = await queryDeclaredRecords(declaredKindFiles(cwd), { cwd });
55
+ const files = declaredKindFiles(cwd);
56
+ const set = await queryDeclaredRecords(files, { cwd });
49
57
  const unread: string[] = [];
50
- for (const doc of set.kinds) {
58
+ for (const [i, doc] of set.kinds.entries()) {
51
59
  if ("error" in doc) {
52
60
  unread.push(`${doc.declared.path}: ${doc.error.code}`);
53
61
  continue;
54
62
  }
55
63
  const found = doc.records.find((r) => r.id === id);
56
- if (found) return found as unknown as Record<string, unknown>;
64
+ if (found) {
65
+ const record = found as unknown as Record<string, unknown>;
66
+ // A work item's size, for the slice-tier point (#3150): criteria, files, words and fits_<tier>.
67
+ return output === "work-item" ? { ...record, ...(await workItemSizeFields(files[i].file, found.path, found.data, cwd)) } : record;
68
+ }
57
69
  }
58
70
  throw new InputReadError(`no declared record kind holds a record with id ${id} (read for ${output})${unread.length ? `; not read: ${unread.join(", ")}` : ""}`);
59
71
  }
60
72
 
73
+ /** The size fields of a work item of the kind at `kindFile`, measured from its record file. Empty for a kind with no work block. */
74
+ async function workItemSizeFields(kindFile: string, path: string, data: Record<string, unknown> | null, cwd: string): Promise<Record<string, unknown>> {
75
+ const loaded = await loadRecordKind(kindFile, cwd);
76
+ const work = loaded.kind.work;
77
+ if (!work) return {};
78
+ const located = locateWorkspace(cwd);
79
+ let body = "";
80
+ try {
81
+ body = bodyText(readFileSync(join(located.top ?? located.rootOnDisk, ...path.split("/")), "utf-8"));
82
+ } catch {
83
+ // A record that can't be read again is measured from its front matter alone.
84
+ }
85
+ const size = measureWorkItem(data, body, work.acceptance?.field ?? "acceptance");
86
+ return sizeFields(size, work.tier?.limits ?? DEFAULT_TIER_LIMITS);
87
+ }
88
+
61
89
  function readMember(name: string, cwd: string): Record<string, unknown> {
62
90
  const doc = listWorkspace({ cwd });
63
91
  if ("error" in doc) throw new InputReadError(`the workspace can't be listed: ${doc.error.code}: ${doc.error.message}`);
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The factory reference Op (#3406, ws-087), run through the local executor
3
+ * with stub hooks on a small workspace that carries the reference
4
+ * workspace's decision, work and answer kinds and its points: one item from
5
+ * pick to done, one retry after a failed build, and one ask the understand
6
+ * point refuses. The rules themselves are tested in
7
+ * ../workspace/factory-rules.test.ts.
8
+ */
9
+
10
+ import { spawnSync } from "node:child_process";
11
+ import { cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { describe, expect, test } from "vitest";
14
+ import { withTestDir } from "@intentius/chant-test-utils";
15
+ import * as coreActivities from "./activities";
16
+ import type { ActivityFn } from "./activity-registry";
17
+ import { ACTIVITY_PROFILES } from "./activity-profiles";
18
+ import { factoryOpConfig } from "./factory";
19
+ import { OpRunFailure, runOpLocally, type OpRunResult } from "./local-executor";
20
+ import { answerPoint } from "../workspace/decide";
21
+ import { workspacePoints } from "../workspace/points-cli";
22
+ import { amendRecord } from "../workspace/records-write";
23
+ import { parseFrontMatter } from "../workspace/records";
24
+
25
+ const REF = join(import.meta.dirname, "..", "..", "..", "..", "reference-workspace");
26
+
27
+ const activities = new Map<string, ActivityFn>(Object.entries(coreActivities).filter(([, v]) => typeof v === "function") as [string, ActivityFn][]);
28
+
29
+ function git(args: string[], cwd: string): string {
30
+ const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
31
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
32
+ return r.stdout.trim();
33
+ }
34
+
35
+ const item = (id: string, fields: Record<string, unknown>, body = "") =>
36
+ `---\n${JSON.stringify({ schema: 1, id, title: `Item ${id}`, state: "open", implements: [], needs: [], constrains: ["member:app"], evidence: [], opened_on: "2026-10-01", source: { kind: "workspace", member: "app" }, supersedes: [], ...fields }, null, 2)}\n---\n\n# Item ${id}\n${body}`;
37
+
38
+ /**
39
+ * The builder stub: writes app/<item>.txt, and fails while .fail-<item> exists in the checkout
40
+ * the test controls (FACTORY_FAIL_DIR), so a retry can succeed.
41
+ */
42
+ const BUILDER = `
43
+ const fs = require("node:fs");
44
+ const item = process.env.FACTORY_ITEM;
45
+ if (fs.existsSync(require("node:path").join(process.env.FACTORY_FAIL_DIR, ".fail-" + item))) { console.error("the build broke"); process.exit(1); }
46
+ fs.mkdirSync("app", { recursive: true });
47
+ fs.writeFileSync("app/" + item + ".txt", "built at tier " + process.env.FACTORY_TIER + "\\n");
48
+ `;
49
+ const CHECK = `const fs = require("node:fs"); process.exit(fs.existsSync("app/" + process.env.FACTORY_ITEM + ".txt") ? 0 : 1);`;
50
+
51
+ function workspace(dir: string): void {
52
+ git(["init", "-q", "-b", "main"], dir);
53
+ git(["config", "user.email", "t@chant.dev"], dir);
54
+ git(["config", "user.name", "T"], dir);
55
+ git(["config", "commit.gpgsign", "false"], dir);
56
+ for (const d of ["decisions", "work", "answers"]) mkdirSync(join(dir, d), { recursive: true });
57
+ for (const f of ["decisions/decision.kind.mjs", "decisions/decision.schema.json", "decisions/points.json", "work/work.kind.mjs", "work/work.schema.json", "answers/answer.kind.mjs", "answers/answer.schema.json"]) {
58
+ cpSync(join(REF, f), join(dir, f));
59
+ }
60
+ writeFileSync(
61
+ join(dir, "chant.workspace.json"),
62
+ JSON.stringify({
63
+ name: "factory",
64
+ schema: 1,
65
+ records: [{ kind: "decisions/decision.kind.mjs" }, { kind: "work/work.kind.mjs" }, { kind: "answers/answer.kind.mjs" }],
66
+ members: [{ name: "app", dir: "app", kind: "other", because: "the app" }],
67
+ }),
68
+ );
69
+ mkdirSync(join(dir, "app"), { recursive: true });
70
+ writeFileSync(join(dir, "app", "README.md"), "# app\n");
71
+ mkdirSync(join(dir, "hooks"), { recursive: true });
72
+ writeFileSync(join(dir, "hooks", "builder.cjs"), BUILDER);
73
+ writeFileSync(join(dir, "hooks", "check.cjs"), CHECK);
74
+ writeFileSync(join(dir, "work", "W-101-the-feature.md"), item("W-101", { acceptance: [{ id: "AC-1", text: "app/W-101.txt exists", verification: "unit" }] }));
75
+ writeFileSync(join(dir, "work", "W-102-the-retried-one.md"), item("W-102", { acceptance: [{ id: "AC-1", text: "app/W-102.txt exists", verification: "unit" }] }));
76
+ writeFileSync(join(dir, "work", "W-103-an-ask.md"), item("W-103", { state: "proposed", source: { ask: { said: "Make the page purple.", by: "alice", via: "hud" } } }));
77
+ git(["add", "-A"], dir);
78
+ git(["commit", "-q", "-m", "the workspace"], dir);
79
+ }
80
+
81
+ async function run(dir: string, work?: string, extra: Partial<Parameters<typeof factoryOpConfig>[0]> = {}): Promise<OpRunResult> {
82
+ const config = factoryOpConfig({ cwd: dir, backends: { systemone: { url: "http://127.0.0.1:9", timeoutMs: 200 } }, builder: `${process.execPath} hooks/builder.cjs`, check: `${process.execPath} hooks/check.cjs`, ...extra });
83
+ const prev = process.env.FACTORY_FAIL_DIR;
84
+ process.env.FACTORY_FAIL_DIR = dir;
85
+ try {
86
+ return await runOpLocally(config, activities, ACTIVITY_PROFILES, undefined, { cwd: dir, ledger: { cwd: dir }, ...(work ? { work: { item: work } } : {}) });
87
+ } catch (err) {
88
+ if (err instanceof OpRunFailure) return err.result;
89
+ throw err;
90
+ } finally {
91
+ if (prev === undefined) delete process.env.FACTORY_FAIL_DIR;
92
+ else process.env.FACTORY_FAIL_DIR = prev;
93
+ }
94
+ }
95
+
96
+ const onBranch = (dir: string, id: string, path: string) => {
97
+ const fm = parseFrontMatter(git(["show", `chant/work/${id}:${path}`], dir));
98
+ if (!fm.ok) throw new Error(fm.message);
99
+ return fm.value;
100
+ };
101
+
102
+ describe("the factory reference Op (#3406)", () => {
103
+ test("builds an item from pick to done, retries a failed build only on a person's request, and drops an ask understand refuses", async () => {
104
+ await withTestDir(async (dir) => {
105
+ workspace(dir);
106
+ writeFileSync(join(dir, ".fail-W-102"), "");
107
+
108
+ // W-101: picked first, tier from slice-tier's table, built, checked, recorded done on its branch.
109
+ const first = await run(dir);
110
+ expect(first.workLease).toMatchObject({ item: "W-101", released: true, outcome: "done" });
111
+ const w101 = onBranch(dir, "W-101", "work/W-101-the-feature.md");
112
+ expect(w101).toMatchObject({ state: "done", result: { lease: first.workLease!.token } });
113
+ expect(git(["show", "chant/work/W-101:app/W-101.txt"], dir)).toBe("built at tier small");
114
+ const msg = git(["log", "-1", "--format=%B", "chant/work/W-101"], dir);
115
+ expect(msg).toContain("Chant-Record: work:W-101");
116
+ expect(msg).toContain(`Chant-Lease: ${first.workLease!.token}`);
117
+
118
+ // W-101 waits to be applied, so W-102 is next; its builder fails, and the attempt is kept.
119
+ const second = await run(dir);
120
+ expect(second.workLease).toMatchObject({ item: "W-102", outcome: "not_done" });
121
+ const failed = second.workLease!.token!;
122
+
123
+ // Neither W-101 (built, not applied) nor W-102 (failed, no retry) is picked; W-103 is an ask,
124
+ // and understand escalates it to people, so the run waits on the question.
125
+ const third = await run(dir);
126
+ expect(third.workLease).toMatchObject({ item: "W-103", outcome: "waiting" });
127
+
128
+ // A person asks for W-102 again, naming the failed build; the next run builds it.
129
+ const retry = await amendRecord({ kind: join(dir, "work", "work.kind.mjs"), id: "W-102", fields: JSON.stringify({ retry: { after: failed, by: "alice", at: "2026-10-03T10:00:00Z" } }), cwd: dir });
130
+ expect("error" in retry).toBe(false);
131
+ git(["add", "-A"], dir);
132
+ git(["commit", "-q", "-m", "retry W-102"], dir);
133
+ unlinkSync(join(dir, ".fail-W-102"));
134
+ const fourth = await run(dir, "W-102");
135
+ expect(fourth.workLease).toMatchObject({ item: "W-102", outcome: "done" });
136
+
137
+ // The understand question about W-103 is answered refuse; the next run drops it on its branch.
138
+ const points = await workspacePoints({ cwd: dir });
139
+ if ("error" in points) throw new Error(points.error.message);
140
+ const open = points.questions.find((q) => q.point === "understand" && q.open);
141
+ expect(open).toBeDefined();
142
+ const answered = await answerPoint({ cwd: dir, id: open!.id, answer: "refuse", by: ["alice"] });
143
+ expect("error" in answered).toBe(false);
144
+ git(["add", "-A"], dir);
145
+ git(["commit", "-q", "-m", "refuse W-103"], dir);
146
+ const fifth = await run(dir, "W-103");
147
+ expect(fifth.workLease).toMatchObject({ item: "W-103", outcome: "dropped" });
148
+ expect(onBranch(dir, "W-103", "work/W-103-an-ask.md")).toMatchObject({ state: "dropped" });
149
+ expect(existsSync(join(dir, "app", "W-103.txt"))).toBe(false);
150
+ expect(readFileSync(join(dir, "work", "W-101-the-feature.md"), "utf-8")).toContain('"state": "open"');
151
+ });
152
+ }, 120_000);
153
+ });
154
+
155
+ describe("the factory Op's seams for an orchestrator (studio#382)", () => {
156
+ test("prepare runs before Pick, an uncommitted item is carried into the worktree, the reports pass through, the check ticks each criterion by its own result, and after sees the outcome", async () => {
157
+ await withTestDir(async (dir) => {
158
+ workspace(dir);
159
+ // A builder that reports a run and a check that reports per criterion.
160
+ writeFileSync(join(dir, "hooks", "builder2.cjs"), `${BUILDER}\nconsole.log(JSON.stringify({ ok: true, run: { id: "run-7" }, agent: "builder-small", chantAgent: "factory", records: ["contract:C-1"] }));\n`);
161
+ writeFileSync(join(dir, "hooks", "check2.cjs"), `console.log(JSON.stringify({ criteria: { "AC-1": "pass", "AC-2": "fail" }, built: JSON.parse(process.env.FACTORY_BUILD).agent }));`);
162
+ writeFileSync(join(dir, "hooks", "after.cjs"), `require("node:fs").writeFileSync(require("node:path").join(process.env.FACTORY_FAIL_DIR, "after.json"), JSON.stringify({ outcome: process.env.FACTORY_OUTCOME, commit: process.env.FACTORY_COMMIT, reason: process.env.FACTORY_REASON, check: JSON.parse(process.env.FACTORY_CHECK) }));`);
163
+ // The prepare hook writes W-110 in the checkout and leaves it uncommitted; the others are taken out of the way.
164
+ writeFileSync(
165
+ join(dir, "hooks", "prepare.cjs"),
166
+ `require("node:fs").writeFileSync("work/W-110-prepared.md", ${JSON.stringify(item("W-110", { acceptance: [{ id: "AC-1", text: "one", verification: "unit" }, { id: "AC-2", text: "two", verification: "unit" }] }))});`,
167
+ );
168
+ for (const id of ["W-101", "W-102"]) writeFileSync(join(dir, ".fail-" + id), "");
169
+ git(["add", "-A"], dir);
170
+ git(["commit", "-q", "-m", "hooks"], dir);
171
+ const result = await run(dir, "W-110", {
172
+ kind: "./work/work.kind.mjs",
173
+ prepare: `${process.execPath} hooks/prepare.cjs`,
174
+ builder: `${process.execPath} hooks/builder2.cjs`,
175
+ check: `${process.execPath} hooks/check2.cjs`,
176
+ after: `${process.execPath} hooks/after.cjs`,
177
+ });
178
+ // AC-2 failed its own check, so the item is not done, though the check exited 0.
179
+ expect(result.workLease).toMatchObject({ item: "W-110", outcome: "not_done" });
180
+ const after = JSON.parse(readFileSync(join(dir, "after.json"), "utf-8"));
181
+ expect(after).toMatchObject({ outcome: "not_done", reason: "criteria AC-2 have no passing evidence", check: { built: "builder-small" } });
182
+ // The attempt was committed with why, and the run kept that commit.
183
+ expect(after.commit).toMatch(/^[0-9a-f]{40}$/);
184
+ // The kept attempt holds the carried item with AC-1 ticked and AC-2 failed.
185
+ const kept = git(["for-each-ref", "--format=%(refname)", "refs/chant/kept/"], dir).split("\n").find((r) => r.includes("W-110"))!;
186
+ expect(git(["rev-parse", kept], dir)).toBe(after.commit);
187
+ expect(git(["log", "-1", "--format=%B", kept], dir)).toMatch(/^W-110: not done\n\nNot done: criteria AC-2 have no passing evidence\n\nChant-Agent: factory\nChant-Lease: /);
188
+ const fm = parseFrontMatter(git(["show", `${kept}:work/W-110-prepared.md`], dir));
189
+ if (!fm.ok) throw new Error(fm.message);
190
+ const results = Object.fromEntries((fm.value.evidence as { criterion: string; result: string }[]).map((e) => [e.criterion, e.result]));
191
+ expect(results).toEqual({ "AC-1": "pass", "AC-2": "fail" });
192
+
193
+ // With both criteria passing, the item is done and its commit carries the builder's run.
194
+ writeFileSync(join(dir, "hooks", "check2.cjs"), `console.log(JSON.stringify({ criteria: { "AC-1": "pass", "AC-2": "pass" } }));`);
195
+ git(["add", "-A"], dir);
196
+ git(["commit", "-q", "-m", "a passing check"], dir);
197
+ const { readLeaseHistory } = await import("../lifecycle/work-lease");
198
+ const failed = (await readLeaseHistory("W-110", { cwd: dir })).records.filter((r) => r.event === "claim").at(-1) as { token: string };
199
+ const amended = await amendRecord({ kind: join(dir, "work", "work.kind.mjs"), id: "W-110", fields: JSON.stringify({ retry: { after: failed.token, by: "alice" } }), cwd: dir });
200
+ expect("error" in amended).toBe(false);
201
+ const done = await run(dir, "W-110", { builder: `${process.execPath} hooks/builder2.cjs`, check: `${process.execPath} hooks/check2.cjs`, after: `${process.execPath} hooks/after.cjs` });
202
+ expect(done.workLease).toMatchObject({ item: "W-110", outcome: "done" });
203
+ const body = git(["log", "-1", "--format=%B", "chant/work/W-110"], dir);
204
+ expect(body).toContain("Chant-Run: run-7");
205
+ expect(body).toContain("Chant-Agent: factory");
206
+ expect(body).toContain("Chant-Record: work:W-110\nChant-Record: contract:C-1");
207
+ expect(JSON.parse(readFileSync(join(dir, "after.json"), "utf-8")).outcome).toBe("done");
208
+ });
209
+ }, 120_000);
210
+ });
211
+
212
+ describe("an ask's item is the builder's only for its plan (studio#382)", () => {
213
+ test("a builder that changes an open ask beyond its acceptance criteria has the item put back, and the build is not done", async () => {
214
+ await withTestDir(async (dir) => {
215
+ workspace(dir);
216
+ for (const id of ["W-101", "W-102"]) writeFileSync(join(dir, ".fail-" + id), "");
217
+ writeFileSync(join(dir, "work", "W-120-an-open-ask.md"), item("W-120", { source: { ask: { said: "Add a page.", by: "alice", via: "hud" } } }));
218
+ writeFileSync(
219
+ join(dir, "hooks", "rewrite.cjs"),
220
+ `const fs = require("node:fs"); const f = "work/W-120-an-open-ask.md"; fs.writeFileSync(f, fs.readFileSync(f, "utf8").replace('"title": "Item W-120"', '"title": "Renamed"')); fs.writeFileSync("app/W-120.txt", "the page\\n");`,
221
+ );
222
+ git(["add", "-A"], dir);
223
+ git(["commit", "-q", "-m", "an open ask"], dir);
224
+ const result = await run(dir, "W-120", { builder: `${process.execPath} hooks/rewrite.cjs` });
225
+ expect(result.workLease).toMatchObject({ item: "W-120", outcome: "not_done" });
226
+ const kept = git(["for-each-ref", "--format=%(refname)", "refs/chant/kept/W-120/"], dir);
227
+ expect(git(["show", `${kept}:work/W-120-an-open-ask.md`], dir)).toContain('"title": "Item W-120"');
228
+ expect(git(["log", "-1", "--format=%B", kept], dir)).toContain("the guard put back what the builder changed out of scope: work/W-120-an-open-ask.md (it changed title, and only acceptance is the builder's)");
229
+ });
230
+ }, 120_000);
231
+ });
232
+
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The factory reference Op (#3406, ws-087): the factory's rules declared in
3
+ * core, and an orchestrator's execution plugged in through hooks.
4
+ *
5
+ * ```ts
6
+ * // ops/factory.op.ts
7
+ * export default factoryOp({
8
+ * builder: "node box/ops/factory/builder.mjs",
9
+ * });
10
+ * ```
11
+ *
12
+ * One run is one build of one work item, under that item's lease, in a
13
+ * worktree on `chant/work/<item>`:
14
+ *
15
+ * Pick factoryPick: the buildable items, in order; the lease claims the first nobody holds
16
+ * Ask factoryAsk: the tier (the item's own, or the slice-tier point) and, for an ask, the
17
+ * understand point; an open question stops the run waiting
18
+ * Build factoryBuild: the `builder` hook, unless understand said not to build
19
+ * Check factoryCheck: the `check` hook, or the box's `factory.check`, with its evidence
20
+ * Record factoryRecord: done, dropped, redraft, ask or not_done, which the lease's release
21
+ * takes; a not_done attempt is kept at refs/chant/kept/<item>/<token> by the run
22
+ *
23
+ * The hooks are commands, run with no shell in the worktree:
24
+ *
25
+ * - `builder` (required): how a builder runs, such as Claude Code in the box
26
+ * or a `fountainRun` wrapper. It gets FACTORY_ITEM, FACTORY_TIER,
27
+ * FACTORY_TOKEN, FACTORY_HOLDER, FACTORY_WORKTREE and FACTORY_CONTEXT, edits
28
+ * the worktree, and exits 0 when it finished. It may print a last JSON line
29
+ * `{ "reverted": [paths] }` when its guard put changes back.
30
+ * - `context` (optional): passed to the builder as FACTORY_CONTEXT, the prompt
31
+ * and context bundle the orchestrator assembles.
32
+ * - `check` (optional): the verdict command; the box block's `factory.check`
33
+ * when left out. An infra profile names a plan or `chant lint` here.
34
+ *
35
+ * Publishing a done item is the box's publisher (ws-088), run later by a
36
+ * person or a surface with `chant workspace box publish`; it is not a step.
37
+ */
38
+
39
+ import { Op, phase, activity, shell, type NamedActivityStep } from "./builders";
40
+ import { stepOutput } from "./step-output-ref";
41
+ import { workLeaseOutput } from "./work-lease-run";
42
+ import type { OpConfig } from "./types";
43
+
44
+ export interface FactoryOpOptions {
45
+ /** The Op's name. `factory` by default. */
46
+ name?: string;
47
+ /** The builder hook: a command run in the run's worktree. */
48
+ builder: string;
49
+ /** The context hook: what the builder receives as FACTORY_CONTEXT. */
50
+ context?: string;
51
+ /** The check hook; the box's `factory.check` when left out. It may print `{ "criteria": { "<id>": "pass" | "fail" } }` as its last line to tick each criterion by its own result. */
52
+ check?: string;
53
+ /** The prepare hook: a command run in the workspace before Pick, such as opening the first build's item once the box's intent is decided. */
54
+ prepare?: string;
55
+ /** The after hook: a command run in the worktree once the outcome is recorded, with FACTORY_OUTCOME and FACTORY_COMMIT, such as ending the build's run record. */
56
+ after?: string;
57
+ /** The work kind file, when the declaration names several. */
58
+ kind?: string;
59
+ /** How long a lease is held without a renewal, such as 15m. The lease's default otherwise. */
60
+ ttl?: string;
61
+ /** How long the builder may run. 2h by default. */
62
+ buildTimeout?: string;
63
+ schedule?: OpConfig["schedule"];
64
+ /** Where the workspace is read; the run's working directory when left out. */
65
+ cwd?: string;
66
+ /** Backends for the points' model deciders, in place of `decide.backends` in chant.config. */
67
+ backends?: Record<string, { url: string; key?: unknown; timeoutMs?: number }>;
68
+ }
69
+
70
+ /** The Op's configuration, for a steward that lists it or a test that runs it. */
71
+ export function factoryOpConfig(options: FactoryOpOptions): OpConfig {
72
+ if (!options || typeof options.builder !== "string" || options.builder.trim() === "") {
73
+ throw new Error("factoryOp needs a builder: the command that runs a builder in the run's worktree");
74
+ }
75
+ const lease = workLeaseOutput();
76
+ const kind = options.kind !== undefined ? { kind: options.kind } : {};
77
+ const at = options.cwd !== undefined ? { cwd: options.cwd } : {};
78
+ return {
79
+ name: options.name ?? "factory",
80
+ overview: "Build the next buildable work item under its lease: pick, ask, build, check and record, by the factory's rules (#3406, ws-087)",
81
+ workLease: {
82
+ item: stepOutput("pick", "candidates"),
83
+ outcome: stepOutput("record", "outcome"),
84
+ ...kind,
85
+ ...(options.ttl !== undefined ? { ttl: options.ttl } : {}),
86
+ },
87
+ changesCheckout: true,
88
+ ...(options.schedule ? { schedule: options.schedule } : {}),
89
+ phases: [
90
+ ...(options.prepare !== undefined ? [phase("Prepare", [shell(options.prepare, { ...(options.cwd !== undefined ? { cwd: options.cwd } : {}), id: "prepare" })])] : []),
91
+ phase("Pick", [activity("factoryPick", { ...kind, ...at }, { profile: "fastIdempotent", id: "pick" })]),
92
+ phase("Ask", [activity("factoryAsk", { lease, ...kind, ...at, ...(options.backends ? { backends: options.backends } : {}) }, { profile: "fastIdempotent", id: "ask" })]),
93
+ phase("Build", [
94
+ activity(
95
+ "factoryBuild",
96
+ { lease, ...at, ask: stepOutput("ask"), builder: options.builder, ...(options.context !== undefined ? { context: options.context } : {}) },
97
+ { profile: "atMostOnce", id: "build", timeout: options.buildTimeout ?? "2h" },
98
+ ),
99
+ ]),
100
+ phase("Check", [activity("factoryCheck", { lease, ...at, build: stepOutput("build"), ...(options.check !== undefined ? { check: options.check } : {}) }, { profile: "atMostOnce", id: "check" })]),
101
+ phase("Record", [activity("factoryRecord", { lease, ...at, ask: stepOutput("ask"), build: stepOutput("build"), check: stepOutput("check"), ...(options.after !== undefined ? { after: options.after } : {}) }, { profile: "atMostOnce", id: "record" })]),
102
+ ],
103
+ } as OpConfig;
104
+ }
105
+
106
+ /** The factory reference Op, as an `*.op.ts` file exports it. */
107
+ export function factoryOp(options: FactoryOpOptions): ReturnType<typeof Op> {
108
+ return Op(factoryOpConfig(options));
109
+ }
110
+
111
+ /**
112
+ * The steward's ready step for the factory Op (#382): the keys of the work buildable now, for
113
+ * `declareSteward({ beside: [{ op: factory, ready: factoryReady({ cwd }) }] })`. `also` names a
114
+ * command printing a JSON list of more keys, for work the pick can't see yet.
115
+ */
116
+ export function factoryReady(options: { cwd?: string; kind?: string; also?: string } = {}): NamedActivityStep {
117
+ return activity("factoryReady", { ...options }, { profile: "fastIdempotent" });
118
+ }
package/src/op/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Op, phase, activity, gate, effect, build, kubectlApply, helmInstall, helmInstallPinned, waitForStack, waitForReady,
2
- gitlabPipeline, lifecycleSnapshot, shell, sourceArchive, releasePlan, releaseRecord, releaseRollbackPlan, releaseRollbackRecord, decide, ensureSecret, teardown, envTeardown, k3dUp, k3dDown,
2
+ gitlabPipeline, lifecycleSnapshot, shell, sourceArchive, releasePlan, releaseRecord, composeChangeSet, lifecyclePlanChangeSet, readChangeSetPart, releaseRollbackPlan, releaseRollbackRecord, decide, ensureSecret, teardown, envTeardown, k3dUp, k3dDown,
3
3
  k3sInstall, k3sUninstall, flociUp, flociDown,
4
4
  flociAzUp, flociAzDown, flociGcpUp, flociGcpDown, httpCheck,
5
5
  azGroupEnsure, azGroupDelete, azApply, azDelete, awsApply, awsDelete, gcpApply, gcpDelete, policyGate,
@@ -149,3 +149,11 @@ export type { ChangeSignalGate, ChangeSignalGateOptions, WakeReason } from "./ch
149
149
  export { classifyOpVerbClass, isGated } from "./op-verb-class";
150
150
  export type { OpVerbClass } from "./op-verb-class";
151
151
  export { takeProfileAndId } from "./builders";
152
+
153
+ // The factory reference Op (#3406, ws-087).
154
+ export { factoryOp, factoryOpConfig, factoryReady } from "./factory";
155
+ // The factory's rules themselves (#3406) are values in workspace code, which no level 0 command may load (#2526), so only their
156
+ // types are re-exported here. An orchestrator's own tools, such as a retry command a UI runs, import the values from
157
+ // `@intentius/chant/workspace/factory-rules`.
158
+ export type { FactoryItem, FactoryContext, FactoryClaim, FactoryHold, RetryState } from "../workspace/factory-rules";
159
+ export type { FactoryOpOptions } from "./factory";
@@ -325,3 +325,12 @@ describe("op.json IR", () => {
325
325
  expect("path" in (deployStep.args.context as object)).toBe(false);
326
326
  });
327
327
  });
328
+
329
+ describe("neverOverMcp (chant#3447)", () => {
330
+ it("survives the IR round trip, and is absent when not declared", () => {
331
+ const base = { name: "tf-apply", overview: "apply", phases: [] };
332
+ expect(buildOpIR({ ...base, neverOverMcp: true }).neverOverMcp).toBe(true);
333
+ expect(buildOpIR(base)).not.toHaveProperty("neverOverMcp");
334
+ expect(opConfigFromIR(buildOpIR({ ...base, neverOverMcp: true })).neverOverMcp).toBe(true);
335
+ });
336
+ });