@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
@@ -1,5 +1,6 @@
1
1
  /**
2
- * `chant components promote` (#2530), with the ledger, discovery and the
2
+ * `chant components promote` (#2530), `rollback` (#2531) and `redeploy`
3
+ * (#2604), with the ledger, discovery and the
3
4
  * capability registry replaced by fakes.
4
5
  */
5
6
 
@@ -66,7 +67,7 @@ vi.mock("../../op/gate", async () => {
66
67
  // The run handler pulls in every runtime; the promote needs only its exit code.
67
68
  vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
68
69
 
69
- const { runComponentsPromote, runComponentsRollback } = await import("./promote");
70
+ const { runComponentsPromote, runComponentsRollback, runComponentsRedeploy } = await import("./promote");
70
71
 
71
72
  const api: DriverComponent = {
72
73
  name: "api",
@@ -197,6 +198,37 @@ describe("chant components promote", () => {
197
198
  expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ component: "api", env: "prod" });
198
199
  });
199
200
 
201
+ test("--digest <component>=<digest> promotes the pinned release, not the latest one (#2602)", async () => {
202
+ const web: DriverComponent = { ...api, name: "web" };
203
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [api, web] });
204
+ // Another pipeline recorded sha256:newer after this one recorded sha256:api.
205
+ readReleaseLedgerMock.mockResolvedValue({
206
+ records: [staging, { ...staging, digest: "sha256:newer", timestamp: "2026-01-03T00:00:00.000Z", runId: "run-8" }, { ...staging, component: "web" }],
207
+ malformed: 0,
208
+ });
209
+ expect(await runComponentsPromote(ctx({ digests: ["api=sha256:api"] }))).toBe(0);
210
+ expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
211
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({
212
+ component: "api",
213
+ env: "prod",
214
+ digest: "sha256:api",
215
+ promotedFrom: { env: "staging", runId: "run-7" },
216
+ });
217
+ });
218
+
219
+ test("an empty pinned digest fails and deploys nothing (#2602)", async () => {
220
+ expect(await runComponentsPromote(ctx({ digests: ["api="] }))).toBe(1);
221
+ expect(errors.join("\n")).toMatch(/--digest api= names no digest/);
222
+ expect(ran).toEqual([]);
223
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
224
+ });
225
+
226
+ test("a bare --digest without --component is refused", async () => {
227
+ expect(await runComponentsPromote(ctx({ digest: "sha256:api", digests: ["sha256:api"] }))).toBe(1);
228
+ expect(errors.join("\n")).toMatch(/needs --component/);
229
+ expect(ran).toEqual([]);
230
+ });
231
+
200
232
  test("a component with no publish step is refused before anything runs", async () => {
201
233
  resolveTargetsMock.mockResolvedValue({
202
234
  success: true,
@@ -259,3 +291,87 @@ describe("chant components rollback", () => {
259
291
  expect(errors.join("\n")).toMatch(/chant approve api rollback-ok/);
260
292
  });
261
293
  });
294
+
295
+ describe("chant components redeploy", () => {
296
+ const prod = (digest: string, timestamp: string, runId: string): ReleaseRecord =>
297
+ ({ ...staging, env: "prod", digest, timestamp, runId });
298
+
299
+ beforeEach(() => {
300
+ readReleaseLedgerMock.mockResolvedValue({
301
+ records: [prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1"), prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2")],
302
+ malformed: 0,
303
+ });
304
+ });
305
+
306
+ const rd = (a: Partial<ParsedArgs>) => ctx({ migrateFrom: undefined, migrateTo: undefined, path: "redeploy", extraPositional: "prod", component: "api", ...a });
307
+
308
+ test("an environment and --component are required", async () => {
309
+ expect(await runComponentsRedeploy(rd({ extraPositional: undefined }))).toBe(1);
310
+ expect(errors.join("\n")).toMatch(/--component <name> are required/);
311
+ expect(errors.join("\n")).toMatch(/chant components redeploy <env>/);
312
+ });
313
+
314
+ test("redeploys the current release from its recorded digest and records it", async () => {
315
+ expect(await runComponentsRedeploy(rd({}))).toBe(0);
316
+ expect(readReleaseLedgerMock).toHaveBeenCalledWith("prod");
317
+ // Neither a build nor a publish: the environment already has the artifact.
318
+ expect(ran).toEqual(["prod:apply"]);
319
+ expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
320
+ const written = appendReleaseRecordMock.mock.calls[0][0];
321
+ expect(written).toMatchObject({
322
+ component: "api",
323
+ env: "prod",
324
+ digest: "sha256:new",
325
+ runId: "run-9",
326
+ actor: "bob",
327
+ redeploys: { env: "prod", runId: "run-2", timestamp: "2026-01-02T00:00:00.000Z" },
328
+ });
329
+ expect(written).not.toHaveProperty("restores");
330
+ expect(written).not.toHaveProperty("promotedFrom");
331
+ expect(pushLifecycleMock).toHaveBeenCalledTimes(1);
332
+ expect(errors.join("\n")).toMatch(/Redeployed .*api.* in prod at sha256:new/);
333
+ });
334
+
335
+ test("the same digest rollback refuses is the one redeploy accepts", async () => {
336
+ expect(await runComponentsRollback(rd({ path: "rollback", digest: "sha256:new" }))).toBe(1);
337
+ expect(errors.join("\n")).toMatch(/already the current release/);
338
+ expect(await runComponentsRedeploy(rd({ digest: "sha256:new" }))).toBe(0);
339
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ digest: "sha256:new" });
340
+ });
341
+
342
+ test("a --digest other than the current one is refused before anything runs", async () => {
343
+ expect(await runComponentsRedeploy(rd({ digest: "sha256:old" }))).toBe(1);
344
+ expect(errors.join("\n")).toMatch(/sha256:old is not the current release/);
345
+ expect(ran).toEqual([]);
346
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
347
+ });
348
+
349
+ test("--dry-run prints the plan and deploys nothing", async () => {
350
+ expect(await runComponentsRedeploy(rd({ dryRun: true, json: true }))).toBe(0);
351
+ expect(ran).toEqual([]);
352
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
353
+ const plan = JSON.parse(logs.join("\n"));
354
+ expect(plan).toMatchObject({ from: "prod", to: "prod" });
355
+ expect(plan.items).toEqual([expect.objectContaining({ component: "api", digest: "sha256:new", notRun: ["docker-build", "publish-image"] })]);
356
+ });
357
+
358
+ test("the environment's gate applies to a redeploy", async () => {
359
+ const gated: DriverComponent = {
360
+ ...api,
361
+ deploy: [...api.deploy.slice(0, 2), { phase: "Apply", steps: [{ kind: "gate", gate: "redeploy-ok" }, { kind: "apply" }] }],
362
+ };
363
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [gated] });
364
+ expect(await runComponentsRedeploy(rd({}))).toBe(3);
365
+ expect(ran).not.toContain("prod:apply");
366
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
367
+ expect(errors.join("\n")).toMatch(/chant approve api redeploy-ok/);
368
+ expect(errors.join("\n")).toMatch(/then run the same redeploy again/);
369
+ });
370
+
371
+ test("a failed redeploy records nothing and exits 1", async () => {
372
+ failApplyFor = "api";
373
+ expect(await runComponentsRedeploy(rd({}))).toBe(1);
374
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
375
+ expect(errors.join("\n")).toMatch(/redeploy failed at "api"/);
376
+ });
377
+ });
@@ -1,8 +1,10 @@
1
1
  /**
2
- * `chant components promote --from <env> --to <env>` (#2530) and `chant
3
- * components rollback <env>` (#2531): deploy digests a release ledger already
4
- * records, without a build. A promote takes them from another environment's
5
- * ledger; a rollback takes an earlier release from the environment's own. The
2
+ * `chant components promote --from <env> --to <env>` (#2530), `chant
3
+ * components rollback <env>` (#2531) and `chant components redeploy <env>`
4
+ * (#2604): deploy digests a release ledger already records, without a build.
5
+ * A promote takes them from another environment's ledger; a rollback takes an
6
+ * earlier release from the environment's own, and a redeploy the release it
7
+ * records as current. The
6
8
  * mechanism lives in ../../components/promote.ts; these handlers read the
7
9
  * ledger, print the plan, run it, and append the release records.
8
10
  *
@@ -29,34 +31,39 @@ import { fanOutRegistry } from "../../components/fan-out-support";
29
31
  import { renderDriverHuman } from "../../components/driver-output";
30
32
  import { ndjsonProgressSink } from "../../components/run-progress";
31
33
  import {
34
+ parseDigestPins,
32
35
  planPromotion,
33
36
  planRollback,
37
+ planRedeploy,
34
38
  withoutBuildSteps,
35
39
  runPromotion,
36
40
  promotionRecord,
37
41
  rollbackRecord,
42
+ redeployRecord,
38
43
  gateApprover,
39
44
  type DeployRunInfo,
40
45
  type PromotionItem,
41
46
  type PromotionPlan,
42
47
  } from "../../components/promote";
43
48
  import type { DriverComponent } from "../../components/driver";
44
- import { writeGatedRunSummary } from "../../op/gate-summary";
45
- import { approveCommand } from "../../op/gate";
49
+ import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
50
+ import { approveCommand, describeGateMismatch } from "../../op/gate";
46
51
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
47
52
  import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
48
53
  import { GATED_EXIT_CODE } from "./run";
49
54
  import type { CommandContext } from "../registry";
50
55
 
51
56
  const PROMOTE_USAGE =
52
- "chant components promote --from <env> --to <env> [--component <name> [--digest <sha256:...>]] [--dry-run] [--json]";
57
+ "chant components promote --from <env> --to <env> [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--dry-run] [--json]";
53
58
  const ROLLBACK_USAGE =
54
59
  "chant components rollback <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
60
+ const REDEPLOY_USAGE =
61
+ "chant components redeploy <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
55
62
 
56
- type Verb = "promote" | "rollback";
63
+ type Verb = "promote" | "rollback" | "redeploy";
57
64
 
58
65
  function renderPlan(verb: Verb, plan: PromotionPlan, removed: Map<string, string[]>): void {
59
- console.error(formatBold(verb === "promote" ? `promote ${plan.from} -> ${plan.to}` : `rollback ${plan.to}`));
66
+ console.error(formatBold(verb === "promote" ? `promote ${plan.from} -> ${plan.to}` : `${verb} ${plan.to}`));
60
67
  for (const item of plan.items) {
61
68
  console.error(` ${item.component} ${item.digest}`);
62
69
  console.error(` released to ${plan.from} at ${item.source.timestamp} (run ${item.source.runId}, git ${item.source.gitSha.slice(0, 12)})`);
@@ -117,9 +124,9 @@ async function readLedger(env: string): Promise<ReleaseRecord[]> {
117
124
  }
118
125
 
119
126
  /**
120
- * Prepare, run and record a plan. Shared by promote and rollback, which differ
121
- * only in how the plan was chosen, whether the publish step runs, and which
122
- * field names the earlier release on the new record.
127
+ * Prepare, run and record a plan. Shared by promote, rollback and redeploy,
128
+ * which differ only in how the plan was chosen, whether the publish step runs,
129
+ * and which field names the earlier release on the new record.
123
130
  */
