@intentius/chant 0.80.0 → 0.81.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 (414) hide show
  1. package/dist/audit/discover.d.ts +8 -16
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-html.d.ts +5 -0
  4. package/dist/audit/report-html.d.ts.map +1 -1
  5. package/dist/audit/report-model.d.ts +13 -0
  6. package/dist/audit/report-model.d.ts.map +1 -1
  7. package/dist/audit/terraform-state.d.ts +4 -5
  8. package/dist/audit/terraform-state.d.ts.map +1 -1
  9. package/dist/cli/commands/audit.d.ts +25 -9
  10. package/dist/cli/commands/audit.d.ts.map +1 -1
  11. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.d.ts.map +1 -1
  17. package/dist/cli/commands/update.d.ts +6 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -1
  19. package/dist/cli/commands/vendor.d.ts +5 -0
  20. package/dist/cli/commands/vendor.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts.map +1 -1
  23. package/dist/cli/handlers/dev.d.ts.map +1 -1
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/init.d.ts.map +1 -1
  26. package/dist/cli/handlers/operator.d.ts +7 -0
  27. package/dist/cli/handlers/operator.d.ts.map +1 -1
  28. package/dist/cli/handlers/promote.d.ts +7 -4
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/run-generate.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts +4 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/handlers/vendor.d.ts +8 -3
  34. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  35. package/dist/cli/main.d.ts +12 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/plugins.d.ts +12 -7
  38. package/dist/cli/plugins.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +32 -0
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts +10 -0
  42. package/dist/codegen/fetch.d.ts.map +1 -1
  43. package/dist/components/cli-support.d.ts +3 -1
  44. package/dist/components/cli-support.d.ts.map +1 -1
  45. package/dist/components/discover.d.ts.map +1 -1
  46. package/dist/components/driver.d.ts +16 -2
  47. package/dist/components/driver.d.ts.map +1 -1
  48. package/dist/components/gate-plan.d.ts +40 -0
  49. package/dist/components/gate-plan.d.ts.map +1 -0
  50. package/dist/components/promote.d.ts +60 -7
  51. package/dist/components/promote.d.ts.map +1 -1
  52. package/dist/components/sandbox/driver.d.ts.map +1 -1
  53. package/dist/components/sandbox/run.d.ts.map +1 -1
  54. package/dist/components/verbs/build-archive.d.ts +22 -11
  55. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  56. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  57. package/dist/config-static.d.ts +59 -0
  58. package/dist/config-static.d.ts.map +1 -0
  59. package/dist/config.d.ts +32 -10
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/content-digest.d.ts +17 -0
  62. package/dist/content-digest.d.ts.map +1 -0
  63. package/dist/discovery/files.d.ts +10 -5
  64. package/dist/discovery/files.d.ts.map +1 -1
  65. package/dist/discovery/fold-import.d.ts +20 -4
  66. package/dist/discovery/fold-import.d.ts.map +1 -1
  67. package/dist/discovery/index.d.ts.map +1 -1
  68. package/dist/discovery/sandbox/driver.d.ts +20 -0
  69. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/fork.d.ts +20 -0
  71. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  72. package/dist/discovery/walk.d.ts +119 -0
  73. package/dist/discovery/walk.d.ts.map +1 -0
  74. package/dist/graph-ir.d.ts +4 -1
  75. package/dist/graph-ir.d.ts.map +1 -1
  76. package/dist/join-key.d.ts +31 -0
  77. package/dist/join-key.d.ts.map +1 -0
  78. package/dist/lexicon-module.d.ts +51 -6
  79. package/dist/lexicon-module.d.ts.map +1 -1
  80. package/dist/lexicon.d.ts +58 -1
  81. package/dist/lexicon.d.ts.map +1 -1
  82. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  83. package/dist/lifecycle/digest.d.ts +5 -1
  84. package/dist/lifecycle/digest.d.ts.map +1 -1
  85. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  86. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  87. package/dist/lifecycle/git.d.ts +64 -0
  88. package/dist/lifecycle/git.d.ts.map +1 -1
  89. package/dist/lifecycle/lease.d.ts +8 -1
  90. package/dist/lifecycle/lease.d.ts.map +1 -1
  91. package/dist/lifecycle/legacy-digest.d.ts +41 -8
  92. package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
  93. package/dist/lifecycle/member-ledger.d.ts +67 -0
  94. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  95. package/dist/lifecycle/plan-digest.d.ts +4 -3
  96. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  97. package/dist/lifecycle/release-ledger.d.ts +18 -2
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/status.d.ts +90 -5
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/op/activities/activity-contracts.d.ts +32 -0
  102. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  103. package/dist/op/activities/index.d.ts +2 -0
  104. package/dist/op/activities/index.d.ts.map +1 -1
  105. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  106. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  107. package/dist/op/discover.d.ts.map +1 -1
  108. package/dist/op/gate-name.d.ts +7 -0
  109. package/dist/op/gate-name.d.ts.map +1 -1
  110. package/dist/op/gate-summary.d.ts +16 -0
  111. package/dist/op/gate-summary.d.ts.map +1 -1
  112. package/dist/op/gate.d.ts +16 -2
  113. package/dist/op/gate.d.ts.map +1 -1
  114. package/dist/project-root.d.ts +26 -0
  115. package/dist/project-root.d.ts.map +1 -1
  116. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  117. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  118. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  119. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  120. package/dist/workspace/checks/generated.d.ts +156 -0
  121. package/dist/workspace/checks/generated.d.ts.map +1 -0
  122. package/dist/workspace/checks/ledgers.d.ts +78 -0
  123. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  124. package/dist/workspace/checks/links.d.ts +25 -0
  125. package/dist/workspace/checks/links.d.ts.map +1 -0
  126. package/dist/workspace/checks/pipelines.d.ts +76 -0
  127. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  128. package/dist/workspace/checks.d.ts +161 -0
  129. package/dist/workspace/checks.d.ts.map +1 -0
  130. package/dist/workspace/compose-graph.d.ts +115 -0
  131. package/dist/workspace/compose-graph.d.ts.map +1 -0
  132. package/dist/workspace/compose-reports.d.ts +68 -0
  133. package/dist/workspace/compose-reports.d.ts.map +1 -0
  134. package/dist/workspace/declaration.d.ts +220 -0
  135. package/dist/workspace/declaration.d.ts.map +1 -0
  136. package/dist/workspace/declaration.schema.json +434 -0
  137. package/dist/workspace/generated-files.d.ts +65 -0
  138. package/dist/workspace/generated-files.d.ts.map +1 -0
  139. package/dist/workspace/glob.d.ts +13 -0
  140. package/dist/workspace/glob.d.ts.map +1 -0
  141. package/dist/workspace/graph-cli.d.ts +75 -0
  142. package/dist/workspace/graph-cli.d.ts.map +1 -0
  143. package/dist/workspace/init.d.ts +96 -0
  144. package/dist/workspace/init.d.ts.map +1 -0
  145. package/dist/workspace/jsonc.d.ts +38 -0
  146. package/dist/workspace/jsonc.d.ts.map +1 -0
  147. package/dist/workspace/kinds.d.ts +192 -0
  148. package/dist/workspace/kinds.d.ts.map +1 -0
  149. package/dist/workspace/lineage-check.d.ts +84 -0
  150. package/dist/workspace/lineage-check.d.ts.map +1 -0
  151. package/dist/workspace/lineage-cli.d.ts +31 -0
  152. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  153. package/dist/workspace/lineage-init.d.ts +101 -0
  154. package/dist/workspace/lineage-init.d.ts.map +1 -0
  155. package/dist/workspace/lineage-lock.d.ts +258 -0
  156. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  157. package/dist/workspace/lineage-merge.d.ts +14 -0
  158. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  159. package/dist/workspace/lineage-migrations.d.ts +178 -0
  160. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  161. package/dist/workspace/lineage-update.d.ts +59 -0
  162. package/dist/workspace/lineage-update.d.ts.map +1 -0
  163. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  164. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  165. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  166. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  167. package/dist/workspace/lineage-vendor.d.ts +63 -0
  168. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  169. package/dist/workspace/lineage-version.d.ts +32 -0
  170. package/dist/workspace/lineage-version.d.ts.map +1 -0
  171. package/dist/workspace/links.d.ts +150 -0
  172. package/dist/workspace/links.d.ts.map +1 -0
  173. package/dist/workspace/ls.d.ts +99 -0
  174. package/dist/workspace/ls.d.ts.map +1 -0
  175. package/dist/workspace/member-commands.d.ts +139 -0
  176. package/dist/workspace/member-commands.d.ts.map +1 -0
  177. package/dist/workspace/member-handles.d.ts +54 -0
  178. package/dist/workspace/member-handles.d.ts.map +1 -0
  179. package/dist/workspace/member-pipeline.d.ts +143 -0
  180. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  181. package/dist/workspace/member-run.d.ts +67 -0
  182. package/dist/workspace/member-run.d.ts.map +1 -0
  183. package/dist/workspace/reason-codes.d.ts +65 -0
  184. package/dist/workspace/reason-codes.d.ts.map +1 -0
  185. package/dist/workspace/records-cli.d.ts +28 -4
  186. package/dist/workspace/records-cli.d.ts.map +1 -1
  187. package/dist/workspace/records.d.ts.map +1 -1
  188. package/dist/workspace/root-refusal.d.ts +53 -0
  189. package/dist/workspace/root-refusal.d.ts.map +1 -0
  190. package/dist/workspace/status.d.ts +150 -0
  191. package/dist/workspace/status.d.ts.map +1 -0
  192. package/dist/workspace/template-manifest.d.ts +90 -0
  193. package/dist/workspace/template-manifest.d.ts.map +1 -0
  194. package/dist/workspace/tree.d.ts +36 -0
  195. package/dist/workspace/tree.d.ts.map +1 -0
  196. package/dist/workspace/trust/attestor.d.ts +67 -0
  197. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  198. package/dist/workspace/trust/policy.d.ts +111 -0
  199. package/dist/workspace/trust/policy.d.ts.map +1 -0
  200. package/dist/workspace/trust/provenance.d.ts +67 -0
  201. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  202. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  203. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  204. package/dist/workspace/trust/test-repo.d.ts +42 -0
  205. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  206. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  207. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  208. package/dist/workspace/trust/verify.d.ts +66 -0
  209. package/dist/workspace/trust/verify.d.ts.map +1 -0
  210. package/dist/workspace/which-chant.d.ts +58 -0
  211. package/dist/workspace/which-chant.d.ts.map +1 -0
  212. package/dist/workspace/workspace-kinds.schema.json +133 -0
  213. package/package.json +2 -1
  214. package/src/audit/discover.ts +66 -78
  215. package/src/audit/report-html.ts +3 -1
  216. package/src/audit/report-model.ts +14 -1
  217. package/src/audit/terraform-state.test.ts +4 -9
  218. package/src/audit/terraform-state.ts +4 -5
  219. package/src/cli/commands/audit-truncation.test.ts +167 -0
  220. package/src/cli/commands/audit.ts +89 -60
  221. package/src/cli/commands/check-lexicon.ts +24 -0
  222. package/src/cli/commands/doctor.test.ts +30 -0
  223. package/src/cli/commands/doctor.ts +18 -0
  224. package/src/cli/commands/import-agents.ts +6 -2
  225. package/src/cli/commands/init.ts +22 -3
  226. package/src/cli/commands/lint.test.ts +17 -23
  227. package/src/cli/commands/lint.ts +24 -84
  228. package/src/cli/commands/update.ts +13 -4
  229. package/src/cli/commands/vendor.ts +11 -6
  230. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  231. package/src/cli/handlers/build-member.test.ts +127 -0
  232. package/src/cli/handlers/build.ts +35 -1
  233. package/src/cli/handlers/components.test.ts +127 -0
  234. package/src/cli/handlers/components.ts +70 -8
  235. package/src/cli/handlers/dev.ts +5 -3
  236. package/src/cli/handlers/fan-out.ts +9 -2
  237. package/src/cli/handlers/init.ts +54 -1
  238. package/src/cli/handlers/operator.test.ts +68 -0
  239. package/src/cli/handlers/operator.ts +37 -9
  240. package/src/cli/handlers/promote.test.ts +118 -2
  241. package/src/cli/handlers/promote.ts +82 -16
  242. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  243. package/src/cli/handlers/run-generate.test.ts +6 -8
  244. package/src/cli/handlers/run-generate.ts +36 -1
  245. package/src/cli/handlers/run.test.ts +47 -0
  246. package/src/cli/handlers/run.ts +28 -5
  247. package/src/cli/handlers/vendor.ts +147 -43
  248. package/src/cli/main.test.ts +105 -1
  249. package/src/cli/main.ts +230 -17
  250. package/src/cli/plugins.ts +17 -17
  251. package/src/cli/registry.ts +32 -0
  252. package/src/cli/static-config-read.test.ts +208 -0
  253. package/src/codegen/fetch.test.ts +63 -1
  254. package/src/codegen/fetch.ts +30 -1
  255. package/src/components/cli-support.ts +6 -1
  256. package/src/components/discover.test.ts +27 -57
  257. package/src/components/discover.ts +13 -68
  258. package/src/components/driver.test.ts +78 -1
  259. package/src/components/driver.ts +43 -7
  260. package/src/components/gate-plan.ts +53 -0
  261. package/src/components/promote.test.ts +276 -2
  262. package/src/components/promote.ts +119 -15
  263. package/src/components/sandbox/driver.ts +8 -8
  264. package/src/components/sandbox/run.ts +4 -1
  265. package/src/components/verbs/build-archive.ts +27 -21
  266. package/src/components/verbs/run-agent.ts +3 -5
  267. package/src/config-static.test.ts +136 -0
  268. package/src/config-static.ts +232 -0
  269. package/src/config.test.ts +2 -2
  270. package/src/config.ts +48 -15
  271. package/src/content-digest.test.ts +120 -0
  272. package/src/content-digest.ts +21 -0
  273. package/src/discovery/files.test.ts +10 -10
  274. package/src/discovery/files.ts +25 -81
  275. package/src/discovery/fold-import.ts +49 -5
  276. package/src/discovery/index.ts +1 -0
  277. package/src/discovery/sandbox/driver.ts +73 -35
  278. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  279. package/src/discovery/sandbox/fork.ts +83 -43
  280. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  281. package/src/discovery/walk.test.ts +263 -0
  282. package/src/discovery/walk.ts +334 -0
  283. package/src/effect-receipt.ts +2 -2
  284. package/src/graph-ir.ts +4 -1
  285. package/src/join-key.test.ts +25 -0
  286. package/src/join-key.ts +39 -0
  287. package/src/lexicon-module.ts +108 -10
  288. package/src/lexicon.ts +79 -1
  289. package/src/lifecycle/build-ledger-store.ts +11 -10
  290. package/src/lifecycle/digest.ts +9 -5
  291. package/src/lifecycle/gate-ledger.test.ts +20 -0
  292. package/src/lifecycle/gate-ledger.ts +28 -6
  293. package/src/lifecycle/git.test.ts +13 -1
  294. package/src/lifecycle/git.ts +221 -176
  295. package/src/lifecycle/lease.ts +25 -10
  296. package/src/lifecycle/legacy-digest.test.ts +115 -23
  297. package/src/lifecycle/legacy-digest.ts +60 -19
  298. package/src/lifecycle/member-ledger.test.ts +319 -0
  299. package/src/lifecycle/member-ledger.ts +145 -0
  300. package/src/lifecycle/plan-digest.ts +7 -7
  301. package/src/lifecycle/release-ledger.test.ts +20 -0
  302. package/src/lifecycle/release-ledger.ts +23 -7
  303. package/src/lifecycle/status.test.ts +151 -1
  304. package/src/lifecycle/status.ts +228 -7
  305. package/src/op/activities/activity-contracts.ts +37 -0
  306. package/src/op/activities/index.ts +10 -0
  307. package/src/op/activities/propose-upgrade.ts +206 -0
  308. package/src/op/discover.test.ts +14 -4
  309. package/src/op/discover.ts +19 -27
  310. package/src/op/gate-name.ts +8 -0
  311. package/src/op/gate-summary.test.ts +14 -0
  312. package/src/op/gate-summary.ts +25 -2
  313. package/src/op/gate.ts +36 -7
  314. package/src/op/generate-pipeline.test.ts +14 -5
  315. package/src/project-root.test.ts +66 -1
  316. package/src/project-root.ts +48 -0
  317. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  318. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  319. package/src/workspace/behold-kinds.test.ts +162 -0
  320. package/src/workspace/check-contract.test.ts +127 -0
  321. package/src/workspace/check.schema.json +347 -0
  322. package/src/workspace/checks/generated.test.ts +287 -0
  323. package/src/workspace/checks/generated.ts +475 -0
  324. package/src/workspace/checks/ledgers.test.ts +139 -0
  325. package/src/workspace/checks/ledgers.ts +246 -0
  326. package/src/workspace/checks/links.ts +183 -0
  327. package/src/workspace/checks/pipelines.test.ts +133 -0
  328. package/src/workspace/checks/pipelines.ts +225 -0
  329. package/src/workspace/checks-repo.test.ts +71 -0
  330. package/src/workspace/checks.test.ts +450 -0
  331. package/src/workspace/checks.ts +461 -0
  332. package/src/workspace/compose-graph.test.ts +146 -0
  333. package/src/workspace/compose-graph.ts +228 -0
  334. package/src/workspace/compose-reports.test.ts +109 -0
  335. package/src/workspace/compose-reports.ts +186 -0
  336. package/src/workspace/declaration.schema.json +434 -0
  337. package/src/workspace/declaration.test.ts +347 -0
  338. package/src/workspace/declaration.ts +623 -0
  339. package/src/workspace/generated-files.test.ts +114 -0
  340. package/src/workspace/generated-files.ts +94 -0
  341. package/src/workspace/glob.ts +49 -0
  342. package/src/workspace/graph-cli.ts +220 -0
  343. package/src/workspace/graph-contract.test.ts +204 -0
  344. package/src/workspace/graph.schema.json +662 -0
  345. package/src/workspace/init.test.ts +223 -0
  346. package/src/workspace/init.ts +585 -0
  347. package/src/workspace/jsonc.test.ts +65 -0
  348. package/src/workspace/jsonc.ts +231 -0
  349. package/src/workspace/kinds.test.ts +288 -0
  350. package/src/workspace/kinds.ts +506 -0
  351. package/src/workspace/lineage-check.ts +264 -0
  352. package/src/workspace/lineage-cli.ts +114 -0
  353. package/src/workspace/lineage-init.test.ts +231 -0
  354. package/src/workspace/lineage-init.ts +365 -0
  355. package/src/workspace/lineage-lock.test.ts +224 -0
  356. package/src/workspace/lineage-lock.ts +357 -0
  357. package/src/workspace/lineage-merge.ts +44 -0
  358. package/src/workspace/lineage-migrations.test.ts +178 -0
  359. package/src/workspace/lineage-migrations.ts +343 -0
  360. package/src/workspace/lineage-update.ts +160 -0
  361. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  362. package/src/workspace/lineage-upgrade.test.ts +474 -0
  363. package/src/workspace/lineage-upgrade.ts +624 -0
  364. package/src/workspace/lineage-vendor.test.ts +126 -0
  365. package/src/workspace/lineage-vendor.ts +215 -0
  366. package/src/workspace/lineage-version.ts +127 -0
  367. package/src/workspace/links.test.ts +344 -0
  368. package/src/workspace/links.ts +331 -0
  369. package/src/workspace/ls-contract.test.ts +200 -0
  370. package/src/workspace/ls.schema.json +190 -0
  371. package/src/workspace/ls.ts +254 -0
  372. package/src/workspace/member-commands.e2e.test.ts +244 -0
  373. package/src/workspace/member-commands.test.ts +217 -0
  374. package/src/workspace/member-commands.ts +540 -0
  375. package/src/workspace/member-handles.test.ts +76 -0
  376. package/src/workspace/member-handles.ts +228 -0
  377. package/src/workspace/member-pipeline.test.ts +135 -0
  378. package/src/workspace/member-pipeline.ts +317 -0
  379. package/src/workspace/member-run.ts +146 -0
  380. package/src/workspace/read-contract.test.ts +129 -0
  381. package/src/workspace/reason-codes.test.ts +113 -0
  382. package/src/workspace/reason-codes.ts +81 -0
  383. package/src/workspace/records-cli.ts +86 -14
  384. package/src/workspace/records-contract.test.ts +5 -0
  385. package/src/workspace/records.schema.json +32 -2
  386. package/src/workspace/records.ts +3 -2
  387. package/src/workspace/root-refusal.ts +97 -0
  388. package/src/workspace/status-contract.test.ts +308 -0
  389. package/src/workspace/status.schema.json +210 -0
  390. package/src/workspace/status.ts +412 -0
  391. package/src/workspace/template-manifest.test.ts +79 -0
  392. package/src/workspace/template-manifest.ts +221 -0
  393. package/src/workspace/tree.ts +132 -0
  394. package/src/workspace/trust/attestor.ts +93 -0
  395. package/src/workspace/trust/policy.ts +306 -0
  396. package/src/workspace/trust/provenance.ts +181 -0
  397. package/src/workspace/trust/ssh-commit.ts +138 -0
  398. package/src/workspace/trust/test-repo.ts +138 -0
  399. package/src/workspace/trust/trust.test.ts +397 -0
  400. package/src/workspace/trust/verify-cli.ts +56 -0
  401. package/src/workspace/trust/verify.ts +194 -0
  402. package/src/workspace/which-chant.test.ts +117 -0
  403. package/src/workspace/which-chant.ts +151 -0
  404. package/src/workspace/workspace-kinds.schema.json +133 -0
  405. package/dist/components/unbound-gate-approval.d.ts +0 -24
  406. package/dist/components/unbound-gate-approval.d.ts.map +0 -1
  407. package/dist/discovery/convergence.d.ts +0 -97
  408. package/dist/discovery/convergence.d.ts.map +0 -1
  409. package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
  410. package/src/components/unbound-gate-approval.test.ts +0 -82
  411. package/src/components/unbound-gate-approval.ts +0 -51
  412. package/src/discovery/convergence.test.ts +0 -231
  413. package/src/discovery/convergence.ts +0 -399
  414. package/src/op/discover-convergence.test.ts +0 -70
