@intentius/chant 0.124.30 → 0.124.32

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 (346) hide show
  1. package/bin/chant +7 -2
  2. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  3. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  5. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  6. package/dist/cli/commands/check-lexicon.js +18 -12
  7. package/dist/cli/commands/import.d.ts +10 -0
  8. package/dist/cli/commands/import.d.ts.map +1 -1
  9. package/dist/cli/commands/import.js +37 -0
  10. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  11. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  12. package/dist/cli/commands/onboard.d.ts.map +1 -1
  13. package/dist/cli/commands/onboard.js +8 -4
  14. package/dist/cli/commands/update.d.ts.map +1 -1
  15. package/dist/cli/commands/update.js +2 -1
  16. package/dist/cli/dependencies.d.ts +18 -0
  17. package/dist/cli/dependencies.d.ts.map +1 -0
  18. package/dist/cli/dependencies.js +120 -0
  19. package/dist/cli/handlers/build.d.ts.map +1 -1
  20. package/dist/cli/handlers/build.js +23 -4
  21. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  22. package/dist/cli/handlers/fan-out.js +12 -4
  23. package/dist/cli/handlers/graph.d.ts.map +1 -1
  24. package/dist/cli/handlers/graph.js +16 -6
  25. package/dist/cli/handlers/lint.d.ts.map +1 -1
  26. package/dist/cli/handlers/lint.js +6 -0
  27. package/dist/cli/handlers/operator.d.ts +2 -2
  28. package/dist/cli/handlers/operator.js +3 -3
  29. package/dist/cli/handlers/pr.d.ts +14 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +488 -32
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +3 -1
  34. package/dist/cli/handlers/run.d.ts +13 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +38 -15
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +74 -12
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +65 -0
  41. package/dist/cli/registry.d.ts +29 -2
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/registry.js +6 -2
  44. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  45. package/dist/codegen/lexicon-regen.d.ts +4 -0
  46. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  47. package/dist/codegen/lexicon-regen.js +1 -1
  48. package/dist/codegen/pinned-upgrade.js +2 -2
  49. package/dist/components/capability.d.ts +44 -3
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/capability.js +21 -0
  52. package/dist/components/changed-files.d.ts +7 -1
  53. package/dist/components/changed-files.d.ts.map +1 -1
  54. package/dist/components/changed-files.js +9 -1
  55. package/dist/components/driver.d.ts +38 -0
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/driver.js +47 -6
  58. package/dist/components/fan-out-record.d.ts +6 -1
  59. package/dist/components/fan-out-record.d.ts.map +1 -1
  60. package/dist/components/fan-out-record.js +24 -0
  61. package/dist/components/fan-out-run.d.ts +20 -0
  62. package/dist/components/fan-out-run.d.ts.map +1 -1
  63. package/dist/components/fan-out-run.js +21 -1
  64. package/dist/components/pr-pipeline.d.ts +65 -2
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +107 -15
  67. package/dist/components/pr-run.d.ts +127 -29
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +290 -70
  70. package/dist/components/wave-plan.d.ts +5 -2
  71. package/dist/components/wave-plan.d.ts.map +1 -1
  72. package/dist/components/wave-plan.js +12 -1
  73. package/dist/discovery/fold-import.d.ts.map +1 -1
  74. package/dist/discovery/fold-import.js +16 -6
  75. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  76. package/dist/discovery/sandbox/bundle.js +6 -0
  77. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  78. package/dist/discovery/specifier-resolve.js +8 -2
  79. package/dist/fold/fold.d.ts.map +1 -1
  80. package/dist/fold/fold.js +68 -1
  81. package/dist/fold/subset.d.ts +37 -0
  82. package/dist/fold/subset.d.ts.map +1 -1
  83. package/dist/fold/subset.js +88 -0
  84. package/dist/lexicon-module.d.ts +10 -0
  85. package/dist/lexicon-module.d.ts.map +1 -1
  86. package/dist/lexicon-module.js +53 -8
  87. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  88. package/dist/lexicon-plugin-helpers.js +6 -4
  89. package/dist/lexicon.d.ts +2 -0
  90. package/dist/lexicon.d.ts.map +1 -1
  91. package/dist/lifecycle/affected.d.ts.map +1 -1
  92. package/dist/lifecycle/affected.js +7 -1
  93. package/dist/lifecycle/release-ledger.d.ts +21 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +24 -0
  96. package/dist/lifecycle/work-dir.d.ts +45 -0
  97. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  98. package/dist/lifecycle/work-dir.js +78 -0
  99. package/dist/lint/discover.d.ts +17 -9
  100. package/dist/lint/discover.d.ts.map +1 -1
  101. package/dist/lint/discover.js +62 -54
  102. package/dist/module-source.d.ts +57 -0
  103. package/dist/module-source.d.ts.map +1 -0
  104. package/dist/module-source.js +104 -0
  105. package/dist/op/activities/factory.js +1 -1
  106. package/dist/op/discover.d.ts.map +1 -1
  107. package/dist/op/discover.js +13 -3
  108. package/dist/op/gate.d.ts.map +1 -1
  109. package/dist/op/gate.js +3 -0
  110. package/dist/op/local-executor.d.ts +12 -0
  111. package/dist/op/local-executor.d.ts.map +1 -1
  112. package/dist/op/local-executor.js +22 -3
  113. package/dist/op/op-waves-run.d.ts.map +1 -1
  114. package/dist/op/op-waves-run.js +20 -5
  115. package/dist/op/operator.d.ts +2 -2
  116. package/dist/op/operator.js +2 -2
  117. package/dist/op/redact.d.ts +34 -0
  118. package/dist/op/redact.d.ts.map +1 -0
  119. package/dist/op/redact.js +94 -0
  120. package/dist/op/step-console.d.ts +2 -0
  121. package/dist/op/step-console.d.ts.map +1 -1
  122. package/dist/op/step-console.js +5 -2
  123. package/dist/op/steward.d.ts +6 -4
  124. package/dist/op/steward.d.ts.map +1 -1
  125. package/dist/op/steward.js +5 -3
  126. package/dist/op/work-lease-run.d.ts +20 -7
  127. package/dist/op/work-lease-run.d.ts.map +1 -1
  128. package/dist/op/work-lease-run.js +56 -12
  129. package/dist/pr-forge.d.ts +27 -0
  130. package/dist/pr-forge.d.ts.map +1 -1
  131. package/dist/pr-forge.js +51 -6
  132. package/dist/pr-loop.d.ts +141 -4
  133. package/dist/pr-loop.d.ts.map +1 -1
  134. package/dist/pr-loop.js +122 -6
  135. package/dist/pr-report-url.d.ts +10 -2
  136. package/dist/pr-report-url.d.ts.map +1 -1
  137. package/dist/pr-report-url.js +52 -6
  138. package/dist/ts-loader.d.ts +38 -4
  139. package/dist/ts-loader.d.ts.map +1 -1
  140. package/dist/ts-loader.js +190 -16
  141. package/dist/wave-runtime.mjs +208 -208
  142. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  143. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  144. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  145. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  146. package/dist/workspace/chant-migrations.js +2 -0
  147. package/dist/workspace/checks/boxes.d.ts +6 -6
  148. package/dist/workspace/checks/boxes.js +9 -9
  149. package/dist/workspace/ci-green-forge.d.ts +6 -4
  150. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  151. package/dist/workspace/ci-green.d.ts +28 -12
  152. package/dist/workspace/ci-green.d.ts.map +1 -1
  153. package/dist/workspace/ci-green.js +49 -21
  154. package/dist/workspace/ci-list.schema.json +12 -6
  155. package/dist/workspace/ci-report-url.d.ts +18 -0
  156. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  157. package/dist/workspace/ci-report-url.js +33 -0
  158. package/dist/workspace/ci-run-index.d.ts +108 -0
  159. package/dist/workspace/ci-run-index.d.ts.map +1 -0
  160. package/dist/workspace/ci-run-index.js +164 -0
  161. package/dist/workspace/ci-run-index.schema.json +52 -0
  162. package/dist/workspace/ci-run.d.ts +159 -9
  163. package/dist/workspace/ci-run.d.ts.map +1 -1
  164. package/dist/workspace/ci-run.js +419 -50
  165. package/dist/workspace/ci-runner-key.d.ts +133 -0
  166. package/dist/workspace/ci-runner-key.d.ts.map +1 -0
  167. package/dist/workspace/ci-runner-key.js +237 -0
  168. package/dist/workspace/ci-secrets.d.ts +65 -0
  169. package/dist/workspace/ci-secrets.d.ts.map +1 -0
  170. package/dist/workspace/ci-secrets.js +211 -0
  171. package/dist/workspace/ci-serve.d.ts +67 -4
  172. package/dist/workspace/ci-serve.d.ts.map +1 -1
  173. package/dist/workspace/ci-serve.js +240 -35
  174. package/dist/workspace/declaration.d.ts +8 -0
  175. package/dist/workspace/declaration.d.ts.map +1 -1
  176. package/dist/workspace/declaration.js +11 -2
  177. package/dist/workspace/declaration.schema.json +7 -2
  178. package/dist/workspace/export.d.ts +1 -1
  179. package/dist/workspace/export.d.ts.map +1 -1
  180. package/dist/workspace/export.js +7 -3
  181. package/dist/workspace/ls.d.ts +6 -1
  182. package/dist/workspace/ls.d.ts.map +1 -1
  183. package/dist/workspace/ls.js +1 -0
  184. package/dist/workspace/ls.schema.json +7 -1
  185. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  186. package/dist/workspace/pin-integrity.js +3 -2
  187. package/dist/workspace/pr-report.schema.json +74 -7
  188. package/dist/workspace/reason-codes.d.ts +7 -3
  189. package/dist/workspace/reason-codes.d.ts.map +1 -1
  190. package/dist/workspace/reason-codes.js +9 -4
  191. package/dist/workspace/run-event.schema.json +3 -1
  192. package/dist/workspace/status.schema.json +3 -1
  193. package/dist/workspace/trust/trust-write.d.ts +109 -0
  194. package/dist/workspace/trust/trust-write.d.ts.map +1 -0
  195. package/dist/workspace/trust/trust-write.js +320 -0
  196. package/dist/workspace/trust-write.schema.json +265 -0
  197. package/dist/workspace/write-scope.d.ts +9 -0
  198. package/dist/workspace/write-scope.d.ts.map +1 -1
  199. package/dist/workspace/write-scope.js +11 -1
  200. package/package.json +1 -1
  201. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  202. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  203. package/src/cli/commands/audit.test.ts +2 -2
  204. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  205. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  206. package/src/cli/commands/check-lexicon.test.ts +1 -1
  207. package/src/cli/commands/check-lexicon.ts +16 -11
  208. package/src/cli/commands/import.ts +40 -0
  209. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  210. package/src/cli/commands/init-lexicon.test.ts +8 -5
  211. package/src/cli/commands/onboard.test.ts +45 -0
  212. package/src/cli/commands/onboard.ts +8 -4
  213. package/src/cli/commands/update.test.ts +6 -0
  214. package/src/cli/commands/update.ts +4 -1
  215. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  216. package/src/cli/dependencies.e2e.test.ts +101 -0
  217. package/src/cli/dependencies.test.ts +93 -0
  218. package/src/cli/dependencies.ts +122 -0
  219. package/src/cli/handlers/build.test.ts +69 -0
  220. package/src/cli/handlers/build.ts +20 -4
  221. package/src/cli/handlers/fan-out.test.ts +73 -0
  222. package/src/cli/handlers/fan-out.ts +11 -4
  223. package/src/cli/handlers/graph.test.ts +76 -0
  224. package/src/cli/handlers/graph.ts +19 -12
  225. package/src/cli/handlers/lint.test.ts +42 -0
  226. package/src/cli/handlers/lint.ts +6 -0
  227. package/src/cli/handlers/operator.ts +3 -3
  228. package/src/cli/handlers/pr-waves.test.ts +449 -0
  229. package/src/cli/handlers/pr.test.ts +134 -0
  230. package/src/cli/handlers/pr.ts +523 -31
  231. package/src/cli/handlers/promote.test.ts +26 -1
  232. package/src/cli/handlers/promote.ts +3 -2
  233. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  234. package/src/cli/handlers/run-signer.test.ts +79 -0
  235. package/src/cli/handlers/run.ts +34 -16
  236. package/src/cli/main.test.ts +19 -0
  237. package/src/cli/main.ts +73 -12
  238. package/src/cli/parse-args.ts +48 -0
  239. package/src/cli/registry.ts +35 -4
  240. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  241. package/src/codegen/lexicon-regen.ts +1 -1
  242. package/src/codegen/pinned-upgrade.ts +2 -2
  243. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  244. package/src/components/capability.ts +51 -3
  245. package/src/components/changed-files.test.ts +16 -1
  246. package/src/components/changed-files.ts +9 -1
  247. package/src/components/driver.test.ts +36 -1
  248. package/src/components/driver.ts +76 -4
  249. package/src/components/fan-out-record.ts +29 -1
  250. package/src/components/fan-out-run.test.ts +35 -0
  251. package/src/components/fan-out-run.ts +40 -1
  252. package/src/components/pr-pipeline.test.ts +100 -0
  253. package/src/components/pr-pipeline.ts +132 -17
  254. package/src/components/pr-run.test.ts +323 -18
  255. package/src/components/pr-run.ts +354 -75
  256. package/src/components/wave-plan.ts +16 -4
  257. package/src/discovery/fold-host-method.test.ts +87 -0
  258. package/src/discovery/fold-import.ts +15 -5
  259. package/src/discovery/fold-package-composite.test.ts +27 -0
  260. package/src/discovery/fold-specifiers.test.ts +8 -0
  261. package/src/discovery/sandbox/bundle.ts +6 -0
  262. package/src/discovery/specifier-resolve.ts +8 -2
  263. package/src/fold/fold.test.ts +72 -0
  264. package/src/fold/fold.ts +71 -0
  265. package/src/fold/subset.test.ts +31 -0
  266. package/src/fold/subset.ts +99 -0
  267. package/src/lexicon-dist.e2e.test.ts +254 -0
  268. package/src/lexicon-module.test.ts +47 -0
  269. package/src/lexicon-module.ts +49 -7
  270. package/src/lexicon-plugin-helpers.ts +6 -4
  271. package/src/lexicon.ts +2 -0
  272. package/src/lifecycle/affected.test.ts +7 -0
  273. package/src/lifecycle/affected.ts +6 -1
  274. package/src/lifecycle/release-ledger.ts +34 -0
  275. package/src/lifecycle/work-dir.test.ts +77 -0
  276. package/src/lifecycle/work-dir.ts +82 -0
  277. package/src/lint/discover.test.ts +46 -2
  278. package/src/lint/discover.ts +55 -49
  279. package/src/module-source.test.ts +85 -0
  280. package/src/module-source.ts +112 -0
  281. package/src/op/activities/factory.ts +1 -1
  282. package/src/op/discover-root.test.ts +31 -2
  283. package/src/op/discover.ts +14 -4
  284. package/src/op/gate.test.ts +30 -0
  285. package/src/op/gate.ts +3 -0
  286. package/src/op/local-executor.ts +31 -3
  287. package/src/op/op-waves-run.ts +20 -5
  288. package/src/op/op-waves.test.ts +49 -0
  289. package/src/op/operator.ts +2 -2
  290. package/src/op/redact.ts +93 -0
  291. package/src/op/step-console.ts +7 -2
  292. package/src/op/steward.ts +6 -4
  293. package/src/op/work-lease-run.test.ts +88 -2
  294. package/src/op/work-lease-run.ts +52 -12
  295. package/src/pr-forge-bundle.test.ts +2 -2
  296. package/src/pr-forge.test.ts +71 -0
  297. package/src/pr-forge.ts +70 -6
  298. package/src/pr-loop-bundle.test.ts +2 -1
  299. package/src/pr-loop.test.ts +117 -1
  300. package/src/pr-loop.ts +252 -10
  301. package/src/pr-report-url.ts +58 -6
  302. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  303. package/src/ts-loader.test.ts +89 -1
  304. package/src/ts-loader.ts +180 -13
  305. package/src/workspace/changes.test.ts +1 -1
  306. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  307. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  308. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  309. package/src/workspace/chant-migrations.ts +2 -0
  310. package/src/workspace/checks/boxes.ts +9 -9
  311. package/src/workspace/ci-green-forge.ts +6 -4
  312. package/src/workspace/ci-green-runs.test.ts +53 -11
  313. package/src/workspace/ci-green.test.ts +1 -1
  314. package/src/workspace/ci-green.ts +50 -22
  315. package/src/workspace/ci-list.schema.json +12 -6
  316. package/src/workspace/ci-report-url.test.ts +220 -0
  317. package/src/workspace/ci-report-url.ts +33 -0
  318. package/src/workspace/ci-run-index.schema.json +52 -0
  319. package/src/workspace/ci-run-index.test.ts +392 -0
  320. package/src/workspace/ci-run-index.ts +206 -0
  321. package/src/workspace/ci-run.test.ts +520 -42
  322. package/src/workspace/ci-run.ts +483 -51
  323. package/src/workspace/ci-runner-key.test.ts +204 -0
  324. package/src/workspace/ci-runner-key.ts +275 -0
  325. package/src/workspace/ci-secrets.test.ts +106 -0
  326. package/src/workspace/ci-secrets.ts +213 -0
  327. package/src/workspace/ci-serve.test.ts +282 -39
  328. package/src/workspace/ci-serve.ts +231 -34
  329. package/src/workspace/declaration.schema.json +7 -2
  330. package/src/workspace/declaration.test.ts +23 -0
  331. package/src/workspace/declaration.ts +19 -3
  332. package/src/workspace/export.ts +6 -3
  333. package/src/workspace/ls.schema.json +7 -1
  334. package/src/workspace/ls.ts +7 -2
  335. package/src/workspace/pin-integrity.test.ts +5 -0
  336. package/src/workspace/pin-integrity.ts +3 -2
  337. package/src/workspace/pr-report.schema.json +74 -7
  338. package/src/workspace/reason-codes.test.ts +2 -0
  339. package/src/workspace/reason-codes.ts +9 -4
  340. package/src/workspace/run-event.schema.json +3 -1
  341. package/src/workspace/status.schema.json +3 -1
  342. package/src/workspace/trust/trust-write.test.ts +179 -0
  343. package/src/workspace/trust/trust-write.ts +377 -0
  344. package/src/workspace/trust-write.schema.json +265 -0
  345. package/src/workspace/write-scope.test.ts +29 -1
  346. package/src/workspace/write-scope.ts +12 -1