124
131
  async function deployPlan(
125
132
  ctx: CommandContext,
@@ -135,7 +142,12 @@ async function deployPlan(
135
142
  const removed = new Map<string, string[]>();
136
143
  const refusals: string[] = [];
137
144
  for (const item of plan.items) {
138
- const result = withoutBuildSteps(byName.get(item.component)!, verb === "rollback" ? { pinDigest: item.digest } : {});
145
+ // A rollback and a redeploy deploy a release this environment already
146
+ // received, so neither runs the publish step.
147
+ const result = withoutBuildSteps(
148
+ byName.get(item.component)!,
149
+ verb === "promote" ? {} : { pinDigest: item.digest, verb: `a ${verb}` },
150
+ );
139
151
  if ("error" in result) refusals.push(result.error);
140
152
  else {
141
153
  prepared.push(result.component);
@@ -217,7 +229,9 @@ async function deployPlan(
217
229
  console.error(formatSuccess(
218
230
  verb === "promote"
219
231
  ? `Promoted ${formatBold(r.component)} ${plan.from} -> ${plan.to}: ${r.digest}`
220
- : `Rolled back ${formatBold(r.component)} in ${plan.to} to ${r.digest}`,
232
+ : verb === "rollback"
233
+ ? `Rolled back ${formatBold(r.component)} in ${plan.to} to ${r.digest}`
234
+ : `Redeployed ${formatBold(r.component)} in ${plan.to} at ${r.digest}`,
221
235
  ));
222
236
  }
223
237
  }
@@ -233,7 +247,11 @@ async function deployPlan(
233
247
  if (run.status === "gated" && run.gate) {
234
248
  const pending = run.gate;
235
249
  console.error(formatWarning({ message: `component "${run.gatedComponent}" is gated on "${pending.gate}" — pending approval` }));
236
- console.error(formatInfo(`approve : ${approveCommand(pending.op, pending.gate)}`));
250
+ // #2574: an approval stands, but for another environment, plan or none.
251
+ if (run.gateMismatch) {
252
+ console.error(formatWarning({ message: describeGateMismatch(pending.op, pending.gate, run.gateMismatch) }));
253
+ }
254
+ console.error(formatInfo(`approve : ${approveCommand(pending.op, pending.gate, pending.environment)}`));
237
255
  console.error(formatInfo(`then run the same ${verb} again`));
238
256
  writeGatedRunSummary({
239
257
  op: pending.op,
@@ -242,6 +260,8 @@ async function deployPlan(
242
260
  expiresAt: pending.expiresAt,
243
261
  ...(pending.url ? { url: pending.url } : {}),
244
262
  ...(pending.planDigest ? { planDigest: pending.planDigest } : {}),
263
+ ...(await summaryLedgerPrefix()),
264
+ ...(pending.environment ? { environment: pending.environment } : {}),
245
265
  });
246
266
  return GATED_EXIT_CODE;
247
267
  }
@@ -280,6 +300,23 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
280
300
  const project = await loadProject(ctx, args.component ?? "all");
281
301
  if (typeof project === "number") return project;
282
302
 
303
+ // Without --component, each --digest is <component>=<digest> (#2602): a
304
+ // generated promote job pins every component to the release its own
305
+ // pipeline run recorded, rather than whatever is latest in --from.
306
+ const digests = args.digests ?? (args.digest ? [args.digest] : []);
307
+ let pins: Record<string, string> | undefined;
308
+ if (!args.component && digests.length > 0) {
309
+ const parsed = parseDigestPins(digests);
310
+ if ("error" in parsed) {
311
+ console.error(formatError({ message: parsed.error, hint: PROMOTE_USAGE }));
312
+ return 1;
313
+ }
314
+ pins = parsed.pins;
315
+ } else if (args.component && digests.length > 1) {
316
+ console.error(formatError({ message: "--component takes one --digest", hint: PROMOTE_USAGE }));
317
+ return 1;
318
+ }
319
+
283
320
  await fetchLifecycle().catch(() => false);
284
321
  const plan = planPromotion({
285
322
  from,
@@ -287,7 +324,8 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
287
324
  sourceRecords: await readLedger(from),
288
325
  declared: project.targets.map((c) => c.name),
289
326
  ...(args.component ? { component: args.component } : {}),
290
- ...(args.digest ? { digest: args.digest } : {}),
327
+ ...(args.component && args.digest ? { digest: args.digest } : {}),
328
+ ...(pins ? { pins } : {}),
291
329
  });
292
330
  if ("error" in plan) {
293
331
  console.error(formatError({ message: plan.error, hint: PROMOTE_USAGE }));
@@ -323,3 +361,31 @@ export async function runComponentsRollback(ctx: CommandContext): Promise<number
323
361
  }
324
362
  return deployPlan(ctx, "rollback", plan, project, who.actor, rollbackRecord);
325
363
  }
364
+
365
+ export async function runComponentsRedeploy(ctx: CommandContext): Promise<number> {
366
+ const { args } = ctx;
367
+ const env = args.extraPositional;
368
+ if (!env || !args.component) {
369
+ console.error(formatError({ message: "an environment and --component <name> are required", hint: REDEPLOY_USAGE }));
370
+ return 1;
371
+ }
372
+ const who = actorOrRefuse(ctx);
373
+ if (typeof who === "number") return who;
374
+
375
+ const project = await loadProject(ctx, args.component);
376
+ if (typeof project === "number") return project;
377
+
378
+ await fetchLifecycle().catch(() => false);
379
+ const plan = planRedeploy({
380
+ env,
381
+ records: await readLedger(env),
382
+ declared: project.targets.map((c) => c.name),
383
+ component: args.component,
384
+ ...(args.digest ? { digest: args.digest } : {}),
385
+ });
386
+ if ("error" in plan) {
387
+ console.error(formatError({ message: plan.error, hint: REDEPLOY_USAGE }));
388
+ return 1;
389
+ }
390
+ return deployPlan(ctx, "redeploy", plan, project, who.actor, redeployRecord);
391
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `chant run --generate` inside a workspace member (#2542). Discovery is
3
+ * stubbed; the lexicon plugins are the real ones.
4
+ */
5
+
6
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
7
+ import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import type { ParsedArgs } from "../registry";
10
+ import type { OpConfig } from "../../op/types";
11
+ import { twoMemberWorkspace } from "../../workspace/__fixtures__/two-member-workspace";
12
+
13
+ const discovered = new Map<string, { config: Partial<OpConfig>; filePath: string; exportName: string }>();
14
+ vi.mock("../../op/discover", () => ({
15
+ discoverOps: async () => ({ ops: discovered, errors: [] }),
16
+ }));
17
+
18
+ const { runOp } = await import("./run");
19
+ const { generateOpsPipeline } = await import("../../op/generate-pipeline");
20
+ const { parseArgs } = await import("../main");
21
+
22
+ function ctx(argv: string[]) {
23
+ return { args: parseArgs(argv) as ParsedArgs, plugins: [], serializers: [] };
24
+ }
25
+
26
+ let repo: string;
27
+ let cwd: string;
28
+ let logs: string[];
29
+ beforeEach(() => {
30
+ repo = twoMemberWorkspace();
31
+ cwd = process.cwd();
32
+ discovered.clear();
33
+ discovered.set("nightly-audit", {
34
+ config: { name: "nightly-audit", phases: [], schedule: { cron: "0 6 * * *" } } as Partial<OpConfig>,
35
+ filePath: "nightly-audit.op.ts",
36
+ exportName: "default",
37
+ });
38
+ logs = [];
39
+ vi.spyOn(console, "error").mockImplementation(() => {});
40
+ vi.spyOn(console, "log").mockImplementation((s: string) => {
41
+ logs.push(s);
42
+ });
43
+ });
44
+ afterEach(() => {
45
+ process.chdir(cwd);
46
+ vi.restoreAllMocks();
47
+ rmSync(repo, { recursive: true, force: true });
48
+ });
49
+
50
+ describe("chant run --generate in a workspace member", () => {
51
+ test("github: the file is named after the member, at the repository root, and recorded", async () => {
52
+ process.chdir(join(repo, "services/api"));
53
+ expect(await runOp(ctx(["run", "--generate", "github"]))).toBe(0);
54
+
55
+ expect(readdirSync(join(repo, ".github/workflows"))).toEqual(["api-nightly-audit.yml"]);
56
+ expect(existsSync(join(repo, "services/api/.github"))).toBe(false);
57
+ const member = { name: "api", dir: "services/api", fileDir: ".github/workflows" };
58
+ const direct = await generateOpsPipeline([{ name: "nightly-audit" }], "github", { member });
59
+ const text = readFileSync(join(repo, ".github/workflows/api-nightly-audit.yml"), "utf-8");
60
+ expect(text).toBe(
61
+ "# Generated by chant — do not edit directly. Regenerate with: chant run --generate github (in the member's directory)\n" + direct.files![0].yaml,
62
+ );
63
+ const record = JSON.parse(readFileSync(join(repo, "services/api/.chant/generated.json"), "utf-8"));
64
+ expect(record.files).toEqual([{ path: ".github/workflows/api-nightly-audit.yml", command: "chant run --generate github" }]);
65
+ });
66
+
67
+ test("gitlab: the member's file goes in .gitlab/ci", async () => {
68
+ process.chdir(join(repo, "apps/web"));
69
+ expect(await runOp(ctx(["run", "--generate", "gitlab"]))).toBe(0);
70
+ const text = readFileSync(join(repo, ".gitlab/ci/web-ops.gitlab-ci.yml"), "utf-8");
71
+ expect(text).toContain("web-nightly-audit:");
72
+ expect(text).toContain("cd apps/web");
73
+ });
74
+
75
+ test("--format json names the member and writes nothing", async () => {
76
+ process.chdir(join(repo, "services/api"));
77
+ expect(await runOp(ctx(["run", "--generate", "forgejo", "--format", "json"]))).toBe(0);
78
+ const out = JSON.parse(logs.join("\n"));
79
+ expect(out.member).toBe("api");
80
+ expect(out.files.map((f: { path: string }) => f.path)).toEqual([".forgejo/workflows/api-nightly-audit.yml"]);
81
+ expect(existsSync(join(repo, ".forgejo"))).toBe(false);
82
+ });
83
+ });
@@ -86,16 +86,14 @@ describe("chant run --generate <provider>", () => {
86
86
  }
87
87
  });
88
88
 
89
- test("each github workflow is named after its Op (#2580); forgejo workflows carry no name", async () => {
89
+ test("each github (#2580) and forgejo (#2601) workflow is named after its Op", async () => {
90
90
  for (const provider of ["github", "forgejo"]) {
91
91
  expect(await runOp(ctx(["run", "--generate", provider, "--output", join(dir, provider)]))).toBe(0);
92
- }
93
- const header = `# ${GENERATED_MARKER}. Regenerate with: chant run --generate github\n`;
94
- for (const op of ["nightly-audit", "weekly-report"]) {
95
- const github = readFileSync(join(dir, "github", `${op}.yml`), "utf-8");
96
- expect(github.startsWith(`${header}name: ${op}\n\non:\n`)).toBe(true);
97
- const forgejo = readFileSync(join(dir, "forgejo", `${op}.yml`), "utf-8");
98
- expect(forgejo).not.toMatch(/^name:/m);
92
+ const header = `# ${GENERATED_MARKER}. Regenerate with: chant run --generate ${provider}\n`;
93
+ for (const op of ["nightly-audit", "weekly-report"]) {
94
+ const written = readFileSync(join(dir, provider, `${op}.yml`), "utf-8");
95
+ expect(written.startsWith(`${header}name: ${op}\n\non:\n`)).toBe(true);
96
+ }
99
97
  }
100
98
  });
101
99
 
@@ -28,6 +28,7 @@ import { dirname, join, resolve } from "node:path";
28
28
  import { discoverOps } from "../../op/discover";
29
29
  import { generateOpsPipeline } from "../../op/generate-pipeline";
30
30
  import { GENERATED_MARKER } from "../../discovery/files";
31
+ import { findWorkspaceRoot } from "../../project-root";
31
32
  import type { ComponentPipelineOptions, ScheduledOpSpec } from "../../lexicon";
32
33
  import { formatBold, formatError, formatSuccess } from "../format";
33
34
  import type { CommandContext } from "../registry";
@@ -104,12 +105,46 @@ export async function runOpGenerate(ctx: CommandContext): Promise<number> {
104
105
  }
105
106
  }
106
107
 
107
- const result = await generateOpsPipeline(ops, provider, options);
108
+ // Inside a workspace member (#2542), the files are the member's: filtered
109
+ // to its paths, run in its directory, named after it at the repository
110
+ // root's forge directory, and recorded as its generated files. The
111
+ // workspace module loads only when a declaration was found.
112
+ const found = findWorkspaceRoot(process.cwd());
113
+ let plan: import("../../workspace/member-pipeline").MemberOpPlan | undefined;
114
+ let memberPipeline: typeof import("../../workspace/member-pipeline") | undefined;
115
+ if (found) {
116
+ memberPipeline = await import("../../workspace/member-pipeline");
117
+ try {
118
+ plan = memberPipeline.planMemberOpPipelines({ projectDir: process.cwd(), found, provider, output: args.output, specFile: specPath });
119
+ } catch (err) {
120
+ console.error(formatError({ message: memberPipeline.describeError(err) }));
121
+ return 1;
122
+ }
123
+ }
124
+
125
+ const result = await generateOpsPipeline(ops, provider, plan ? { ...options, member: plan.member } : options);
108
126
  if (!result.success) {
109
127
  console.error(formatError({ message: result.error ?? "Failed to generate Op pipelines" }));
110
128
  return 1;
111
129
  }
112
130
 
131
+ if (plan && memberPipeline) {
132
+ const header = memberPipeline.generatedHeader(plan.command);
133
+ if (args.format === "json") {
134
+ const files = (result.files ?? []).map((f) => ({
135
+ name: f.name,
136
+ path: memberPipeline.repoRelative(plan.ctx, join(plan.outDir, f.name)),
137
+ content: header + f.yaml,
138
+ }));
139
+ console.log(JSON.stringify({ member: plan.member.name, files, jobs: result.jobs ?? [] }, null, 2));
140
+ return 0;
141
+ }
142
+ for (const path of memberPipeline.writeMemberOpPipelines(plan, result.files ?? [])) {
143
+ console.error(formatSuccess(`wrote ${formatBold(path)} for member ${formatBold(plan.member.name)}`));
144
+ }
145
+ return 0;
146
+ }
147
+
113
148
  const header = opPipelineHeader(provider, specPath);
114
149
  const outDir = args.output ?? DEFAULT_OP_PIPELINE_DIRS[provider] ?? ".";
115
150
  const files = (result.files ?? []).map((f) => ({
@@ -1157,6 +1157,53 @@ describe("runOpComponents", () => {
1157
1157
  vi.restoreAllMocks();
1158
1158
  });
1159
1159
 
1160
+ test("--digest-file writes <component>=<digest> for each release the run recorded (#2602)", async () => {
1161
+ runComponentsMock.mockResolvedValue({
1162
+ success: true,
1163
+ selected: ["svc", "infra"],
1164
+ run: {
1165
+ order: ["svc", "infra"],
1166
+ waves: [["svc", "infra"]],
1167
+ results: [
1168
+ { component: "svc", ok: true, status: "ok", records: [] },
1169
+ { component: "infra", ok: true, status: "ok", records: [] },
1170
+ ],
1171
+ ok: true,
1172
+ status: "ok",
1173
+ },
1174
+ });
1175
+ maybeRecordAutoReleaseMock.mockImplementation(async (info: { component: string }) =>
1176
+ info.component === "svc"
1177
+ ? { recorded: true, commit: "a".repeat(40), record: { version: 1, component: "svc", env: "staging", digest: "sha256:abc", gitSha: "x", runId: "local-1", timestamp: "t", actor: "a" } }
1178
+ : { recorded: false, reason: "no-digest" });
1179
+ writeFileSyncMock.mockClear();
1180
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1181
+
1182
+ const exit = await runOpComponents({ args: makeArgs({ path: "all", env: "staging", digestFile: "out/svc.digest" }), plugins: [], serializers: [] });
1183
+
1184
+ expect(exit).toBe(0);
1185
+ const call = writeFileSyncMock.mock.calls.find(([path]) => String(path).endsWith("out/svc.digest"));
1186
+ expect(call?.[1]).toBe("svc=sha256:abc\n");
1187
+ vi.restoreAllMocks();
1188
+ });
1189
+
1190
+ test("--digest-file is written empty when nothing was recorded (#2602)", async () => {
1191
+ runComponentsMock.mockResolvedValue({
1192
+ success: true,
1193
+ selected: ["svc"],
1194
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
1195
+ });
1196
+ maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1197
+ writeFileSyncMock.mockClear();
1198
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1199
+
1200
+ await runOpComponents({ args: makeArgs({ path: "svc", digestFile: "svc.digest" }), plugins: [], serializers: [] });
1201
+
1202
+ const call = writeFileSyncMock.mock.calls.find(([path]) => String(path).endsWith("svc.digest"));
1203
+ expect(call?.[1]).toBe("");
1204
+ vi.restoreAllMocks();
1205
+ });
1206
+
1160
1207
  test("a release-write error is surfaced as a warning but does not change the exit code", async () => {
1161
1208
  runComponentsMock.mockResolvedValue({
1162
1209
  success: true,
@@ -6,8 +6,8 @@ import { discoverOps } from "../../op/discover";
6
6
  import type { OpConfig } from "../../op/types";
7
7
  import { loadActivities, loadProfiles } from "../../op/activity-registry";
8
8
  import { runOpLocally, findPolicyGateStep, OpRunFailure, type StepRecord } from "../../op/local-executor";
9
- import { approveCommand } from "../../op/gate";
10
- import { writeGatedRunSummary, type GatedRunSummary } from "../../op/gate-summary";
9
+ import { approveCommand, describeGateMismatch } from "../../op/gate";
10
+ import { summaryLedgerPrefix, writeGatedRunSummary, type GatedRunSummary } from "../../op/gate-summary";
11
11
  import { createLocalOpRuntime } from "../../op/runtimes/local";
12
12
  import type { OpRuntimeProvider, OpRunStatus } from "../../op/runtime";
13
13
  import { renderHuman, renderJson } from "../../op/local-output";
@@ -21,6 +21,7 @@ import { ndjsonProgressSink } from "../../components/run-progress";
21
21
  import { maybeRecordAutoRelease } from "../../components/auto-release";
22
22
  import { maybePersistBuildManifest } from "../../components/manifest-persistence";
23
23
  import type { DriverComponentResult } from "../../components/driver";
24
+ import type { ReleaseRecord } from "../../lifecycle/release-ledger";
24
25
  import { runOpGenerate } from "./run-generate";
25
26
 
26
27
  /**
@@ -588,13 +589,16 @@ export async function runOp(ctx: CommandContext): Promise<number> {
588
589
  * ledger for work `chant run --components` records. Each result is filtered on
589
590
  * its own `ok`, so a fan-out that partly succeeded records exactly the
590
591
  * components that did.
592
+ *
593
+ * Returns the records it wrote, for `--digest-file` (#2602).
591
594
  */
592
595
  export async function recordAutoReleasesForRun(
593
596
  results: DriverComponentResult[],
594
597
  env: string,
595
598
  runId: string,
596
599
  disabled: boolean,
597
- ): Promise<void> {
600
+ ): Promise<ReleaseRecord[]> {
601
+ const recorded: ReleaseRecord[] = [];
598
602
  for (const componentResult of results) {
599
603
  if (!componentResult.ok) continue;
600
604
  const outcome = await maybeRecordAutoRelease(
@@ -614,6 +618,7 @@ export async function recordAutoReleasesForRun(
614
618
  message: `release record for "${componentResult.component}"@${env} was not recorded: ${outcome.error}`,
615
619
  }));
616
620
  } else if (outcome.recorded) {
621
+ recorded.push(outcome.record);
617
622
  console.error(formatInfo(
618
623
  `Recorded release: ${formatBold(componentResult.component)}@${env} -> ${outcome.record.digest} (commit ${outcome.commit.slice(0, 7)})`,
619
624
  ));
@@ -633,6 +638,7 @@ export async function recordAutoReleasesForRun(
633
638
  ));
634
639
  }
635
640
  }
641
+ return recorded;
636
642
  }
637
643
 
638
644
  // ── chant run --components <name|all> ────────────────────────────────────────
@@ -760,7 +766,12 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
760
766
  console.error(formatWarning({
761
767
  message: `component "${result.gated.component}" is gated on "${gate.gate}" — pending approval`,
762
768
  }));
763
- console.error(formatInfo(`approve : ${approveCommand(gate.op, gate.gate)}`));
769
+ // #2574: an approval stands, but for another plan or for none. Say so,
770
+ // with the command that replaces it.
771
+ if (result.gated.mismatch) {
772
+ console.error(formatWarning({ message: describeGateMismatch(gate.op, gate.gate, result.gated.mismatch) }));
773
+ }
774
+ console.error(formatInfo(`approve : ${approveCommand(gate.op, gate.gate, gate.environment)}`));
764
775
  if (gate.url) console.error(formatInfo(`approve at: ${gate.url}`));
765
776
  console.error(formatInfo(`expires : ${gate.expiresAt}`));
766
777
  // #2310: this run's own append reached only the local chant/lifecycle
@@ -782,6 +793,8 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
782
793
  ...(gate.url ? { url: gate.url } : {}),
783
794
  ...(result.gated.pushed === false ? { pushed: false, pushWarning: result.gated.pushWarning } : {}),
784
795
  ...(gate.planDigest ? { planDigest: gate.planDigest } : {}),
796
+ ...(await summaryLedgerPrefix()),
797
+ ...(gate.environment ? { environment: gate.environment } : {}),
785
798
  },
786
799
  gatedExit,
787
800
  );
@@ -790,7 +803,16 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
790
803
 
791
804
  if (result.success && result.run) {
792
805
  const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord);
793
- await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
806
+ const recorded = await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
807
+ // `--digest-file` (#2602): the releases this run recorded, one
808
+ // `<component>=<digest>` line each, the form `chant components promote
809
+ // --digest` takes. A generated promote job reads it to promote exactly
810
+ // what this job built, not whatever is latest in the environment.
811
+ if (ctx.args.digestFile) {
812
+ const digestPath = resolve(ctx.args.digestFile);
813
+ mkdirSync(dirname(digestPath), { recursive: true });
814
+ writeFileSync(digestPath, recorded.map((r) => `${r.component}=${r.digest}\n`).join(""));
815
+ }
794
816
  }
795
817
 
796
818
  return result.success ? 0 : 1;
@@ -903,6 +925,7 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
903
925
  ? { pushed: false, pushWarning: status.result.gatePushWarning }
904
926
  : {}),
905
927
  ...(pending?.planDigest ? { planDigest: pending.planDigest } : {}),
928
+ ...(await summaryLedgerPrefix()),
906
929
  },
907
930
  gatedExit,
908
931
  );