@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
@@ -33,19 +33,28 @@
33
33
  * `--resume` carries the earlier attempt's progress, `remainingFanOut` narrows
34
34
  * the plan before the gate is decided, and the digest is carried rather than
35
35
  * recomputed, so the approval still stands.
36
+ *
37
+ * `--wave-gate <name>` (#3049) swaps the one gate for one per wave: the same
38
+ * repeated command stops at each wave's gate in turn, each planned after the
39
+ * wave before it applied, and `chant approve fan-out <name>-wave-<n>` answers
40
+ * it. `--wave <n>` runs one wave, for a CI job per wave.
36
41
  */
37
42
 
38
43
  import { resolve, dirname } from "node:path";
39
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
44
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
40
45
  import { loadChantConfig, resolveAutoReleaseDisabled, type ChantConfig } from "../../config";
41
46
  import { samePlanDigest } from "../../lifecycle/plan-digest";
42
47
  import { affectedStacks } from "../../lifecycle/affected";
48
+ import { changedFilesBetween, lexiconChangedUnits } from "../../components/changed-files";
43
49
  import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
44
- import { runFanOut } from "../../components/fan-out-run";
50
+ import { EarlierWaveNotAppliedError, runFanOut } from "../../components/fan-out-run";
51
+ import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt } from "../../components/fan-out-record";
52
+ import { describeChangedWave, withWaveRecord, type WaveRecord } from "../../gated-waves";
45
53
  import { renderFanOutHuman, renderFanOutJson, renderFanOutPlan } from "../../components/fan-out-output";
46
54
  import { ndjsonProgressSink } from "../../components/run-progress";
47
55
  import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
48
56
  import { approveCommand } from "../../op/gate";
57
+ import { getHeadCommit } from "../../lifecycle/git";
49
58
  import { remainingFanOut, type ChangedUnits, type FanOutProgress } from "../../components/fan-out";
50
59
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
51
60
  import { formatError, formatInfo, formatWarning } from "../format";
@@ -54,37 +63,6 @@ import { resolveBuildRoot } from "./lifecycle";
54
63
  import { GATED_EXIT_CODE, recordAutoReleasesForRun } from "./run";
55
64
  import type { CommandContext } from "../registry";
56
65
 
57
-
58
-
59
- /**
60
- * What an attempt left behind, and what the next one reads (`--resume`).
61
- *
62
- * The digest is stored alongside the progress because progress is only
63
- * meaningful for the plan it was made against. A fan-out derived from different
64
- * source is a different fan-out, and carrying "cluster-a already applied" into
65
- * it would be a claim about work nobody did.
66
- */
67
- interface FanOutAttempt {
68
- digest: string;
69
- completed: string[];
70
- failed: string[];
71
- }
72
-
73
- function readAttempt(path: string): FanOutAttempt | undefined {
74
- if (!existsSync(path)) return undefined;
75
- const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<FanOutAttempt>;
76
- return {
77
- digest: typeof parsed.digest === "string" ? parsed.digest : "",
78
- completed: Array.isArray(parsed.completed) ? parsed.completed.filter((n): n is string => typeof n === "string") : [],
79
- failed: Array.isArray(parsed.failed) ? parsed.failed.filter((n): n is string => typeof n === "string") : [],
80
- };
81
- }
82
-
83
- function writeAttempt(path: string, attempt: FanOutAttempt): void {
84
- mkdirSync(dirname(path), { recursive: true });
85
- writeFileSync(path, JSON.stringify(attempt, null, 2) + "\n");
86
- }
87
-
88
66
  /**
89
67
  * The stack-level change signal, from whichever end the invocation supplied.
90
68
  *
@@ -100,13 +78,20 @@ function writeAttempt(path: string, attempt: FanOutAttempt): void {
100
78
  * consumes a changed export is affected at stack granularity in the same way a
101
79
  * changed component is at component granularity.
102
80
  */