@@ -24,6 +24,8 @@ const writtenTemplates: unknown[] = [];
24
24
  let configEnvironments: unknown[] | undefined;
25
25
  let publishedDigest = "sha256:api";
26
26
  let failApplyFor: string | undefined;
27
+ /** What `failApplyFor`'s apply throws; a plain error when unset. */
28
+ let applyError: Error | undefined;
27
29
 
28
30
  vi.mock("../../lifecycle/git", () => ({
29
31
  getHeadCommit: async () => "abc123",
@@ -58,7 +60,7 @@ vi.mock("../../components/fan-out-support", () => ({
58
60
  step("docker-build", () => ({ digest: "sha256:rebuilt" }));
59
61
  step("publish-image", () => ({ digest: publishedDigest, uri: `reg/api@${publishedDigest}` }));
60
62
  step("apply", (ctx) => {
61
- if (ctx.component === failApplyFor) throw new Error("apply failed");
63
+ if (ctx.component === failApplyFor) throw applyError ?? new Error("apply failed");
62
64
  return {};
63
65
  });
64
66
  // #4045: a planner whose member is provisional.
@@ -221,6 +223,7 @@ beforeEach(() => {
221
223
  configEnvironments = undefined;
222
224
  publishedDigest = "sha256:api";
223
225
  failApplyFor = undefined;
226
+ applyError = undefined;
224
227
  errors = [];
225
228
  logs = [];
226
229
  vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.join(" ")); });
@@ -358,6 +361,21 @@ describe("chant components promote", () => {
358
361
  expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ component: "api", env: "prod" });
359
362
  });
360
363
 
364
+ test.each([
365
+ ["marked planMoved", 3, { planMoved: true }],
366
+ ["with exitCode 3", 3, { exitCode: 3 }],
367
+ ["with exitCode 7", 1, { exitCode: 7 }],
368
+ ])("an apply that refuses its plan %s exits %i, and what deployed is still recorded (#3997)", async (_label, code, mark) => {
369
+ const web: DriverComponent = { ...api, name: "web" };
370
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [api, web] });
371
+ readReleaseLedgerMock.mockResolvedValue({ records: [staging, { ...staging, component: "web" }], malformed: 0 });
372
+ failApplyFor = "web";
373
+ applyError = Object.assign(new Error("web: refusing to apply, the plan moved"), mark);
374
+ expect(await runComponentsPromote(ctx({}))).toBe(code);
375
+ expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
376
+ expect(errors.join("\n")).toMatch(/promote failed at "web"/);
377
+ });
378
+
361
379
  test("--digest <component>=<digest> promotes the pinned release, not the latest one (#2602)", async () => {
362
380
  const web: DriverComponent = { ...api, name: "web" };
363
381
  resolveTargetsMock.mockResolvedValue({ success: true, targets: [api, web] });
@@ -795,6 +813,13 @@ describe("chant components redeploy", () => {
795
813
  ]);
796
814
  expect(errors.join("\n")).toMatch(/redeploy failed at "api"/);
797
815
  });
816
+
817
+ test("a redeploy whose apply refuses a moved plan exits 3 (#3997)", async () => {
818
+ failApplyFor = "api";
819
+ applyError = Object.assign(new Error("api: refusing to apply, the plan moved"), { planMoved: true });
820
+ expect(await runComponentsRedeploy(rd({}))).toBe(3);
821
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
822
+ });
798
823
  });