@@ -23,6 +23,7 @@ const awsEmulatorStub = {
23
23
  const getHeadCommitMock = vi.fn();
24
24
  const fetchLifecycleMock = vi.fn();
25
25
  const pushLifecycleMock = vi.fn();
26
+ const readSnapshotMock = vi.fn();
26
27
 
27
28
  const appendReleaseRecordMock = vi.fn();
28
29
  const readReleaseLedgerMock = vi.fn();
@@ -38,6 +39,8 @@ vi.mock("../../lifecycle/git", () => ({
38
39
  getHeadCommit: (...args: unknown[]) => getHeadCommitMock(...args),
39
40
  fetchLifecycle: (...args: unknown[]) => fetchLifecycleMock(...args),
40
41
  pushLifecycle: (...args: unknown[]) => pushLifecycleMock(...args),
42
+ readSnapshot: (...args: unknown[]) => readSnapshotMock(...args),
43
+ snapshotStorageKey: (lexicon: string, stack?: string) => (stack ? `${stack}__${lexicon}` : lexicon),
41
44
  StaleLifecycleBranchError: class StaleLifecycleBranchError extends Error {},
42
45
  }));
43
46
 
@@ -125,6 +128,7 @@ describe("components handlers", () => {
125
128
  getHeadCommitMock.mockReset().mockResolvedValue("abc123headsha");
126
129
  fetchLifecycleMock.mockReset().mockResolvedValue(true);
127
130
  pushLifecycleMock.mockReset().mockResolvedValue(true);
131
+ readSnapshotMock.mockReset().mockResolvedValue(null);
128
132
  appendReleaseRecordMock.mockReset();
129
133
  readReleaseLedgerMock.mockReset().mockResolvedValue({ records: [], malformed: 0 });
130
134
  listReleaseEnvironmentsMock.mockReset().mockResolvedValue([]);
@@ -567,6 +571,129 @@ describe("components handlers", () => {
567
571
  });
568
572
  });
569
573
 
574
+ // #2513: `--live` could never report `drifted` — the change set was built
575
+ // with no prior observation, so a live, declared entity was always `noop`,
576
+ // and nothing compared the live identity with the recorded release. These
577
+ // go through the handler end to end with a fake lexicon.
578
+ describe("drifted (#2513)", () => {
579
+ const released = {
580
+ version: 1,
581
+ component: "svc",
582
+ env: "prod",
583
+ digest: "sha256:released",
584
+ gitSha: "1111111aaaaaaa",
585
+ runId: "run-1",
586
+ timestamp: "2026-02-01T00:00:00.000Z",
587
+ actor: "alice",
588
+ };
589
+
590
+ async function statusRow(
591
+ resources: Record<string, ResourceMetadata>,
592
+ opts: { snapshot?: { timestamp: string; resources: Record<string, ResourceMetadata> }; failRead?: boolean } = {},
593
+ ) {
594
+ readReleaseLedgerMock.mockResolvedValue({ records: [released], malformed: 0 });
595
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["svc"] }));
596
+ readSnapshotMock.mockImplementation(async (env: string, key: string) =>
597
+ opts.snapshot && env === "prod" && key === "aws"
598
+ ? JSON.stringify({ lexicon: "aws", environment: "prod", commit: "c", ...opts.snapshot })
599
+ : null,
600
+ );
601
+ const plugins: LexiconPlugin[] = [
602
+ createMockPlugin({
603
+ name: "aws",
604
+ emulator: awsEmulatorStub,
605
+ describeResources: opts.failRead
606
+ ? async () => { throw new Error("credentials expired"); }
607
+ : staticDescribeResources(resources),
608
+ }),
609
+ ];
610
+ stdoutBuf.length = 0;
611
+ const exit = await runComponentsStatus({
612
+ args: makeArgs({ extraPositional: "prod", live: true, json: true }),
613
+ plugins,
614
+ serializers: plugins.map((p) => p.serializer),
615
+ });
616
+ expect(exit).toBe(0);
617
+ return JSON.parse(stdoutBuf.join(""))[0];
618
+ }
619
+
620
+ test("reconciled: live identity matches the recorded digest and git sha", async () => {
621
+ const row = await statusRow({ svc: meta({ attributes: { digest: "sha256:released", gitSha: "1111111" } }) });
622
+ expect(row).toMatchObject({ component: "svc", reconciliation: "reconciled", live: true });
623
+ });
624
+
625
+ test("drifted: live resource reports a digest other than the recorded one", async () => {
626
+ const row = await statusRow({ svc: meta({ attributes: { digest: "sha256:foreign" } }) });
627
+ expect(row).toMatchObject({ component: "svc", reconciliation: "drifted", live: true });
628
+ expect(row.detail).toContain("sha256:foreign");
629
+ });
630
+
631
+ test("drifted: live resource reports a different git sha", async () => {
632
+ const row = await statusRow({ svc: meta({ attributes: { gitSha: "2222222bbbbbbb" } }) });
633
+ expect(row).toMatchObject({ reconciliation: "drifted" });
634
+ expect(row.detail).toContain("2222222bbbbbbb");
635
+ });
636
+
637
+ test("drifted: live attributes changed since a snapshot taken after the release", async () => {
638
+ const row = await statusRow(
639
+ { svc: meta({ attributes: { revision: "7" } }) },
640
+ { snapshot: { timestamp: "2026-02-01T00:05:00.000Z", resources: { svc: meta({ attributes: { revision: "6" } }) } } },
641
+ );
642
+ expect(row).toMatchObject({ reconciliation: "drifted" });
643
+ expect(row.detail).toContain("attributes.revision");
644
+ });
645
+
646
+ test("drifted: chant's ownership marker disappeared since a post-release snapshot", async () => {
647
+ const row = await statusRow(
648
+ { svc: meta({ ownership: "foreign" }) },
649
+ { snapshot: { timestamp: "2026-02-02T00:00:00.000Z", resources: { svc: meta({ ownership: "owned" }) } } },
650
+ );
651
+ expect(row).toMatchObject({ reconciliation: "drifted" });
652
+ expect(row.detail).toContain("ownership marker");
653
+ });
654
+
655
+ test("reconciled: a snapshot older than the release does not attribute the release's own change to drift", async () => {
656
+ const row = await statusRow(
657
+ { svc: meta({ attributes: { revision: "7" } }) },
658
+ { snapshot: { timestamp: "2026-01-15T00:00:00.000Z", resources: { svc: meta({ attributes: { revision: "6" } }) } } },
659
+ );
660
+ expect(row).toMatchObject({ reconciliation: "reconciled" });
661
+ });
662
+
663
+ test("reconciled: unchanged since the snapshot, and no identity reported (no identity is not drift)", async () => {
664
+ const row = await statusRow(
665
+ { svc: meta() },
666
+ { snapshot: { timestamp: "2026-02-01T00:05:00.000Z", resources: { svc: meta() } } },
667
+ );
668
+ expect(row).toMatchObject({ reconciliation: "reconciled" });
669
+ });
670
+
671
+ test("stale: recorded, and the provider confirmed nothing live", async () => {
672
+ const row = await statusRow(
673
+ {},
674
+ { snapshot: { timestamp: "2026-02-01T00:05:00.000Z", resources: { svc: meta({ attributes: { digest: "sha256:foreign" } }) } } },
675
+ );
676
+ expect(row).toMatchObject({ reconciliation: "stale", live: false });
677
+ });
678
+
679
+ test("unknown: a failed read is never drifted, whatever the snapshot says", async () => {
680
+ const row = await statusRow(
681
+ {},
682
+ {
683
+ failRead: true,
684
+ snapshot: { timestamp: "2026-02-01T00:05:00.000Z", resources: { svc: meta({ attributes: { digest: "sha256:foreign" } }) } },
685
+ },
686
+ );
687
+ expect(row).toMatchObject({ reconciliation: "unknown" });
688
+ expect(row).not.toHaveProperty("live");
689
+ });
690
+
691
+ test("reads the same unstacked snapshot `lifecycle plan` reads", async () => {
692
+ await statusRow({ svc: meta() });
693
+ expect(readSnapshotMock).toHaveBeenCalledWith("prod", "aws");
694
+ });
695
+ });
696
+
570
697
  test("reports malformed ledger lines as a warning without failing", async () => {
571
698
  readReleaseLedgerMock.mockResolvedValue({ records: [], malformed: 2 });
572
699
  const ctx = { args: makeArgs({ extraPositional: "prod" }), plugins: [], serializers: [] };
@@ -28,7 +28,8 @@
28
28
  */
29
29
  import { resolve, join, dirname } from "node:path";
30
30
  import { mkdir, readFile, writeFile } from "node:fs/promises";
31
- import { getHeadCommit, fetchLifecycle, pushLifecycle, StaleLifecycleBranchError } from "../../lifecycle/git";
31
+ import { getHeadCommit, fetchLifecycle, pushLifecycle, readSnapshot, snapshotStorageKey, StaleLifecycleBranchError } from "../../lifecycle/git";
32
+ import type { LifecycleSnapshot } from "../../lifecycle/types";
32
33
  import {
33
34
  appendReleaseRecord,
34
35
  readReleaseLedger,
@@ -37,7 +38,7 @@ import {
37
38
  resolveRunId,
38
39
  InvalidReleaseRecordError,
39
40
  } from "../../lifecycle/release-ledger";
40
- import { reconcileStatus, liveEvidenceFromChangeSet, compareAcrossEnvironments, mergeLiveEvidence, type LiveComponentEvidence } from "../../lifecycle/status";
41
+ import { reconcileStatus, liveEvidenceFromChangeSet, compareAcrossEnvironments, mergeLiveEvidence, type LiveComponentEvidence, type EntityLiveRead } from "../../lifecycle/status";
41
42
  import { commandBuildParams } from "../build-params-cli";
42
43
  import { buildChangeSet } from "../../lifecycle/change-set";
43
44
  import { buildLedgerEntries, componentBomSummary, type BuildLedgerEntry } from "../../lifecycle/build-ledger";
@@ -51,11 +52,12 @@ import { build } from "../../build";
51
52
  import { discoverComponents } from "../../components/discover";
52
53
  import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
53
54
  import type { CommandContext } from "../registry";
54
- import type { LexiconPlugin } from "../../lexicon";
55
+ import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
55
56
  import { normalizeObservation, mergeObservations, unobservedAll, type NormalizedObservation } from "../../observation";
56
57
  import type { Phase, Component } from "../../components/component";
57
58
  import { deployUnits } from "../../components/deploy-units";
58
59
  import { sortedJsonReplacer } from "../../utils";
60
+ import { withoutReadFlags } from "../../lifecycle/legacy-digest";
59
61
 
60
62
  /**
61
63
  * chant components release <env> --component <name> --digest <sha256:...>
@@ -276,7 +278,8 @@ export async function runComponentsExport(ctx: CommandContext): Promise<number>
276
278
  }
277
279
  }
278
280
 
279
- await writeFile(join(targetDir, "manifest.json"), JSON.stringify(manifest, sortedJsonReplacer, 2));
281
+ // The exported manifest is the recorded one; read-side flags (#2514) stay out of it.
282
+ await writeFile(join(targetDir, "manifest.json"), JSON.stringify(withoutReadFlags(manifest), sortedJsonReplacer, 2));
280
283
 
281
284
  const missing = results.filter((r) => r.status === "missing");
282
285
 
@@ -284,6 +287,7 @@ export async function runComponentsExport(ctx: CommandContext): Promise<number>
284
287
  console.log(JSON.stringify({
285
288
  component: manifest.component,
286
289
  manifestDigest: manifest.manifestDigest,
290
+ ...(manifest.flags ? { flags: manifest.flags } : {}),
287
291
  outDir: targetDir,
288
292
  entries: results,
289
293
  }, null, 2));
@@ -318,6 +322,7 @@ interface StatusJsonRow {
318
322
  runId: string;
319
323
  timestamp: string;
320
324
  actor: string;
325
+ flags?: string[];
321
326
  } | null;
322
327
  build: {
323
328
  manifestDigest: string;
@@ -464,6 +469,42 @@ async function observeComponentStacks(
464
469
  return evidence;
465
470
  }
466
471
 
472
+ /**
473
+ * The last lifecycle snapshot(s) for one lexicon, as `components status --live`
474
+ * reads them for its drift baseline (#2513). Reads the stack-keyed snapshot for
475
+ * each stack the status read targets and the unstacked one `lifecycle plan`
476
+ * reads, merging resources (a stack's own snapshot wins) and remembering, per
477
+ * entity, which snapshot's timestamp it came from. A missing or unreadable
478
+ * snapshot is no baseline, never an error: with none, nothing can have changed
479
+ * since, and the status falls back to identity and presence.
480
+ */
481
+ async function readStatusBaseline(
482
+ environment: string,
483
+ lexicon: string,
484
+ stacks: Array<string | undefined>,
485
+ ): Promise<{ resources: Record<string, ResourceMetadata> | undefined; timestamps: Map<string, string> }> {
486
+ const keys = [...new Set([...stacks.filter((s): s is string => !!s).map((s) => snapshotStorageKey(lexicon, s)), snapshotStorageKey(lexicon)])];
487
+ let resources: Record<string, ResourceMetadata> | undefined;
488
+ const timestamps = new Map<string, string>();
489
+ for (const key of keys) {
490
+ let snap: LifecycleSnapshot | undefined;
491
+ try {
492
+ const content = await readSnapshot(environment, key);
493
+ snap = content ? (JSON.parse(content) as LifecycleSnapshot) : undefined;
494
+ } catch {
495
+ snap = undefined;
496
+ }
497
+ if (!snap?.resources) continue;
498
+ resources ??= {};
499
+ for (const [name, meta] of Object.entries(snap.resources)) {
500
+ if (Object.prototype.hasOwnProperty.call(resources, name)) continue;
501
+ resources[name] = meta;
502
+ if (snap.timestamp) timestamps.set(name, snap.timestamp);
503
+ }
504
+ }
505
+ return { resources, timestamps };
506
+ }
507
+
467
508
  export async function runComponentsStatus(ctx: CommandContext): Promise<number> {
468
509
  const { args, plugins, serializers } = ctx;
469
510
  const requestedEnv = args.extraPositional;
@@ -554,6 +595,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
554
595
  for (const stack of config.stacks ?? []) componentStackNames.add(stack.name);
555
596
  const readTargets: Array<string | undefined> = componentStackNames.size ? [...componentStackNames] : [undefined];
556
597
  const merged: { env: string; entries: import("../../lifecycle/change-set").ChangeSetEntry[] } = { env: environment, entries: [] };
598
+ const observations = new Map<string, EntityLiveRead>();
557
599
  for (const plugin of plugins) {
558
600
  if (!plugin.describeResources) continue;
559
601
  const declared = new Set<string>();
@@ -593,15 +635,29 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
593
635
  console.error(formatWarning({ message: `${plugin.name}: describeResources failed — ${message} (components in this lexicon report unknown, not stale)` }));
594
636
  observed = { resources: {}, unobserved: unobservedAll(declared, "read-failed", message, entities), queried: {}, sources: {}, notes: [] };
595
637
  }
638
+ // The last lifecycle snapshot is the prior observation drift is
639
+ // measured against (#2513) — the same one `lifecycle plan` reads, so
640
+ // `drifted` here means what `update` means there. Without it every
641
+ // live, declared entity classified `noop` and `drifted` could not be
642
+ // reached. Each entity remembers its snapshot's timestamp so the
643
+ // status join can ignore a change made before the release.
644
+ const baseline = await readStatusBaseline(environment, plugin.name, readTargets);
596
645
  const cs = buildChangeSet(environment, {
597
646
  declared,
598
647
  observedNow: observed.resources,
599
- observedThen: undefined,
648
+ observedThen: baseline.resources,
600
649
  unobserved: observed.unobserved,
601
650
  }, { lexicon: plugin.name });
602
651
  merged.entries.push(...cs.entries);
652
+ for (const entry of cs.entries) {
653
+ observations.set(entry.name, {
654
+ now: observed.resources[entry.name],
655
+ then: baseline.resources?.[entry.name],
656
+ thenAt: baseline.timestamps.get(entry.name),
657
+ });
658
+ }
603
659
  }
604
- liveEvidence = liveEvidenceFromChangeSet(merged, liveNameMapping);
660
+ liveEvidence = liveEvidenceFromChangeSet(merged, liveNameMapping, { observations });
605
661
 
606
662
  // Multi-stack component projects (each component owns its own stack) are
607
663
  // invisible to the entity-keyed, single-stack `describeResources` above —
@@ -652,6 +708,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
652
708
  runId: row.recorded.runId,
653
709
  timestamp: row.recorded.timestamp,
654
710
  actor: row.recorded.actor,
711
+ ...(row.recorded.flags ? { flags: row.recorded.flags } : {}),
655
712
  }
656
713
  : null,
657
714
  build: row.build
@@ -729,14 +786,19 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
729
786
  row.env.padEnd(12) +
730
787
  digestShort.padEnd(20) +
731
788
  row.reconciliation.padEnd(14) +
732
- row.detail,
789
+ row.detail +
790
+ (row.recorded?.flags?.length ? ` [${row.recorded.flags.join(", ")}]` : ""),
733
791
  );
734
792
  }