103
- async function resolveChangedUnits(
81
+ export async function resolveChangedUnits(
104
82
  ctx: CommandContext,
105
83
  config: ChantConfig,
84
+ opts: {
85
+ /** Measure from this commit instead of `--base` (a pull request's merge base, #3183). */
86
+ base?: string;
87
+ /** Build both refs for `lifecycle affected` only when the project declares `stacks`. */
88
+ stacksOnly?: boolean;
89
+ } = {},
106
90
  ): Promise<{ units: ChangedUnits } | { error: string; hint?: string }> {
107
91
  const { args } = ctx;
92
+ const base = opts.base ?? args.base;
108
93
  const fromFile = args.fromAffected;
109
- if (args.base && fromFile) {
94
+ if (base && fromFile) {
110
95
  return {
111
96
  error: "--base and --from-affected both name a change signal",
112
97
  hint: "Pass --base <ref> to derive it here, or --from-affected <file> to read one `chant lifecycle affected --json` already wrote.",
@@ -136,7 +121,7 @@ async function resolveChangedUnits(
136
121
  };
137
122
  }
138
123
 
139
- if (!args.base) {
124
+ if (!base) {
140
125
  return {
141
126
  error: "A change signal is required: chant components fan-out --base <ref> [--head <ref>]",
142
127
  hint: "Or pass --from-affected <file> with the JSON `chant lifecycle affected --base <ref> --json` wrote.",
@@ -144,23 +129,37 @@ async function resolveChangedUnits(
144
129
  }
145
130
 
146
131
  try {
147
- const result = await affectedStacks({
148
- // Components are discovered from the current directory, the convention
149
- // every component command follows. In a multi-stack project the diff
150
- // reads the same root, because `stacks[].src` is written relative to it.
151
- projectPath: config.stacks?.length ? resolve(".") : resolveBuildRoot(args, config),
152
- serializers: ctx.plugins.map((p) => p.serializer),
153
- baseRef: args.base,
154
- headRef: args.head,
155
- includeDependents: args.includeDependents,
156
- // A project that declares its stacks gets an answer keyed by stack name,
157
- // which is the name a component's deploy step uses. Without this the diff
158
- // answers by lexicon partition and the join below claims none of it.
159
- ...(config.stacks ? { stacks: config.stacks } : {}),
160
- });
132
+ // A unit that is not chant source, such as a Terraform root, never shows
133
+ // in the artifact diff below; the lexicon that owns it answers from the
134
+ // changed paths (#3183).
135
+ const projectRoot = resolve(".");
136
+ const lexiconUnits = ctx.plugins.some((p) => p.changedUnits)
137
+ ? await lexiconChangedUnits(ctx.plugins, {
138
+ projectRoot,
139
+ config: config as Record<string, unknown>,
140
+ changedFiles: await changedFilesBetween(projectRoot, base, args.head ?? "HEAD"),
141
+ })
142
+ : [];
143
+ const result =
144
+ opts.stacksOnly && !config.stacks?.length
145
+ ? { changed: [], dependents: [], indeterminate: [] }
146
+ : await affectedStacks({
147
+ // Components are discovered from the current directory, the convention
148
+ // every component command follows. In a multi-stack project the diff
149
+ // reads the same root, because `stacks[].src` is written relative to it.
150
+ projectPath: config.stacks?.length ? resolve(".") : resolveBuildRoot(args, config),
151
+ serializers: ctx.plugins.map((p) => p.serializer),
152
+ baseRef: base,
153
+ headRef: args.head,
154
+ includeDependents: args.includeDependents,
155
+ // A project that declares its stacks gets an answer keyed by stack name,
156
+ // which is the name a component's deploy step uses. Without this the diff
157
+ // answers by lexicon partition and the join below claims none of it.
158
+ ...(config.stacks ? { stacks: config.stacks } : {}),
159
+ });
161
160
  return {
162
161
  units: {
163
- changed: [...new Set([...result.changed, ...result.dependents])].sort(),
162
+ changed: [...new Set([...result.changed, ...result.dependents, ...lexiconUnits])].sort(),
164
163
  indeterminate: result.indeterminate,
165
164
  },
166
165
  };
@@ -190,6 +189,21 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
190
189
  return 1;
191
190
  }
192
191
 
192
+ // The two gate modes (#3049). `--gate` approves the whole set once, as
193
+ // #2417 decided; `--wave-gate` approves each wave once the waves before it
194
+ // applied. They answer different questions, so one run takes one of them.
195
+ if (args.gate && args.waveGate) {
196
+ console.error(formatError({
197
+ message: "--gate and --wave-gate both name a gate",
198
+ hint: "Pass --gate <name> for one approval over the whole set, or --wave-gate <name> for one per wave.",
199
+ }));
200
+ return 1;
201
+ }
202
+ if (args.wave !== undefined && !args.waveGate) {
203
+ console.error(formatError({ message: "--wave runs one wave of a gated-wave fan-out, so it needs --wave-gate <name>" }));
204
+ return 1;
205
+ }
206
+
193
207
  const signal = await resolveChangedUnits(ctx, config);
194
208
  if ("error" in signal) {
195
209
  console.error(formatError({ message: signal.error, ...(signal.hint ? { hint: signal.hint } : {}) }));
@@ -204,6 +218,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
204
218
  sandbox: args.sandbox,
205
219
  buildParams: paramsResolution.provenance,
206
220
  config,
221
+ ...(args.canary?.length ? { canary: args.canary } : {}),
207
222
  });
208
223
  } catch (err) {
209
224
  // A cycle or an unknown `dependsOn` refuses here, over the whole graph and
@@ -226,6 +241,9 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
226
241
  }
227
242
 
228
243
  const gate = args.gate ? { op: FAN_OUT_GATE_OP, gate: args.gate } : undefined;
244
+ const waveGate = args.waveGate
245
+ ? { op: FAN_OUT_GATE_OP, gate: args.waveGate, ...(args.wave !== undefined ? { only: args.wave } : {}) }
246
+ : undefined;
229
247
 
230
248
  // `--resume`: what an earlier attempt at this same plan finished. Read before
231
249
  // the dry-run branch so a plan-only invocation shows what is actually left
@@ -234,11 +252,13 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
234
252
  // approval survives the resume rather than being re-asked for.
235
253
  const resumePath = args.resume ? resolve(args.resume) : undefined;
236
254
  let priorCompleted: string[] = [];
255
+ let priorOutputs: Record<string, Record<string, unknown>> = {};
256
+ let priorWaves: WaveRecord[] = [];
237
257
  let progress: FanOutProgress | undefined;
238
258
  if (resumePath) {
239
259
  let attempt: FanOutAttempt | undefined;
240
260
  try {
241
- attempt = readAttempt(resumePath);
261
+ attempt = readFanOutAttempt(resumePath);
242
262
  } catch (err) {
243
263
  console.error(formatError({ message: `--resume: could not read "${args.resume}": ${err instanceof Error ? err.message : String(err)}` }));
244
264
  return 1;
@@ -255,16 +275,22 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
255
275
  // exists to retry them — the operator repeated the command precisely
256
276
  // because the thing that failed is now expected to work.
257
277
  priorCompleted = attempt.completed;
278
+ priorOutputs = Object.fromEntries(
279
+ Object.entries(attempt.outputs).filter(([name]) => attempt.completed.includes(name)),
280
+ );
258
281
  progress = { completed: attempt.completed };
282
+ priorWaves = attempt.waves ?? [];
259
283
  }
260
284
  }
261
285
 
262
286
  if (args.dryRun) {
263
287
  // `remainingFanOut` carries the digest rather than minting a new one, so
264
288
  // the digest printed here is the same string that approves the real run.
265
- const plan = progress ? remainingFanOut(derived.plan, derived.components, progress) : derived.plan;
289
+ // A gated-wave fan-out keeps the derivation's wave numbers, since they
290
+ // name the gates, so its plan is printed whole.
291
+ const plan = progress && !waveGate ? remainingFanOut(derived.plan, derived.components, progress) : derived.plan;
266
292
  if (args.json) renderFanOutJson(plan);
267
- else renderFanOutPlan(plan, { ...(gate ? { gate } : {}) });
293
+ else renderFanOutPlan(plan, { ...(gate ? { gate } : {}), ...(waveGate ? { waveGate } : {}) });
268
294
  return 0;
269
295
  }
270
296
 
@@ -279,13 +305,72 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
279
305
  }
280
306
 
281
307
  const registry = await fanOutRegistry(projectPath, config);
282
- const result = await runFanOut(derived.plan, derived.components, registry, {
283
- env: args.env ?? "local",
284
- componentOutputs: seededOutputs,
285
- ...(gate ? { gate } : {}),
286
- ...(progress ? { progress } : {}),
287
- ...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
288
- });
308
+ // #3061: the commit this run deploys, for the workloads' vcs.ref.head.revision.
309
+ const revision = await getHeadCommit().catch(() => undefined);
310
+
311
+ // The record is rewritten as each component settles, not only once the run
312
+ // returns. A fan-out killed in its third wave (a cancelled CI job, a lost
313
+ // runner, ctrl-c) then still knows that the first two applied, and the
314
+ // repeated command picks up from there instead of redoing them.
315
+ const completedSoFar = new Set(priorCompleted);
316
+ const failedSoFar: string[] = [];
317
+ const outputsSoFar = { ...priorOutputs };
318
+ let wavesSoFar = priorWaves;
319
+ const recordAttempt = (): void => {
320
+ if (!resumePath) return;
321
+ const attempt: FanOutAttempt = {
322
+ digest: derived.plan.digest,
323
+ completed: [...completedSoFar].sort(),
324
+ failed: [...failedSoFar].sort(),
325
+ outputs: outputsSoFar,
326
+ ...(wavesSoFar.length > 0 ? { waves: wavesSoFar } : {}),
327
+ };
328
+ writeFanOutAttempt(resumePath, attempt);
329
+ };
330
+
331
+ let result;
332
+ try {
333
+ result = await runFanOut(derived.plan, derived.components, registry, {
334
+ env: args.env ?? "local",
335
+ releaseIdentity: () => (revision ? { revision } : {}),
336
+ // A completed component's recorded outputs win over a `--seed-outputs`
337
+ // entry for the same name, as a fresh apply's outputs merge over a seed in
338
+ // the driver: the record holds what this fan-out itself applied.
339
+ componentOutputs: { ...seededOutputs, ...priorOutputs },
340
+ ...(gate ? { gate } : {}),
341
+ ...(waveGate ? { waveGate } : {}),
342
+ ...(waveGate
343
+ ? {
344
+ onWaveSettled: (record: WaveRecord) => {
345
+ wavesSoFar = withWaveRecord(wavesSoFar, record);
346
+ recordAttempt();
347
+ },
348
+ }
349
+ : {}),
350
+ ...(progress ? { progress } : {}),
351
+ ...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
352
+ ...(resumePath
353
+ ? {
354
+ onComponentSettled: (settled, outputs) => {
355
+ if (settled.status === "ok") {
356
+ completedSoFar.add(settled.component);
357
+ if (outputs) outputsSoFar[settled.component] = outputs;
358
+ } else if (settled.status === "fail") {
359
+ failedSoFar.push(settled.component);
360
+ }
361
+ recordAttempt();
362
+ },
363
+ }
364
+ : {}),
365
+ });
366
+ } catch (err) {
367
+ // `--wave <n>` reached before an earlier wave applied: nothing ran.
368
+ if (err instanceof EarlierWaveNotAppliedError) {
369
+ console.error(formatError({ message: err.message }));
370
+ return 1;
371
+ }
372
+ throw err;
373
+ }
289
374
 
290
375
  if (args.dumpOutputs) {
291
376
  const dumpPath = resolve(args.dumpOutputs);
@@ -293,25 +378,28 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
293
378
  writeFileSync(dumpPath, JSON.stringify(result.componentOutputs, null, 2));
294
379
  }
295
380
 
296
- // Written even on a failure and even on a gate: the next attempt needs to
297
- // know what this one got through, and a gated attempt got through nothing,
298
- // which is itself worth recording against this plan's digest.
299
- if (resumePath) {
300
- writeAttempt(resumePath, {
301
- digest: result.plan.digest,
302
- completed: [...new Set([...priorCompleted, ...result.completed])].sort(),
303
- failed: result.failed,
304
- });
305
- }
381
+ // Written again at the end, even on a failure and even on a gate: a gated
382
+ // attempt got through nothing, which is itself worth recording against this
383
+ // plan's digest.
384
+ recordAttempt();
306
385
 
307
386
  if (args.json) renderFanOutJson(result);
308
- else renderFanOutHuman(result, { ...(gate ? { gate } : {}) });
387
+ else renderFanOutHuman(result, { ...(gate ? { gate } : {}), ...(waveGate ? { waveGate } : {}) });
388
+
389
+ // A wave whose set moved after its approval stops with both digests named
390
+ // (#3049), the #2300 refusal on a set. Printed on stderr even under --json,
391
+ // since it is the line a CI log reader is looking for.
392
+ for (const wave of result.waves ?? []) {
393
+ if (wave.status === "gated" && wave.approved) console.error(formatWarning({ message: describeChangedWave(wave) }));
394
+ }
309
395
 
310
396
  // The same durable trace `chant run --components` leaves (#597), for the
311
397
  // components this fan-out actually applied. A partial fan-out records the
312
398
  // branches that finished and nothing else, which is the whole reason the
313
399
  // runner reports `completed` separately from `failed`.
314
- if (result.status !== "gated") {
400
+ // A gated-wave run can stop at a later wave's gate after earlier waves
401
+ // applied, and those applies are recorded like any other.
402
+ if (result.status !== "gated" || result.completed.length > 0) {
315
403
  await recordAutoReleasesForRun(
316
404
  result.results,
317
405
  args.env ?? "local",
@@ -320,7 +408,9 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
320
408
  );
321
409
  }
322
410
 
323
- if (result.status === "gated" && result.gate) {
411
+ // A failed root outranks a gate in the status, but a gated-wave run that
412
+ // stopped at a wave's gate still prints how to approve it.
413
+ if (result.gate && (result.status === "gated" || waveGate)) {
324
414
  const pending = result.gate;
325
415
  // The pending fact's own plan: the fan-out's digest for the gate over the
326
416
  // set, or a component's plan and environment for a gate inside one (#2574).
@@ -337,7 +427,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
337
427
  ...(await summaryLedgerPrefix()),
338
428
  ...(pending.environment ? { environment: pending.environment } : {}),
339
429
  });
340
- return GATED_EXIT_CODE;
430
+ return result.status === "gated" ? GATED_EXIT_CODE : 1;
341
431
  }
342
432
 
343
433
  return result.status === "ok" ? 0 : 1;
@@ -637,6 +637,9 @@ async function runComponentGraphView(
637
637
  // `chant workspace graph --composites` labels a component without
638
638
  // importing its source.
639
639
  ...(graph.archetypes?.[name] ? { archetype: graph.archetypes[name] } : {}),
640
+ // The environments the component declares (#3153), where each release goes, so
641
+ // `chant workspace graph --composites` lists them without importing its source.
642
+ ...(graph.environments?.[name] ? { environments: graph.environments[name] } : {}),
640
643
  },
641
644
  // Deep-link the node to its `*.component.ts` (behold's inspect panel).
642
645
  ...(graph.files?.[name] ? { sourceLoc: { file: graph.files[name] } } : {}),
@@ -455,6 +455,27 @@ describe("runApprove", () => {
455
455
  vi.unstubAllEnvs();
456
456
  });
457
457
 
458
+ // #3402 — a relayed approval names who relayed it, typed, not in --note.
459
+ test("--relayed-by is recorded on the resolution line, and a blank one is refused", async () => {
460
+ seedPending("fountain-apply", "rollout-gate", PLAN_A);
461
+ appendGateResolutionMock.mockResolvedValue({
462
+ commit: "sha",
463
+ record: { version: 1, op: "fountain-apply", gate: "rollout-gate", resolvedBy: "github:alice", relayedBy: "github:hud-follower", timestamp: "2026-01-01T00:00:00.000Z" },
464
+ });
465
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
466
+
467
+ expect(await runApprove(ctx({ path: "fountain-apply", extraPositional: "rollout-gate", actor: "github:alice", relayedBy: "github:hud-follower" }))).toBe(0);
468
+ expect(appendGateResolutionMock).toHaveBeenCalledWith(
469
+ expect.objectContaining({ resolvedBy: "github:alice", relayedBy: "github:hud-follower" }),
470
+ );
471
+ expect(errSpy.mock.calls.flat().join("\n")).toMatch(/resolved by github:alice, relayed by github:hud-follower,/);
472
+
473
+ appendGateResolutionMock.mockClear();
474
+ expect(await runApprove(ctx({ path: "fountain-apply", extraPositional: "rollout-gate", actor: "github:alice", relayedBy: "a\nb" }))).toBe(1);
475
+ expect(appendGateResolutionMock).not.toHaveBeenCalled();
476
+ errSpy.mockRestore();
477
+ });
478
+
458
479
  test("without --url, the surrounding PR/MR job is itself the address", async () => {
459
480
  vi.stubEnv("GITHUB_SERVER_URL", "https://github.com");
460
481
  vi.stubEnv("GITHUB_REPOSITORY", "INTENTIUS/chant");
@@ -134,6 +134,9 @@ async function collectOperatorSubscribers(
134
134
  }
135
135
  }
136
136
 
137
+ /** A pull request's gate op, `pr-<number>` (#3183, `prOp` in ../../pr-loop.ts). */
138
+ const PR_GATE_OP = /^pr-\d+$/;
139
+
137
140
  // ── chant operator ──────────────────────────────────────────────────────────
138
141
 
139
142
  /**
@@ -882,7 +885,9 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
882
885
  console.error(formatInfo(
883
886
  opName === FAN_OUT_GATE_OP
884
887
  ? `The next \`chant components fan-out\` decides this gate from scratch and records a fresh pending fact.`
885
- : opName === WORKSPACE_UPGRADE_GATE_OP
888
+ : PR_GATE_OP.test(opName)
889
+ ? `The next \`chant components pr-apply\` decides this gate from scratch and records a fresh pending fact.`
890
+ : opName === WORKSPACE_UPGRADE_GATE_OP
886
891
  ? `The next \`chant workspace upgrade ${gate}\` decides this gate from scratch and records a fresh pending fact.`
887
892
  : `The next \`chant run ${opName}\` decides this gate from scratch and records a fresh pending fact.`,
888
893
  ));
@@ -903,6 +908,7 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
903
908
  ...(ctx.args.roles ? { roles: ctx.args.roles } : {}),
904
909
  ...(ctx.args.agent ? { agent: true } : {}),
905
910
  ...(ctx.args.sign !== undefined ? { sign: ctx.args.sign } : {}),
911
+ ...(ctx.args.relayedBy !== undefined ? { relayedBy: ctx.args.relayedBy } : {}),
906
912
  });
907
913
  if (!outcome.ok) return 1;
908
914
 
@@ -910,7 +916,9 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
910
916
  `This records the resolution as a fact; it does not itself re-run anything. ` +
911
917
  (opName === FAN_OUT_GATE_OP
912
918
  ? `Repeat the \`chant components fan-out\` command and it walks through gate "${gate}".`
913
- : opName === WORKSPACE_UPGRADE_GATE_OP
919
+ : PR_GATE_OP.test(opName)
920
+ ? `The pull request's apply (\`chant components pr-apply\`) walks through gate "${gate}" when it plans this same digest.`
921
+ : opName === WORKSPACE_UPGRADE_GATE_OP
914
922
  ? `Repeat \`chant workspace upgrade ${gate}\` with the same target, and it applies the patch this approval names.`
915
923
  : `Run \`chant run ${opName}\` and it walks through gate "${gate}".`),
916
924
  ));
@@ -938,6 +946,13 @@ export interface GateApprovalOptions {
938
946
  actor?: string;
939
947
  /** `--note` — free-text prose. */
940
948
  note?: string;
949
+ /**
950
+ * `--relayed-by <principal>` (#3402): who carried this approval to chant for
951
+ * the approver, such as a follower program relaying a person's approval.
952
+ * Recorded as `relayedBy` on the resolution line and covered by `--sign`.
953
+ * The approver is still `--actor`; the relay is not an approver.
954
+ */
955
+ relayedBy?: string;
941
956
  /** `--url` — the approval surface, validated as absolute http/https. */
942
957
  url?: string;
943
958
  /**
@@ -1101,6 +1116,16 @@ export async function recordGateApproval(
1101
1116
  return { ok: false };
1102
1117
  }
1103
1118
 
1119
+ // #3402: the relay is a principal, on one line (the seal's payload is LF-joined).
1120
+ const relayedBy = opts.relayedBy?.trim();
1121
+ if (opts.relayedBy !== undefined && (!relayedBy || /[\r\n]/.test(relayedBy))) {
1122
+ console.error(formatError({
1123
+ message: `--relayed-by needs one principal on one line (got ${JSON.stringify(opts.relayedBy)})`,
1124
+ hint: "Name who relayed the approval, such as --relayed-by github:<login> or a principal the signers file lists.",
1125
+ }));
1126
+ return { ok: false };
1127
+ }
1128
+
1104
1129
  const resolvedBy = opts.actor
1105
1130
  ?? (isModelAuthored(origin)
1106
1131
  // A channel that cannot attest to a name does not get to write one down.
@@ -1126,6 +1151,7 @@ export async function recordGateApproval(
1126
1151
  const scope = scopeSource(cwd);
1127
1152
  try {
1128
1153
  identity.refuseUnidentified(scope, [resolvedBy], opts.actor !== undefined ? "--actor" : "the approver");
1154
+ if (relayedBy) identity.refuseUnidentified(scope, [relayedBy], "--relayed-by");
1129
1155
  } catch (err) {
1130
1156
  if (!(err instanceof identity.IdentityError)) throw err;
1131
1157
  console.error(formatError({
@@ -1158,6 +1184,7 @@ export async function recordGateApproval(
1158
1184
  ...(environment !== undefined ? { environment } : {}),
1159
1185
  ...(planDigest !== undefined ? { planDigest } : {}),
1160
1186
  resolvedBy,
1187
+ ...(relayedBy ? { relayedBy } : {}),
1161
1188
  timestamp,
1162
1189
  });
1163
1190
  } finally {
@@ -1176,6 +1203,7 @@ export async function recordGateApproval(
1176
1203
  ...(environment !== undefined ? { environment } : {}),
1177
1204
  ...(planDigest !== undefined ? { planDigest } : {}),
1178
1205
  resolvedBy,
1206
+ ...(relayedBy ? { relayedBy } : {}),
1179
1207
  timestamp,
1180
1208
  seal,
1181
1209
  });
@@ -1230,6 +1258,7 @@ export async function recordGateApproval(
1230
1258
  op: opName,
1231
1259
  gate,
1232
1260
  resolvedBy,
1261
+ ...(relayedBy ? { relayedBy } : {}),
1233
1262
  timestamp,
1234
1263
  ...(seal ? { seal } : {}),
1235
1264
  approver,
@@ -1247,7 +1276,9 @@ export async function recordGateApproval(
1247
1276
  );
1248
1277
 
1249
1278
  console.error(formatSuccess(
1250
- `Gate "${gate}" on "${opName}" resolved by ${record.resolvedBy} at ${record.timestamp}` +
1279
+ `Gate "${gate}" on "${opName}" resolved by ${record.resolvedBy}` +
1280
+ (record.relayedBy ? `, relayed by ${record.relayedBy},` : "") +
1281
+ ` at ${record.timestamp}` +
1251
1282
  (record.url ? ` (${record.url})` : "") +
1252
1283
  (record.seal ? `, signed with ${record.seal.key}` : "") +
1253
1284
  (pushed ? "" : " (local only — the push did not land)"),