799
824
 
800
825
  describe("--events-json on promote, rollback and redeploy (#4041)", () => {
@@ -58,7 +58,7 @@ import {
58
58
  type PromotionItem,
59
59
  type PromotionPlan,
60
60
  } from "../../components/promote";
61
- import type { DriverComponent } from "../../components/driver";
61
+ import { failedRunExitCode, type DriverComponent } from "../../components/driver";
62
62
  import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
63
63
  import { approveCommand, describeGateMismatch, gateIsSealed, gitGateLedgerPort, type GateLedgerPort } from "../../op/gate";
64
64
  import type { ReleaseGateBinding } from "../../components/driver";
@@ -522,7 +522,8 @@ async function deployPlan(
522
522
 
523
523
  if (run.status !== "ok") {
524
524
  console.error(formatError({ message: `${verb} failed at "${run.failedComponent ?? "unknown"}"` }));
525
- return 1;
525
+ // A step that refused a plan that moved since it was approved exits 3 (#3997).
526
+ return failedRunExitCode(run.results);
526
527
  }
527
528
  return 0;
528
529
  }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `chant run --components` exits 3 when a step refuses a plan that moved
3
+ * since it was approved (#3997), on the single-component path and on the
4
+ * driver's `all` and list paths. Discovery and the capability registry are
5
+ * fakes; the run is the real `runComponents` on the local runtime.
6
+ */
7
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
8
+ import type { Component } from "../../components/component";
9
+ import type { ParsedArgs } from "../registry";
10
+
11
+ const discoverComponentsMock = vi.fn();
12
+ vi.mock("../../components/discover", () => ({
13
+ discoverComponents: (...args: unknown[]) => discoverComponentsMock(...args),
14
+ }));
15
+
16
+ vi.mock("../../config", async () => {
17
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
18
+ return { ...actual, loadChantConfig: async () => ({ config: {} }) };
19
+ });
20
+
21
+ /** The error a component's step throws, in place of applying. */
22
+ const refusal = new Map<string, Error>();
23
+
24
+ vi.mock("../../op/runtimes/local", async () => {
25
+ const { CapabilityRegistry } = await import("../../components/capability");
26
+ const { runComponents } = await import("../../components/cli-support");
27
+ const { memoryGateLedgerPort } = await import("../../op/gate");
28
+ const registry = new CapabilityRegistry();
29
+ registry.register({
30
+ kind: "fake-apply",
31
+ async run(ctx: { component: string }) {
32
+ const err = refusal.get(ctx.component);
33
+ if (err) throw err;
34
+ return {};
35
+ },
36
+ } as never);
37
+ return {
38
+ createLocalOpRuntime: () => ({
39
+ name: "local",
40
+ runComponents: (path: string, selector: string, options: object) =>
41
+ runComponents(path, selector, { ...options, registry, config: {}, revision: null, gates: memoryGateLedgerPort() }),
42
+ }),
43
+ };
44
+ });
45
+
46
+ const { runOpComponents } = await import("./run");
47
+
48
+ const component = (name: string, dependsOn: string[] = []): Component => ({
49
+ name,
50
+ dependsOn,
51
+ deploy: [{ phase: "Apply", steps: [{ kind: "fake-apply" }] }],
52
+ });
53
+
54
+ const ctx = (selector: string) =>
55
+ ({
56
+ args: { command: "run", path: selector, components: true, on: "local", format: "", fix: false, watch: false, verbose: false, help: false, live: false } as ParsedArgs,
57
+ plugins: [],
58
+ }) as never;
59
+
60
+ beforeEach(() => {
61
+ refusal.clear();
62
+ const estate = [component("net"), component("app", ["net"])];
63
+ discoverComponentsMock.mockResolvedValue({
64
+ components: new Map(estate.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
65
+ sourceFiles: [],
66
+ errors: [],
67
+ });
68
+ vi.spyOn(console, "error").mockImplementation(() => {});
69
+ vi.spyOn(console, "log").mockImplementation(() => {});
70
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
71
+ });
72
+
73
+ afterEach(() => {
74
+ vi.restoreAllMocks();
75
+ });
76
+
77
+ const marks: Array<[string, number, object]> = [
78
+ ["marked planMoved", 3, { planMoved: true }],
79
+ ["with exitCode 3", 3, { exitCode: 3 }],
80
+ ["with exitCode 7", 1, { exitCode: 7 }],
81
+ ["unmarked", 1, {}],
82
+ ];
83
+
84
+ describe("chant run --components: a step that refuses a moved plan exits 3 (#3997)", () => {
85
+ test.each(marks)("one component, its step refusing %s: exit %i", async (_label, code, mark) => {
86
+ refusal.set("app", Object.assign(new Error("app: refusing to apply, the plan moved"), mark));
87
+ expect(await runOpComponents(ctx("app"))).toBe(code);
88
+ });
89
+
90
+ test.each(marks)("all, a step refusing %s: exit %i", async (_label, code, mark) => {
91
+ refusal.set("net", Object.assign(new Error("net: refusing to apply, the plan moved"), mark));
92
+ expect(await runOpComponents(ctx("all"))).toBe(code);
93
+ });
94
+
95
+ test("a list that pulls in a dependency whose step refuses a moved plan exits 3", async () => {
96
+ refusal.set("net", Object.assign(new Error("net: refusing to apply, the plan moved"), { planMoved: true }));
97
+ expect(await runOpComponents(ctx("app,net"))).toBe(3);
98
+ });
99
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * #4156: `chant run`'s runner key is opt-in hardening. A `CHANT_RUNNER_KEY`
3
+ * that `.chant/trust.json` does not list leaves the run unsigned with no
4
+ * warning, unless the declaration sets `ci.green.signedSince`. A key that is
5
+ * not an Ed25519 PEM is an error either way.
6
+ */
7
+
8
+ import { createPublicKey, generateKeyPairSync } from "node:crypto";
9
+ import { afterEach, describe, expect, test, vi } from "vitest";
10
+ import { cleanScratch, declaration, git, scratchDir, writeFiles } from "../../workspace/__fixtures__/contract-repo";
11
+ import { sshFromEd25519 } from "../../workspace/trust/dsse";
12
+ import type { CommandContext } from "../registry";
13
+ import { runSignerFor } from "./run";
14
+
15
+ const home = process.cwd();
16
+ const saved = process.env.CHANT_RUNNER_KEY;
17
+ afterEach(() => {
18
+ process.chdir(home);
19
+ if (saved === undefined) delete process.env.CHANT_RUNNER_KEY;
20
+ else process.env.CHANT_RUNNER_KEY = saved;
21
+ vi.restoreAllMocks();
22
+ cleanScratch();
23
+ });
24
+
25
+ function key(): { pem: string; ssh: string } {
26
+ const { privateKey } = generateKeyPairSync("ed25519");
27
+ const jwk = createPublicKey(privateKey).export({ format: "jwk" }) as { x: string };
28
+ return { pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(), ssh: sshFromEd25519(Buffer.from(jwk.x, "base64url")) };
29
+ }
30
+
31
+ function repo(green?: Record<string, unknown>): string {
32
+ const root = scratchDir("chant-run-signer-");
33
+ git(root, "init", "-q");
34
+ for (const [k, v] of [["user.name", "t"], ["user.email", "t@example.com"], ["commit.gpgsign", "false"]]) git(root, "config", k!, v!);
35
+ writeFiles(root, {
36
+ "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], green ? { ci: { green } } : {}),
37
+ ".chant/trust.json": JSON.stringify({ schema: 1, runners: [{ principal: "ci-bot", class: "runner", key: key().ssh }], roles: { runner: ["ci-bot"] } }),
38
+ "app/a.txt": "a\n",
39
+ });
40
+ git(root, "add", "-A");
41
+ git(root, "commit", "-q", "-m", "init");
42
+ return root;
43
+ }
44
+
45
+ async function signer(root: string, pem: string, verbose = false) {
46
+ process.chdir(root);
47
+ process.env.CHANT_RUNNER_KEY = pem;
48
+ const err: string[] = [];
49
+ vi.spyOn(console, "error").mockImplementation((s: string) => void err.push(String(s)));
50
+ const result = await runSignerFor({ args: { verbose } } as unknown as CommandContext);
51
+ return { result, err: err.join("\n") };
52
+ }
53
+
54
+ describe("chant run's runner key from the environment (#4156)", () => {
55
+ test("a key trust.json does not list leaves the run unsigned and quiet, with one line under --verbose", async () => {
56
+ const root = repo();
57
+ const quiet = await signer(root, key().pem);
58
+ expect(quiet.result).toBeUndefined();
59
+ expect(quiet.err).toBe("");
60
+ const verbose = await signer(root, key().pem, true);
61
+ expect(verbose.result).toBeUndefined();
62
+ expect(verbose.err).toContain("CHANT_RUNNER_KEY is set, and the run is not signed");
63
+ });
64
+
65
+ test("with ci.green.signedSince the same key warns", async () => {
66
+ const root = repo();
67
+ const head = git(root, "rev-parse", "HEAD");
68
+ writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: { branch: "main", phases: { test: ["test"] }, require: ["test"], signedSince: head } } }) });
69
+ const r = await signer(root, key().pem);
70
+ expect(r.result).toBeUndefined();
71
+ expect(r.err).toContain(`ci.green.signedSince (${head}) will not count it`);
72
+ });
73
+
74
+ test("a key that is not an Ed25519 PEM is an error", async () => {
75
+ const r = await signer(repo(), "not a key");
76
+ expect(r.result).toBeNull();
77
+ expect(r.err).toContain("CHANT_RUNNER_KEY is not an Ed25519 private key in PEM");
78
+ });
79
+ });
@@ -31,7 +31,7 @@ import type { RunEvent } from "../../run-event";
31
31
  import { openCliRunEvents, refusesBothStreams, stdoutLine } from "../run-events";
32
32
  import { maybeRecordAutoRelease } from "../../components/auto-release";
33
33
  import { maybePersistBuildManifest } from "../../components/manifest-persistence";
34
- import type { DriverComponentResult } from "../../components/driver";
34
+ import { failedRunExitCode, type DriverComponentResult } from "../../components/driver";
35
35
  import { isComponentList, type ComponentRunPlan, type RunComponentsResult } from "../../components/cli-support";
36
36
  import { resolveCiRun, type ReleaseRecord, type RunOrigin } from "../../lifecycle/release-ledger";
37
37
  import { gateAdmission, gateApprover } from "../../components/promote";
@@ -730,9 +730,9 @@ type StewardTurnGate =
730
730
  * `chant run <op>` for an Op a declared steward lists is one of that
731
731
  * steward's turns, not a run beside it (#2750, follow-up to #2731).
732
732
  *
733
- * On fountain, the run belongs on the steward's thread: without `--on
733
+ * On fountain, the run belongs in the steward's conversation: without `--on
734
734
  * fountain` this refuses outright, naming the command that posts it there
735
- * instead (the fountain runtime's own turn already refuses a busy teammate,
735
+ * instead (the fountain runtime's own turn already refuses a busy steward,
736
736
  * unretried — see chant-fountain-ops).
737
737
  *
738
738
  * In the local form, the run takes the steward's turn lease
@@ -771,15 +771,15 @@ async function stewardTurnGate(opName: string, ctx: CommandContext): Promise<Ste
771
771
  const form = stewardFormFor(owner, env);
772
772
 
773
773
  if (form === "fountain") {
774
- // `run.on: "fountain"` asks for the steward's thread as much as the flag
774
+ // `run.on: "fountain"` asks for the steward's conversation as much as the flag
775
775
  // does (#2523).
776
776
  if ((await chooseOpRuntime(ctx, true))?.on === "fountain") return { ok: true };
777
777
  return {
778
778
  ok: false,
779
779
  message: `Op "${opName}" is one of steward "${owner.name}"'s Ops, which runs on fountain in environment "${env}"`,
780
780
  hint:
781
- `Run \`chant run ${opName} --on fountain\` so it posts to "${owner.name}"'s thread. ` +
782
- `A plain \`chant run ${opName}\` would run it outside that thread, beside the steward instead of one of its turns.`,
781
+ `Run \`chant run ${opName} --on fountain\` so it posts to "${owner.name}"'s conversation. ` +
782
+ `A plain \`chant run ${opName}\` would run it outside that conversation, beside the steward instead of one of its turns.`,
783
783
  };