735
793
 
736
794
  if (comparisons) {
737
795
  console.log(`\n${formatBold(`Cross-check: ${requestedEnv} vs ${args.compareTo}`)}`);
738
796
  for (const c of comparisons) {
739
- const verdict = c.same ? "same build" : "DIFFERENT builds";
797
+ const verdict = c.same
798
+ ? "same build"
799
+ : c.mixedDigestForms
800
+ ? "different digests, one recorded before real SHA-256 (legacy-digest), so this cannot tell"
801
+ : "DIFFERENT builds";
740
802
  console.log(` ${c.component}: ${c.digestA ?? "(none)"} vs ${c.digestB ?? "(none)"} — ${verdict}`);
741
803
  }
742
804
  }
@@ -1,5 +1,5 @@
1
1
  import { resolve } from "node:path";
2
- import { formatError, formatSuccess } from "../format";
2
+ import { formatError, formatSuccess, formatWarning } from "../format";
3
3
  import type { CommandContext } from "../registry";
4
4
 
5
5
  export async function runDevGenerate(ctx: CommandContext): Promise<number> {
@@ -34,8 +34,10 @@ export async function runDevOnboard(ctx: CommandContext): Promise<number> {
34
34
 
35
35
  const { onboardCommand, printOnboardResult } = await import("../commands/onboard");
36
36
  // chant#2578 — so a lexicon the project declares by path is recognised.
37
- const { recordProjectLexicons } = await import("../plugins");
38
- await recordProjectLexicons(process.cwd());
37
+ // chant#2591 — read without running chant.config.ts.
38
+ const { recordProjectLexicons, unknownPathLexiconsNotice } = await import("../plugins");
39
+ const notice = unknownPathLexiconsNotice(recordProjectLexicons(process.cwd()));
40
+ if (notice !== undefined) console.error(formatWarning({ message: notice }));
39
41
  const result = onboardCommand({ name, verbose: ctx.args.verbose });
40
42
  await printOnboardResult(result, name);
41
43
  return result.success ? 0 : 1;
@@ -43,7 +43,8 @@ import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
43
43
  import { runFanOut } from "../../components/fan-out-run";
44
44
  import { renderFanOutHuman, renderFanOutJson, renderFanOutPlan } from "../../components/fan-out-output";
45
45
  import { ndjsonProgressSink } from "../../components/run-progress";
46
- import { writeGatedRunSummary } from "../../op/gate-summary";
46
+ import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
47
+ import { approveCommand } from "../../op/gate";
47
48
  import { remainingFanOut, type ChangedUnits, type FanOutProgress } from "../../components/fan-out";
48
49
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
49
50
  import { formatError, formatInfo, formatWarning } from "../format";
@@ -320,7 +321,11 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
320
321
 
321
322
  if (result.status === "gated" && result.gate) {
322
323
  const pending = result.gate;
323
- console.error(formatInfo(`approve : chant approve ${pending.op} ${pending.gate} --plan ${result.plan.digest}`));
324
+ // The pending fact's own plan: the fan-out's digest for the gate over the
325
+ // set, or a component's plan and environment for a gate inside one (#2574).
326
+ console.error(formatInfo(
327
+ `approve : ${approveCommand(pending.op, pending.gate, pending.environment)} --plan ${pending.planDigest ?? result.plan.digest}`,
328
+ ));
324
329
  writeGatedRunSummary({
325
330
  op: pending.op,
326
331
  gate: pending.gate,
@@ -328,6 +333,8 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
328
333
  expiresAt: pending.expiresAt,
329
334
  ...(pending.url ? { url: pending.url } : {}),
330
335
  ...(pending.planDigest ? { planDigest: pending.planDigest } : {}),
336
+ ...(await summaryLedgerPrefix()),
337
+ ...(pending.environment ? { environment: pending.environment } : {}),
331
338
  });
332
339
  return GATED_EXIT_CODE;
333
340
  }
@@ -1,9 +1,11 @@
1
- import { formatError } from "../format";
1
+ import { formatError, formatSuccess, formatWarning } from "../format";
2
2
  import type { CommandContext } from "../registry";
3
3
 
4
4
  export async function runInit(ctx: CommandContext): Promise<number> {
5
5
  const { args } = ctx;
6
6
 
7
+ if (args.migrateFrom !== undefined) return runInitFrom(ctx);
8
+
7
9
  if (!args.lexicon) {
8
10
  console.error(formatError({
9
11
  message: "Missing --lexicon flag",
@@ -47,3 +49,54 @@ export async function runInitLexicon(ctx: CommandContext): Promise<number> {
47
49
  await printInitLexiconResult(result);
48
50
  return result.success ? 0 : 1;
49
51
  }
52
+
53
+ /**
54
+ * `chant init --from <repo>@<ref>[#<member>] [--param <name>=<value>]... [path]`
55
+ * (#2540, #2627): copy a template repository at a ref, substitute the
56
+ * parameters its `chant.template.json` declares, and record its lineage.
57
+ * The template brings its own lexicons and configuration, so `--lexicon` and
58
+ * `--template` do not apply.
59
+ */
60
+ async function runInitFrom(ctx: CommandContext): Promise<number> {
61
+ const { args } = ctx;
62
+ const usage = "Usage: chant init --from <repo>@<ref>[#<member>] [--param <name>=<value>]... [path]";
63
+ if (!args.migrateFrom) {
64
+ console.error(formatError({ message: "--from needs a template: <repo>@<ref>[#<member>]", hint: usage }));
65
+ return 1;
66
+ }
67
+ if (args.lexicon || args.template) {
68
+ console.error(formatError({ message: "--from cannot be combined with --lexicon or --template", hint: usage }));
69
+ return 1;
70
+ }
71
+ if (args.paramsFile) {
72
+ console.error(formatError({ message: "--params-file does not apply to init --from; pass each value with --param <name>=<value>", hint: usage }));
73
+ return 1;
74
+ }
75
+ const { parseParamArgs } = await import("../../workspace/template-manifest");
76
+ let params: Record<string, string>;
77
+ try {
78
+ params = parseParamArgs(args.param ?? []);
79
+ } catch (err) {
80
+ console.error(formatError({ message: (err as Error).message, hint: usage }));
81
+ return 1;
82
+ }
83
+ const { initFromCommand } = await import("../../workspace/lineage-init");
84
+ const result = await initFromCommand({
85
+ from: args.migrateFrom,
86
+ path: args.path === "." ? undefined : args.path,
87
+ force: args.force,
88
+ params,
89
+ });
90
+ if (!result.success) {
91
+ console.error(formatError({ message: result.error ?? "init --from failed", hint: usage }));
92
+ return 1;
93
+ }
94
+ for (const warning of result.warnings) console.error(formatWarning({ message: warning }));
95
+ console.log(formatSuccess("Created:"));
96
+ for (const file of result.createdFiles) console.log(` ${file}`);
97
+ console.log("");
98
+ console.log(`Lineage: ${result.spec!.id} at ${result.spec!.ref} (${result.commit!.slice(0, 12)}), recorded in .chant/workspace.lock.json`);
99
+ const parameters = Object.entries(result.parameters ?? {});
100
+ if (parameters.length > 0) console.log(`Parameters: ${parameters.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ")}`);
101
+ return 0;
102
+ }
@@ -897,6 +897,74 @@ describe("runApprove — the resolution names the plan it approves (#2300)", ()
897
897
  });
898
898
  });
899
899
 
900
+ /**
901
+ * #2574 — a component's gate ledger holds every environment's pending facts.
902
+ * The approval records the environment it answers, and `--env` picks it.
903
+ */
904
+ describe("runApprove — a component gate approval names its environment (#2574)", () => {
905
+ const PLAN_STAGING = `sha256:${"c".repeat(64)}`;
906
+ const PLAN_PROD = `sha256:${"d".repeat(64)}`;
907
+ function seedTwoEnvironments(): void {
908
+ readGateLedgerMock.mockResolvedValue({
909
+ resolutions: [],
910
+ pending: [
911
+ {
912
+ version: 1, kind: "pending", op: "search-service", gate: "release",
913
+ timestamp: "2026-01-01T00:00:00.000Z", expiresAt: "2099-01-01T00:00:00.000Z",
914
+ planDigest: PLAN_STAGING, environment: "staging",
915
+ },
916
+ {
917
+ version: 1, kind: "pending", op: "search-service", gate: "release",
918
+ timestamp: "2026-01-02T00:00:00.000Z", expiresAt: "2099-01-01T00:00:00.000Z",
919
+ planDigest: PLAN_PROD, environment: "prod",
920
+ },
921
+ ],
922
+ malformed: 0,
923
+ });
924
+ appendGateResolutionMock.mockImplementation(async (input: object) => ({ commit: "sha", record: { version: 1, ...input } }));
925
+ }
926
+
927
+ test("without --env it approves the newest pending fact and records its environment", async () => {
928
+ seedTwoEnvironments();
929
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
930
+
931
+ expect(await runApprove(ctx({ path: "search-service", extraPositional: "release", actor: "alex" }))).toBe(0);
932
+ expect(appendGateResolutionMock).toHaveBeenCalledWith(expect.objectContaining({ planDigest: PLAN_PROD, environment: "prod" }));
933
+ expect(errSpy.mock.calls.map((c) => String(c[0])).join("\n")).toContain(`approves the plan ${PLAN_PROD} in environment "prod"`);
934
+ errSpy.mockRestore();
935
+ });
936
+
937
+ test("--env picks that environment's pending fact", async () => {
938
+ seedTwoEnvironments();
939
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
940
+
941
+ expect(await runApprove(ctx({ path: "search-service", extraPositional: "release", actor: "alex", env: "staging" }))).toBe(0);
942
+ expect(appendGateResolutionMock).toHaveBeenCalledWith(expect.objectContaining({ planDigest: PLAN_STAGING, environment: "staging" }));
943
+ errSpy.mockRestore();
944
+ });
945
+
946
+ test("--env with no pending fact there refuses and writes nothing", async () => {
947
+ seedTwoEnvironments();
948
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
949
+
950
+ expect(await runApprove(ctx({ path: "search-service", extraPositional: "release", actor: "alex", env: "dev" }))).toBe(1);
951
+ expect(appendGateResolutionMock).not.toHaveBeenCalled();
952
+ expect(errSpy.mock.calls.map((c) => String(c[0])).join("\n")).toContain('no pending fact in environment "dev"');
953
+ errSpy.mockRestore();
954
+ });
955
+
956
+ test("--env is ignored for a gate whose pending facts record no environment", async () => {
957
+ seedPending("fountain-apply", "rollout-gate", PLAN_A);
958
+ appendGateResolutionMock.mockImplementation(async (input: object) => ({ commit: "sha", record: { version: 1, ...input } }));
959
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
960
+
961
+ expect(await runApprove(ctx({ path: "fountain-apply", extraPositional: "rollout-gate", actor: "alex", env: "prod" }))).toBe(0);
962
+ expect(appendGateResolutionMock.mock.calls[0][0]).toMatchObject({ planDigest: PLAN_A });
963
+ expect(appendGateResolutionMock.mock.calls[0][0]).not.toHaveProperty("environment");
964
+ errSpy.mockRestore();
965
+ });
966
+ });
967
+
900
968
  describe("runApprove — the ledger branch is read before it is appended to (#2303)", () => {
901
969
  test("reads the branch before recording the resolution", async () => {
902
970
  seedPending("fountain-apply", "rollout-gate", PLAN_A);
@@ -44,7 +44,7 @@ import {
44
44
  type GateApprover, type GatePolicyDecision, type GatePolicyEvaluator, type ResolvedGateApproval,
45
45
  } from "../../op/gate-approval";
46
46
  import { approverOf, tallyGateApprovals } from "../../op/gate";
47
- import { FAN_OUT_GATE_OP } from "../../op/gate-name";
47
+ import { FAN_OUT_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "../../op/gate-name";
48
48
  import { pushLifecycle, requireLifecycleLedger } from "../../lifecycle/git";
49
49
  import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
50
50
  import type { CommandContext } from "../registry";
@@ -689,7 +689,9 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
689
689
  console.error(formatInfo(
690
690
  opName === FAN_OUT_GATE_OP
691
691
  ? `The next \`chant components fan-out\` decides this gate from scratch and records a fresh pending fact.`
692
- : `The next \`chant run ${opName}\` decides this gate from scratch and records a fresh pending fact.`,
692
+ : opName === WORKSPACE_UPGRADE_GATE_OP
693
+ ? `The next \`chant workspace upgrade ${gate}\` decides this gate from scratch and records a fresh pending fact.`
694
+ : `The next \`chant run ${opName}\` decides this gate from scratch and records a fresh pending fact.`,
693
695
  ));
694
696
  return 0;
695
697
  }
@@ -703,6 +705,7 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
703
705
  note: ctx.args.note,
704
706
  url: ctx.args.url,
705
707
  plan: ctx.args.plan,
708
+ ...(ctx.args.env !== undefined ? { environment: ctx.args.env } : {}),
706
709
  allowSameOrigin: ctx.args.allowSameOrigin,
707
710
  ...(ctx.args.roles ? { roles: ctx.args.roles } : {}),
708
711
  ...(ctx.args.agent ? { agent: true } : {}),
@@ -713,7 +716,9 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
713
716
  `This records the resolution as a fact; it does not itself re-run anything. ` +
714
717
  (opName === FAN_OUT_GATE_OP
715
718
  ? `Repeat the \`chant components fan-out\` command and it walks through gate "${gate}".`
716
- : `Run \`chant run ${opName}\` and it walks through gate "${gate}".`),
719
+ : opName === WORKSPACE_UPGRADE_GATE_OP
720
+ ? `Repeat \`chant workspace upgrade ${gate}\` with the same target, and it applies the patch this approval names.`
721
+ : `Run \`chant run ${opName}\` and it walks through gate "${gate}".`),
717
722
  ));
718
723
  return 0;
719
724
  }
@@ -747,6 +752,13 @@ export interface GateApprovalOptions {
747
752
  * stays one command: run, read what it planned, approve it.
748
753
  */
749
754
  plan?: string;
755
+ /**
756
+ * `--env <env>` (#2574) — which environment's pending fact a component gate
757
+ * approval answers. A component's gate ledger holds every environment's
758
+ * facts, so without it the newest one is approved. Ignored for a gate whose
759
+ * pending facts record no environment (every Op gate).
760
+ */
761
+ environment?: string;
750
762
  /** `--role` (#2508) — roles the approver claims. */
751
763
  roles?: string[];
752
764
  /**
@@ -786,7 +798,7 @@ export async function recordGateApproval(
786
798
  // `fan-out` is the op name every `chant components fan-out` gate is recorded
787
799
  // under (../handlers/fan-out.ts). It is a command rather than a declaration,
788
800
  // so there is no `*.op.ts` to find and nothing is wrong when none is there.
789
- if (!ops.has(opName) && opName !== FAN_OUT_GATE_OP) {
801
+ if (!ops.has(opName) && opName !== FAN_OUT_GATE_OP && opName !== WORKSPACE_UPGRADE_GATE_OP) {
790
802
  console.error(formatWarning({
791
803
  message: `Op "${opName}" was not found among discovered *.op.ts declarations — recording the resolution anyway`,
792
804
  }));
@@ -826,6 +838,18 @@ export async function recordGateApproval(
826
838
  // third fallback: approving with no plan in sight is the behaviour
827
839
  // INTENTIUS/choudoufu#1026 measured, where the resolution authorised the
828
840
  // next run rather than anything anyone had seen.
841
+ // #2574: a component gate's pending facts carry the environment they were
842
+ // recorded in, and an approval answers one environment. `--env` picks it;
843
+ // without it, the newest standing fact decides, and the success line below
844
+ // names the environment it chose.
845
+ const ledgerBefore = await readGateLedger(opName);
846
+ const pendingFor = (all: PendingGateRecord[]): PendingGateRecord[] => {
847
+ const bound = all.some((p) => p.gate === gate && p.environment !== undefined);
848
+ return bound && opts.environment !== undefined ? all.filter((p) => p.environment === opts.environment) : all;
849
+ };
850
+ const standing = latestPendingGate(pendingFor(ledgerBefore.pending), gate);
851
+ const environment = standing?.environment;
852
+
829
853
  let planDigest: string | undefined;
830
854
  if (opts.plan !== undefined) {
831
855
  if (!isPlanDigest(opts.plan)) {
@@ -837,10 +861,12 @@ export async function recordGateApproval(
837
861
  }
838
862
  planDigest = opts.plan;
839
863
  } else {
840
- const standing = latestPendingGate((await readGateLedger(opName)).pending, gate);
841
864
  if (!standing) {
842
865
  console.error(formatError({
843
- message: `Gate "${gate}" on "${opName}" has no pending fact, so there is no plan to approve`,
866
+ message:
867
+ opts.environment !== undefined && ledgerBefore.pending.some((p) => p.gate === gate && p.environment !== undefined)
868
+ ? `Gate "${gate}" on "${opName}" has no pending fact in environment "${opts.environment}", so there is no plan to approve`
869
+ : `Gate "${gate}" on "${opName}" has no pending fact, so there is no plan to approve`,
844
870
  hint:
845
871
  `Run \`chant run ${opName}\` first — it plans, stops at the gate, and records the plan this ` +
846
872
  `approval would be for. To approve a plan you already have the digest for, pass ` +
@@ -859,7 +885,7 @@ export async function recordGateApproval(
859
885
  // the way #2300 refuses a plan nobody approved.
860
886
  const origin = opts.origin ?? currentGateOrigin();
861
887
  const ledger = await readGateLedger(opName);
862
- const standingForOrigin = latestPendingGate(ledger.pending, gate);
888
+ const standingForOrigin = latestPendingGate(pendingFor(ledger.pending), gate);
863
889
  const refusal = sameOriginRefusal(standingForOrigin?.origin, origin);
864
890
  if (refusal && !opts.allowSameOrigin) {
865
891
  console.error(formatError({
@@ -935,6 +961,7 @@ export async function recordGateApproval(
935
961
  ...(opts.note ? { note: opts.note } : {}),
936
962
  ...(url ? { url } : {}),
937
963
  ...(planDigest !== undefined ? { planDigest } : {}),
964
+ ...(environment !== undefined ? { environment } : {}),
938
965
  origin,
939
966
  ...(refusal && opts.allowSameOrigin ? { sameOriginOverride: true } : {}),
940
967
  });
@@ -950,8 +977,9 @@ export async function recordGateApproval(
950
977
  ));
951
978
  if (record.planDigest) {
952
979
  console.error(formatInfo(
953
- `This approves the plan ${record.planDigest}, and only that plan. A run whose fresh plan ` +
954
- "differs refuses rather than applying it.",
980
+ `This approves the plan ${record.planDigest}` +
981
+ (record.environment !== undefined ? ` in environment "${record.environment}"` : "") +
982
+ ", and only that plan. A run whose fresh plan differs refuses rather than applying it.",
955
983
  ));
956
984
  }
957
985
  if (approval) {