784
784
  }
785
785
 
@@ -1283,7 +1283,10 @@ async function runComponentSelection(ctx: CommandContext): Promise<number> {
1283
1283
  }
1284
1284
  }
1285
1285
 
1286
- return result.success ? 0 : 1;
1286
+ // A step that refused a plan that moved since it was approved exits 3
1287
+ // (#3997). `--gated-exit` doesn't remap it: the run failed.
1288
+ if (result.success) return 0;
1289
+ return result.run ? failedRunExitCode(result.run.results) : 1;
1287
1290
  }
1288
1291
 
1289
1292
  /**
@@ -1359,9 +1362,10 @@ async function runOpOnResolvedRuntime(ctx: CommandContext): Promise<number> {
1359
1362
  return 1;
1360
1363
  }
1361
1364
 
1362
- // #4044: a runner key signs the run's record as it settles, so `ci tick`
1363
- // can count the run. `--key` must be a runner key; the PEM in
1364
- // CHANT_RUNNER_KEY that is not one leaves the run unsigned, with a warning.
1365
+ // #4044: a runner key signs the run's record as it settles, so a reader
1366
+ // can see who ran it. `--key` must be a runner key; the PEM in
1367
+ // CHANT_RUNNER_KEY that trust.json does not list leaves the run unsigned,
1368
+ // with a warning only when ci.green.signedSince asks for signed runs (#4156).
1365
1369
  const signRecord = await runSignerFor(ctx);
1366
1370
  if (signRecord === null) {
1367
1371
  await stewardGate?.release?.();
@@ -1564,10 +1568,13 @@ const RUNNER_KEY_ENV = "CHANT_RUNNER_KEY";
1564
1568
  * What signs the run's record (#4044): the runner key `--key` names, or the
1565
1569
  * PEM in `CHANT_RUNNER_KEY`, checked against `.chant/trust.json` at `HEAD`.
1566
1570
  * Undefined when neither is given. Null, after printing why, when `--key`
1567
- * is not a runner key; an unusable `CHANT_RUNNER_KEY` only warns, since the
1568
- * variable may be set for `ci tick` in a job that also runs Ops.
1571
+ * is not a runner key, or either is not an Ed25519 PEM. A `CHANT_RUNNER_KEY`
1572
+ * that trust.json does not list leaves the run unsigned (#4156): quietly,
1573
+ * with a line under `--verbose`, unless `ci.green.signedSince` asks for
1574
+ * signed runs, which warns. The variable may be set for `ci tick` in a job
1575
+ * that also runs Ops.
1569
1576
  */
1570
- async function runSignerFor(ctx: CommandContext): Promise<((record: OpRunRecord) => NonNullable<OpRunRecord["statement"]>) | undefined | null> {
1577
+ export async function runSignerFor(ctx: CommandContext): Promise<((record: OpRunRecord) => NonNullable<OpRunRecord["statement"]>) | undefined | null> {
1571
1578
  const fromFlag = ctx.args.key !== undefined;
1572
1579
  let pem: string | undefined;
1573
1580
  try {
@@ -1577,9 +1584,17 @@ async function runSignerFor(ctx: CommandContext): Promise<((record: OpRunRecord)
1577
1584
  return null;
1578
1585
  }
1579
1586
  if (pem === undefined) return undefined;
1587
+ // Loaded only when a key is given, so a run without one loads no workspace code (level 0, #2525).
1588
+ const { opRunSigner, signedRunsRequired } = await import("../../workspace/ci-green");
1589
+ const { loadRunnerKey } = await import("../../workspace/trust/dsse");
1590
+ try {
1591
+ loadRunnerKey(pem);
1592
+ } catch (err) {
1593
+ // A key that is there and is not one is an error either way: the caller asked to sign (#4156).
1594
+ console.error(formatError({ message: `${fromFlag ? `--key ${ctx.args.key}` : RUNNER_KEY_ENV} is not an Ed25519 private key in PEM: ${err instanceof Error ? err.message : String(err)}` }));
1595
+ return null;
1596
+ }
1580
1597
  try {
1581
- // Loaded only when a key is given, so a run without one loads no workspace code (level 0, #2525).
1582
- const { opRunSigner } = await import("../../workspace/ci-green");
1583
1598
  return opRunSigner(process.cwd(), pem);
1584
1599
  } catch (err) {
1585
1600
  const message = err instanceof Error ? err.message : String(err);
@@ -1587,7 +1602,10 @@ async function runSignerFor(ctx: CommandContext): Promise<((record: OpRunRecord)
1587
1602
  console.error(formatError({ message: `the run would not be signed: ${message}` }));
1588
1603
  return null;
1589
1604
  }
1590
- console.error(formatWarning({ message: `${RUNNER_KEY_ENV} is set, and the run will not be signed: ${message}` }));
1605
+ // #4156: signing is opt-in, so a key trust.json does not list warns only when ci.green.signedSince asks for signed runs.
1606
+ const required = signedRunsRequired(process.cwd());
1607
+ if (required) console.error(formatWarning({ message: `${RUNNER_KEY_ENV} is set, and the run will not be signed, so ci.green.signedSince (${required}) will not count it: ${message}` }));
1608
+ else if (ctx.args.verbose) console.error(`${RUNNER_KEY_ENV} is set, and the run is not signed: ${message}`);
1591
1609
  return undefined;
1592
1610
  }
1593
1611
  }
@@ -666,6 +666,18 @@ describe("resolveCommand", () => {
666
666
  expect(result!.compound).toBe(false);
667
667
  });
668
668
 
669
+ test("`sync` is an alias for `update` (#3988)", () => {
670
+ const result = resolveCommand(makeArgs({ command: "sync" }), [...testRegistry, { name: "update", handler: noop }]);
671
+ expect(result!.def.name).toBe("update");
672
+ expect(result!.compound).toBe(false);
673
+ });
674
+
675
+ test("`sync <path>` resolves to `update` with the path kept (#3988)", () => {
676
+ const result = resolveCommand(makeArgs({ command: "sync", path: "./proj" }), [...testRegistry, { name: "update", handler: noop }]);
677
+ expect(result!.def.name).toBe("update");
678
+ expect(result!.compound).toBe(false);
679
+ });
680
+
669
681
  test("resolves components status as compound command", () => {
670
682
  const result = resolveCommand(makeArgs({ command: "components", path: "status" }), testRegistry);
671
683
  expect(result!.def.name).toBe("components status");
@@ -836,6 +848,7 @@ describe("workspace init and ls (#2534)", () => {
836
848
  "workspace runs",
837
849
  "workspace signers",
838
850
  "workspace status",
851
+ "workspace trust",
839
852
  "workspace upgrade",
840
853
  "workspace verify",
841
854
  "workspace versions",
@@ -1020,4 +1033,10 @@ describe("chant --help", () => {
1020
1033
  const commands = new Set(commandRegistry.map((def) => def.name.split(" ")[0]));
1021
1034
  expect([...commands].filter((name) => !listed.has(name))).toEqual([]);
1022
1035
  });
1036
+
1037
+ test("names `sync` as the alias of `update` (#3988)", () => {
1038
+ const source = readFileSync(join(import.meta.dirname, "main.ts"), "utf-8");
1039
+ const help = source.slice(source.indexOf("function printHelp("), source.indexOf("Options:", source.indexOf("function printHelp(")));
1040
+ expect(help).toMatch(/^ {2}update \(alias: sync\)/m);
1041
+ });
1023
1042
  });
package/src/cli/main.ts CHANGED
@@ -238,34 +238,60 @@ CI:
238
238
  its window, newest first, each green, revoked or
239
239
  undecided with its required phases' results. Reads
240
240
  local tags; --forge reads check runs for undecided
241
- commits, and source runs reads the signed Op runs on
242
- the local chant/lifecycle. --since replaces the window.
241
+ commits, and source runs reads the Op runs on the
242
+ local chant/lifecycle, signed or not. --since replaces the window.
243
243
  ci tick [--dry-run] [--forge github] [--source checks|runs] [--key <runner key.pem>]
244
244
  Tag each commit within ci.green's window whose
245
245
  required phases all passed ci/green/<sha>, and revoke
246
246
  a green one that now fails ci/revoked/<sha>. Reads
247
247
  check runs from the forge, or with source runs
248
248
  (ci.green.source or --source) the Op runs on
249
- chant/lifecycle that a runner key signed. Pushes the
249
+ chant/lifecycle; with ci.green.signedSince, the ones a
250
+ runner key signed. Pushes the
250
251
  tags. --key, or the PEM in CHANT_RUNNER_KEY, signs
251
252
  each tag with a runner key.
252
253
  ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>]
253
- [--forge github|gitlab|forgejo] [--gate <name>] [--branch <name>]
254
- [--events-json] [--report-url <url>] [--image <image>]
254
+ [--forge github|gitlab|forgejo] [--gate <name>] [--wave-gate <name>]
255
+ [--branch <name>] [--events-json] [--report-url <url>] [--image <image>]
255
256
  [--before-script <line>]... [--pr-build]
256
257
  [--pr-build-command <line>] [--chant <command>]
257
- [--pass-env <NAME>]... [--key <runner key.pem>] [-o <dir>]
258
+ [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>]
259
+ [-o <dir>] [--wave <k>] [--no-require-review]
260
+ [--no-status] [--status-url <url template>]
261
+ [--in-place | --local] [--no-push] [--verbose]
258
262
  Run the pull-request pipeline's plan or apply job as
259
263
  a pipeline Op in a fresh worktree of --commit (default
260
264
  HEAD) with a clean environment. --base and --pr stand
261
265
  in for the forge's event values. --pr-build runs
262
266
  the job's build and lint lines first. Records the run as
263
- pr-plan or pr-apply on chant/lifecycle, signed with
264
- --key or CHANT_RUNNER_KEY, so ci tick from runs counts
265
- it, and copies the report to -o (default .chant/pr).
267
+ pr-plan or pr-apply on chant/lifecycle, which ci tick
268
+ from runs counts, signed when --key or CHANT_RUNNER_KEY
269
+ gives a runner key, and copies the report to -o
270
+ (default .chant/pr). --in-place runs in this checkout
271
+ at HEAD, as a generated forge job does, and names the
272
+ forge's run in the record; --no-push keeps the record
273
+ local. Exits with the command's code. With a forge
274
+ token it sets a chant/pr-plan or chant/pr-apply commit
275
+ status (pending, then success or failure); --no-status
276
+ turns that off, and --status-url links it.
277
+ --secrets (or CHANT_CI_SECRETS) names a dotenv or
278
+ JSON file only its owner can read whose values the
279
+ job gets, and which are shown as *** in the events,
280
+ the record, the report and the output.
281
+ With --report-url it also adds the run to
282
+ <url>/runs/index.json, newest first, cut to the
283
+ newest --run-index-limit entries (default 200).
284
+ Without --report-url, the workspace's ci.reportUrl.
285
+ --local runs the job on this machine: the record is
286
+ kept on the local chant/lifecycle with runOrigin
287
+ local and is never pushed, no status is set, nothing
288
+ goes under a report URL, and ci tick does not count it.
289
+ ci runs [--report-url <url>] [--json]
290
+ List the runs in the index at the report URL (default
291
+ the workspace's ci.reportUrl), newest first.
266
292
  ci serve --forge forgejo|github --repo <owner/repo> --env <env>
267
293
  [--interval <s>] [--max-jobs <n>] [--state <dir>]
268
- [--author <login>]... [--once] [--rerun-failed]
294
+ [--author <login>]... [--once] [--rerun-failed] [--unsigned]
269
295
  [ci run's job flags]
270
296
  Poll the forge for open pull requests and the target
271
297
  branch's head, and run ci run plan for each new pull
@@ -275,7 +301,16 @@ CI:
275
301
  each apply's resume record there, and runs at most
276
302
  --max-jobs (default 1) jobs at once. --once runs one
277
303
  poll and waits for its jobs. --rerun-failed runs a
278
- failed apply or plan again.
304
+ failed apply or plan again. Signs runs with a runner
305
+ key when .chant/trust.json at the target's head lists
306
+ it, and serves unsigned otherwise; with
307
+ ci.green.signedSince it refuses to start without one
308
+ ci serve|run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]
309
+ Make the host's runner key (Ed25519, mode 0600) in the
310
+ state directory when there is none and print its public
311
+ key. --trust commits its runner entry on a branch of its
312
+ own and pushes it for review; the target branch is never
313
+ written
279
314
  ci workflow [--workflow <name>]... [--output <file>] Write
280
315
  [--chant <command>] [--install <command>]
281
316
  [--token-secret <NAME> | --app-id-var <VAR> --app-key-secret <NAME>]
@@ -701,6 +736,13 @@ Workspace (level 1, #2524):
701
736
  threshold of the old one. A commit an agent run made
702
737
  is attested by the run's signed statement; with
703
738
  attested-runs, only those commits must be
739
+ workspace trust runner add <principal> --key <ssh-ed25519 line or file> [--class runner|service] [--by <principal>] [--expect <digest>] [--dry-run]
740
+ workspace trust runner remove <principal> [--by <principal>] [--expect <digest>] [--dry-run]
741
+ List a runner's key under runners in .chant/trust.json,
742
+ granting a runner the runner role, or take both away,
743
+ editing the file in place. --expect refuses the write
744
+ when the file's digest has changed. Prints the
745
+ trust-write document and the new digest; never commits
704
746
  workspace signers [--json] [rotate [--threshold <n>] | sign --key <file>]
705
747
  Show the signer history at base, or propose the next
706
748
  signer set and sign it with a current signer's key
@@ -899,6 +941,10 @@ Component release ledger + status:
899
941
  member's loop, gate op pr-<n>-<member>;
900
942
  --resume <file> finishes an apply that failed
901
943
  partway under the same approval;
944
+ --wave-gate <name>: apply a wave at a time, each
945
+ planned after the one before applied, a moved
946
+ wave waiting at <name>-wave-<k>, with --wave <k>
947
+ for one wave per CI job;
902
948
  --events-json, both commands: the run-event
903
949
  stream on stdout; --report-url <s3 or file URL>,
904
950
  both commands: copy the report, the change set
@@ -968,7 +1014,8 @@ Servers:
968
1014
  serve mcp Start the MCP server (stdio)
969
1015
 
970
1016
  Project:
971
- update Sync lexicon types into .chant/types/
1017
+ update (alias: sync) Sync lexicon types into .chant/types/ (does not install
1018
+ packages: run npm install first)
972
1019
  doctor Check project health and configuration
973
1020
 
974
1021
  Options:
@@ -1187,6 +1234,9 @@ async function loadPluginsOrExit(path: string, lexicon?: string): Promise<import
1187
1234
  plugins = await loadPlugins(lexiconNames);
1188
1235
  } catch (error) {
1189
1236
  const errorMessage = error instanceof Error ? error.message : String(error);
1237
+ // #4116: a lexicon package the project has not installed is named as such first.
1238
+ const { reportUninstalledDependencies } = await import("./dependencies");
1239
+ reportUninstalledDependencies([errorMessage], resolve(path));
1190
1240
  console.error(formatError({ message: errorMessage }));
1191
1241
  process.exit(1);
1192
1242
  }
@@ -1335,6 +1385,8 @@ export const commandRegistry: CommandDef[] = [
1335
1385
  { name: "ci workflow", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiWorkflow(ctx) },
1336
1386
  // #4094: one pull-request job as a pipeline Op in a fresh checkout, the chant runner's first slice.
1337
1387
  { name: "ci run", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run")).runCiRun(ctx) },
1388
+ // #4098: the run index at the report URL.
1389
+ { name: "ci runs", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run-index")).runCiRuns(ctx) },
1338
1390
  { name: "ci serve", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-serve")).runCiServe(ctx) },
1339
1391
  // #4026: the release cut from a green commit (ws-104). Imported on first use.
1340
1392
  { name: "release cut", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCut(ctx) },
@@ -1439,6 +1491,8 @@ export const commandRegistry: CommandDef[] = [
1439
1491
  // #2548 — an agent session's member, write scope and spec, for a session that resumes.
1440
1492
  { name: "workspace agent", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/agent-cli")).runWorkspaceAgent(ctx) },
1441
1493
  { name: "workspace verify", handler: async (ctx) => (await import("../workspace/trust/verify-cli")).runWorkspaceVerify(ctx) },
1494
+ // #3510 — a runner entry of .chant/trust.json, written through chant so a box repo never edits it by hand (ws-074).
1495
+ { name: "workspace trust", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/trust/trust-write")).runWorkspaceTrust(ctx) },
1442
1496
  { name: "workspace signers", handler: async (ctx) => (await import("../workspace/trust/signers-cli")).runWorkspaceSigners(ctx) },
1443
1497
  { name: "workspace evidence", handler: async (ctx) => (await import("../workspace/trust/evidence-cli")).runWorkspaceEvidence(ctx) },
1444
1498
 
@@ -1691,6 +1745,13 @@ const isMain = isEntryPoint(process.argv[1], import.meta.url);
1691
1745
  if (isMain) {
1692
1746
  main().catch(async (err) => {
1693
1747
  const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
1748
+ // #4116: a failure to import a package the project has not installed is named as such first.
1749
+ try {
1750
+ const { reportUninstalledDependencies } = await import("./dependencies");
1751
+ reportUninstalledDependencies([err instanceof Error ? err.message : String(err)], process.cwd());
1752
+ } catch {
1753
+ // The original error below is what matters.
1754
+ }
1694
1755
  if (verbose && err instanceof Error && err.stack) {
1695
1756
  console.error(err.stack);
1696
1757
  } else {