@intentius/plywood 0.3.2 → 0.3.5

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.
package/dist/plywood.mjs CHANGED
@@ -16,6 +16,14 @@ var __export = (target, all) => {
16
16
  __defProp(target, name, { get: all[name], enumerable: true });
17
17
  };
18
18
 
19
+ // <define:__PLYWOOD_IMAGE_DIGESTS__>
20
+ var define_PLYWOOD_IMAGE_DIGESTS_default;
21
+ var init_define_PLYWOOD_IMAGE_DIGESTS = __esm({
22
+ "<define:__PLYWOOD_IMAGE_DIGESTS__>"() {
23
+ define_PLYWOOD_IMAGE_DIGESTS_default = { "ghcr.io/intentius/plywood-pulumi:0.3.5-pulumi3.272.0": "sha256:a1288d42eaba8ec556ed3ea40a24cfda94a8413576b7f0129b30ab7620639f07", "ghcr.io/intentius/plywood-plunge:0.3.5-plunge0.2.0": "sha256:4384ee4b92c8b0cb00c4833bc0118a820a0c3620ad78dc50d9e21fb0d9fff370" };
24
+ }
25
+ });
26
+
19
27
  // src/config/types.ts
20
28
  function isObject(v) {
21
29
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -45,6 +53,7 @@ var BACKEND_SCHEMA;
45
53
  var init_types = __esm({
46
54
  "src/config/types.ts"() {
47
55
  "use strict";
56
+ init_define_PLYWOOD_IMAGE_DIGESTS();
48
57
  BACKEND_SCHEMA = {
49
58
  type: "string",
50
59
  description: "The backend URL. A plunge+ URL (plunge+file://, plunge+s3://) picks the plunge engine."
@@ -89,6 +98,7 @@ var DEFAULT_KEY_ENV, VARIABLE, REVIEW_KEYS, AGENT_KEYS, modelSchema, agents;
89
98
  var init_agents = __esm({
90
99
  "src/config/agents.ts"() {
91
100
  "use strict";
101
+ init_define_PLYWOOD_IMAGE_DIGESTS();
92
102
  init_types();
93
103
  DEFAULT_KEY_ENV = "ANTHROPIC_API_KEY";
94
104
  VARIABLE = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -182,6 +192,7 @@ var APPLY_WHEN, DEFAULT_APPLY_WHEN, APPLY_REQUIRES, APPLY_MERGE, MERGE_METHODS,
182
192
  var init_apply = __esm({
183
193
  "src/config/apply.ts"() {
184
194
  "use strict";
195
+ init_define_PLYWOOD_IMAGE_DIGESTS();
185
196
  init_types();
186
197
  APPLY_WHEN = ["merge", "pull-request"];
187
198
  DEFAULT_APPLY_WHEN = "merge";
@@ -261,7 +272,7 @@ function gateOf(doc) {
261
272
  return typeof doc.gate === "string" && GATES.includes(doc.gate) ? doc.gate : DEFAULT_GATE;
262
273
  }
263
274
  function previewOf(doc) {
264
- return doc.preview === true;
275
+ return doc.preview !== false;
265
276
  }
266
277
  function quorumOf(rules) {
267
278
  return Object.entries(rules).map(([glob, r]) => {
@@ -273,12 +284,13 @@ var GATES, DEFAULT_GATE, APPROVALS, DEFAULT_APPROVAL, DEFAULT_PREVIEW, APPROVAL_
273
284
  var init_approval = __esm({
274
285
  "src/config/approval.ts"() {
275
286
  "use strict";
287
+ init_define_PLYWOOD_IMAGE_DIGESTS();
276
288
  init_types();
277
289
  GATES = ["always", "on-destructive", "never"];
278
290
  DEFAULT_GATE = "on-destructive";
279
291
  APPROVALS = ["ledger", "sealed", "pr-review"];
280
292
  DEFAULT_APPROVAL = "ledger";
281
- DEFAULT_PREVIEW = false;
293
+ DEFAULT_PREVIEW = true;
282
294
  APPROVAL_RULE_KEYS = ["role", "roles", "quorum"];
283
295
  approval = {
284
296
  name: "approval",
@@ -289,7 +301,7 @@ var init_approval = __esm({
289
301
  preview: {
290
302
  type: "boolean",
291
303
  default: DEFAULT_PREVIEW,
292
- description: "true: plywood init sets approvals.preview in the waves spec, so a merged wave applies only the plans its pull request previewed, or exits 4. Known limits: plywood#55 (the second of two disjoint merges is refused) and plywood#56 (a pull request that creates an autonamed resource is refused)."
304
+ description: "true (the default): plywood init sets approvals.preview in the waves spec, so a merged wave applies only the plans its pull request previewed, or exits 4. false leaves it out."
293
305
  },
294
306
  approval_rules: {
295
307
  type: "object",
@@ -351,6 +363,7 @@ var PLYWOOD_KEY_FOR, chantKeys;
351
363
  var init_chant_keys = __esm({
352
364
  "src/config/chant-keys.ts"() {
353
365
  "use strict";
366
+ init_define_PLYWOOD_IMAGE_DIGESTS();
354
367
  init_types();
355
368
  PLYWOOD_KEY_FOR = {
356
369
  workflow: "apply.when",
@@ -431,6 +444,7 @@ var PRODUCT, TAGLINE, OP_NAME, PR_GATE, VERBS, BIN, NOTIFY_WORDS, PRODUCT_WORDS,
431
444
  var init_words = __esm({
432
445
  "src/words.ts"() {
433
446
  "use strict";
447
+ init_define_PLYWOOD_IMAGE_DIGESTS();
434
448
  PRODUCT = "plywood";
435
449
  TAGLINE = "Pulumi ops and governance.";
436
450
  OP_NAME = "pulumi-up";
@@ -496,7 +510,7 @@ ${r.tail}` : ""}`,
496
510
  };
497
511
  DRIFT_OP_PREFIX = "plywood-drift-";
498
512
  LOCK_GRAIN_WORDS = {
499
- perEngine: (engine2) => engine2 === "plunge" ? "resource, plunge's default (no ci.lockGrain; chant's pulumi lexicon sets it)" : engine2 === "pulumi" ? "stack, stock Pulumi's default (no ci.lockGrain; chant's pulumi lexicon sets it)" : "per engine: resource on plunge, stack on stock Pulumi (no ci.lockGrain; chant's pulumi lexicon sets it)"
513
+ perEngine: (engine3) => engine3 === "plunge" ? "resource, plunge's default (no ci.lockGrain; chant's pulumi lexicon sets it)" : engine3 === "pulumi" ? "stack, stock Pulumi's default (no ci.lockGrain; chant's pulumi lexicon sets it)" : "per engine: resource on plunge, stack on stock Pulumi (no ci.lockGrain; chant's pulumi lexicon sets it)"
500
514
  };
501
515
  PLUNGE_TIMING_ATTRIBUTES = {
502
516
  resources: `${PRODUCT}.plunge.resources`,
@@ -525,10 +539,17 @@ function controlOf(doc) {
525
539
  }
526
540
  return out;
527
541
  }
542
+ function controlBuildArgv(doc, member) {
543
+ const settings = controlOf(doc);
544
+ if (!settings) return [];
545
+ const { repo, ...rest } = { member, ...settings };
546
+ return ["--control", repo, ...Object.entries(rest).flatMap(([k, v]) => ["--control-setting", `${k}=${v}`])];
547
+ }
528
548
  var CONTROL_KEYS, CONTROL_FORGES, control;
529
549
  var init_control = __esm({
530
550
  "src/config/control.ts"() {
531
551
  "use strict";
552
+ init_define_PLYWOOD_IMAGE_DIGESTS();
532
553
  init_words();
533
554
  init_types();
534
555
  CONTROL_KEYS = {
@@ -597,37 +618,27 @@ var init_control = __esm({
597
618
  var version, dependencies;
598
619
  var init_package = __esm({
599
620
  "package.json"() {
600
- version = "0.3.2";
621
+ version = "0.3.5";
601
622
  dependencies = {
602
- "@intentius/chant": "0.124.67",
603
- "@intentius/chant-lexicon-forgejo": "0.124.67",
604
- "@intentius/chant-lexicon-github": "0.124.67",
605
- "@intentius/chant-lexicon-gitlab": "0.124.67",
606
- "@intentius/chant-lexicon-pulumi": "0.124.67"
607
- };
608
- }
609
- });
610
-
611
- // src/image-digests.json
612
- var image_digests_default;
613
- var init_image_digests = __esm({
614
- "src/image-digests.json"() {
615
- image_digests_default = {
616
- "ghcr.io/intentius/plywood-pulumi:0.3.2-pulumi3.272.0": "sha256:eeb44ac18dc594acd8a56dfc1a6ed00751154f6b6bfba8bbdff97a513f4b9f84",
617
- "ghcr.io/intentius/plywood-plunge:0.3.2-plunge0.2.0": "sha256:5239ffabd5f5b7a4ffc5cbbe1628a34990bc95549cd4a1bef27953d3dfb7d8a9"
623
+ "@intentius/chant": "0.124.79",
624
+ "@intentius/chant-lexicon-forgejo": "0.124.79",
625
+ "@intentius/chant-lexicon-github": "0.124.79",
626
+ "@intentius/chant-lexicon-gitlab": "0.124.79",
627
+ "@intentius/chant-lexicon-pulumi": "0.124.79"
618
628
  };
619
629
  }
620
630
  });
621
631
 
622
632
  // src/images.ts
623
- function engineDir(engine2, version2) {
624
- return `${ENGINE_ROOT}/${engine2}-${version2}`;
633
+ import { CI_DOCKER_ARCH, ciFetchScript, plungeInstallLine, pulumiInstallLine } from "@intentius/chant-lexicon-pulumi/ci-setup";
634
+ function engineDir(engine3, version2) {
635
+ return `${ENGINE_ROOT}/${engine3}-${version2}`;
625
636
  }
626
- function engineBin(engine2, version2) {
627
- return `${engineDir(engine2, version2)}/${engine2}`;
637
+ function engineBin(engine3, version2) {
638
+ return `${engineDir(engine3, version2)}/${engine3}`;
628
639
  }
629
- function release(engine2, version2) {
630
- if (engine2 === "pulumi") {
640
+ function release(engine3, version2) {
641
+ if (engine3 === "pulumi") {
631
642
  return {
632
643
  base: `https://github.com/pulumi/pulumi/releases/download/v${version2}`,
633
644
  archive: (arch) => `pulumi-v${version2}-linux-${arch}.tar.gz`,
@@ -640,8 +651,8 @@ function release(engine2, version2) {
640
651
  checksums: `plunge-v${version2}-checksums.txt`
641
652
  };
642
653
  }
643
- function pinnedSums(engine2, version2) {
644
- const r = release(engine2, version2);
654
+ function pinnedSums(engine3, version2) {
655
+ const r = release(engine3, version2);
645
656
  const out = {};
646
657
  for (const arch of ARCHES) {
647
658
  const sum = PINNED_SHA256[r.archive(arch)];
@@ -649,54 +660,10 @@ function pinnedSums(engine2, version2) {
649
660
  }
650
661
  return out;
651
662
  }
652
- function fetchScript(engine2, version2, dir, arch, checksumsUrl) {
653
- const r = release(engine2, version2);
654
- const sums = pinnedSums(engine2, version2);
655
- const pinCase = Object.keys(sums).length ? `case "$a" in ${ARCHES.filter((x) => sums[x]).map((x) => `${x}) want=${sums[x]} ;;`).join(" ")} *) want= ;; esac` : "want=";
656
- const file = `${r.archive("x64").replace("-x64.", "-$a.")}`;
657
- return [
658
- `a=${arch}`,
659
- `f="${file}"`,
660
- `t="$(mktemp -d)"`,
661
- `curl -fsSLo "$t/$f" "${r.base}/$f"`,
662
- `curl -fsSLo "$t/sums" ${checksumsUrl ?? `"${r.base}/${r.checksums}"`}`,
663
- // The archive's line, with any ./ or * before the name taken off.
664
- `line="$(sed -n "s#^\\([0-9a-f]\\{64\\}\\) *[*.]*/*$f\\$#\\1#p" "$t/sums")"`,
665
- `{ [ -n "$line" ] || { echo "${engine2} ${version2}: $f is not in ${r.checksums}" >&2; exit 1; }; }`,
666
- pinCase,
667
- `{ [ -z "$want" ] || [ "$line" = "$want" ] || { echo "${engine2} ${version2}: ${r.checksums} lists $line for $f; plywood pins $want" >&2; exit 1; }; }`,
668
- `echo "$line $t/$f" | sha256sum -c - >/dev/null`,
669
- `mkdir -p "${dir}"`,
670
- `tar -xzf "$t/$f" -C "${dir}" --strip-components=1`
671
- ].join(" && ");
672
- }
673
- function installLine(engine2, version2, link = true) {
674
- const chant = chantLines;
675
- if (link && chant) {
676
- const sums = pinnedSums(engine2, version2);
677
- const options = { root: ENGINE_ROOT, ...Object.keys(sums).length ? { sha256: sums } : {} };
678
- return engine2 === "pulumi" ? chant.pulumiInstallLine(version2, options) : chant.plungeInstallLine(version2, options);
679
- }
680
- return plywoodInstallLine(engine2, version2, link);
681
- }
682
- function plywoodInstallLine(engine2, version2, link = true) {
683
- const dir = engineDir(engine2, version2);
684
- const linkCmd = `ln -sf "${dir}/${engine2}" /usr/local/bin/${engine2}`;
685
- const install = `(set -e; ${fetchScript(engine2, version2, dir, JOB_ARCH)})`;
686
- return link ? `{ [ -x "${dir}/${engine2}" ] || ${install}; } && ${linkCmd}` : `[ -x "${dir}/${engine2}" ] || ${install}`;
687
- }
688
- function useChantInstallLines() {
689
- loading ??= (async () => {
690
- try {
691
- const m = await import("@intentius/chant-lexicon-pulumi/ci-setup");
692
- const ok = typeof m.ciRelease === "function" && typeof m.PINNED_SHA256 === "object" && typeof m.pulumiInstallLine === "function" && typeof m.plungeInstallLine === "function";
693
- chantLines = ok ? m : void 0;
694
- } catch {
695
- chantLines = void 0;
696
- }
697
- return chantLines !== void 0;
698
- })();
699
- return loading;
663
+ function installLine(engine3, version2, link = true) {
664
+ const sums = pinnedSums(engine3, version2);
665
+ const options = { root: ENGINE_ROOT, ...Object.keys(sums).length ? { sha256: sums } : {}, link };
666
+ return engine3 === "pulumi" ? pulumiInstallLine(version2, options) : plungeInstallLine(version2, options);
700
667
  }
701
668
  function installLines(pins) {
702
669
  const seen = /* @__PURE__ */ new Set();
@@ -710,15 +677,15 @@ function installLines(pins) {
710
677
  }
711
678
  return out;
712
679
  }
713
- function imageName(engine2) {
714
- return `plywood-${engine2}`;
680
+ function imageName(engine3) {
681
+ return `plywood-${engine3}`;
715
682
  }
716
- function imageTag(engine2, version2 = version) {
717
- return `${version2}-${engine2}${TOOL_VERSIONS[engine2]}`;
683
+ function imageTag(engine3, version2 = version) {
684
+ return `${version2}-${engine3}${TOOL_VERSIONS[engine3]}`;
718
685
  }
719
- function imageFor(engine2, table = image_digests_default) {
720
- const repository = `${REGISTRY}/${imageName(engine2)}`;
721
- const tag = imageTag(engine2);
686
+ function imageFor(engine3, table = digests) {
687
+ const repository = `${REGISTRY}/${imageName(engine3)}`;
688
+ const tag = imageTag(engine3);
722
689
  const digest = table[`${repository}:${tag}`];
723
690
  return { repository, tag, ...digest ? { digest } : {} };
724
691
  }
@@ -730,23 +697,20 @@ function parsePlugin(s) {
730
697
  return m ? { kind: m[1] ?? "resource", name: m[2], version: m[3] } : void 0;
731
698
  }
732
699
  function chantBuildFlags(o) {
733
- const lines2 = [...o.beforeScript ?? [], ...o.prLoop?.engineSetup ?? []];
700
+ const lines2 = o.beforeScript ?? [];
734
701
  return [...o.image ? ["--image", o.image] : [], ...lines2.flatMap((l) => ["--before-script", l])];
735
702
  }
736
- function chantPipelineOptions(p, chantReadsEngineSetup) {
703
+ function chantPipelineOptions(p) {
737
704
  const lines2 = p.engineSetup ?? [];
738
- const out = { ...p.image ? { image: p.image } : {}, ...p.runner !== void 0 ? { runner: p.runner } : {} };
739
- if (lines2.length === 0) return out;
740
- if (!p.image && chantReadsEngineSetup) out.prLoop = { engineSetup: [...lines2] };
741
- else out.beforeScript = [...lines2];
742
- return out;
705
+ return { ...p.image ? { image: p.image } : {}, ...p.runner !== void 0 ? { runner: p.runner } : {}, ...lines2.length ? { beforeScript: [...lines2] } : {} };
743
706
  }
744
- var REGISTRY, TOOL_VERSIONS, ARCHES, PINNED_SHA256, ENGINE_ROOT, JOB_ARCH, chantLines, loading, DIGEST, PLUGIN;
707
+ var digests, REGISTRY, TOOL_VERSIONS, ARCHES, PINNED_SHA256, ENGINE_ROOT, DIGEST, RELEASE_LABEL_ARG, LABEL_REF, PLUGIN;
745
708
  var init_images = __esm({
746
709
  "src/images.ts"() {
747
710
  "use strict";
711
+ init_define_PLYWOOD_IMAGE_DIGESTS();
748
712
  init_package();
749
- init_image_digests();
713
+ digests = typeof define_PLYWOOD_IMAGE_DIGESTS_default !== "undefined" ? define_PLYWOOD_IMAGE_DIGESTS_default : {};
750
714
  REGISTRY = "ghcr.io/intentius";
751
715
  TOOL_VERSIONS = {
752
716
  pulumi: "3.272.0",
@@ -761,8 +725,9 @@ var init_images = __esm({
761
725
  "plunge-v0.2.0-linux-arm64.tar.gz": "c176a82f038164085aa19c8ad6530d6022e560e124050e535dbe364ae0473357"
762
726
  };
763
727
  ENGINE_ROOT = "/opt/plywood";
764
- JOB_ARCH = `"$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')"`;
765
728
  DIGEST = /@sha256:[0-9a-f]{64}$/;
729
+ RELEASE_LABEL_ARG = "CHANT_RELEASE_LABEL";
730
+ LABEL_REF = `\${${RELEASE_LABEL_ARG}}`;
766
731
  PLUGIN = /^(?:(resource|converter|language|analyzer|tool):)?([a-z0-9][a-z0-9-]*)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
767
732
  }
768
733
  });
@@ -831,8 +796,8 @@ function userImage(doc) {
831
796
  if (typeof doc.image === "string") return doc.image;
832
797
  return isObject(doc.image) && typeof doc.image.name === "string" ? doc.image.name : void 0;
833
798
  }
834
- function linkedVersion(engine2, doc, versions) {
835
- const top = pinFor(doc[engine2], void 0) ?? TOOL_VERSIONS[engine2];
799
+ function linkedVersion(engine3, doc, versions) {
800
+ const top = pinFor(doc[engine3], void 0) ?? TOOL_VERSIONS[engine3];
836
801
  if (versions.length === 0) return top;
837
802
  const count = /* @__PURE__ */ new Map();
838
803
  for (const v of versions) count.set(v, (count.get(v) ?? 0) + 1);
@@ -843,14 +808,15 @@ var VERSION, LABEL, RUNNER_STAGES, IMAGE_KEYS, versionSchema, runnerSpecSchema,
843
808
  var init_images2 = __esm({
844
809
  "src/config/images.ts"() {
845
810
  "use strict";
811
+ init_define_PLYWOOD_IMAGE_DIGESTS();
846
812
  init_images();
847
813
  init_types();
848
814
  VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
849
815
  LABEL = /^[^\s,]+$/;
850
816
  RUNNER_STAGES = ["default", "preview", "up", "drift"];
851
817
  IMAGE_KEYS = ["name", "plugins"];
852
- versionSchema = (engine2, example) => ({
853
- description: `The ${engine2} release the jobs run, such as ${example}: one for every stack, or a map of stack glob to release. plywood's image's release when absent.`,
818
+ versionSchema = (engine3, example) => ({
819
+ description: `The ${engine3} release the jobs run, such as ${example}: one for every stack, or a map of stack glob to release. plywood's image's release when absent.`,
854
820
  anyOf: [
855
821
  { type: "string", pattern: VERSION.source },
856
822
  { type: "object", minProperties: 1, additionalProperties: { type: "string", pattern: VERSION.source } }
@@ -910,22 +876,22 @@ var init_images2 = __esm({
910
876
  const engineOfMember = (e) => e ?? out.memberDefaults.engine ?? "pulumi";
911
877
  const per = { pulumi: [], plunge: [] };
912
878
  const chosen = out.members.map((m) => {
913
- const engine2 = engineOfMember(m.engine);
914
- const version2 = pinFor(doc[engine2], m.name) ?? pinFor(doc[engine2], void 0) ?? TOOL_VERSIONS[engine2];
915
- per[engine2].push(version2);
916
- return { m, engine: engine2, version: version2 };
879
+ const engine3 = engineOfMember(m.engine);
880
+ const version2 = pinFor(doc[engine3], m.name) ?? pinFor(doc[engine3], void 0) ?? TOOL_VERSIONS[engine3];
881
+ per[engine3].push(version2);
882
+ return { m, engine: engine3, version: version2 };
917
883
  });
918
884
  const engines = new Set(chosen.map((c) => c.engine));
919
885
  engines.add(engineOfMember(void 0));
920
886
  if (doc.plunge !== void 0) engines.add("plunge");
921
887
  const linked = Object.fromEntries([...engines].map((e) => [e, linkedVersion(e, doc, per[e])]));
922
- const pins = [...engines].map((engine2) => ({ engine: engine2, version: linked[engine2], linked: true }));
923
- for (const { m, engine: engine2, version: version2 } of chosen) {
924
- if (doc[engine2] !== void 0) m.engineVersion = version2;
925
- if (version2 !== linked[engine2]) {
926
- m.engineBin = engineBin(engine2, version2);
888
+ const pins = [...engines].map((engine3) => ({ engine: engine3, version: linked[engine3], linked: true }));
889
+ for (const { m, engine: engine3, version: version2 } of chosen) {
890
+ if (doc[engine3] !== void 0) m.engineVersion = version2;
891
+ if (version2 !== linked[engine3]) {
892
+ m.engineBin = engineBin(engine3, version2);
927
893
  m.engineRoot = ENGINE_ROOT;
928
- pins.push({ engine: engine2, version: version2, linked: false });
894
+ pins.push({ engine: engine3, version: version2, linked: false });
929
895
  }
930
896
  }
931
897
  out.pipeline.engineSetup = installLines(pins);
@@ -1051,6 +1017,7 @@ var STACK_KEYS, ACTION_KEYS, WAVES_KEYS, STACK_NAME, ACTION_NAME, ACTION_TYPE_PR
1051
1017
  var init_stacks = __esm({
1052
1018
  "src/config/stacks.ts"() {
1053
1019
  "use strict";
1020
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1054
1021
  init_approval();
1055
1022
  init_types();
1056
1023
  STACK_KEYS = ["dir", "paths", "gate", "backend", "actions"];
@@ -1215,6 +1182,7 @@ var DRIFT_RESPONSES, DEFAULT_DRIFT_RESPONSE, EVENT_RESPONSES, EVENT_KEYS, RESPON
1215
1182
  var init_drift = __esm({
1216
1183
  "src/config/drift.ts"() {
1217
1184
  "use strict";
1185
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1218
1186
  init_images2();
1219
1187
  init_stacks();
1220
1188
  init_types();
@@ -1339,6 +1307,7 @@ var ENGINES, PLUNGE_URL, engine;
1339
1307
  var init_engine = __esm({
1340
1308
  "src/config/engine.ts"() {
1341
1309
  "use strict";
1310
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1342
1311
  init_stacks();
1343
1312
  init_types();
1344
1313
  ENGINES = ["pulumi", "plunge"];
@@ -1472,6 +1441,7 @@ var IDENTITY_CLOUDS, CLOUD_SETTINGS, SETTINGS_CLOUDS, REQUIRED, IDENTITY_KEYS, S
1472
1441
  var init_identity = __esm({
1473
1442
  "src/config/identity.ts"() {
1474
1443
  "use strict";
1444
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1475
1445
  init_types();
1476
1446
  IDENTITY_CLOUDS = ["aws", "gcp", "azure", "pulumi"];
1477
1447
  CLOUD_SETTINGS = {
@@ -1630,6 +1600,7 @@ var VARIABLE3, RESERVED_VARIABLES, environmentSchema, jobs;
1630
1600
  var init_jobs = __esm({
1631
1601
  "src/config/jobs.ts"() {
1632
1602
  "use strict";
1603
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1633
1604
  init_types();
1634
1605
  init_images2();
1635
1606
  VARIABLE3 = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -1736,6 +1707,7 @@ var LATER_KEYS, later;
1736
1707
  var init_later = __esm({
1737
1708
  "src/config/later.ts"() {
1738
1709
  "use strict";
1710
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1739
1711
  LATER_KEYS = {};
1740
1712
  later = {
1741
1713
  name: "later",
@@ -1749,10 +1721,10 @@ var init_later = __esm({
1749
1721
  });
1750
1722
 
1751
1723
  // src/config/locks.ts
1752
- function lockGrainFor(doc, engine2) {
1724
+ function lockGrainFor(doc, engine3) {
1753
1725
  const set = locksOf(doc).grain;
1754
1726
  if (set) return set;
1755
- return engine2 ? DEFAULT_LOCK_GRAIN[engine2] : "per-engine";
1727
+ return engine3 ? DEFAULT_LOCK_GRAIN[engine3] : "per-engine";
1756
1728
  }
1757
1729
  function locksOf(doc) {
1758
1730
  const l = isObject(doc.locks) ? doc.locks : {};
@@ -1770,6 +1742,7 @@ var LOCKS_WHEN, DEFAULT_LOCKS_WHEN, LOCK_GRAINS, DEFAULT_LOCK_GRAIN, CI_LOCKS_OF
1770
1742
  var init_locks = __esm({
1771
1743
  "src/config/locks.ts"() {
1772
1744
  "use strict";
1745
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1773
1746
  init_engine();
1774
1747
  init_stacks();
1775
1748
  init_types();
@@ -1814,11 +1787,6 @@ var init_locks = __esm({
1814
1787
  });
1815
1788
 
1816
1789
  // src/config/observe.ts
1817
- function notifyForChant(notify2, chant) {
1818
- if (notify2.relay === void 0 || typeof chant?.NOTIFY_RELAY_FLOOR === "string") return { ...notify2 };
1819
- const { relay: _relay, ...rest } = notify2;
1820
- return rest;
1821
- }
1822
1790
  function variable(v, key) {
1823
1791
  if (v === void 0) return [];
1824
1792
  return typeof v === "string" && VARIABLE4.test(v) ? [] : [error(key, "names an environment variable, such as SLACK_WEBHOOK_URL; the URL or key itself never goes in plywood.yml")];
@@ -1849,16 +1817,16 @@ function telemetryOf(doc) {
1849
1817
  traces: t.traces !== false
1850
1818
  };
1851
1819
  }
1852
- var NOTIFY_EVENTS, VARIABLE4, DEFAULT_SERVICE_NAME, REPORT_SCHEMES, NEWER_SCHEMES, WEBHOOK_VAR, observe;
1820
+ var NOTIFY_EVENTS, VARIABLE4, DEFAULT_SERVICE_NAME, REPORT_SCHEMES, WEBHOOK_VAR, observe;
1853
1821
  var init_observe = __esm({
1854
1822
  "src/config/observe.ts"() {
1855
1823
  "use strict";
1824
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1856
1825
  init_types();
1857
1826
  NOTIFY_EVENTS = { "wave-waiting": "waiting", "up-refused": "refused", "up-failed": "failed", drift: "drift" };
1858
1827
  VARIABLE4 = /^[A-Za-z_][A-Za-z0-9_]*$/;
1859
1828
  DEFAULT_SERVICE_NAME = "plywood";
1860
1829
  REPORT_SCHEMES = ["s3", "gs", "az", "file"];
1861
- NEWER_SCHEMES = /* @__PURE__ */ new Set(["gs", "az"]);
1862
1830
  WEBHOOK_VAR = { type: "object", additionalProperties: false, required: ["webhook"], properties: { webhook: { type: "string", pattern: VARIABLE4.source, description: "The variable holding the incoming webhook's URL." } } };
1863
1831
  observe = {
1864
1832
  name: "notify, telemetry and reports",
@@ -1914,7 +1882,6 @@ var init_observe = __esm({
1914
1882
  if (n.relay !== void 0) {
1915
1883
  out.push(...variable(n.relay, "notify.relay"));
1916
1884
  if (n.slack === void 0 && n.teams === void 0) out.push(error("notify.relay", "needs notify.slack or notify.teams: approvals come back from a chat message"));
1917
- else out.push(warning("notify.relay", "needs chant >= the release after 0.124.54 (chant#4611); plywood init leaves it out of the waves spec on a chant without the relay"));
1918
1885
  }
1919
1886
  for (const chat of ["slack", "teams"]) {
1920
1887
  const c = n[chat];
@@ -1954,7 +1921,6 @@ var init_observe = __esm({
1954
1921
  out.push(...onlyKeys(r, ["url"], "reports"));
1955
1922
  const scheme = typeof r.url === "string" ? /^([a-z0-9]+):\/\/\S+$/.exec(r.url)?.[1] : void 0;
1956
1923
  if (!scheme || !REPORT_SCHEMES.includes(scheme)) out.push(error("reports.url", `is a ${REPORT_SCHEMES.map((s) => `${s}://`).join(", ")} URL`));
1957
- else if (NEWER_SCHEMES.has(scheme)) out.push(warning("reports.url", `${scheme}:// reports need chant >= the release after 0.124.54 (chant#4554); the pinned chant takes s3:// and file://`));
1958
1924
  }
1959
1925
  }
1960
1926
  return out;
@@ -1970,21 +1936,7 @@ var init_observe = __esm({
1970
1936
 
1971
1937
  // src/config/policy.ts
1972
1938
  import { posix } from "node:path";
1973
- function useChantPolicySource() {
1974
- policySourceLoading ??= (async () => {
1975
- try {
1976
- const m = await import("@intentius/chant/policy-source");
1977
- policySource = typeof m.CI_POLICY_SOURCE_FLOOR === "string" && typeof m.assertGatePolicySource === "function";
1978
- } catch {
1979
- policySource = false;
1980
- }
1981
- return policySource;
1982
- })();
1983
- return policySourceLoading;
1984
- }
1985
- function chantHasPolicySource() {
1986
- return policySource;
1987
- }
1939
+ import { assertGatePolicySource } from "@intentius/chant/policy-source";
1988
1940
  function repoPath(v, key) {
1989
1941
  if (typeof v !== "string" || v.trim() === "") return [error(key, "is a path inside the repository")];
1990
1942
  if (v.startsWith("/") || v.startsWith("-") || v.split("/").includes("..")) return [error(key, `is ${JSON.stringify(v)}; give a path relative to the repository root, inside it`)];
@@ -2019,13 +1971,13 @@ function policySourceOf(doc) {
2019
1971
  if (!isObject(src) || typeof src.repo !== "string" || typeof src.ref !== "string") return void 0;
2020
1972
  return { repo: src.repo, ref: src.ref, ...typeof src.path === "string" ? { path: cleanPath(src.path) } : {} };
2021
1973
  }
2022
- function chantPolicyOf(doc, chantReadsSource = chantHasPolicySource()) {
1974
+ function chantPolicyOf(doc) {
2023
1975
  const p = doc.policy;
2024
1976
  if (!isObject(p)) return void 0;
2025
1977
  const rego = typeof p.rego === "string" ? cleanPath(p.rego) : void 0;
2026
1978
  const command = Array.isArray(p.command) ? p.command : void 0;
2027
- const source = chantReadsSource ? policySourceOf(doc) : void 0;
2028
- const packs = chantReadsSource ? policyPacksOf(doc) : [];
1979
+ const source = policySourceOf(doc);
1980
+ const packs = policyPacksOf(doc);
2029
1981
  if (rego === void 0 && command === void 0 && p.engine === void 0 && p.namespace === void 0 && !source && packs.length === 0) return void 0;
2030
1982
  return {
2031
1983
  ...typeof p.engine === "string" ? { engine: p.engine } : {},
@@ -2036,26 +1988,22 @@ function chantPolicyOf(doc, chantReadsSource = chantHasPolicySource()) {
2036
1988
  ...packs.length ? { packs } : {}
2037
1989
  };
2038
1990
  }
2039
- function stepPolicyPacksOf(doc, chantReadsSource = chantHasPolicySource()) {
2040
- return chantReadsSource && policySourceOf(doc) ? [] : policyPacksOf(doc);
1991
+ function stepPolicyPacksOf(doc) {
1992
+ return policySourceOf(doc) ? [] : policyPacksOf(doc);
2041
1993
  }
2042
- var POLICY_ENGINES, POLICY_KEYS, PACK_KEYS, SOURCE_KEYS, SOURCE_REPO, SOURCE_REF, policySource, policySourceLoading, NO_SUCH_KEY, SOURCE_NEEDS_CHANT, policy;
1994
+ var POLICY_ENGINES, POLICY_KEYS, PACK_KEYS, NO_SUCH_KEY, policy;
2043
1995
  var init_policy = __esm({
2044
1996
  "src/config/policy.ts"() {
2045
1997
  "use strict";
1998
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2046
1999
  init_types();
2047
2000
  POLICY_ENGINES = ["conftest", "opa"];
2048
2001
  POLICY_KEYS = ["rego", "engine", "namespace", "command", "packs", "source", "providers"];
2049
2002
  PACK_KEYS = ["dir", "config"];
2050
- SOURCE_KEYS = ["repo", "ref", "path"];
2051
- SOURCE_REPO = /^(https?:\/\/[^\s/]+\/\S+|ssh:\/\/[^\s/]+\/\S+|file:\/\/\/?\S+|[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:\S+)$/;
2052
- SOURCE_REF = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
2053
- policySource = false;
2054
2003
  NO_SUCH_KEY = {
2055
2004
  allow_unattended: "chant's gate denies and holds only; a non-empty change always waits for a person, so there is no unattended outcome",
2056
2005
  outcomes: "a Rego rule's name sets its outcome: deny_* denies, hold_* holds, warn_* is a note line"
2057
2006
  };
2058
- SOURCE_NEEDS_CHANT = "is a shared policy source from a git ref, which needs chant >= the release after 0.124.54 (chant#4613's ci.policy.source); on this chant, vendor the rules into the repository, where they are read at base";
2059
2007
  policy = {
2060
2008
  name: "policy",
2061
2009
  keys: ["policy"],
@@ -2073,7 +2021,7 @@ var init_policy = __esm({
2073
2021
  type: "object",
2074
2022
  additionalProperties: false,
2075
2023
  required: ["repo", "ref"],
2076
- description: "A shared rule set from a git ref, fetched by the job at the ref the base names (chant >= the release after 0.124.54). packs are then paths inside it.",
2024
+ description: "A shared rule set from a git ref, fetched by the job at the ref the base names. packs are then paths inside it.",
2077
2025
  properties: {
2078
2026
  repo: { type: "string", description: "The repository: https://, ssh:// or file:// URL, or git@<host>:<path>." },
2079
2027
  ref: { type: "string", description: "A tag, a branch or a full commit." },
@@ -2114,16 +2062,13 @@ var init_policy = __esm({
2114
2062
  const out = [];
2115
2063
  for (const k of Object.keys(p)) if (k in NO_SUCH_KEY) out.push(error(`policy.${k}`, `is not a plywood key: ${NO_SUCH_KEY[k]}`));
2116
2064
  if (p.source !== void 0) {
2117
- if (!chantHasPolicySource()) out.push(error("policy.source", SOURCE_NEEDS_CHANT));
2118
- else if (!isObject(p.source)) out.push(error("policy.source", "is { repo, ref, path }"));
2119
- else {
2120
- const src = p.source;
2121
- out.push(...onlyKeys(src, SOURCE_KEYS, "policy.source"));
2122
- if (typeof src.repo !== "string" || !SOURCE_REPO.test(src.repo) || src.repo.startsWith("-")) out.push(error("policy.source.repo", "is a git URL: https://, ssh://, file:// or git@<host>:<path>"));
2123
- if (typeof src.ref !== "string" || !SOURCE_REF.test(src.ref) || src.ref.includes("..") || src.ref.endsWith("/")) out.push(error("policy.source.ref", "is a tag, a branch or a commit"));
2124
- if (src.path !== void 0) out.push(...repoPath(src.path, "policy.source.path"));
2125
- if (p.rego !== void 0) out.push(error("policy.rego", "goes with no policy.source: with a source, policy.source.path names the Rego directory inside it"));
2065
+ try {
2066
+ assertGatePolicySource("policy.source", p.source);
2067
+ } catch (e) {
2068
+ const m = /^(\S+) (.*)$/.exec(e.message);
2069
+ out.push(error(m?.[1] ?? "policy.source", m?.[2] ?? e.message));
2126
2070
  }
2071
+ if (p.rego !== void 0) out.push(error("policy.rego", "goes with no policy.source: with a source, policy.source.path names the Rego directory inside it"));
2127
2072
  }
2128
2073
  out.push(...onlyKeys(Object.fromEntries(Object.entries(p).filter(([k]) => !(k in NO_SUCH_KEY))), POLICY_KEYS, "policy"));
2129
2074
  if (p.rego !== void 0) out.push(...repoPath(p.rego, "policy.rego"));
@@ -2173,6 +2118,7 @@ var DEFAULT_REVIEW_STACK_NAME, TTL, REVIEW_STACK_NAME, STACK_KEY, reviewStacks;
2173
2118
  var init_review_stacks = __esm({
2174
2119
  "src/config/review-stacks.ts"() {
2175
2120
  "use strict";
2121
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2176
2122
  init_types();
2177
2123
  DEFAULT_REVIEW_STACK_NAME = "pr-{number}";
2178
2124
  TTL = /^\d+[mhd]$/;
@@ -2227,6 +2173,7 @@ var FORGES, GLOB_MAP, copy, routing;
2227
2173
  var init_routing = __esm({
2228
2174
  "src/config/routing.ts"() {
2229
2175
  "use strict";
2176
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2230
2177
  init_types();
2231
2178
  FORGES = ["github", "gitlab", "forgejo"];
2232
2179
  GLOB_MAP = {
@@ -2322,6 +2269,7 @@ var GROUPS, CONFIG_NAMES, ConfigError;
2322
2269
  var init_config = __esm({
2323
2270
  "src/config/index.ts"() {
2324
2271
  "use strict";
2272
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2325
2273
  init_agents();
2326
2274
  init_apply();
2327
2275
  init_approval();
@@ -2362,6 +2310,7 @@ function toChantSettings(config2) {
2362
2310
  var init_chant_config = __esm({
2363
2311
  "src/chant-config.ts"() {
2364
2312
  "use strict";
2313
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2365
2314
  init_config();
2366
2315
  }
2367
2316
  });
@@ -2385,25 +2334,25 @@ function configCheck(cwd, file) {
2385
2334
  const lines2 = problems.map((p) => `${path}: ${p.key} ${p.message}${p.severity === "warning" ? " (warning)" : ""}`);
2386
2335
  if (errors.length) return { ok: false, lines: lines2, envelope: { file: path, problems } };
2387
2336
  const doc = config2.doc;
2388
- const engine2 = engineOf(doc);
2337
+ const engine3 = engineOf(doc);
2389
2338
  const locks2 = locksOf(doc);
2390
2339
  const later2 = Object.keys(doc).filter((k) => k in LATER_KEYS);
2391
2340
  const settings = {
2392
- engine: engine2?.engine ?? null,
2341
+ engine: engine3?.engine ?? null,
2393
2342
  applyWhen: applyWhenOf(doc),
2394
2343
  gate: gateOf(doc),
2395
2344
  approval: approvalOf(doc),
2396
2345
  locksWhen: locks2.when,
2397
- lockGrain: lockGrainFor(doc, engine2?.engine)
2346
+ lockGrain: lockGrainFor(doc, engine3?.engine)
2398
2347
  };
2399
2348
  lines2.push(
2400
2349
  `${path}: ok`,
2401
- ` engine ${engine2 ? `${engine2.engine} (${engine2.because})` : "not set here; chant reads PULUMI_BACKEND_URL, then Pulumi.yaml"}`,
2350
+ ` engine ${engine3 ? `${engine3.engine} (${engine3.because})` : "not set here; chant reads PULUMI_BACKEND_URL, then Pulumi.yaml"}`,
2402
2351
  ` apply.when ${settings.applyWhen}`,
2403
2352
  ` gate ${settings.gate}`,
2404
2353
  ` approval ${settings.approval}`,
2405
2354
  ` locks.when ${locks2.when} (ci.locks ${CI_LOCKS_OF[locks2.when]})`,
2406
- ` locks.grain ${locks2.grain ? `${locks2.grain} (ci.lockGrain ${LOCK_GRAIN_OF[locks2.grain]})` : LOCK_GRAIN_WORDS.perEngine(engine2?.engine)}`,
2355
+ ` locks.grain ${locks2.grain ? `${locks2.grain} (ci.lockGrain ${LOCK_GRAIN_OF[locks2.grain]})` : LOCK_GRAIN_WORDS.perEngine(engine3?.engine)}`,
2407
2356
  ` policy ${policyLine(doc)}`,
2408
2357
  ...later2.length ? [` read as written, not yet passed to chant: ${later2.join(", ")}`] : []
2409
2358
  );
@@ -2424,6 +2373,7 @@ function policyLine(doc) {
2424
2373
  var init_config_check = __esm({
2425
2374
  "src/config-check.ts"() {
2426
2375
  "use strict";
2376
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2427
2377
  init_chant_config();
2428
2378
  init_apply();
2429
2379
  init_approval();
@@ -2438,315 +2388,43 @@ var init_config_check = __esm({
2438
2388
  });
2439
2389
 
2440
2390
  // src/chant-product.ts
2441
- function hasChant4620(chant) {
2442
- return typeof chant.prCommentPreviewJob === "function" && typeof chant.PR_PREVIEW_FLOOR === "string";
2443
- }
2444
- function hasChant4607(chant) {
2445
- return typeof chant.pendingGatesByPlan === "function" && typeof chant.parseRequiresList === "function";
2446
- }
2447
- function needsChantMessage(what) {
2448
- return `${what} needs chant >= the release after 0.124.54, which carries @intentius/chant/ci-product (chant#4550). This plywood was built with chant ${BUILT_CHANT}, which lacks it. Upgrade plywood once that chant is out, or build against a local chant with 'just chant-local'.`;
2449
- }
2450
- function isChantProduct(m) {
2451
- if (!m || typeof m !== "object") return false;
2452
- const r = m;
2453
- return REQUIRED2.every((name) => typeof r[name] === "function");
2454
- }
2455
2391
  function chantProduct() {
2456
- cached2 ??= (async () => {
2457
- try {
2458
- const m = await import("@intentius/chant/ci-product");
2459
- return isChantProduct(m) ? m : void 0;
2460
- } catch {
2461
- return void 0;
2462
- }
2463
- })();
2464
- return cached2;
2392
+ cached ??= import("@intentius/chant/ci-product").then((m) => m);
2393
+ return cached;
2465
2394
  }
2466
- var REQUIRED2, BUILT_CHANT, cached2;
2395
+ var cached;
2467
2396
  var init_chant_product = __esm({
2468
2397
  "src/chant-product.ts"() {
2469
2398
  "use strict";
2470
- REQUIRED2 = ["assertProductWords", "productPipelineJobs", "approveLine", "runChantInProcess"];
2471
- BUILT_CHANT = true ? "0.124.67" : "unknown";
2472
- }
2473
- });
2474
-
2475
- // src/chant-next.ts
2476
- function hasChantRespond(chant) {
2477
- return typeof chant?.CI_RESPOND_FLOOR === "string";
2478
- }
2479
- function hasChantPrLoopFlags(chant) {
2480
- return typeof chant?.PR_LOOP_BUILD_FLAGS_FLOOR === "string";
2481
- }
2482
- function hasChantApprovalStatus(chant) {
2483
- return typeof chant?.APPROVAL_STATUS_FLOOR === "string";
2484
- }
2485
- function hasChantPullRequestApprovalStatus(chant) {
2486
- return typeof chant?.APPROVAL_STATUS_PULL_REQUEST_FLOOR === "string";
2487
- }
2488
- function hasChantWavePreview() {
2489
- wavePreview ??= (async () => {
2490
- try {
2491
- const m = await import("@intentius/chant/op");
2492
- return typeof m.OP_WAVES_PREVIEW_FLOOR === "string";
2493
- } catch {
2494
- return false;
2495
- }
2496
- })();
2497
- return wavePreview;
2498
- }
2499
- function withWavePreview(spec, forge) {
2500
- const approvals = spec.approvals && typeof spec.approvals === "object" && !Array.isArray(spec.approvals) ? spec.approvals : {};
2501
- const next = approvals.preview !== void 0 ? spec : { ...spec, approvals: { ...approvals, preview: true } };
2502
- if (forge === "gitlab" || next.approvals.preview !== true || !Array.isArray(next.waves)) return next;
2503
- const waves = next.waves.map((w) => ({
2504
- ...w,
2505
- variables: { [WAVE_TOKEN_VARIABLE]: "${{ github.token }}", ...w.variables ?? {} }
2506
- }));
2507
- return { ...next, waves };
2508
- }
2509
- function chantAudit() {
2510
- audit ??= import("@intentius/chant/ci-audit").then((m) => m);
2511
- return audit;
2512
- }
2513
- var wavePreview, WAVE_TOKEN_VARIABLE, GITLAB_READ_TOKEN, audit;
2514
- var init_chant_next = __esm({
2515
- "src/chant-next.ts"() {
2516
- "use strict";
2517
- WAVE_TOKEN_VARIABLE = "GITHUB_TOKEN";
2518
- GITLAB_READ_TOKEN = "CHANT_FORGE_TOKEN";
2519
- }
2520
- });
2521
-
2522
- // src/init/own-jobs.ts
2523
- function indentOf(line) {
2524
- return line.length - line.trimStart().length;
2525
- }
2526
- function isContent(line) {
2527
- const t = line.trim();
2528
- return t !== "" && !t.startsWith("#");
2529
- }
2530
- function jobBlocks(text, forge) {
2531
- const lines2 = text.split("\n");
2532
- const out = /* @__PURE__ */ new Map();
2533
- let start = 0;
2534
- let end = lines2.length;
2535
- let depth = 0;
2536
- if (forge !== "gitlab") {
2537
- const at = lines2.findIndex((l) => /^jobs:\s*(#.*)?$/.test(l));
2538
- if (at < 0) return out;
2539
- start = at + 1;
2540
- const next = lines2.findIndex((l, i) => i > at && indentOf(l) === 0 && isContent(l));
2541
- end = next < 0 ? lines2.length : next;
2542
- depth = 2;
2543
- }
2544
- let name;
2545
- let body = [];
2546
- const flush = () => {
2547
- if (name !== void 0) {
2548
- while (body.length > 0 && body[body.length - 1].trim() === "") body.pop();
2549
- out.set(name, body.join("\n"));
2550
- }
2551
- name = void 0;
2552
- body = [];
2553
- };
2554
- for (let i = start; i < end; i++) {
2555
- const line = lines2[i];
2556
- if (isContent(line) && indentOf(line) === depth) {
2557
- const m = KEY.exec(line.slice(depth));
2558
- flush();
2559
- if (m) {
2560
- name = m[1].replace(/^["']|["']$/g, "");
2561
- body = [line];
2562
- }
2563
- continue;
2564
- }
2565
- if (isContent(line) && indentOf(line) < depth) {
2566
- flush();
2567
- continue;
2568
- }
2569
- if (name !== void 0) body.push(line);
2570
- }
2571
- flush();
2572
- if (forge === "gitlab") {
2573
- for (const k of [...out.keys()]) if (GITLAB_KEYWORDS.has(k)) out.delete(k);
2574
- }
2575
- return out;
2576
- }
2577
- function ownJobs(previous, generated, forge) {
2578
- const ours = jobBlocks(generated, forge);
2579
- const out = /* @__PURE__ */ new Map();
2580
- for (const [name, block] of jobBlocks(previous, forge)) {
2581
- if (ours.has(name) || PLYWOOD_LINE.test(block)) continue;
2582
- out.set(name, block);
2583
- }
2584
- return out;
2585
- }
2586
- function withOwnJobs(generated, own, forge) {
2587
- if (own.size === 0) return generated;
2588
- const indent = forge === "gitlab" ? "" : " ";
2589
- const block = [`${indent}${OWN_JOBS_LINE}`, ...own.values()].join("\n");
2590
- const lines2 = generated.replace(/\n+$/, "").split("\n");
2591
- if (forge !== "gitlab") {
2592
- const at = lines2.findIndex((l) => /^jobs:\s*(#.*)?$/.test(l));
2593
- if (at >= 0) {
2594
- const next = lines2.findIndex((l, i) => i > at && indentOf(l) === 0 && isContent(l));
2595
- if (next >= 0) {
2596
- let cut = next;
2597
- while (cut > at + 1 && lines2[cut - 1].trim() === "") cut--;
2598
- return [...lines2.slice(0, cut), block, ...lines2.slice(cut)].join("\n") + "\n";
2599
- }
2600
- }
2601
- }
2602
- return [...lines2, block].join("\n") + "\n";
2603
- }
2604
- function keepOwnJobs(previous, generated, forge) {
2605
- if (previous === void 0) return { text: generated, kept: [] };
2606
- const own = ownJobs(previous, generated, forge);
2607
- return { text: withOwnJobs(generated, own, forge), kept: [...own.keys()] };
2608
- }
2609
- var OWN_JOBS_LINE, GITLAB_KEYWORDS, PLYWOOD_LINE, KEY;
2610
- var init_own_jobs = __esm({
2611
- "src/init/own-jobs.ts"() {
2612
- "use strict";
2613
- OWN_JOBS_LINE = "# Your own jobs, kept as they are by plywood init and plywood ci.";
2614
- GITLAB_KEYWORDS = /* @__PURE__ */ new Set(["default", "include", "stages", "variables", "workflow", "image", "services", "cache", "before_script", "after_script", "spec"]);
2615
- PLYWOOD_LINE = /\bplywood\b\s+(components|ci|run|lock|unlock|check|preview|up|drift)\b/;
2616
- KEY = /^([A-Za-z0-9_.-]+|"[^"]+"|'[^']+'):(\s|$)/;
2617
- }
2618
- });
2619
-
2620
- // src/init/note-control.ts
2621
- import { emitYAML } from "@intentius/chant/yaml";
2622
- function indentOf2(line) {
2623
- return line.length - line.trimStart().length;
2624
- }
2625
- function jobRange(lines2, name, depth) {
2626
- const head = new RegExp(`^${" ".repeat(depth)}${name}:\\s*(#.*)?$`);
2627
- const start = lines2.findIndex((l) => head.test(l));
2628
- if (start < 0) return void 0;
2629
- let end = lines2.length;
2630
- for (let i = start + 1; i < lines2.length; i++) {
2631
- const l = lines2[i];
2632
- if (l.trim() === "" || l.trimStart().startsWith("#")) continue;
2633
- if (indentOf2(l) <= depth) {
2634
- end = i;
2635
- break;
2636
- }
2637
- }
2638
- return [start, end];
2639
- }
2640
- function withNoteControl(text, forge, step) {
2641
- if (!step) return text;
2642
- const lines2 = text.split("\n");
2643
- const range = jobRange(lines2, NOTE_JOB, forge === "gitlab" ? 0 : 2);
2644
- if (!range) return text;
2645
- const [start, end] = range;
2646
- const job = lines2.slice(start, end);
2647
- if (job.some((l) => l.includes(step.stepName)) || job.some((l) => l.includes("CHANT_CONTROL_DIR="))) return text;
2648
- if (forge === "gitlab") {
2649
- const at2 = job.findIndex((l) => /^ {4}- /.test(l) && l.includes("components pr-post"));
2650
- if (at2 < 0) return text;
2651
- const item2 = emitYAML([step.script], 2).replace(/^\n/, "");
2652
- return [...lines2.slice(0, start + at2), ...item2.split("\n"), ...lines2.slice(start + at2)].join("\n");
2653
- }
2654
- const at = job.findIndex((l) => /^ {6}- name: Post the plan note\b/.test(l));
2655
- if (at < 0) return text;
2656
- const item = emitYAML([{ name: step.stepName, if: "always()", ...Object.keys(step.env).length ? { env: step.env } : {}, run: step.script }], 3).replace(/^\n/, "");
2657
- return [...lines2.slice(0, start + at), ...item.split("\n"), ...lines2.slice(start + at)].join("\n");
2658
- }
2659
- var NOTE_JOB;
2660
- var init_note_control = __esm({
2661
- "src/init/note-control.ts"() {
2662
- "use strict";
2663
- NOTE_JOB = "plan-note";
2664
- }
2665
- });
2666
-
2667
- // src/init/run-group.ts
2668
- function withRunGroup(text, forge, key) {
2669
- if (forge !== "forgejo" || TOP_CONCURRENCY.test(text)) return text;
2670
- const jobs2 = TOP_JOBS.exec(text);
2671
- if (!jobs2) return text;
2672
- const name = (TOP_NAME.exec(text)?.[1] ?? "plywood").replace(/^["']|["']$/g, "");
2673
- const group = key === "commit" ? `${name}-\${{ github.event_name }}-\${{ github.sha }}` : `${name}-\${{ github.event_name == 'pull_request' && github.ref || github.run_id }}`;
2674
- const cancel = key === "commit" ? "false" : "${{ github.event_name == 'pull_request' }}";
2675
- const block = [
2676
- "# Without a group Forgejo cancels the run in progress when a push starts another run of this workflow.",
2677
- key === "commit" ? "# One group for each commit keeps a merge's wave jobs running to completion when another merge lands (plywood#51)." : "# A push to main and a comment get a group of their own, so the record job runs to completion (plywood#51).",
2678
- "concurrency:",
2679
- ` group: ${yamlScalar(group)}`,
2680
- ` cancel-in-progress: ${yamlScalar(cancel)}`,
2681
- "",
2682
- ""
2683
- ].join("\n");
2684
- return text.slice(0, jobs2.index) + block + text.slice(jobs2.index);
2685
- }
2686
- function yamlScalar(value) {
2687
- return value === "false" ? value : JSON.stringify(value);
2688
- }
2689
- var TOP_CONCURRENCY, TOP_JOBS, TOP_NAME;
2690
- var init_run_group = __esm({
2691
- "src/init/run-group.ts"() {
2692
- "use strict";
2693
- TOP_CONCURRENCY = /^concurrency:/m;
2694
- TOP_JOBS = /^jobs:/m;
2695
- TOP_NAME = /^name:\s*(.+?)\s*$/m;
2399
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2696
2400
  }
2697
2401
  });
2698
2402
 
2699
2403
  // src/init/workflow.ts
2700
2404
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2701
2405
  import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
2406
+ import { jobBlocks, ownJobsOf } from "@intentius/chant/ci-own-jobs";
2702
2407
  async function writeWorkflow(chant, input) {
2703
2408
  const file = isAbsolute(input.output) ? input.output : join2(input.root, input.output);
2704
2409
  const previous = existsSync2(file) ? readFileSync2(file, "utf8") : void 0;
2705
2410
  mkdirSync(dirname2(file), { recursive: true });
2706
- const code = await chant.runChantInProcess([...input.argv, "--output", file], input.words);
2411
+ const code = await chant.runChantInProcess([...input.argv, "--keep-own-jobs", "--run-group", "pull-request", "--output", file], input.words);
2707
2412
  if (code !== 0 || !existsSync2(file)) {
2708
2413
  if (previous !== void 0) writeFileSync(file, previous);
2709
2414
  return { code: code !== 0 ? code : 1, file, status: "failed", kept: [] };
2710
2415
  }
2711
- const generated = withRunGroup(withNoteControl(readFileSync2(file, "utf8"), input.forge, input.noteControl), input.forge, "pull-request");
2712
- const { text, kept } = keepOwnJobs(previous, generated, input.forge);
2713
- writeFileSync(file, text);
2416
+ const text = readFileSync2(file, "utf8");
2417
+ const kept = previous === void 0 ? [] : keptJobs(previous, text, input.forge, input.words.bin);
2714
2418
  return { code, file, status: text === previous ? "unchanged" : "written", kept };
2715
2419
  }
2420
+ function keptJobs(previous, written, forge, bin) {
2421
+ const now = jobBlocks(written, forge);
2422
+ return [...ownJobsOf(previous, "", forge, bin ? [bin] : [])].filter(([name, block]) => now.get(name) === block).map(([name]) => name);
2423
+ }
2716
2424
  var init_workflow = __esm({
2717
2425
  "src/init/workflow.ts"() {
2718
2426
  "use strict";
2719
- init_own_jobs();
2720
- init_note_control();
2721
- init_run_group();
2722
- }
2723
- });
2724
-
2725
- // src/review-stacks.ts
2726
- function chantReviewStacks() {
2727
- cached3 ??= (async () => {
2728
- try {
2729
- const m = await import("@intentius/chant-lexicon-pulumi");
2730
- const plugin = m.pulumiPlugin;
2731
- return typeof m.initReviewStack === "function" && typeof m.removeReviewStack === "function" && typeof plugin?.prEnvTemplate === "function" && typeof plugin?.prEnvTeardown === "function";
2732
- } catch {
2733
- return false;
2734
- }
2735
- })();
2736
- return cached3;
2737
- }
2738
- function reviewSuffixOf(pattern) {
2739
- const m = pattern ? /^pr-\{number\}-([a-z0-9][a-z0-9-]*)$/.exec(pattern) : null;
2740
- return m ? m[1] : void 0;
2741
- }
2742
- var REVIEW_GATE, REVIEW_GATE_WORD, reviewOp, cached3, REVIEW_STACKS_NEED_CHANT;
2743
- var init_review_stacks2 = __esm({
2744
- "src/review-stacks.ts"() {
2745
- "use strict";
2746
- REVIEW_GATE = "pr-env";
2747
- REVIEW_GATE_WORD = "review";
2748
- reviewOp = (pr) => `pr-${pr}`;
2749
- REVIEW_STACKS_NEED_CHANT = "review_stacks is set, and the installed @intentius/chant-lexicon-pulumi cannot run a review stack end to end (initReviewStack, removeReviewStack, and the plugin's prEnvTemplate and prEnvTeardown from chant#4609), so plywood writes no ci.prEnvironment and the workflow has no review stack jobs yet";
2427
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2750
2428
  }
2751
2429
  });
2752
2430
 
@@ -2754,11 +2432,11 @@ var init_review_stacks2 = __esm({
2754
2432
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
2755
2433
  import { dirname as dirname3, join as join3 } from "node:path";
2756
2434
  function wavesEngine(pipeline) {
2757
- const o = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} }, false);
2435
+ const o = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} });
2758
2436
  return { ...o.image ? { image: o.image } : {}, ...o.beforeScript?.length ? { beforeScript: o.beforeScript } : {} };
2759
2437
  }
2760
- function wavesSpecText(spec, engine2 = {}) {
2761
- const options = { ...engine2.image ? { image: engine2.image } : {}, ...engine2.beforeScript?.length ? { beforeScript: engine2.beforeScript } : {} };
2438
+ function wavesSpecText(spec, engine3 = {}) {
2439
+ const options = { ...engine3.image ? { image: engine3.image } : {}, ...engine3.beforeScript?.length ? { beforeScript: engine3.beforeScript } : {} };
2762
2440
  return JSON.stringify(Object.keys(options).length ? { waves: spec, options } : spec, null, 2) + "\n";
2763
2441
  }
2764
2442
  function unwrapWavesSpec(doc) {
@@ -2821,28 +2499,13 @@ function writeUpOp(cwd, spec) {
2821
2499
  writeFileSync2(path, text);
2822
2500
  return true;
2823
2501
  }
2824
- function binLines(waves, bin) {
2825
- const binArgs = bin.split(/\s+/).filter(Boolean);
2826
- return waves.jobs.map((job) => {
2827
- const own = job.command.slice(0, binArgs.length).join(" ") === binArgs.join(" ");
2828
- const rest = own ? job.command.slice(binArgs.length) : job.command.slice(1);
2829
- return { chant: ["chant", ...rest].join(" "), product: [...binArgs, ...rest].join(" ") };
2830
- });
2831
- }
2832
- function withBinLines(text, lines2) {
2833
- const sorted = [...lines2].sort((a, b) => b.chant.length - a.chant.length);
2834
- return text.replace(/chant run wave [^\n"']*/g, (found) => {
2835
- const hit = sorted.find((l) => found.trimEnd() === l.chant);
2836
- return hit ? hit.product : found;
2837
- });
2838
- }
2839
2502
  function generatedFor(dir, specFile) {
2840
2503
  if (!existsSync3(dir)) return [];
2841
2504
  return readdirSync(dir).filter((f) => /\.ya?ml$/.test(f)).map((f) => join3(dir, f)).filter((p) => (readFileSync3(p, "utf8").split("\n", 1)[0] ?? "").includes(`--spec ${specFile}`));
2842
2505
  }
2843
- async function writeWaveJobs(chant, forge, waves, words, table, io, engine2 = {}) {
2506
+ async function writeWaveJobs(chant, forge, waves, words, table, io, engine3 = {}) {
2844
2507
  const specPath = join3(io.cwd, waves.specFile);
2845
- const specText = wavesSpecText(waves.spec, engine2);
2508
+ const specText = wavesSpecText(waves.spec, engine3);
2846
2509
  const before = existsSync3(specPath) ? readFileSync3(specPath, "utf8") : void 0;
2847
2510
  if (before !== specText) {
2848
2511
  mkdirSync2(dirname3(specPath), { recursive: true });
@@ -2851,18 +2514,14 @@ async function writeWaveJobs(chant, forge, waves, words, table, io, engine2 = {}
2851
2514
  }
2852
2515
  if (writeUpOp(io.cwd, waves.spec)) io.err(`${PRODUCT} ci: wrote ${UP_OP_FILE}, the Op the waves spec names; commit it with the workflow`);
2853
2516
  const outDir = WAVES_OUTPUT_DIR[forge];
2854
- const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", waves.specFile, "--output", outDir], words);
2517
+ const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", waves.specFile, "--run-group", "commit", "--output", outDir], words);
2855
2518
  if (code !== table.done) return code;
2856
2519
  const files = generatedFor(join3(io.cwd, outDir), waves.specFile);
2857
2520
  if (files.length === 0) {
2858
2521
  io.err(`${PRODUCT} ci: chant wrote no waves workflow for ${waves.specFile} under ${outDir}`);
2859
2522
  return table.failed;
2860
2523
  }
2861
- const lines2 = hasChant4607(chant) ? void 0 : binLines(waves, words.bin);
2862
2524
  for (const file of files) {
2863
- const text = readFileSync3(file, "utf8");
2864
- const next = withRunGroup(lines2 ? withBinLines(text, lines2) : text, forge, "commit");
2865
- if (next !== text) writeFileSync2(file, next);
2866
2525
  io.err(`${PRODUCT} ci: waves workflow at ${file.slice(io.cwd.length + 1)}`);
2867
2526
  }
2868
2527
  if (forge === "gitlab") {
@@ -2874,9 +2533,8 @@ var WAVES_OUTPUT_DIR, UP_OP_FILE;
2874
2533
  var init_waves_workflow = __esm({
2875
2534
  "src/waves-workflow.ts"() {
2876
2535
  "use strict";
2877
- init_chant_product();
2536
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2878
2537
  init_images();
2879
- init_run_group();
2880
2538
  init_words();
2881
2539
  WAVES_OUTPUT_DIR = {
2882
2540
  github: ".github/workflows",
@@ -2887,12 +2545,34 @@ var init_waves_workflow = __esm({
2887
2545
  }
2888
2546
  });
2889
2547
 
2548
+ // src/chant-next.ts
2549
+ function withWavePreview(spec, forge) {
2550
+ const approvals = spec.approvals && typeof spec.approvals === "object" && !Array.isArray(spec.approvals) ? spec.approvals : {};
2551
+ const next = approvals.preview !== void 0 ? spec : { ...spec, approvals: { ...approvals, preview: true } };
2552
+ if (forge === "gitlab" || next.approvals.preview !== true || !Array.isArray(next.waves)) return next;
2553
+ const waves = next.waves.map((w) => ({
2554
+ ...w,
2555
+ variables: { [WAVE_TOKEN_VARIABLE]: "${{ github.token }}", ...w.variables ?? {} }
2556
+ }));
2557
+ return { ...next, waves };
2558
+ }
2559
+ function chantAudit() {
2560
+ audit ??= import("@intentius/chant/ci-audit").then((m) => m);
2561
+ return audit;
2562
+ }
2563
+ var WAVE_TOKEN_VARIABLE, GITLAB_READ_TOKEN, audit;
2564
+ var init_chant_next = __esm({
2565
+ "src/chant-next.ts"() {
2566
+ "use strict";
2567
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2568
+ WAVE_TOKEN_VARIABLE = "GITHUB_TOKEN";
2569
+ GITLAB_READ_TOKEN = "CHANT_FORGE_TOKEN";
2570
+ }
2571
+ });
2572
+
2890
2573
  // src/agent-jobs.ts
2891
2574
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
2892
2575
  import { dirname as dirname4, join as join4 } from "node:path";
2893
- function hasChantAgent(chant) {
2894
- return typeof chant?.CI_AGENT_FLOOR === "string";
2895
- }
2896
2576
  function stageRunner(runner, stage) {
2897
2577
  if (runner === void 0) return void 0;
2898
2578
  if (typeof runner === "string" || Array.isArray(runner) || "group" in runner) return runner;
@@ -2915,22 +2595,21 @@ function chantAgentOptions(a, doc) {
2915
2595
  };
2916
2596
  }
2917
2597
  function driftOpFields(chant, doc, op, respond) {
2918
- if (!hasChantAgent(chant)) return {};
2919
2598
  const runner = stageRunner(toChantSettings({ doc }).pipeline.runner, "drift");
2920
2599
  const a = agentOf(doc);
2921
2600
  const wantsAgent = respond === "pull-request" && a?.drift === true;
2922
- const agent = wantsAgent ? chant.productDriftAgent ? chant.productDriftAgent(PRODUCT_WORDS, op, chantAgentOptions(a, doc)) : { on: "drift", ...chantAgentOptions(a, doc) } : void 0;
2601
+ const agent = wantsAgent ? chant.productDriftAgent(PRODUCT_WORDS, op, chantAgentOptions(a, doc)) : void 0;
2923
2602
  return { ...runner !== void 0 ? { runner } : {}, ...agent ? { agent } : {} };
2924
2603
  }
2925
2604
  function agentOpSpec(spec, doc) {
2926
2605
  const p = toChantSettings({ doc }).pipeline;
2927
- const engine2 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} }, false);
2606
+ const engine3 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} });
2928
2607
  const runner = stageRunner(p.runner, "preview");
2929
2608
  return {
2930
2609
  ops: [{ ...spec, ...runner !== void 0 && spec.runner === void 0 ? { runner } : {} }],
2931
2610
  options: {
2932
- ...engine2.beforeScript ? { beforeScript: engine2.beforeScript } : {},
2933
- ...engine2.image ? { image: engine2.image } : {}
2611
+ ...engine3.beforeScript ? { beforeScript: engine3.beforeScript } : {},
2612
+ ...engine3.image ? { image: engine3.image } : {}
2934
2613
  }
2935
2614
  };
2936
2615
  }
@@ -2944,10 +2623,6 @@ function writeIfChanged(cwd, file, text) {
2944
2623
  async function writeAgentJobs(chant, forge, env, doc, words, table, io) {
2945
2624
  const a = agentOf(doc);
2946
2625
  if (!a?.comment) return table.done;
2947
- if (!hasChantAgent(chant)) {
2948
- io.err(`${PRODUCT} ci: ${AGENT_NEEDS_CHANT}`);
2949
- return table.done;
2950
- }
2951
2626
  const agentWords = { ...words, commands: { ...words.commands, agent: "agent" } };
2952
2627
  let spec;
2953
2628
  try {
@@ -2973,10 +2648,11 @@ async function writeAgentJobs(chant, forge, env, doc, words, table, io) {
2973
2648
  if (forge === "gitlab") io.err(`${PRODUCT} ci: on GitLab a pipeline of the default branch started with CHANT_AGENT_PR and CHANT_AGENT_ASK runs the agent; scope CHANT_FORGE_TOKEN to environment ${AGENT_OP}-push`);
2974
2649
  return table.done;
2975
2650
  }
2976
- var AGENT_OP, AGENT_SPEC_FILE, AGENT_NEEDS_CHANT, OP_OUTPUT_DIR;
2651
+ var AGENT_OP, AGENT_SPEC_FILE, OP_OUTPUT_DIR;
2977
2652
  var init_agent_jobs = __esm({
2978
2653
  "src/agent-jobs.ts"() {
2979
2654
  "use strict";
2655
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2980
2656
  init_chant_config();
2981
2657
  init_agents();
2982
2658
  init_config();
@@ -2984,7 +2660,6 @@ var init_agent_jobs = __esm({
2984
2660
  init_words();
2985
2661
  AGENT_OP = "plywood-agent";
2986
2662
  AGENT_SPEC_FILE = ".chant/ops/plywood-agent.spec.json";
2987
- AGENT_NEEDS_CHANT = "agent needs a chant with the agent job (chant#4616), and the installed one has none, so no agent job was written";
2988
2663
  OP_OUTPUT_DIR = { github: ".github/workflows", forgejo: ".forgejo/workflows", gitlab: "." };
2989
2664
  }
2990
2665
  });
@@ -3003,7 +2678,6 @@ async function sendDriftNotice(input) {
3003
2678
  const drifted = input.results.filter((r) => r.ok && r.drifted > 0);
3004
2679
  if (drifted.length === 0) return void 0;
3005
2680
  const fingerprint = runFingerprint(input.results, input.stackFingerprint);
3006
- if (!input.chant) return { sent: false, reason: DRIFT_NOTIFY_NEEDS_CHANT, fingerprint };
3007
2681
  if (drifted.every((r) => r.issue?.action === "unchanged")) {
3008
2682
  return { sent: false, reason: "no drift event was sent: every drifted stack's finding is the one its issue already holds", fingerprint };
3009
2683
  }
@@ -3021,62 +2695,39 @@ async function sendDriftNotice(input) {
3021
2695
  }
3022
2696
  function chantDriftNotify() {
3023
2697
  loaded ??= (async () => {
3024
- try {
3025
- const product = await import("@intentius/chant/ci-product");
3026
- const notify2 = await import("@intentius/chant/notify");
3027
- const fns = [product.driftNotifyFields, product.productSendOptions, notify2.makeNotifyEvent, notify2.sendNotify];
3028
- if (!fns.every((f) => typeof f === "function")) return void 0;
3029
- return {
3030
- driftNotifyFields: product.driftNotifyFields,
3031
- productSendOptions: product.productSendOptions,
3032
- makeNotifyEvent: notify2.makeNotifyEvent,
3033
- sendNotify: notify2.sendNotify
3034
- };
3035
- } catch {
3036
- return void 0;
3037
- }
2698
+ const product = await import("@intentius/chant/ci-product");
2699
+ const notify2 = await import("@intentius/chant/notify");
2700
+ return {
2701
+ driftNotifyFields: product.driftNotifyFields,
2702
+ productSendOptions: product.productSendOptions,
2703
+ makeNotifyEvent: notify2.makeNotifyEvent,
2704
+ sendNotify: notify2.sendNotify
2705
+ };
3038
2706
  })();
3039
2707
  return loaded;
3040
2708
  }
3041
- var DRIFT_NOTIFY_NEEDS_CHANT, loaded;
2709
+ var loaded;
3042
2710
  var init_drift_notify = __esm({
3043
2711
  "src/drift-notify.ts"() {
3044
2712
  "use strict";
3045
- DRIFT_NOTIFY_NEEDS_CHANT = "no drift event was sent: the installed chant has no driftNotifyFields (chant#4610)";
2713
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3046
2714
  }
3047
2715
  });
3048
2716
 
3049
2717
  // src/jobs.ts
3050
2718
  var jobs_exports = {};
3051
2719
  __export(jobs_exports, {
3052
- JOBS_NEED_CHANT: () => JOBS_NEED_CHANT,
3053
2720
  environmentFor: () => environmentFor,
3054
2721
  groupName: () => groupName,
3055
- hasChant4625: () => hasChant4625,
3056
2722
  jobVariables: () => jobVariables,
3057
2723
  prJobsFor: () => prJobsFor,
3058
2724
  prLoopJobFlags: () => prLoopJobFlags,
3059
2725
  prLoopJobOptions: () => prLoopJobOptions,
3060
2726
  prLoopRunners: () => prLoopRunners,
3061
2727
  secretReference: () => secretReference,
3062
- setChant4625ForTests: () => setChant4625ForTests,
3063
2728
  wantsChant4625: () => wantsChant4625,
3064
2729
  withJobSettings: () => withJobSettings
3065
2730
  });
3066
- function hasChant4625() {
3067
- detected ??= (async () => {
3068
- try {
3069
- const m = await import("@intentius/chant/parallelism");
3070
- return typeof m.mapLimited === "function" && typeof m.parallelismProblem === "function";
3071
- } catch {
3072
- return false;
3073
- }
3074
- })();
3075
- return detected;
3076
- }
3077
- function setChant4625ForTests(has) {
3078
- detected = has === "reset" ? void 0 : Promise.resolve(has);
3079
- }
3080
2731
  function secretReference(forge, secret) {
3081
2732
  return forge === "gitlab" ? `$${secret}` : `\${{ secrets.${secret} }}`;
3082
2733
  }
@@ -3099,7 +2750,7 @@ function groupName(environment, taken) {
3099
2750
  taken.add(name);
3100
2751
  return name;
3101
2752
  }
3102
- function withJobSettings(spec, s, forge, opts) {
2753
+ function withJobSettings(spec, s, forge) {
3103
2754
  const variables = jobVariables(forge, s);
3104
2755
  const waves = (Array.isArray(spec.waves) ? spec.waves : []).map((wave) => {
3105
2756
  const out = { ...wave };
@@ -3128,7 +2779,7 @@ function withJobSettings(spec, s, forge, opts) {
3128
2779
  out.groups = [...byEnv.values()].map((g) => ({ name: groupName(g.environment, taken), runs: g.runs, ...g.environment ? { environment: { ...g.environment } } : {} }));
3129
2780
  return out;
3130
2781
  });
3131
- return { ...spec, waves, ...opts.parallelism && s.parallelism !== void 0 && spec.step !== void 0 ? { parallelism: s.parallelism } : {} };
2782
+ return { ...spec, waves, ...s.parallelism !== void 0 && spec.step !== void 0 ? { parallelism: s.parallelism } : {} };
3132
2783
  }
3133
2784
  function prLoopRunners(runner) {
3134
2785
  if (runner === void 0) return void 0;
@@ -3164,24 +2815,15 @@ function wantsChant4625(s, runner) {
3164
2815
  }
3165
2816
  async function prJobsFor(pipeline) {
3166
2817
  if (!wantsChant4625(pipeline.jobs, pipeline.runner)) return { options: {}, flags: [], notes: [] };
3167
- if (!await hasChant4625()) {
3168
- const asked = [
3169
- ...pipeline.runner !== void 0 ? ["runners"] : [],
3170
- ...pipeline.jobs && Object.keys(pipeline.jobs.variables).length ? ["env"] : [],
3171
- ...pipeline.jobs && Object.keys(pipeline.jobs.secrets).length ? ["pass"] : [],
3172
- ...pipeline.jobs?.parallelism !== void 0 ? ["parallelism"] : []
3173
- ];
3174
- return { options: {}, flags: [], notes: [`${asked.join(", ")} on the pull request jobs ${JOBS_NEED_CHANT}; the waves and drift jobs have ${asked.filter((k) => k !== "parallelism").join(", ") || "none of them"}`] };
3175
- }
3176
2818
  const { flags, notes } = prLoopJobFlags(pipeline.jobs, pipeline.runner);
3177
2819
  return { options: prLoopJobOptions(pipeline.jobs, pipeline.runner), flags, notes };
3178
2820
  }
3179
- var detected, JOBS_NEED_CHANT, GROUP_NAME;
2821
+ var GROUP_NAME;
3180
2822
  var init_jobs2 = __esm({
3181
2823
  "src/jobs.ts"() {
3182
2824
  "use strict";
2825
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3183
2826
  init_images2();
3184
- JOBS_NEED_CHANT = "needs chant's runners, variables, passed secrets and parallelism on the pull request jobs (chant#4625), which this chant lacks";
3185
2827
  GROUP_NAME = /[^a-z0-9_-]+/g;
3186
2828
  }
3187
2829
  });
@@ -3399,6 +3041,7 @@ var SKIP, MAX_DEPTH, ENV_ORDER, PROD_RANK, WORKFLOW_DIRS;
3399
3041
  var init_detect = __esm({
3400
3042
  "src/init/detect.ts"() {
3401
3043
  "use strict";
3044
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3402
3045
  SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor", "venv", ".venv", "__pycache__", "target", "bin", "obj"]);
3403
3046
  MAX_DEPTH = 8;
3404
3047
  ENV_ORDER = ["local", "dev", "development", "sandbox", "test", "testing", "qa", "ci", "stage", "staging", "preprod", "uat", "prod", "production"];
@@ -3418,6 +3061,7 @@ function fill(text, values) {
3418
3061
  var init_templates = __esm({
3419
3062
  "src/templates.ts"() {
3420
3063
  "use strict";
3064
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3421
3065
  }
3422
3066
  });
3423
3067
 
@@ -3547,6 +3191,7 @@ var WAVE_NAME, STRICTNESS, MEMBER_NAME, PULUMI_LEXICON;
3547
3191
  var init_render = __esm({
3548
3192
  "src/init/render.ts"() {
3549
3193
  "use strict";
3194
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3550
3195
  init_templates();
3551
3196
  init_words();
3552
3197
  init_detect();
@@ -3600,11 +3245,28 @@ var REFUSALS;
3600
3245
  var init_refusals = __esm({
3601
3246
  "src/refusals.ts"() {
3602
3247
  "use strict";
3248
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3603
3249
  init_words();
3604
3250
  REFUSALS = ["waiting", "moved", "held", "superseded", "denied", "declined", "misplaced"];
3605
3251
  }
3606
3252
  });
3607
3253
 
3254
+ // src/review-stacks.ts
3255
+ function reviewSuffixOf(pattern) {
3256
+ const m = pattern ? /^pr-\{number\}-([a-z0-9][a-z0-9-]*)$/.exec(pattern) : null;
3257
+ return m ? m[1] : void 0;
3258
+ }
3259
+ var REVIEW_GATE, REVIEW_GATE_WORD, reviewOp;
3260
+ var init_review_stacks2 = __esm({
3261
+ "src/review-stacks.ts"() {
3262
+ "use strict";
3263
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3264
+ REVIEW_GATE = "pr-env";
3265
+ REVIEW_GATE_WORD = "review";
3266
+ reviewOp = (pr) => `pr-${pr}`;
3267
+ }
3268
+ });
3269
+
3608
3270
  // src/up.ts
3609
3271
  import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
3610
3272
  import { join as join6 } from "node:path";
@@ -3714,8 +3376,7 @@ function approvalStatusTarget(opts, doc, env) {
3714
3376
  return { ...forge ? { forge } : {}, ...branch ? { branch } : {} };
3715
3377
  }
3716
3378
  async function refreshApprovalStatus(chant, spec, opts, io) {
3717
- if (!hasChantPullRequestApprovalStatus(chant)) return;
3718
- const posts = chant.opWavesPostsApprovalStatus ? chant.opWavesPostsApprovalStatus(spec) : wavesTakePullRequestApproval(spec);
3379
+ const posts = chant.opWavesPostsApprovalStatus(spec);
3719
3380
  if (!posts) return;
3720
3381
  const path = findConfig(io.cwd);
3721
3382
  const doc = path ? loadConfig(path).doc : void 0;
@@ -3731,7 +3392,6 @@ async function refreshApprovalStatus(chant, spec, opts, io) {
3731
3392
  }
3732
3393
  async function upTargetByPlan(chant, digest, opts = {}) {
3733
3394
  const needsPr = `the ${PR_GATE} gate is a pull request's: name it with --pr <n> (or PR_NUMBER), or approve pr-<n>`;
3734
- if (!hasChant4607(chant)) return { error: `${needsPr}. The installed chant cannot find a pull request from a plan digest (chant#4607)` };
3735
3395
  let gates;
3736
3396
  try {
3737
3397
  gates = await chant.pendingGatesByPlan(digest, { pullRequestsOnly: true, ...opts.cwd ? { cwd: opts.cwd } : {} });
@@ -3861,8 +3521,6 @@ async function upBeforeMerge(number, args, json, table, io) {
3861
3521
  const forge = forgeFlag.value ?? configured ?? "github";
3862
3522
  if (!FORGES.includes(forge)) return usage(`--forge ${forge} is not one of ${FORGES.join(", ")}`, table, io);
3863
3523
  return withChant(`/${PRODUCT} up`, table, io, async (chant) => {
3864
- const note = requiresNotHonoured(chant, config2.doc);
3865
- if (note) io.err(`${PRODUCT}: ${note}`);
3866
3524
  const argv = [
3867
3525
  "components",
3868
3526
  "pr-apply",
@@ -3912,12 +3570,11 @@ var OVERRIDE_OP, WAVES_SPEC_FILE, UP_STEP_KIND, PR_OP, APPROVE_PASS, APPROVE_SWI
3912
3570
  var init_up = __esm({
3913
3571
  "src/up.ts"() {
3914
3572
  "use strict";
3573
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3915
3574
  init_apply();
3916
- init_chant_next();
3917
3575
  init_config();
3918
3576
  init_routing();
3919
3577
  init_refusals();
3920
- init_chant_product();
3921
3578
  init_review_stacks2();
3922
3579
  init_stages();
3923
3580
  init_words();
@@ -4026,9 +3683,7 @@ async function drift2(args, json, table, io) {
4026
3683
  }
4027
3684
  }
4028
3685
  if (respond === "pull-request" && results.some((r) => r.drifted > 0)) {
4029
- io.err(
4030
- hasChantAgent(chant) ? `${PRODUCT} drift: respond.drift is pull-request; in CI the agent job after this one opens or updates the pull request (agent.drift)` : `${PRODUCT} drift: respond.drift is pull-request; this chant has no agent job (chant#4616), so this run kept the issue only`
4031
- );
3686
+ io.err(`${PRODUCT} drift: respond.drift is pull-request; in CI the agent job after this one opens or updates the pull request (agent.drift)`);
4032
3687
  }
4033
3688
  const notice = await driftNotice(chant, doc, results, envFlag.value, io);
4034
3689
  const drifted = results.filter((r) => r.drifted > 0).map((r) => r.stack);
@@ -4066,12 +3721,12 @@ async function drift2(args, json, table, io) {
4066
3721
  }
4067
3722
  async function driftNotice(chant, doc, results, environment, io) {
4068
3723
  const notify2 = chantNotifyOf(doc);
4069
- const loaded2 = notifiesDrift(notify2) ? await chantDriftNotify() : void 0;
3724
+ if (!notifiesDrift(notify2)) return void 0;
4070
3725
  return sendDriftNotice({
4071
3726
  notify: notify2,
4072
3727
  results,
4073
3728
  stackFingerprint: driftFingerprint,
4074
- chant: loaded2 ? { ...loaded2, words: chant.assertProductWords(PRODUCT_WORDS) } : void 0,
3729
+ chant: { ...await chantDriftNotify(), words: chant.assertProductWords(PRODUCT_WORDS) },
4075
3730
  env: io.env,
4076
3731
  ...environment ? { environment } : {},
4077
3732
  log: io.err,
@@ -4186,20 +3841,19 @@ function driftOpFile(doc, stacks2, schedule) {
4186
3841
  }
4187
3842
  function driftOpSpec(doc, forge, stacks2, schedule, chant, opts = {}) {
4188
3843
  const respond = driftResponseOf(doc);
4189
- const agentJob = hasChantAgent(chant) && respond === "pull-request";
4190
- const findingMode = respond === "none" ? "report" : respond === "pull-request" && !agentJob ? forge === "gitlab" ? "merge-request" : "pull-request" : "issue";
3844
+ const findingMode = respond === "none" ? "report" : "issue";
4191
3845
  const pipeline = toChantSettings({ doc }).pipeline;
4192
- const engine2 = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} }, false);
3846
+ const engine3 = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} });
4193
3847
  return {
4194
- ops: stacks2.map((s) => ({ name: driftOpName(s), trigger: { kind: "cron", schedule }, findingMode, ...driftOpFields(chant, doc, driftOpName(s), respond), ...withNotifyVariables(driftJobFields(forge, pipeline.jobs, s), driftNotifyVariables(forge, chantNotifyOf(doc))), ...opts.cloud ? driftCloudFields(doc, forge, s, respond, opts.env ?? "production", { contentsWrite: opts.contentsWrite === true }) : {} })),
3848
+ ops: stacks2.map((s) => ({ name: driftOpName(s), trigger: { kind: "cron", schedule }, findingMode, ...driftOpFields(chant, doc, driftOpName(s), respond), ...withNotifyVariables(driftJobFields(forge, pipeline.jobs, s), driftNotifyVariables(forge, chantNotifyOf(doc))), ...opts.cloud ? driftCloudFields(doc, forge, s, respond, opts.env ?? "production") : {} })),
4195
3849
  options: {
4196
3850
  runCommand: [...BIN.split(/\s+/), "drift", "--op", "{name}", "--forge", forge],
4197
- beforeScript: [SNAPSHOTS_FETCH, ...engine2.beforeScript ?? []],
4198
- ...engine2.image ? { image: engine2.image } : {}
3851
+ beforeScript: [SNAPSHOTS_FETCH, ...engine3.beforeScript ?? []],
3852
+ ...engine3.image ? { image: engine3.image } : {}
4199
3853
  }
4200
3854
  };
4201
3855
  }
4202
- function driftCloudFields(doc, forge, stack, respond, env, opts = {}) {
3856
+ function driftCloudFields(doc, forge, stack, respond, env) {
4203
3857
  const settings = toChantSettings({ doc });
4204
3858
  const decl = settings.ci.identity?.cloud;
4205
3859
  const ids = decl ? resolveTargetCloudIdentity(decl, env, stack, stack) : void 0;
@@ -4208,7 +3862,7 @@ function driftCloudFields(doc, forge, stack, respond, env, opts = {}) {
4208
3862
  const waves = wavesHolding(doc, [stack]);
4209
3863
  const command = waves === "all" ? `${BIN} up` : waves.map((k) => `${BIN} up --wave ${k}`).join(" && ");
4210
3864
  const fields = driftJobFields(forge, settings.pipeline.jobs, stack);
4211
- const permissions = opts.contentsWrite && forge !== "gitlab" ? { permissions: { contents: "write" } } : {};
3865
+ const permissions = forge !== "gitlab" ? { permissions: { contents: "write" } } : {};
4212
3866
  out.update = { command, ...ids?.apply ? { cloud: ids.apply } : {}, ...fields, ...permissions };
4213
3867
  return out;
4214
3868
  }
@@ -4242,7 +3896,7 @@ async function writeDriftJobs(chant, forge, doc, words, table, io, env = "produc
4242
3896
  return table.done;
4243
3897
  }
4244
3898
  if (writeIfChanged2(io.cwd, DRIFT_OP_FILE, driftOpFile(doc, settings.stacks, settings.schedule))) io.err(`${PRODUCT} ci: wrote ${DRIFT_OP_FILE}; commit it with the workflow`);
4245
- if (writeIfChanged2(io.cwd, DRIFT_SPEC_FILE, JSON.stringify(driftOpSpec(doc, forge, settings.stacks, settings.schedule, chant, { cloud: true, contentsWrite: true, env }), null, 2) + "\n")) io.err(`${PRODUCT} ci: wrote ${DRIFT_SPEC_FILE}`);
3899
+ if (writeIfChanged2(io.cwd, DRIFT_SPEC_FILE, JSON.stringify(driftOpSpec(doc, forge, settings.stacks, settings.schedule, chant, { cloud: true, env }), null, 2) + "\n")) io.err(`${PRODUCT} ci: wrote ${DRIFT_SPEC_FILE}`);
4246
3900
  const outDir = OP_OUTPUT_DIR2[forge];
4247
3901
  const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", DRIFT_SPEC_FILE, "--output", outDir], words);
4248
3902
  if (code !== table.done) return code;
@@ -4256,6 +3910,7 @@ var DRIFT_OP_FILE, DRIFT_SPEC_FILE, SNAPSHOTS_REF, SNAPSHOTS_FETCH, DRIFT_STATUS
4256
3910
  var init_drift2 = __esm({
4257
3911
  "src/drift.ts"() {
4258
3912
  "use strict";
3913
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4259
3914
  init_chant_next();
4260
3915
  init_images2();
4261
3916
  init_chant_config();
@@ -4293,6 +3948,7 @@ var TIPS;
4293
3948
  var init_tips = __esm({
4294
3949
  "src/tips.ts"() {
4295
3950
  "use strict";
3951
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4296
3952
  TIPS = [
4297
3953
  {
4298
3954
  id: "rename-without-aliases",
@@ -4409,9 +4065,6 @@ async function mcp(args, _json, table, io) {
4409
4065
  if (report.missing) return usage("--report-url takes a URL", table, io);
4410
4066
  if (report.rest.length) return usage(`mcp does not take ${report.rest.join(" ")}`, table, io);
4411
4067
  return withChant(`${PRODUCT} mcp`, table, io, (chant) => {
4412
- if (typeof chant.PRODUCT_MCP_HOOK_FLOOR !== "string") {
4413
- io.err(`${PRODUCT} mcp: this chant has no product tool hook (chant#4555), so the server serves chant's read tools and none of ${PRODUCT}'s`);
4414
- }
4415
4068
  const argv = ["serve", "mcp", "--read-only", ...propose ? ["--propose"] : []];
4416
4069
  return chant.runChantInProcess(argv, PRODUCT_WORDS, { mcp: plywoodMcpHook(report.value, propose) });
4417
4070
  });
@@ -4447,7 +4100,7 @@ function reviewOpFile(doc) {
4447
4100
  function reviewOpSpec(doc) {
4448
4101
  const settings = toChantSettings({ doc });
4449
4102
  const p = settings.pipeline;
4450
- const engine2 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} }, false);
4103
+ const engine3 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} });
4451
4104
  return {
4452
4105
  ops: [
4453
4106
  {
@@ -4459,8 +4112,8 @@ function reviewOpSpec(doc) {
4459
4112
  }
4460
4113
  ],
4461
4114
  options: {
4462
- ...engine2.beforeScript ? { beforeScript: engine2.beforeScript } : {},
4463
- ...engine2.image ? { image: engine2.image } : {}
4115
+ ...engine3.beforeScript ? { beforeScript: engine3.beforeScript } : {},
4116
+ ...engine3.image ? { image: engine3.image } : {}
4464
4117
  }
4465
4118
  };
4466
4119
  }
@@ -4490,6 +4143,7 @@ var PERSON_VERBS, READ_INSTRUCTIONS, REVIEW_OP, REVIEW_OP_FILE, REVIEW_SPEC_FILE
4490
4143
  var init_agents2 = __esm({
4491
4144
  "src/agents.ts"() {
4492
4145
  "use strict";
4146
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4493
4147
  init_chant_config();
4494
4148
  init_config_check();
4495
4149
  init_agents();
@@ -4526,6 +4180,7 @@ var PROVIDERS_PACK_DIR, PROVIDERS_PACK_CONFIG, PROVIDERS_REGO_FILE, PROVIDERS_PO
4526
4180
  var init_policy_presets = __esm({
4527
4181
  "src/policy-presets.ts"() {
4528
4182
  "use strict";
4183
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4529
4184
  PROVIDERS_PACK_DIR = "policy/plywood-providers";
4530
4185
  PROVIDERS_PACK_CONFIG = "policy/plywood-providers.json";
4531
4186
  PROVIDERS_REGO_FILE = "plywood_providers.rego";
@@ -4607,19 +4262,9 @@ function bucketTarget(url, env) {
4607
4262
  }
4608
4263
  return void 0;
4609
4264
  }
4610
- function needsBucketCheckMessage(builtChant) {
4611
- return `the bucket check needs chant >= the release after 0.124.54, which carries @intentius/chant/bucket-check (chant#4558). This plywood was built with chant ${builtChant}, which lacks it, so the bucket was not checked.`;
4612
- }
4613
4265
  function chantBucketCheck() {
4614
- cached4 ??= (async () => {
4615
- try {
4616
- const m = await import("@intentius/chant/bucket-check");
4617
- return typeof m.checkBucket === "function" ? m : void 0;
4618
- } catch {
4619
- return void 0;
4620
- }
4621
- })();
4622
- return cached4;
4266
+ cached2 ??= import("@intentius/chant/bucket-check").then((m) => m);
4267
+ return cached2;
4623
4268
  }
4624
4269
  function bucketLines(report) {
4625
4270
  const where = `${report.store === "azblob" ? "container" : "bucket"} ${report.bucket}`;
@@ -4632,47 +4277,38 @@ function bucketLines(report) {
4632
4277
  }
4633
4278
  return out;
4634
4279
  }
4635
- var cached4;
4280
+ var cached2;
4636
4281
  var init_backend = __esm({
4637
4282
  "src/init/backend.ts"() {
4638
4283
  "use strict";
4284
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4639
4285
  }
4640
4286
  });
4641
4287
 
4642
4288
  // src/init/adopt.ts
4643
4289
  function chantAdopt() {
4644
- cached5 ??= (async () => {
4645
- try {
4646
- const run = await import("@intentius/chant-lexicon-pulumi/run");
4647
- const lexicon = await import("@intentius/chant-lexicon-pulumi");
4648
- const args = run.plungeAdoptArgs;
4649
- const allowed = run.runAllowedPlunge;
4650
- const runner = run.defaultPlungeRunner;
4651
- const parse = lexicon.parsePlungeAdoptView;
4652
- const failure = run.pulumiRunFailure;
4653
- if (typeof args !== "function" || typeof allowed !== "function" || typeof runner !== "function" || typeof parse !== "function") return void 0;
4654
- return {
4655
- hasOp: typeof lexicon.PulumiAdoptOp === "function",
4656
- async preview({ dir, stack, backend }) {
4657
- try {
4658
- const { stdout } = await allowed(runner, args({ stack }), dir, { PULUMI_BACKEND_URL: backend });
4659
- const view = parse(stdout);
4660
- if (!view) throw new Error("plunge printed no adopt document");
4661
- return view;
4662
- } catch (err) {
4663
- const f = failure ? failure(err) : { message: err.message };
4664
- const view = parse(f.stdout);
4665
- if (view) return view;
4666
- if (f.missingBinary) throw new Error("the plunge binary was not found on PATH");
4667
- throw new Error((f.stderr?.trim() || f.message).split("\n").slice(-3).join("\n"));
4668
- }
4290
+ cached3 ??= (async () => {
4291
+ const { plungeAdoptArgs, runAllowedPlunge, defaultPlungeRunner, pulumiRunFailure } = await import("@intentius/chant-lexicon-pulumi/run");
4292
+ const { parsePlungeAdoptView } = await import("@intentius/chant-lexicon-pulumi");
4293
+ const parse = (t) => parsePlungeAdoptView(t);
4294
+ return {
4295
+ async preview({ dir, stack, backend }) {
4296
+ try {
4297
+ const { stdout } = await runAllowedPlunge(defaultPlungeRunner, plungeAdoptArgs({ stack }), dir, { PULUMI_BACKEND_URL: backend });
4298
+ const view = parse(stdout);
4299
+ if (!view) throw new Error("plunge printed no adopt document");
4300
+ return view;
4301
+ } catch (err) {
4302
+ const f = pulumiRunFailure(err);
4303
+ const view = parse(f.stdout);
4304
+ if (view) return view;
4305
+ if (f.missingBinary) throw new Error("the plunge binary was not found on PATH");
4306
+ throw new Error((f.stderr?.trim() || f.message).split("\n").slice(-3).join("\n"));
4669
4307
  }
4670
- };
4671
- } catch {
4672
- return void 0;
4673
- }
4308
+ }
4309
+ };
4674
4310
  })();
4675
- return cached5;
4311
+ return cached3;
4676
4312
  }
4677
4313
  function adoptCommand(s, backend) {
4678
4314
  const cd = s.dir === "." ? "" : `cd ${s.dir} && `;
@@ -4682,10 +4318,6 @@ async function adoptPreviews(stacks2, chant, root) {
4682
4318
  const out = [];
4683
4319
  for (const s of stacks2) {
4684
4320
  const command = adoptCommand(s, s.url);
4685
- if (!chant) {
4686
- out.push({ key: s.key, backend: s.url, command, error: "not run" });
4687
- continue;
4688
- }
4689
4321
  try {
4690
4322
  const view = await chant.preview({ dir: s.dir === "." ? root : `${root}/${s.dir}`, stack: s.stack, backend: s.url });
4691
4323
  out.push({ key: s.key, backend: s.url, command, view });
@@ -4700,9 +4332,8 @@ function fates(view) {
4700
4332
  for (const r of view.resources) n[r.fate] = (n[r.fate] ?? 0) + 1;
4701
4333
  return Object.entries(n).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([f, c]) => `${f} ${c}`).join(", ");
4702
4334
  }
4703
- function adoptLines(previews, chant) {
4335
+ function adoptLines(previews) {
4704
4336
  if (previews.length === 0) return [];
4705
- if (!chant) return [NEEDS_ADOPT_MESSAGE, ...previews.map((p) => ` ${p.key}: ${p.command}`)];
4706
4337
  const out = ["Adopt previews (plunge stack adopt --json, which writes nothing):"];
4707
4338
  for (const p of previews) {
4708
4339
  const v = p.view;
@@ -4768,14 +4399,14 @@ function adoptTodo(stacks2) {
4768
4399
  `Adopt the stacks onto plunge before their first plunge update: npx chant run ${ADOPT_OP}, then npx chant approve ${ADOPT_OP} <gate> for each of ${gates.join(", ")} after reading its preview, then npx chant run ${ADOPT_OP} again`
4769
4400
  ];
4770
4401
  }
4771
- var ADOPT_OP, ADOPT_OP_FILE, cached5, NEEDS_ADOPT_MESSAGE;
4402
+ var ADOPT_OP, ADOPT_OP_FILE, cached3;
4772
4403
  var init_adopt = __esm({
4773
4404
  "src/init/adopt.ts"() {
4774
4405
  "use strict";
4406
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4775
4407
  init_detect();
4776
4408
  ADOPT_OP = "plywood-adopt";
4777
4409
  ADOPT_OP_FILE = "plywood.adopt.op.ts";
4778
- NEEDS_ADOPT_MESSAGE = "the adopt previews and the adopt Op need @intentius/chant-lexicon-pulumi >= the release after 0.124.54 (chant#4562); run each preview by hand:";
4779
4410
  }
4780
4411
  });
4781
4412
 
@@ -4791,9 +4422,9 @@ function plungeUrlOf(url) {
4791
4422
  const scheme = url.split("://")[0];
4792
4423
  return { pointer: `is on ${url}, and plunge has no plunge+ form for ${scheme}. Move it to a file, s3, azblob or gs backend first: ${MOVE_A_STACK_GUIDE}` };
4793
4424
  }
4794
- function plungeStacks(detected2, env) {
4795
- return detected2.stacks.map((s) => {
4796
- const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
4425
+ function plungeStacks(detected, env) {
4426
+ return detected.stacks.map((s) => {
4427
+ const project = detected.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
4797
4428
  const b = backendOf(project, {}, env);
4798
4429
  const to = plungeUrlOf(b.url);
4799
4430
  return {
@@ -4838,6 +4469,7 @@ var INIT_ENGINES, MOVE_A_STACK_GUIDE, PLUNGE_FORMS;
4838
4469
  var init_engine2 = __esm({
4839
4470
  "src/init/engine.ts"() {
4840
4471
  "use strict";
4472
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4841
4473
  init_engine();
4842
4474
  init_backend();
4843
4475
  INIT_ENGINES = ["pulumi", "plunge"];
@@ -4861,6 +4493,7 @@ var PRESET_NAMES, DEFAULT_PRESET, PRESETS;
4861
4493
  var init_presets = __esm({
4862
4494
  "src/init/presets.ts"() {
4863
4495
  "use strict";
4496
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4864
4497
  PRESET_NAMES = ["merge", "before-merge", "approve-all"];
4865
4498
  DEFAULT_PRESET = "merge";
4866
4499
  PRESETS = [
@@ -4886,26 +4519,17 @@ var init_presets = __esm({
4886
4519
  // src/stack-order.ts
4887
4520
  import { join as join9 } from "node:path";
4888
4521
  function chantReferenceReader() {
4889
- cached6 ??= (async () => {
4890
- try {
4891
- const lexicon = await import("@intentius/chant-lexicon-pulumi");
4892
- if (typeof lexicon.pulumiStackReferences !== "function" || typeof lexicon.stackReferenceOwner !== "function") return void 0;
4893
- let run = {};
4894
- try {
4895
- run = await import("@intentius/chant-lexicon-pulumi/run");
4896
- } catch {
4897
- }
4898
- return {
4899
- pulumiStackReferences: lexicon.pulumiStackReferences,
4900
- stackReferenceOwner: lexicon.stackReferenceOwner,
4901
- ...typeof run.memberExportOf === "function" ? { memberExportOf: run.memberExportOf } : {},
4902
- ...typeof lexicon.readMemberExport === "function" ? { readMemberExport: lexicon.readMemberExport } : {}
4903
- };
4904
- } catch {
4905
- return void 0;
4906
- }
4522
+ cached4 ??= (async () => {
4523
+ const lexicon = await import("@intentius/chant-lexicon-pulumi");
4524
+ const run = await import("@intentius/chant-lexicon-pulumi/run");
4525
+ return {
4526
+ pulumiStackReferences: lexicon.pulumiStackReferences,
4527
+ stackReferenceOwner: lexicon.stackReferenceOwner,
4528
+ memberExportOf: run.memberExportOf,
4529
+ readMemberExport: lexicon.readMemberExport
4530
+ };
4907
4531
  })();
4908
- return cached6;
4532
+ return cached4;
4909
4533
  }
4910
4534
  async function exportOf(reader, root, s) {
4911
4535
  const dir = join9(root, s.dir);
@@ -4929,7 +4553,6 @@ async function stackOrder(root, stacks2, after = {}) {
4929
4553
  for (const upstream of ups) if (keys.has(reader2) && keys.has(upstream)) add({ reader: reader2, upstream, because: "waves.after" });
4930
4554
  }
4931
4555
  const reader = await chantReferenceReader();
4932
- if (!reader) return { edges, outside, reads: false };
4933
4556
  const owners = stacks2.map((s) => ({ component: s.key, project: s.project, stack: s.stack }));
4934
4557
  for (const s of stacks2) {
4935
4558
  const exported = await exportOf(reader, root, s);
@@ -4940,7 +4563,7 @@ async function stackOrder(root, stacks2, after = {}) {
4940
4563
  else outside.push({ stack: s.key, reference: ref.name, why: found.outside });
4941
4564
  }
4942
4565
  }
4943
- return { edges, outside, reads: true };
4566
+ return { edges, outside };
4944
4567
  }
4945
4568
  function orderWaves(waves, edges, canary = []) {
4946
4569
  const unmet = [];
@@ -4989,16 +4612,16 @@ function orderWaves(waves, edges, canary = []) {
4989
4612
  }
4990
4613
  function orderLines(order, unmet) {
4991
4614
  const out = [];
4992
- if (!order.reads) out.push("the installed @intentius/chant-lexicon-pulumi reads no StackReferences (chant#4547), so the waves follow waves.after alone");
4993
4615
  for (const e of order.edges) out.push(`${e.reader} updates after ${e.upstream} (${e.because === "stack-reference" ? "its StackReference" : "waves.after"})`);
4994
4616
  for (const o of order.outside) out.push(`${o.stack}'s StackReference ${o.reference} orders nothing: ${o.why}`);
4995
4617
  for (const u of unmet) out.push(`${u.reader} should update after ${u.upstream}, and cannot: ${u.why}; list them in one wave, or reorder waves`);
4996
4618
  return out;
4997
4619
  }
4998
- var cached6;
4620
+ var cached4;
4999
4621
  var init_stack_order = __esm({
5000
4622
  "src/stack-order.ts"() {
5001
4623
  "use strict";
4624
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5002
4625
  }
5003
4626
  });
5004
4627
 
@@ -5016,9 +4639,9 @@ function repoJoin(...parts) {
5016
4639
  const out = posix2.normalize(parts.filter((p) => p !== "" && p !== ".").join("/") || ".").replace(/^\.\//, "").replace(/\/+$/, "");
5017
4640
  return out === "" ? "." : out;
5018
4641
  }
5019
- function projectAt(detected2, dir) {
4642
+ function projectAt(detected, dir) {
5020
4643
  const want = repoJoin(dir);
5021
- return detected2.projects.find((p) => repoJoin(p.dir) === want);
4644
+ return detected.projects.find((p) => repoJoin(p.dir) === want);
5022
4645
  }
5023
4646
  function stackEntry(keys, key) {
5024
4647
  const stacks2 = isObject(keys.stacks) ? keys.stacks : keys.stacks = {};
@@ -5053,6 +4676,7 @@ var IMPORT_SOURCES;
5053
4676
  var init_types2 = __esm({
5054
4677
  "src/import/types.ts"() {
5055
4678
  "use strict";
4679
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5056
4680
  init_types();
5057
4681
  IMPORT_SOURCES = ["pulumi-actions", "deployments", "spacelift", "env0"];
5058
4682
  }
@@ -5061,10 +4685,10 @@ var init_types2 = __esm({
5061
4685
  // src/import/deployments.ts
5062
4686
  import { readdirSync as readdirSync6 } from "node:fs";
5063
4687
  import { basename as basename2, dirname as dirname8, join as join11 } from "node:path";
5064
- function deploymentFiles(root, detected2, named) {
4688
+ function deploymentFiles(root, detected, named) {
5065
4689
  if (named.length) return [...named];
5066
4690
  const out = [];
5067
- for (const p of detected2.projects) {
4691
+ for (const p of detected.projects) {
5068
4692
  let names;
5069
4693
  try {
5070
4694
  names = readdirSync6(join11(root, p.dir)).sort();
@@ -5079,6 +4703,7 @@ var SETTINGS_FILE, NOT_CARRIED, importDeployments;
5079
4703
  var init_deployments = __esm({
5080
4704
  "src/import/deployments.ts"() {
5081
4705
  "use strict";
4706
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5082
4707
  init_types();
5083
4708
  init_detect();
5084
4709
  init_types2();
@@ -5094,7 +4719,7 @@ var init_deployments = __esm({
5094
4719
  "gitHub.deployPullRequest": "set apply.when: pull-request and update with /plywood up",
5095
4720
  "gitHub.repository": "plywood runs in the repository it is set up in"
5096
4721
  };
5097
- importDeployments = (root, detected2, named) => {
4722
+ importDeployments = (root, detected, named) => {
5098
4723
  let keys = {};
5099
4724
  const unsupported = [];
5100
4725
  const notes = [];
@@ -5102,7 +4727,7 @@ var init_deployments = __esm({
5102
4727
  const not = (file, setting, why) => {
5103
4728
  unsupported.push({ file, setting, why: why ?? NOT_CARRIED[setting] ?? "plywood has no setting for it" });
5104
4729
  };
5105
- for (const file of deploymentFiles(root, detected2, named)) {
4730
+ for (const file of deploymentFiles(root, detected, named)) {
5106
4731
  const m = SETTINGS_FILE.exec(basename2(file));
5107
4732
  if (!m) {
5108
4733
  not(file, "(file)", "names no stack: call it Pulumi.<stack>.deploy.yaml (or .json) beside the project's Pulumi.yaml");
@@ -5121,7 +4746,7 @@ var init_deployments = __esm({
5121
4746
  const fileDir = repoJoin(posixRel(root, dirname8(join11(root, file))));
5122
4747
  const source = isObject(settings.sourceContext) && isObject(settings.sourceContext.git) ? settings.sourceContext.git : {};
5123
4748
  const dir = typeof source.repoDir === "string" ? repoJoin(source.repoDir) : fileDir;
5124
- const project = projectAt(detected2, dir)?.name ?? projectAt(detected2, fileDir)?.name;
4749
+ const project = projectAt(detected, dir)?.name ?? projectAt(detected, fileDir)?.name;
5125
4750
  if (!project) {
5126
4751
  not(file, "sourceContext.git.repoDir", `${dir} holds no Pulumi.yaml`);
5127
4752
  continue;
@@ -5176,9 +4801,9 @@ var init_deployments = __esm({
5176
4801
  });
5177
4802
 
5178
4803
  // src/import/env0.ts
5179
- function stackFor(detected2, names) {
4804
+ function stackFor(detected, names) {
5180
4805
  for (const n of names) {
5181
- const hits = detected2.stacks.filter((s) => s.key === n || s.stack === n || detected2.stacks.filter((x) => x.project === s.project).length === 1 && s.project === n);
4806
+ const hits = detected.stacks.filter((s) => s.key === n || s.stack === n || detected.stacks.filter((x) => x.project === s.project).length === 1 && s.project === n);
5182
4807
  if (hits.length === 1) return hits[0].key;
5183
4808
  }
5184
4809
  return void 0;
@@ -5187,11 +4812,12 @@ var ENV0_WORKFLOW_FILES, ENV0_FLOW_FILES, importEnv0;
5187
4812
  var init_env0 = __esm({
5188
4813
  "src/import/env0.ts"() {
5189
4814
  "use strict";
4815
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5190
4816
  init_types();
5191
4817
  init_types2();
5192
4818
  ENV0_WORKFLOW_FILES = ["env0.workflow.yml", "env0.workflow.yaml"];
5193
4819
  ENV0_FLOW_FILES = ["env0.yml", "env0.yaml"];
5194
- importEnv0 = (root, detected2, named) => {
4820
+ importEnv0 = (root, detected, named) => {
5195
4821
  const keys = {};
5196
4822
  const unsupported = [];
5197
4823
  const notes = [];
@@ -5209,7 +4835,7 @@ var init_env0 = __esm({
5209
4835
  for (const [id, raw] of Object.entries(envs)) {
5210
4836
  const e = isObject(raw) ? raw : {};
5211
4837
  const names = [id, ...typeof e.name === "string" ? [e.name] : [], ...typeof e.templateName === "string" ? [e.templateName] : []];
5212
- const key = stackFor(detected2, names);
4838
+ const key = stackFor(detected, names);
5213
4839
  if (key) stackOf.set(id, key);
5214
4840
  else unsupported.push({ file: workflow, setting: `environments.${id}`, why: `names no stack of this repository (${names.join(", ")}); list it under stacks and waves.after by hand` });
5215
4841
  for (const k of Object.keys(e).filter((x) => !["name", "templateName", "needs"].includes(x))) {
@@ -5287,6 +4913,7 @@ var WORKFLOW_DIRS2, CARRIED, WHY, PULUMI_CLOUD, importPulumiActions;
5287
4913
  var init_pulumi_actions = __esm({
5288
4914
  "src/import/pulumi-actions.ts"() {
5289
4915
  "use strict";
4916
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5290
4917
  init_types();
5291
4918
  init_types2();
5292
4919
  WORKFLOW_DIRS2 = [".github/workflows", ".forgejo/workflows", ".gitea/workflows"];
@@ -5303,7 +4930,7 @@ var init_pulumi_actions = __esm({
5303
4930
  plan: "plywood's update applies the preview its approval names, through chant"
5304
4931
  };
5305
4932
  PULUMI_CLOUD = /^https:\/\/(?:app|api)\.pulumi\.com\/?$/;
5306
- importPulumiActions = (root, detected2, named) => {
4933
+ importPulumiActions = (root, detected, named) => {
5307
4934
  let keys = {};
5308
4935
  const unsupported = [];
5309
4936
  const notes = [];
@@ -5339,7 +4966,7 @@ var init_pulumi_actions = __esm({
5339
4966
  const dir = repoJoin(typeof w["work-dir"] === "string" ? w["work-dir"] : ".");
5340
4967
  const parts = stackName.split("/");
5341
4968
  const stack = parts[parts.length - 1];
5342
- const project = projectAt(detected2, dir)?.name ?? (parts.length === 3 ? parts[1] : void 0);
4969
+ const project = projectAt(detected, dir)?.name ?? (parts.length === 3 ? parts[1] : void 0);
5343
4970
  if (!project) {
5344
4971
  unsupported.push({ file, setting: `${where}.with.stack-name`, why: `${dir} holds no Pulumi.yaml, so ${stackName} names no project` });
5345
4972
  return;
@@ -5392,11 +5019,12 @@ var SPACELIFT_FILES, CARRIED2, importSpacelift;
5392
5019
  var init_spacelift = __esm({
5393
5020
  "src/import/spacelift.ts"() {
5394
5021
  "use strict";
5022
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5395
5023
  init_types();
5396
5024
  init_types2();
5397
5025
  SPACELIFT_FILES = [".spacelift/config.yml", ".spacelift/config.yaml"];
5398
5026
  CARRIED2 = ["project_root", "runner_image"];
5399
- importSpacelift = (root, detected2, named) => {
5027
+ importSpacelift = (root, detected, named) => {
5400
5028
  const keys = {};
5401
5029
  const unsupported = [];
5402
5030
  const notes = [];
@@ -5423,7 +5051,7 @@ var init_spacelift = __esm({
5423
5051
  const own = isObject(raw) ? raw : {};
5424
5052
  const s = { ...defaults, ...own };
5425
5053
  const dir = repoJoin(typeof s.project_root === "string" ? s.project_root : ".");
5426
- const project = projectAt(detected2, dir);
5054
+ const project = projectAt(detected, dir);
5427
5055
  if (!project) {
5428
5056
  unsupported.push({ file, setting: `stacks.${id}.project_root`, why: `${dir} holds no Pulumi.yaml` });
5429
5057
  continue;
@@ -5472,9 +5100,9 @@ function parseFrom(value) {
5472
5100
  if (!source) return { error: `--from ${value} is not one of ${IMPORT_SOURCES.join(", ")}, and its path names none; give <source>=${value}` };
5473
5101
  return { source, files: [file] };
5474
5102
  }
5475
- function runImport(root, detected2, from) {
5103
+ function runImport(root, detected, from) {
5476
5104
  try {
5477
- return IMPORTERS[from.source](root, detected2, from.files);
5105
+ return IMPORTERS[from.source](root, detected, from.files);
5478
5106
  } catch (e) {
5479
5107
  const file = from.files[0] ?? "(its files)";
5480
5108
  return { source: from.source, files: [], keys: {}, unsupported: [{ file, setting: "(file)", why: `could not be read: ${e.message}` }], notes: [] };
@@ -5484,6 +5112,7 @@ var IMPORTERS;
5484
5112
  var init_import = __esm({
5485
5113
  "src/import/index.ts"() {
5486
5114
  "use strict";
5115
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5487
5116
  init_deployments();
5488
5117
  init_env0();
5489
5118
  init_pulumi_actions();
@@ -5533,11 +5162,11 @@ function parseInitArgs(args) {
5533
5162
  };
5534
5163
  const preset2 = take("--preset");
5535
5164
  const gate = take("--gate");
5536
- const engine2 = take("--engine");
5165
+ const engine3 = take("--engine");
5537
5166
  const forge = take("--forge");
5538
5167
  const env = take("--env");
5539
5168
  const fromFlag = take("--from");
5540
- const bad = [preset2, gate, engine2, forge, env, fromFlag].find((t) => t.error);
5169
+ const bad = [preset2, gate, engine3, forge, env, fromFlag].find((t) => t.error);
5541
5170
  if (bad) return { error: bad.error };
5542
5171
  const flags = /* @__PURE__ */ new Set(["--no-bucket-check", "--dry-run", "--yes", "-y"]);
5543
5172
  const extra = rest.filter((a) => !flags.has(a));
@@ -5545,7 +5174,7 @@ function parseInitArgs(args) {
5545
5174
  if (preset2.value !== void 0 && !presetByName(preset2.value)) return { error: `--preset ${preset2.value} is not one of ${PRESET_NAMES.join(", ")}` };
5546
5175
  const g = gate.value !== void 0 ? gateFlag(gate.value) : void 0;
5547
5176
  if (gate.value !== void 0 && !g) return { error: `--gate ${gate.value} is not one of always, on-destructive (or destroy), never` };
5548
- if (engine2.value !== void 0 && !INIT_ENGINES.includes(engine2.value)) return { error: `--engine ${engine2.value} is not one of ${INIT_ENGINES.join(", ")}` };
5177
+ if (engine3.value !== void 0 && !INIT_ENGINES.includes(engine3.value)) return { error: `--engine ${engine3.value} is not one of ${INIT_ENGINES.join(", ")}` };
5549
5178
  if (forge.value !== void 0 && !FORGES.includes(forge.value)) return { error: `--forge ${forge.value} is not one of ${FORGES.join(", ")}` };
5550
5179
  const from = fromFlag.value !== void 0 ? parseFrom(fromFlag.value) : void 0;
5551
5180
  if (from && "error" in from) return { error: from.error };
@@ -5553,7 +5182,7 @@ function parseInitArgs(args) {
5553
5182
  ...from ? { from } : {},
5554
5183
  ...preset2.value ? { preset: preset2.value } : {},
5555
5184
  ...g ? { gate: g } : {},
5556
- ...engine2.value ? { engine: engine2.value } : {},
5185
+ ...engine3.value ? { engine: engine3.value } : {},
5557
5186
  ...forge.value ? { forge: forge.value } : {},
5558
5187
  env: env.value ?? "production",
5559
5188
  bucketCheck: !rest.includes("--no-bucket-check"),
@@ -5574,7 +5203,7 @@ function put(root, rel, text, dryRun, note) {
5574
5203
  function existingConfig(root) {
5575
5204
  return CONFIG_NAMES.find((n) => existsSync10(join13(root, n)));
5576
5205
  }
5577
- async function newConfigKeys(detected2, opts, io) {
5206
+ async function newConfigKeys(detected, opts, io) {
5578
5207
  const notes = [];
5579
5208
  let presetName = opts.preset;
5580
5209
  if (!presetName && io.ask && !opts.yes) {
@@ -5584,31 +5213,31 @@ async function newConfigKeys(detected2, opts, io) {
5584
5213
  const preset2 = presetByName(presetName ?? DEFAULT_PRESET);
5585
5214
  const keys = structuredClone(preset2.keys);
5586
5215
  if (opts.gate) keys.gate = opts.gate;
5587
- const engine2 = engineStep({ engine: opts.engine, detected: detected2, env: io.env });
5588
- Object.assign(keys, engine2.keys);
5589
- notes.push(...engine2.notes);
5590
- const engineBackends = engine2.keys.backend !== void 0 || engine2.keys.stacks !== void 0;
5216
+ const engine3 = engineStep({ engine: opts.engine, detected, env: io.env });
5217
+ Object.assign(keys, engine3.keys);
5218
+ notes.push(...engine3.notes);
5219
+ const engineBackends = engine3.keys.backend !== void 0 || engine3.keys.stacks !== void 0;
5591
5220
  let forge;
5592
5221
  if (opts.forge) {
5593
5222
  forge = { kind: opts.forge, because: "--forge" };
5594
- if (detected2.forge?.kind !== opts.forge) keys.forge = { kind: opts.forge };
5595
- else if (detected2.forge.url) keys.forge = { kind: opts.forge, url: detected2.forge.url };
5596
- } else if (detected2.forge) {
5597
- forge = { kind: detected2.forge.kind, because: detected2.forge.because };
5598
- if (detected2.forge.url) keys.forge = { kind: detected2.forge.kind, url: detected2.forge.url };
5223
+ if (detected.forge?.kind !== opts.forge) keys.forge = { kind: opts.forge };
5224
+ else if (detected.forge.url) keys.forge = { kind: opts.forge, url: detected.forge.url };
5225
+ } else if (detected.forge) {
5226
+ forge = { kind: detected.forge.kind, because: detected.forge.because };
5227
+ if (detected.forge.url) keys.forge = { kind: detected.forge.kind, url: detected.forge.url };
5599
5228
  } else {
5600
5229
  const kind = io.ask && !opts.yes ? await io.ask("Forge", FORGES, "github") : "github";
5601
5230
  forge = { kind, because: io.ask && !opts.yes ? "you chose it" : "origin names no forge init knows, so github; change forge.kind in plywood.yml" };
5602
5231
  keys.forge = { kind };
5603
5232
  }
5604
5233
  const fromEnv = io.env.PULUMI_BACKEND_URL?.trim();
5605
- if (fromEnv && !engineBackends && detected2.projects.some((p) => !p.backendUrl)) {
5234
+ if (fromEnv && !engineBackends && detected.projects.some((p) => !p.backendUrl)) {
5606
5235
  keys.backend = fromEnv;
5607
5236
  notes.push(`backend ${fromEnv} came from PULUMI_BACKEND_URL; it is in plywood.yml so the jobs use it too`);
5608
5237
  }
5609
5238
  return { keys, notes, forge };
5610
5239
  }
5611
- function initStacks(detected2, doc, members = []) {
5240
+ function initStacks(detected, doc, members = []) {
5612
5241
  const listed = stacksOf(doc);
5613
5242
  const pinned = new Map(members.filter((m) => m.engineBin && m.engineVersion).map((m) => [m.name, m]));
5614
5243
  const out = [];
@@ -5616,8 +5245,8 @@ function initStacks(detected2, doc, members = []) {
5616
5245
  const add = (key, project, stack, dir) => {
5617
5246
  const own = listed[key] ?? {};
5618
5247
  const backend = own.backend ?? (typeof doc.backend === "string" ? doc.backend : void 0);
5619
- const engine2 = engineOf(doc, backend)?.engine;
5620
- const grain = lockGrainFor(doc, engine2);
5248
+ const engine3 = engineOf(doc, backend)?.engine;
5249
+ const grain = lockGrainFor(doc, engine3);
5621
5250
  const stackDir = own.dir ?? dir;
5622
5251
  const packs = packsForStack(stepPolicyPacksOf(doc), stackDir);
5623
5252
  const pin = pinned.get(key);
@@ -5626,7 +5255,7 @@ function initStacks(detected2, doc, members = []) {
5626
5255
  project,
5627
5256
  stack,
5628
5257
  dir: stackDir,
5629
- ...engine2 ? { engine: engine2 } : {},
5258
+ ...engine3 ? { engine: engine3 } : {},
5630
5259
  // plywood#22: the step's backend, so a preview and an update read this stack's store, not the job's PULUMI_BACKEND_URL.
5631
5260
  ...backend ? { backend } : {},
5632
5261
  ...pin ? { engineVersion: pin.engineVersion, ...pin.engineRoot ? { engineRoot: pin.engineRoot } : {} } : {},
@@ -5638,7 +5267,7 @@ function initStacks(detected2, doc, members = []) {
5638
5267
  });
5639
5268
  seen.add(key);
5640
5269
  };
5641
- for (const s of detected2.stacks) add(s.key, s.project, s.stack, s.dir);
5270
+ for (const s of detected.stacks) add(s.key, s.project, s.stack, s.dir);
5642
5271
  for (const [key, own] of Object.entries(listed)) {
5643
5272
  if (seen.has(key)) continue;
5644
5273
  const [project, stack] = key.split("/");
@@ -5646,30 +5275,22 @@ function initStacks(detected2, doc, members = []) {
5646
5275
  }
5647
5276
  return out;
5648
5277
  }
5649
- function forgeFor2(doc, detected2, opts) {
5278
+ function forgeFor2(doc, detected, opts) {
5650
5279
  if (opts.forge) return { kind: opts.forge, because: "--forge" };
5651
5280
  const kind = isObject(doc.forge) ? doc.forge.kind : void 0;
5652
5281
  if (kind) return { kind, because: "plywood.yml says forge.kind" };
5653
- if (detected2.forge) return { kind: detected2.forge.kind, because: detected2.forge.because };
5282
+ if (detected.forge) return { kind: detected.forge.kind, because: detected.forge.because };
5654
5283
  return { kind: "github", because: "origin names no forge init knows, so github" };
5655
5284
  }
5656
- async function chantWriteCiHook() {
5657
- try {
5658
- const m = await import("@intentius/chant/components/check-hooks");
5659
- return typeof m.writeCiHook === "function" ? m.writeCiHook : void 0;
5660
- } catch {
5661
- return void 0;
5662
- }
5663
- }
5664
5285
  async function runInit(opts, table, io) {
5665
- const detected2 = detect(io.cwd);
5666
- const { root } = detected2;
5286
+ const detected = detect(io.cwd);
5287
+ const { root } = detected;
5667
5288
  const files = [];
5668
5289
  const notes = [];
5669
5290
  const todo = [];
5670
5291
  const buckets = [];
5671
5292
  const adopt = [];
5672
- const imported = opts.from ? runImport(root, detected2, opts.from) : void 0;
5293
+ const imported = opts.from ? runImport(root, detected, opts.from) : void 0;
5673
5294
  if (imported && imported.files.length === 0) notes.push(`--from ${imported.source} found no file to read${imported.notes.length ? `: ${imported.notes.join("; ")}` : ""}`);
5674
5295
  const fail = (message, forge2 = { kind: "github", because: "" }) => ({
5675
5296
  ok: false,
@@ -5684,8 +5305,8 @@ async function runInit(opts, table, io) {
5684
5305
  adopt,
5685
5306
  ...imported ? { imported } : {}
5686
5307
  });
5687
- if (detected2.projects.length === 0) return fail(`no Pulumi.yaml under ${root}; plywood init sets up a repository that has a Pulumi program`);
5688
- if (detected2.stacks.length === 0) notes.push("no Pulumi.<stack>.yaml beside any Pulumi.yaml, so no stack is set up; run pulumi stack init, then plywood init again");
5308
+ if (detected.projects.length === 0) return fail(`no Pulumi.yaml under ${root}; plywood init sets up a repository that has a Pulumi program`);
5309
+ if (detected.stacks.length === 0) notes.push("no Pulumi.<stack>.yaml beside any Pulumi.yaml, so no stack is set up; run pulumi stack init, then plywood init again");
5689
5310
  let config2;
5690
5311
  let forge;
5691
5312
  const kept = existingConfig(root);
@@ -5699,10 +5320,10 @@ async function runInit(opts, table, io) {
5699
5320
  const onPlunge = opts.engine === "plunge" && engineOf(config2.doc)?.engine === "plunge";
5700
5321
  const ignored = [opts.preset && "--preset", opts.gate && "--gate", opts.engine && !onPlunge && "--engine", imported && "--from"].filter(Boolean);
5701
5322
  if (ignored.length) notes.push(`${kept} is kept as it is, so ${ignored.join(", ")} changed nothing; set the keys there`);
5702
- if (opts.engine === "plunge" && !onPlunge) notes.push(...keptPlungeLines(kept, detected2, io.env));
5703
- forge = forgeFor2(config2.doc, detected2, opts);
5323
+ if (opts.engine === "plunge" && !onPlunge) notes.push(...keptPlungeLines(kept, detected, io.env));
5324
+ forge = forgeFor2(config2.doc, detected, opts);
5704
5325
  } else {
5705
- const made = await newConfigKeys(detected2, opts, io);
5326
+ const made = await newConfigKeys(detected, opts, io);
5706
5327
  notes.push(...made.notes);
5707
5328
  forge = made.forge;
5708
5329
  const text = plywoodYml(imported ? mergeKeys(made.keys, imported.keys) : made.keys);
@@ -5715,15 +5336,13 @@ async function runInit(opts, table, io) {
5715
5336
  if (problems.length) return fail(problems.map((p) => `${kept}: ${p.key} ${p.message}`).join("\n"), forge);
5716
5337
  const doc = config2.doc;
5717
5338
  const settings = toChantSettings(config2);
5718
- const reviewStacks2 = config2.doc.review_stacks !== void 0 ? await chantReviewStacks() : false;
5719
- if (settings.ci.prEnvironment && !reviewStacks2) delete settings.ci.prEnvironment;
5720
- const reviewSuffix = reviewStacks2 ? reviewSuffixOf(settings.pipeline.reviewStackName) : void 0;
5721
- const stacks2 = initStacks(detected2, doc, settings.members);
5339
+ const reviewSuffix = config2.doc.review_stacks !== void 0 ? reviewSuffixOf(settings.pipeline.reviewStackName) : void 0;
5340
+ const stacks2 = initStacks(detected, doc, settings.members);
5722
5341
  const order = await stackOrder(root, stacks2, settings.pipeline.after);
5723
5342
  const ordered = orderWaves(wavesOf(stacks2, settings.pipeline.waves), order.edges, settings.pipeline.canary);
5724
5343
  const waves = ordered.waves;
5725
- if (order.edges.length || order.outside.length || ordered.unmet.length || !order.reads && stacks2.length > 1) notes.push(...orderLines(order, ordered.unmet));
5726
- const declaration = declarationStep(root, detected2, settings, opts.dryRun);
5344
+ if (order.edges.length || order.outside.length || ordered.unmet.length) notes.push(...orderLines(order, ordered.unmet));
5345
+ const declaration = declarationStep(root, detected, settings, opts.dryRun);
5727
5346
  files.push(declaration.file);
5728
5347
  if (existsSync10(join13(root, CHANT_CONFIG_FILE))) {
5729
5348
  const text = readFileSync12(join13(root, CHANT_CONFIG_FILE), "utf8");
@@ -5733,19 +5352,16 @@ async function runInit(opts, table, io) {
5733
5352
  } else files.push(put(root, CHANT_CONFIG_FILE, chantConfigTs(), opts.dryRun));
5734
5353
  files.push(put(root, COMPONENTS_FILE, componentsFile(stacks2, settings.pipeline.after, reviewSuffix ? { reviewSuffix } : {}), opts.dryRun));
5735
5354
  files.push(...providersPresetStep(root, doc, opts.dryRun, notes));
5736
- if (detected2.preCommit) files.push(await preCommitStep(root, detected2.preCommit, opts.dryRun));
5737
- for (const wf of detected2.pulumiActions) {
5355
+ if (detected.preCommit) files.push(await preCommitStep(root, detected.preCommit, opts.dryRun));
5356
+ for (const wf of detected.pulumiActions) {
5738
5357
  notes.push(`${wf} runs pulumi/actions. init leaves it as it is; once plywood's previews and updates replace it, remove it, or keep it for the stacks plywood does not run`);
5739
5358
  }
5740
5359
  const chant = await chantProduct();
5741
- if (!chant) {
5742
- const message = needsChantMessage("Writing the waves spec and the workflow");
5743
- files.push({ file: WAVES_SPEC_FILE2, status: "skipped", note: message }, { file: WORKFLOW_FILE[forge.kind], status: "skipped", note: "needs the same chant" });
5744
- } else {
5745
- let spec = await wavesSpecStep(chant, stacks2, doc, settings, detected2.defaultBranch, waves, forge.kind);
5360
+ {
5361
+ let spec = await wavesSpecStep(chant, stacks2, doc, settings, detected.defaultBranch, waves, forge.kind);
5746
5362
  if (settings.pipeline.jobs) {
5747
5363
  try {
5748
- spec = withJobSettings(spec, settings.pipeline.jobs, forge.kind, { parallelism: await hasChant4625() });
5364
+ spec = withJobSettings(spec, settings.pipeline.jobs, forge.kind);
5749
5365
  } catch (e) {
5750
5366
  files.push({ file: WAVES_SPEC_FILE2, status: "failed", note: e.message });
5751
5367
  spec = void 0;
@@ -5757,10 +5373,10 @@ async function runInit(opts, table, io) {
5757
5373
  }
5758
5374
  if (spec) files.push(...await workflowStep(chant, root, doc, settings, forge.kind, opts, stacks2.length > 0 ? spec : void 0, table, io));
5759
5375
  }
5760
- if (opts.bucketCheck) notes.push(...await bucketStep(detected2, doc, io.env, buckets));
5761
- adopt.push(...await adoptStep(root, detected2, doc, io.env, declaration.doc, opts.dryRun, files, notes, todo));
5762
- todo.push(...todoLines(detected2, doc, forge.kind, io.env));
5763
- if (forge.kind === "gitlab" && chant && stacks2.length > 0 && previewOf(doc) && await hasChantWavePreview()) {
5376
+ if (opts.bucketCheck) notes.push(...await bucketStep(detected, doc, io.env, buckets));
5377
+ adopt.push(...await adoptStep(root, detected, doc, io.env, declaration.doc, opts.dryRun, files, notes, todo));
5378
+ todo.push(...todoLines(detected, doc, forge.kind, io.env));
5379
+ if (forge.kind === "gitlab" && stacks2.length > 0 && previewOf(doc)) {
5764
5380
  todo.push(`Add a ${GITLAB_READ_TOKEN} CI/CD variable (a project access token with the read_api scope, masked and protected) so a wave can read the merge request it applies; without it the wave refuses`);
5765
5381
  }
5766
5382
  const failed = files.find((f) => f.status === "failed");
@@ -5778,9 +5394,9 @@ async function runInit(opts, table, io) {
5778
5394
  ...imported ? { imported } : {}
5779
5395
  };
5780
5396
  }
5781
- function declarationStep(root, detected2, settings, dryRun) {
5397
+ function declarationStep(root, detected, settings, dryRun) {
5782
5398
  if (existsSync10(join13(root, "chant.workspace.jsonc"))) {
5783
- return { file: { file: "chant.workspace.jsonc", status: "kept", note: `add a member of kind pulumi for each of ${detected2.projects.map((p) => p.dir).join(", ")}, and pin ${PULUMI_LEXICON}@${CHANT_PIN}` } };
5399
+ return { file: { file: "chant.workspace.jsonc", status: "kept", note: `add a member of kind pulumi for each of ${detected.projects.map((p) => p.dir).join(", ")}, and pin ${PULUMI_LEXICON}@${CHANT_PIN}` } };
5784
5400
  }
5785
5401
  let existing;
5786
5402
  const file = join13(root, DECLARATION_FILE);
@@ -5793,7 +5409,7 @@ function declarationStep(root, detected2, settings, dryRun) {
5793
5409
  return { file: { file: DECLARATION_FILE, status: "failed", note: `is not JSON init can merge into (${e.message})` } };
5794
5410
  }
5795
5411
  }
5796
- const doc = workspaceDeclaration(existing, { name: repoName(root), projects: detected2.projects, ci: settings.ci, lexiconVersion: CHANT_PIN });
5412
+ const doc = workspaceDeclaration(existing, { name: repoName(root), projects: detected.projects, ci: settings.ci, lexiconVersion: CHANT_PIN });
5797
5413
  if (existing && JSON.stringify(existing) === JSON.stringify(doc)) return { file: { file: DECLARATION_FILE, status: "unchanged" }, doc };
5798
5414
  return { file: put(root, DECLARATION_FILE, jsonText(doc), dryRun), doc };
5799
5415
  }
@@ -5813,10 +5429,7 @@ function providersPresetStep(root, doc, dryRun, notes) {
5813
5429
  return out;
5814
5430
  }
5815
5431
  async function preCommitStep(root, config2, dryRun) {
5816
- const write = await chantWriteCiHook();
5817
- if (!write) {
5818
- return { file: DECLARATION_FILE, status: "skipped", note: `${config2} found; running it as a check hook needs chant >= the release after 0.124.54 (chant#4556's writeCiHook). This plywood was built with chant ${BUILT_CHANT}` };
5819
- }
5432
+ const { writeCiHook: write } = await import("@intentius/chant/components/check-hooks");
5820
5433
  if (dryRun || !existsSync10(join13(root, DECLARATION_FILE))) return { file: DECLARATION_FILE, status: "skipped", note: `${config2} would run as the check hook pre-commit` };
5821
5434
  const before = readFileSync12(join13(root, DECLARATION_FILE), "utf8");
5822
5435
  try {
@@ -5828,7 +5441,7 @@ async function preCommitStep(root, config2, dryRun) {
5828
5441
  return { file: DECLARATION_FILE, status: after === before ? "unchanged" : "written", note: `${config2} runs as the check hook pre-commit; the check job's image needs pre-commit` };
5829
5442
  }
5830
5443
  async function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered, forge) {
5831
- const runner = waveRunner(chantPipelineOptions(settings.pipeline, false).runner);
5444
+ const runner = waveRunner(chantPipelineOptions(settings.pipeline).runner);
5832
5445
  const rego = isObject(doc.policy) && (typeof doc.policy.rego === "string" || doc.policy.source !== void 0 || doc.policy.command !== void 0);
5833
5446
  const base = wavesSpec({
5834
5447
  ...rego ? { policy: true } : {},
@@ -5838,11 +5451,10 @@ async function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered, for
5838
5451
  branch,
5839
5452
  ...runner !== void 0 ? { runner } : {},
5840
5453
  ...settings.pipeline.quorum?.length ? { quorum: settings.pipeline.quorum } : {},
5841
- // notify.relay only on a chant with the relay (chant#4611); an older chant refuses the setting.
5842
- ...settings.pipeline.notify ? { notify: { ...notifyForChant(settings.pipeline.notify, chant) } } : {}
5454
+ ...settings.pipeline.notify ? { notify: { ...settings.pipeline.notify } } : {}
5843
5455
  });
5844
- const named = chant.productWavesSpec ? chant.productWavesSpec(base, PRODUCT_WORDS) : base;
5845
- return previewOf(doc) && await hasChantWavePreview() ? withWavePreview(named, forge) : named;
5456
+ const named = chant.productWavesSpec(base, PRODUCT_WORDS);
5457
+ return previewOf(doc) ? withWavePreview(named, forge) : named;
5846
5458
  }
5847
5459
  async function workflowStep(chant, root, doc, settings, forge, opts, spec, table, io) {
5848
5460
  const output = WORKFLOW_FILE[forge];
@@ -5851,17 +5463,16 @@ async function workflowStep(chant, root, doc, settings, forge, opts, spec, table
5851
5463
  try {
5852
5464
  const { noteControlTarget: noteControlTarget2 } = await Promise.resolve().then(() => (init_control2(), control_exports));
5853
5465
  const control3 = await noteControlTarget2(doc, root);
5854
- plan = workflowPlan(chant, doc, forge, opts.env, spec, chantBuildFlags(chantPipelineOptions(settings.pipeline, false)), { reviewStacks: await chantReviewStacks(), prJobs, ...control3 ? { control: control3 } : {} });
5466
+ plan = workflowPlan(chant, doc, forge, opts.env, spec, chantBuildFlags(chantPipelineOptions(settings.pipeline)), { prJobs, ...control3 ? { control: control3 } : {} });
5855
5467
  } catch (e) {
5856
5468
  return [{ file: output, status: "failed", note: e.message }];
5857
5469
  }
5858
5470
  if (opts.dryRun) return [{ file: output, status: "skipped", note: "--dry-run: chant's generator was not run" }];
5859
5471
  const argv = plan.argv.map((a, i) => i === 1 && a === "." ? root : a);
5860
- const written = await writeWorkflow(chant, { root, forge, output, argv, words: plan.prWords, noteControl: plan.jobs.post.control });
5472
+ const written = await writeWorkflow(chant, { root, forge, output, argv, words: plan.prWords });
5861
5473
  if (written.status === "failed") return [{ file: output, status: "failed", note: `chant's generator exited ${written.code}; the old file is back as it was` }];
5862
5474
  const notes = [
5863
5475
  ...written.kept.length ? [`kept your own jobs: ${written.kept.join(", ")}`] : [],
5864
- ...doc.review_stacks !== void 0 && !plan.argv.includes("--pr-env") ? [REVIEW_STACKS_NEED_CHANT] : [],
5865
5476
  ...prJobs.notes,
5866
5477
  ...plan.notes,
5867
5478
  ...writtenNotes(plan, written.file)
@@ -5873,18 +5484,17 @@ async function workflowStep(chant, root, doc, settings, forge, opts, spec, table
5873
5484
  }
5874
5485
  return out;
5875
5486
  }
5876
- async function bucketStep(detected2, doc, env, reports) {
5487
+ async function bucketStep(detected, doc, env, reports) {
5877
5488
  const listed = stacksOf(doc);
5878
5489
  const urls = /* @__PURE__ */ new Set();
5879
- for (const s of detected2.stacks) {
5880
- const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
5490
+ for (const s of detected.stacks) {
5491
+ const project = detected.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
5881
5492
  const b = backendOf(project, { ...listed[s.key]?.backend ? { stack: listed[s.key].backend } : {}, ...typeof doc.backend === "string" ? { all: doc.backend } : {} }, env);
5882
5493
  if (b.url && !isPulumiCloud(b.url)) urls.add(b.url);
5883
5494
  }
5884
5495
  const targets = [...urls].map((u) => bucketTarget(u, env)).filter((t) => t !== void 0);
5885
5496
  if (targets.length === 0) return [];
5886
5497
  const check2 = await chantBucketCheck();
5887
- if (!check2) return [needsBucketCheckMessage(BUILT_CHANT)];
5888
5498
  const lines2 = ["State buckets:"];
5889
5499
  for (const t of targets) {
5890
5500
  if (!t.ok) {
@@ -5901,12 +5511,12 @@ async function bucketStep(detected2, doc, env, reports) {
5901
5511
  }
5902
5512
  return lines2;
5903
5513
  }
5904
- async function adoptStep(root, detected2, doc, env, declaration, dryRun, files, notes, todo) {
5514
+ async function adoptStep(root, detected, doc, env, declaration, dryRun, files, notes, todo) {
5905
5515
  const listed = stacksOf(doc);
5906
5516
  const all = typeof doc.backend === "string" ? doc.backend : void 0;
5907
5517
  const targets = [];
5908
- for (const s of detected2.stacks) {
5909
- const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
5518
+ for (const s of detected.stacks) {
5519
+ const project = detected.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
5910
5520
  const own = listed[s.key];
5911
5521
  const b = backendOf(project, { ...own?.backend ? { stack: own.backend } : {}, ...all ? { all } : {} }, env);
5912
5522
  if (b.url && /^plunge\+/.test(b.url)) targets.push({ key: s.key, project: s.project, projectDir: s.dir, dir: own?.dir ?? s.dir, stack: s.stack, url: b.url });
@@ -5914,11 +5524,7 @@ async function adoptStep(root, detected2, doc, env, declaration, dryRun, files,
5914
5524
  if (targets.length === 0) return [];
5915
5525
  const chant = await chantAdopt();
5916
5526
  const previews = await adoptPreviews(targets, chant, root);
5917
- notes.push(...adoptLines(previews, chant));
5918
- if (!chant?.hasOp) {
5919
- files.push({ file: ADOPT_OP_FILE, status: "skipped", note: `the adopt Op needs @intentius/chant-lexicon-pulumi >= the release after 0.124.54 (chant#4562's PulumiAdoptOp). This plywood was built with chant ${BUILT_CHANT}` });
5920
- return previews;
5921
- }
5527
+ notes.push(...adoptLines(previews));
5922
5528
  const left = stacksToAdopt(targets, previews);
5923
5529
  if (left.length === 0) {
5924
5530
  if (existsSync10(join13(root, ADOPT_OP_FILE))) files.push({ file: ADOPT_OP_FILE, status: "kept", note: "every stack is adopted; delete it" });
@@ -5929,17 +5535,17 @@ async function adoptStep(root, detected2, doc, env, declaration, dryRun, files,
5929
5535
  todo.push(...adoptTodo(rows));
5930
5536
  return previews;
5931
5537
  }
5932
- function keptPlungeLines(kept, detected2, env) {
5933
- const step = engineStep({ engine: "plunge", detected: detected2, env });
5538
+ function keptPlungeLines(kept, detected, env) {
5539
+ const step = engineStep({ engine: "plunge", detected, env });
5934
5540
  const entries = Object.entries(step.keys);
5935
5541
  if (entries.length === 0) return step.notes;
5936
5542
  const yaml = entries.map(([k, v]) => emitYAMLEntry2(k, v)).join("\n").split("\n").filter((l) => l !== "");
5937
5543
  return [...step.notes, `To move to plunge, set these keys in ${kept}:`, ...yaml.map((l) => ` ${l}`)];
5938
5544
  }
5939
- function todoLines(detected2, doc, forge, env) {
5545
+ function todoLines(detected, doc, forge, env) {
5940
5546
  const out = [];
5941
5547
  const all = typeof doc.backend === "string" ? doc.backend : void 0;
5942
- const cloud = detected2.projects.some((p) => isPulumiCloud(backendOf(p, all ? { all } : {}, env).url));
5548
+ const cloud = detected.projects.some((p) => isPulumiCloud(backendOf(p, all ? { all } : {}, env).url));
5943
5549
  const where = forge === "gitlab" ? "CI/CD variables" : "repository secrets";
5944
5550
  const traded = identityClouds(doc);
5945
5551
  if (cloud && traded.includes("pulumi")) {
@@ -5947,9 +5553,9 @@ function todoLines(detected2, doc, forge, env) {
5947
5553
  } else if (cloud) {
5948
5554
  out.push(`Set pulumi_cloud.org and identity.preview.pulumi and identity.up.pulumi in plywood.yml, so each job trades its OIDC token for its own Pulumi Cloud token, or add PULUMI_ACCESS_TOKEN_PREVIEW and PULUMI_ACCESS_TOKEN_UP to the ${where}`);
5949
5555
  }
5950
- if (traded.some((c) => c !== "pulumi")) out.push(`Let each role identity names trust this repository's OIDC tokens: the preview role for pull requests, the up role for ${detected2.defaultBranch} only`);
5556
+ if (traded.some((c) => c !== "pulumi")) out.push(`Let each role identity names trust this repository's OIDC tokens: the preview role for pull requests, the up role for ${detected.defaultBranch} only`);
5951
5557
  else if (!cloud) out.push("Give the preview job and the update job their cloud access, each its own role through OIDC (identity in plywood.yml); the backend bucket's read and write go with them");
5952
- out.push(`Make ${PRODUCT}/preview a required status on ${detected2.defaultBranch}`);
5558
+ out.push(`Make ${PRODUCT}/preview a required status on ${detected.defaultBranch}`);
5953
5559
  return out;
5954
5560
  }
5955
5561
  function initLines(r) {
@@ -5985,11 +5591,11 @@ var DECLARATION_FILE, CHANT_CONFIG_FILE, COMPONENTS_FILE, WAVES_SPEC_FILE2, CHAN
5985
5591
  var init_init = __esm({
5986
5592
  "src/init.ts"() {
5987
5593
  "use strict";
5594
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5988
5595
  init_package();
5989
5596
  init_chant_product();
5990
5597
  init_chant_config();
5991
5598
  init_identity();
5992
- init_observe();
5993
5599
  init_policy();
5994
5600
  init_policy_presets();
5995
5601
  init_approval();
@@ -6028,13 +5634,11 @@ var init_init = __esm({
6028
5634
  var control_exports = {};
6029
5635
  __export(control_exports, {
6030
5636
  CONTROLLED_KEYS: () => CONTROLLED_KEYS,
6031
- CONTROL_NEEDS_CHANT: () => CONTROL_NEEDS_CHANT,
6032
5637
  chantControl: () => chantControl,
6033
5638
  control: () => control2,
6034
5639
  controlGenerator: () => controlGenerator,
6035
5640
  controlTarget: () => controlTarget,
6036
5641
  controlledConfigText: () => controlledConfigText,
6037
- hasWavesControl: () => hasWavesControl,
6038
5642
  noteControlTarget: () => noteControlTarget,
6039
5643
  projectSettings: () => projectSettings,
6040
5644
  setChantControlForTests: () => setChantControlForTests,
@@ -6047,15 +5651,10 @@ import { createRequire } from "node:module";
6047
5651
  import { dirname as dirname10, join as join14 } from "node:path";
6048
5652
  import { pathToFileURL } from "node:url";
6049
5653
  import { emitYAMLEntry as emitYAMLEntry3, parseYAML as parseYAML4 } from "@intentius/chant/yaml";
6050
- function hasWavesControl(chant) {
6051
- return typeof chant.recordControlAppliedFromEnv === "function";
6052
- }
6053
5654
  async function wavesControl(cwd) {
6054
5655
  const file = findConfig(cwd);
6055
5656
  if (!file || controlOf(loadConfig(file).doc) === void 0) return { none: true };
6056
5657
  const chant = await chantControl();
6057
- if (!chant) return { missing: CONTROL_NEEDS_CHANT };
6058
- if (!hasWavesControl(chant)) return { missing: "the waves workflow tells the control repo with a chant that has chant#4617" };
6059
5658
  const target = await controlTarget(chant, cwd);
6060
5659
  if ("error" in target) return target;
6061
5660
  const settings = { ...controlOf(loadConfig(file).doc), member: target.member };
@@ -6067,7 +5666,7 @@ async function noteControlTarget(doc, root) {
6067
5666
  const settings = controlOf(doc);
6068
5667
  if (!settings) return void 0;
6069
5668
  const chant = await chantControl();
6070
- if (!chant || chant.controlSettingsProblems(settings).length > 0) return void 0;
5669
+ if (chant.controlSettingsProblems(settings).length > 0) return void 0;
6071
5670
  try {
6072
5671
  return chant.resolveControlTarget(settings, repoName(root));
6073
5672
  } catch {
@@ -6075,18 +5674,11 @@ async function noteControlTarget(doc, root) {
6075
5674
  }
6076
5675
  }
6077
5676
  function chantControl() {
6078
- cached7 ??= (async () => {
6079
- try {
6080
- const m = await import("@intentius/chant/ci-control");
6081
- return REQUIRED3.every((n) => typeof m[n] === "function") ? m : void 0;
6082
- } catch {
6083
- return void 0;
6084
- }
6085
- })();
6086
- return cached7;
5677
+ cached5 ??= import("@intentius/chant/ci-control").then((m) => m);
5678
+ return cached5;
6087
5679
  }
6088
5680
  function setChantControlForTests(fake) {
6089
- cached7 = fake === "reset" ? void 0 : Promise.resolve(fake);
5681
+ cached5 = fake === "reset" ? void 0 : Promise.resolve(fake);
6090
5682
  }
6091
5683
  function projectSettings(defaults, own) {
6092
5684
  return { ...defaults ?? {}, ...own };
@@ -6132,7 +5724,6 @@ async function control2(args, json, table, io) {
6132
5724
  const subs = ["init", "put", "import", "list", "retire", "reconcile", "dispatch"];
6133
5725
  if (!sub || !subs.includes(sub)) return usage(`control takes one of ${subs.join(", ")}`, table, io);
6134
5726
  const chant = await chantControl();
6135
- if (!chant) return usage(`${CONTROL_NEEDS_CHANT}. Upgrade plywood once that chant is out, or build against a local chant with 'just chant-local'.`, table, io);
6136
5727
  const emit = (envelope, lines2) => {
6137
5728
  if (json) io.out(JSON.stringify({ command: `control ${sub}`, ...envelope }));
6138
5729
  else for (const l of lines2) io.out(l);
@@ -6180,8 +5771,8 @@ async function controlInit(chant, args, emit, table, io) {
6180
5771
  const before = existsSync11(path) ? readFileSync13(path, "utf8") : void 0;
6181
5772
  const status = before === file.content ? "unchanged" : "written";
6182
5773
  if (!dryRun && status === "written") {
6183
- const { mkdirSync: mkdirSync8 } = await import("node:fs");
6184
- mkdirSync8(dirname10(path), { recursive: true });
5774
+ const { mkdirSync: mkdirSync7 } = await import("node:fs");
5775
+ mkdirSync7(dirname10(path), { recursive: true });
6185
5776
  writeFileSync7(path, file.content);
6186
5777
  }
6187
5778
  emit({ ok: true, exitCode: table.done, forge, file: file.path, status, dryRun, notes: file.notes }, [
@@ -6316,10 +5907,11 @@ async function controlReconcile(chant, args, emit, table, io) {
6316
5907
  emit({ ok: !failed, exitCode: code, mode, outcomes }, chant.describeReconcile(outcomes, mode).split("\n"));
6317
5908
  return code;
6318
5909
  }
6319
- var CONTROLLED_KEYS, CONTROL_NEEDS_CHANT, WAVES_CONTROL_KEYS, REQUIRED3, cached7, validateControlSettings;
5910
+ var CONTROLLED_KEYS, WAVES_CONTROL_KEYS, cached5, validateControlSettings;
6320
5911
  var init_control2 = __esm({
6321
5912
  "src/control.ts"() {
6322
5913
  "use strict";
5914
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6323
5915
  init_config();
6324
5916
  init_control();
6325
5917
  init_routing();
@@ -6328,9 +5920,7 @@ var init_control2 = __esm({
6328
5920
  init_stages();
6329
5921
  init_words();
6330
5922
  CONTROLLED_KEYS = ["gate", "approval", "approval_rules", "pulumi", "plunge", "image"];
6331
- CONTROL_NEEDS_CHANT = "the control repo needs chant >= the release after 0.124.54, which carries @intentius/chant/ci-control (chant#4568)";
6332
5923
  WAVES_CONTROL_KEYS = ["repo", "op", "forge", "member", "tokenSecret", "readTokenSecret", "ref", "apiBase"];
6333
- REQUIRED3 = ["controlDispatchRequest", "putControlEntry", "reconcileProjects"];
6334
5924
  validateControlSettings = (_entry, _project, settings) => {
6335
5925
  const out = [];
6336
5926
  for (const key of Object.keys(settings)) {
@@ -6364,13 +5954,8 @@ function positionals(args) {
6364
5954
  }
6365
5955
  return out;
6366
5956
  }
6367
- async function withChant(what, table, io, run) {
6368
- const chant = await chantProduct();
6369
- if (!chant) {
6370
- io.err(`${PRODUCT}: ${needsChantMessage(what)}`);
6371
- return table.failed;
6372
- }
6373
- return run(chant);
5957
+ async function withChant(_what, _table, _io, run) {
5958
+ return run(await chantProduct());
6374
5959
  }
6375
5960
  function passthrough(argv, table, io) {
6376
5961
  if (argv[0] === "run" && argv[1] === "wave") return runWave(argv, table, io);
@@ -6433,7 +6018,6 @@ async function preview(args, json, table, io) {
6433
6018
  const gate = rest.includes("--gate") ? [] : ["--gate", PR_GATE];
6434
6019
  const argv = ["components", "pr-plan", "--base", at, "--pr", number, ...gate, ...stack ? ["--member", stack] : [], ...rest, ...json ? ["--json"] : []];
6435
6020
  return withChant(`${PRODUCT} preview`, table, io, async (chant) => {
6436
- if (targeted && !hasChant4620(chant)) return usage(PREVIEW_TARGET_NEEDS_CHANT, table, io);
6437
6021
  return chant.runChantInProcess(argv, PRODUCT_WORDS);
6438
6022
  });
6439
6023
  }
@@ -6472,12 +6056,6 @@ async function ci(args, json, table, io) {
6472
6056
  const env = envFlag.value ?? "production";
6473
6057
  const output = outFlag.value ?? WORKFLOW_FILE[forge];
6474
6058
  return withChant(`${PRODUCT} ci`, table, io, async (chant) => {
6475
- const reviewStacks2 = await chantReviewStacks();
6476
- if (config2.doc.review_stacks !== void 0 && !reviewStacks2) io.err(`${PRODUCT}: ${REVIEW_STACKS_NEED_CHANT}`);
6477
- if (applyWhenOf(config2.doc) === "pull-request") {
6478
- const note = requiresNotHonoured(chant, config2.doc);
6479
- if (note) io.err(`${PRODUCT}: ${note}`);
6480
- }
6481
6059
  let waves;
6482
6060
  try {
6483
6061
  waves = readWavesSpec(io.cwd);
@@ -6485,12 +6063,12 @@ async function ci(args, json, table, io) {
6485
6063
  return usage(`.chant/op-waves/${OP_NAME}.spec.json: ${e.message}`, table, io);
6486
6064
  }
6487
6065
  const pipeline = toChantSettings(config2).pipeline;
6488
- const { prJobsFor: prJobsFor2, hasChant4625: hasChant46252, withJobSettings: withJobSettings2 } = await Promise.resolve().then(() => (init_jobs2(), jobs_exports));
6066
+ const { prJobsFor: prJobsFor2, withJobSettings: withJobSettings2 } = await Promise.resolve().then(() => (init_jobs2(), jobs_exports));
6489
6067
  const prJobs = await prJobsFor2(pipeline);
6490
6068
  for (const note of prJobs.notes) io.err(`${PRODUCT}: ${note}`);
6491
6069
  if (waves !== void 0 && pipeline.jobs) {
6492
6070
  try {
6493
- waves = withJobSettings2(waves, pipeline.jobs, forge, { parallelism: await hasChant46252() });
6071
+ waves = withJobSettings2(waves, pipeline.jobs, forge);
6494
6072
  } catch (e) {
6495
6073
  io.err(`${PRODUCT}: ${e.message}`);
6496
6074
  return table.failed;
@@ -6500,7 +6078,8 @@ async function ci(args, json, table, io) {
6500
6078
  const noteControl = await noteControlTarget2(config2.doc, io.cwd);
6501
6079
  let plan;
6502
6080
  try {
6503
- plan = workflowPlan(chant, config2.doc, forge, env, waves, [], { reviewStacks: reviewStacks2, prJobs, ...noteControl ? { control: noteControl } : {} });
6081
+ const engine3 = chantBuildFlags(chantPipelineOptions(pipeline));
6082
+ plan = workflowPlan(chant, config2.doc, forge, env, waves, engine3, { prJobs, ...noteControl ? { control: noteControl } : {} });
6504
6083
  } catch (e) {
6505
6084
  io.err(`${PRODUCT}: ${e.message}`);
6506
6085
  return table.failed;
@@ -6541,9 +6120,8 @@ async function ci(args, json, table, io) {
6541
6120
  if (jobs2.action) io.err(` action ${jobs2.action.command} (${jobs2.action.comment} <stack> <name>, or a dispatch with inputs ${jobs2.action.inputs.member} and ${jobs2.action.inputs.action})`);
6542
6121
  }
6543
6122
  if (!json && "control" in control3) io.err(` control after the last wave, tells ${control3.repo} how the waves ended`);
6544
- else if (!json && "missing" in control3) io.err(` control the waves workflow sends no dispatch: ${control3.missing}; a job of your own after the last wave runs '${PRODUCT} control dispatch'`);
6545
6123
  if (dryRun) return table.done;
6546
- const written = await writeWorkflow(chant, { root: io.cwd, forge, output, argv: plan.argv, words: plan.prWords, noteControl: plan.jobs.post.control });
6124
+ const written = await writeWorkflow(chant, { root: io.cwd, forge, output, argv: plan.argv, words: plan.prWords });
6547
6125
  if (!json && written.kept.length) io.err(` kept your own jobs: ${written.kept.join(", ")}`);
6548
6126
  if (written.code === table.done) for (const note of writtenNotes(plan, written.file)) io.err(`${PRODUCT}: ${note}`);
6549
6127
  if (json) io.out(JSON.stringify({ command: "ci", ok: written.code === 0, exitCode: written.code, dryRun, ...summary, status: written.status, kept: written.kept }));
@@ -6568,21 +6146,20 @@ function workflowPlan(chant, doc, forge, env, waves, extra = [], opts = {}) {
6568
6146
  const shape = pipelineShape(doc, forge, waves);
6569
6147
  const notes = [];
6570
6148
  const pipeline = toChantSettings({ doc }).pipeline;
6571
- const respond = respondOptions(chant, pipeline, shape.applyBeforeMerge, notes);
6149
+ const respond = respondOptions(pipeline, notes);
6572
6150
  const applyMerge = shape.applyBeforeMerge ? pipeline.applyMerge : void 0;
6573
- if (approvalOf(doc) === "pr-review" && shape.waves && hasChantApprovalStatus(chant) && !hasChantPullRequestApprovalStatus(chant)) notes.push(APPROVAL_STATUS_NOTE);
6574
6151
  const actionJob = actionJobFor(doc, forge, notes);
6575
6152
  const jobsWanted = actionJob ? [...shape.jobs, "action"] : shape.jobs;
6576
6153
  const words = wordsForJobs(jobsWanted);
6577
6154
  const requires = shape.applyBeforeMerge ? requiresArgv(chant, doc) : [];
6578
- const previewComment = hasChant4620(chant);
6579
6155
  const jobs2 = chant.productPipelineJobs(forge, env, {
6580
6156
  words,
6581
6157
  ...shape.applyBeforeMerge ? { applyBeforeMerge: true } : {},
6582
6158
  ...shape.locks ? { locks: shape.locks } : {},
6583
6159
  ...shape.waves ? { waves } : {},
6584
6160
  ...requires.length ? { applyRequires: requires[1].split(",") } : {},
6585
- ...previewComment ? { previewComment: true } : {},
6161
+ // chant#4620: `/plywood preview` previews the pull request again.
6162
+ previewComment: true,
6586
6163
  // chant#4619: the note job's read of the control repo comes back as post.control.
6587
6164
  ...opts.control !== void 0 ? { control: { target: opts.control } } : {},
6588
6165
  // plywood#29: runners, env, pass and parallelism on the pull request jobs, with chant#4625 (src/jobs.ts).
@@ -6608,12 +6185,14 @@ function workflowPlan(chant, doc, forge, env, waves, extra = [], opts = {}) {
6608
6185
  ...shape.applyBeforeMerge ? ["--apply-before-merge", ...requires] : [],
6609
6186
  ...applyMerge ? applyMergeArgv(applyMerge) : [],
6610
6187
  ...respond.argv,
6611
- ...previewComment ? ["--preview-comment"] : [],
6188
+ "--preview-comment",
6612
6189
  "--stale-notes",
6613
6190
  ...actionJob ? ["--action-job"] : [],
6614
- // Review stacks (plywood#15): chant's pr-env jobs, only once the pulumi lexicon can remove what they create.
6615
- ...doc.review_stacks !== void 0 && opts.reviewStacks ? ["--pr-env"] : [],
6191
+ // Review stacks (plywood#15): chant's pr-env jobs.
6192
+ ...doc.review_stacks !== void 0 ? ["--pr-env"] : [],
6616
6193
  ...opts.prJobs?.flags ?? [],
6194
+ // chant#4772: the control repo, so the note job the build writes reads its records (plywood#28).
6195
+ ...opts.control !== void 0 ? controlBuildArgv(doc, opts.control.member) : [],
6617
6196
  ...extra
6618
6197
  ];
6619
6198
  return { jobs: jobs2, words, prWords, argv, notes, ...applyMerge ? { applyMerge } : {} };
@@ -6626,15 +6205,11 @@ function actionJobFor(doc, forge, notes) {
6626
6205
  function applyMergeArgv(m) {
6627
6206
  return ["--merge", "auto", "--merge-token-env", m.tokenEnv, ...m.method ? ["--merge-method", m.method] : []];
6628
6207
  }
6629
- function respondOptions(chant, pipeline, applyBeforeMerge, notes) {
6208
+ function respondOptions(pipeline, notes) {
6630
6209
  const r = pipeline.respond;
6631
6210
  if (pipeline.decide && r?.description !== "check") notes.push(DECIDE_LEFT_OFF);
6632
6211
  if (!r || Object.keys(r).length === 0) return { options: {}, argv: [] };
6633
6212
  const keys = Object.keys(r).map((e) => e.replace("-", "_"));
6634
- if (!hasChantRespond(chant)) {
6635
- notes.push(`respond.${keys.join(", respond.")} ${keys.length === 1 ? "needs" : "need"} chant's responses to pipeline events (chant#4629), which this chant lacks; chant's defaults hold`);
6636
- return { options: {}, argv: [] };
6637
- }
6638
6213
  const responses = { ...r };
6639
6214
  const check2 = responses.description === "check";
6640
6215
  const decide = check2 ? pipeline.decide : void 0;
@@ -6643,14 +6218,7 @@ function respondOptions(chant, pipeline, applyBeforeMerge, notes) {
6643
6218
  notes.push("respond.description is check and decide names no backend and model, so the description check is left off");
6644
6219
  }
6645
6220
  const options = { respond: responses, ...decide ? { decide: { ...decide } } : {} };
6646
- if (hasChantPrLoopFlags(chant)) return { options, argv: respondArgv(responses, decide) };
6647
- const prSide = [
6648
- ...responses.description === "check" ? ["respond.description"] : [],
6649
- ...applyBeforeMerge && responses["apply-failed"] === "off" ? ["respond.apply_failed"] : [],
6650
- ...applyBeforeMerge && responses["wave-refused"] === "off" ? ["respond.wave_refused"] : []
6651
- ];
6652
- if (prSide.length) notes.push(`${prSide.join(", ")} ${prSide.length === 1 ? "reaches" : "reach"} no pull request job yet: chant's build --pr-loop writes no responses into them, so the note and the update before merge keep chant's defaults; the waves workflow takes wave_refused and apply_failed`);
6653
- return { options, argv: [] };
6221
+ return { options, argv: respondArgv(responses, decide) };
6654
6222
  }
6655
6223
  function respondArgv(responses, decide) {
6656
6224
  const list = Object.keys(CHANT_RESPOND_DEFAULTS).filter((e) => responses[e] !== void 0 && responses[e] !== CHANT_RESPOND_DEFAULTS[e]).map((e) => `${e}=${responses[e]}`).join(",");
@@ -6659,35 +6227,30 @@ function respondArgv(responses, decide) {
6659
6227
  }
6660
6228
  function requiresArgv(chant, doc) {
6661
6229
  const listed = applyRequiresOf(doc);
6662
- if (listed.length === APPLY_REQUIRES.length || !hasChant4607(chant)) return [];
6230
+ if (listed.length === APPLY_REQUIRES.length) return [];
6663
6231
  const parsed = chant.parseRequiresList(listed);
6664
6232
  return "checks" in parsed ? ["--requires", parsed.checks.join(",")] : [];
6665
6233
  }
6666
- function requiresNotHonoured(chant, doc) {
6667
- const listed = applyRequiresOf(doc);
6668
- if (listed.length === APPLY_REQUIRES.length || hasChant4607(chant)) return void 0;
6669
- return `apply.requires lists ${listed.join(", ")}, and the installed chant has no pr-apply --requires (chant#4607), so the update before merge asks all four checks: ${APPLY_REQUIRES.join(", ")}`;
6670
- }
6671
6234
  function usage(message, table, io) {
6672
6235
  io.err(`${PRODUCT}: ${message}`);
6673
6236
  return table.failed;
6674
6237
  }
6675
- var CHANT_PASSTHROUGH, VALUED, PREVIEW_TARGET_NEEDS_CHANT, WORKFLOW_FILE, ACTIONS_NOT_ON_GITLAB, APPROVAL_STATUS_NOTE, CHANT_RESPOND_DEFAULTS, DECIDE_LEFT_OFF;
6238
+ var CHANT_PASSTHROUGH, VALUED, WORKFLOW_FILE, ACTIONS_NOT_ON_GITLAB, CHANT_RESPOND_DEFAULTS, DECIDE_LEFT_OFF;
6676
6239
  var init_stages = __esm({
6677
6240
  "src/stages.ts"() {
6678
6241
  "use strict";
6242
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6679
6243
  init_chant_product();
6680
6244
  init_chant_config();
6681
- init_chant_next();
6682
- init_approval();
6683
6245
  init_config_check();
6684
6246
  init_apply();
6685
6247
  init_config();
6248
+ init_control();
6686
6249
  init_locks();
6687
6250
  init_stacks();
6688
6251
  init_routing();
6252
+ init_images();
6689
6253
  init_workflow();
6690
- init_review_stacks2();
6691
6254
  init_waves_workflow();
6692
6255
  init_drift2();
6693
6256
  init_refusals();
@@ -6696,20 +6259,22 @@ var init_stages = __esm({
6696
6259
  init_words();
6697
6260
  CHANT_PASSTHROUGH = ["components", "run", "lock", "unlock", "query", "ledger", "audit-log"];
6698
6261
  VALUED = /* @__PURE__ */ new Set(["--base", "--pr", "--forge", "--env", "--output", "-o", "--stack", "--file", "--head", "--gate", "--branch", "--report-url", "--target"]);
6699
- PREVIEW_TARGET_NEEDS_CHANT = "--target needs a chant whose pr-plan passes it to the pulumi-up step (chant#4620), which this chant lacks";
6700
6262
  WORKFLOW_FILE = {
6701
6263
  github: ".github/workflows/plywood.yml",
6702
6264
  forgejo: ".forgejo/workflows/plywood.yml",
6703
6265
  gitlab: ".gitlab-ci.yml"
6704
6266
  };
6705
6267
  ACTIONS_NOT_ON_GITLAB = "plywood.yml declares actions, and a GitLab pipeline starts on no comment or dispatch with inputs, so it has no action job; run 'plywood action <stack> <name>' from a pipeline of your own";
6706
- APPROVAL_STATUS_NOTE = `approval is pr-review, and chant posts its approval status only for waves that wait on pr-review; plywood's waves wait on the pull request's own approval, so no ${PRODUCT}/approval status is posted before merge`;
6707
6268
  CHANT_RESPOND_DEFAULTS = Object.freeze({ "wave-refused": "diff", "apply-failed": "triage", description: "off" });
6708
6269
  DECIDE_LEFT_OFF = "decide is set and respond.description is not check, so plywood leaves the decide service off the pull request jobs";
6709
6270
  }
6710
6271
  });
6711
6272
 
6273
+ // src/bin.ts
6274
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6275
+
6712
6276
  // src/chant-resolve.ts
6277
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6713
6278
  import * as nodeModule from "node:module";
6714
6279
  var CHANT_SPECIFIER = /^@intentius\/chant(?:-lexicon-[a-z0-9-]+)?(?:\/|$)/;
6715
6280
  function chantResolveHook(from) {
@@ -6735,50 +6300,22 @@ function installChantResolution(from = import.meta.url) {
6735
6300
  }
6736
6301
 
6737
6302
  // src/cli.ts
6303
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6738
6304
  init_config_check();
6739
6305
 
6740
6306
  // src/exit-codes.ts
6741
- var FALLBACK_EXIT = Object.freeze({
6742
- done: 0,
6743
- failed: 1,
6744
- drift: 2,
6745
- waiting: 3,
6746
- moved: 4,
6747
- held: 5,
6748
- superseded: 6,
6749
- denied: 7,
6750
- declined: 8,
6751
- misplaced: 9
6752
- });
6753
- var NAMES = Object.keys(FALLBACK_EXIT).filter((n) => n !== "misplaced");
6754
- function isExitTable(t) {
6755
- if (!t || typeof t !== "object") return false;
6756
- const r = t;
6757
- return NAMES.every((n) => Number.isInteger(r[n])) && (r.misplaced === void 0 || Number.isInteger(r.misplaced));
6758
- }
6759
- function withMisplaced(t) {
6760
- return Number.isInteger(t.misplaced) ? t : Object.freeze({ ...t, misplaced: FALLBACK_EXIT.misplaced });
6761
- }
6762
- var cached;
6763
- function exitCodes() {
6764
- cached ??= (async () => {
6765
- try {
6766
- const m = await import("@intentius/chant/exit-codes");
6767
- const table = m.CHANT_EXIT;
6768
- if (isExitTable(table)) return { table: withMisplaced(table), source: "chant" };
6769
- } catch {
6770
- }
6771
- return { table: FALLBACK_EXIT, source: "fallback" };
6772
- })();
6773
- return cached;
6774
- }
6307
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6308
+ import { CHANT_EXIT } from "@intentius/chant/exit-codes";
6309
+ var EXIT = CHANT_EXIT;
6775
6310
 
6776
6311
  // src/actions.ts
6312
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6777
6313
  init_chant_product();
6778
6314
  init_stacks();
6779
6315
  init_stages();
6780
6316
 
6781
6317
  // src/stack-ops.ts
6318
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6782
6319
  init_chant_config();
6783
6320
  init_identity();
6784
6321
  init_config();
@@ -6787,28 +6324,18 @@ init_adopt();
6787
6324
  init_detect();
6788
6325
  init_refusals();
6789
6326
  init_stages();
6790
- init_chant_product();
6791
6327
  init_words();
6792
6328
  import { existsSync as existsSync13 } from "node:fs";
6793
6329
  import { dirname as dirname11, isAbsolute as isAbsolute2, join as join16, resolve as resolve2 } from "node:path";
6794
6330
  var STACK_OPS_OP = "pulumi-stack-ops";
6795
- var REQUIRED4 = ["gatedStackExport", "gatedStackCancel", "gatedStackMove"];
6796
6331
  var cachedLocks;
6797
6332
  function chantStackLockReader() {
6798
- cachedLocks ??= (async () => {
6799
- try {
6800
- const m = await import("@intentius/chant-lexicon-pulumi");
6801
- return typeof m.pulumiStackLockIds === "function" ? m.pulumiStackLockIds : void 0;
6802
- } catch {
6803
- return void 0;
6804
- }
6805
- })();
6333
+ cachedLocks ??= import("@intentius/chant-lexicon-pulumi").then((m) => m.pulumiStackLockIds);
6806
6334
  return cachedLocks;
6807
6335
  }
6808
6336
  async function lockToCancel(given, stack, env) {
6809
6337
  if (given) return { lockId: given, read: false };
6810
6338
  const reader = await chantStackLockReader();
6811
- if (!reader) return { error: STACK_OP_WORDS.needsLock };
6812
6339
  let locks2;
6813
6340
  try {
6814
6341
  locks2 = await reader({ dir: stack.dir, stack: stack.stack, ...stack.project ? { project: stack.project } : {}, ...stack.engine ? { engine: stack.engine } : {}, env });
@@ -6821,34 +6348,10 @@ async function lockToCancel(given, stack, env) {
6821
6348
  return { lockId: locks2.ids[0], read: true };
6822
6349
  }
6823
6350
  var cachedOps;
6824
- var cachedRollback;
6825
6351
  function chantStackOps() {
6826
- cachedOps ??= (async () => {
6827
- try {
6828
- const m = await import("@intentius/chant-lexicon-pulumi");
6829
- return REQUIRED4.every((name) => typeof m[name] === "function") ? m : void 0;
6830
- } catch {
6831
- return void 0;
6832
- }
6833
- })();
6352
+ cachedOps ??= import("@intentius/chant-lexicon-pulumi").then((m) => m);
6834
6353
  return cachedOps;
6835
6354
  }
6836
- function chantStackRollback() {
6837
- cachedRollback ??= (async () => {
6838
- try {
6839
- const m = await import("@intentius/chant-lexicon-pulumi");
6840
- if (typeof m.pulumiReleaseInputs !== "function") return false;
6841
- const copy2 = await import("@intentius/chant-lexicon-pulumi/release-copy");
6842
- return typeof copy2.prepareReleaseCopy === "function";
6843
- } catch {
6844
- return false;
6845
- }
6846
- })();
6847
- return cachedRollback;
6848
- }
6849
- function needsStackOpsMessage(what, feature, after = "0.124.54") {
6850
- return `${what} needs @intentius/chant-lexicon-pulumi >= the release after ${after}, which carries ${feature}. This plywood was built with chant ${BUILT_CHANT}, which lacks it. Upgrade plywood once that chant is out, or build against a local chant with 'just chant-local'.`;
6851
- }
6852
6355
  function readConfig(cwd) {
6853
6356
  const path = findConfig(cwd);
6854
6357
  if (!path) return { error: `no plywood.yml in ${cwd} or above it; run '${PRODUCT} init' first` };
@@ -6944,10 +6447,6 @@ function reportOutcome(verb, outcome, gate, rules, done, json, table, io) {
6944
6447
  }
6945
6448
  async function runRequest(verb, json, table, io, call) {
6946
6449
  const ops = await chantStackOps();
6947
- if (!ops) {
6948
- io.err(`${PRODUCT}: ${needsStackOpsMessage(`${PRODUCT} ${verb}`, "the gated stack operations (chant#4578)")}`);
6949
- return table.failed;
6950
- }
6951
6450
  try {
6952
6451
  return await call(ops);
6953
6452
  } catch (e) {
@@ -7058,7 +6557,7 @@ async function moveStack(args, json, table, io) {
7058
6557
  if (backend) return usage(backend, table, io);
7059
6558
  const rules = requestRules(read.config);
7060
6559
  const destName = destStackName(source, dest, read.config);
7061
- const engine2 = source.engine ?? dest.engine;
6560
+ const engine3 = source.engine ?? dest.engine;
7062
6561
  return runRequest("move", json, table, io, async (ops) => {
7063
6562
  const outcome = await ops.gatedStackMove({
7064
6563
  by: requesterOf(common.by, io.env),
@@ -7069,13 +6568,13 @@ async function moveStack(args, json, table, io) {
7069
6568
  source: { member: source.key, dir: source.dir, stack: source.stack },
7070
6569
  dest: { member: dest.key, dir: dest.dir, stack: destName },
7071
6570
  urns,
7072
- ...engine2 ? { engine: engine2 } : {}
6571
+ ...engine3 ? { engine: engine3 } : {}
7073
6572
  });
7074
6573
  return reportOutcome("move", outcome, `move:${source.stack}:${destName}`, rules, (r) => STACK_OP_WORDS.moved(source.key, dest.key, r), json, table, io);
7075
6574
  });
7076
6575
  }
7077
- async function moveOntoPlunge(engine2, args, json, table, io) {
7078
- if (engine2 !== "plunge") return usage(`--onto takes plunge, not '${engine2}'; a move between backends is an export and an import (see '${PRODUCT} export')`, table, io);
6576
+ async function moveOntoPlunge(engine3, args, json, table, io) {
6577
+ if (engine3 !== "plunge") return usage(`--onto takes plunge, not '${engine3}'; a move between backends is an export and an import (see '${PRODUCT} export')`, table, io);
7079
6578
  const extra = args.filter((a) => a.startsWith("-"));
7080
6579
  if (extra.length) return usage(`move --onto does not take ${extra.join(" ")}`, table, io);
7081
6580
  const named = positionals(args);
@@ -7125,10 +6624,6 @@ async function rollbackStack(args, json, table, io) {
7125
6624
  argv = ["components", "rollback", env.value ?? environmentOf(stack.stack), "--component", slug(stack.key), ...digest.value ? ["--digest", digest.value] : []];
7126
6625
  }
7127
6626
  argv.push(...actor.value ? ["--actor", actor.value] : [], ...switches, ...json ? ["--json"] : []);
7128
- if (!await chantStackRollback()) {
7129
- io.err(`${PRODUCT}: ${needsStackOpsMessage(`${PRODUCT} rollback`, "rollback by release for stacks (chant#4542)")}`);
7130
- return table.failed;
7131
- }
7132
6627
  return withChant(`${PRODUCT} rollback`, table, io, async (chant) => {
7133
6628
  const code = await chant.runChantInProcess(argv, PRODUCT_WORDS);
7134
6629
  if (code === table.waiting && !json) io.err(STACK_OP_WORDS.rollbackWaiting);
@@ -7146,7 +6641,6 @@ function chantStackAction() {
7146
6641
  }
7147
6642
  async function fromEvent(env) {
7148
6643
  const chant = await chantProduct();
7149
- if (!chant) return { error: `${PRODUCT} action --from-event reads the action job's comment or inputs through chant's productActionFromEnv, and chant's product entry did not load` };
7150
6644
  const words = chant.assertProductWords({ ...PRODUCT_WORDS, commands: { ...PRODUCT_WORDS.commands, ...ACTION_COMMAND } });
7151
6645
  const asked = chant.productActionFromEnv(words, env);
7152
6646
  if (asked.kind === "refused") return { error: `the action job was asked for no action: ${asked.reason}` };
@@ -7217,6 +6711,7 @@ async function action(args, json, table, io) {
7217
6711
  }
7218
6712
 
7219
6713
  // src/audit.ts
6714
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7220
6715
  init_detect();
7221
6716
  init_stages();
7222
6717
  init_words();
@@ -7275,6 +6770,7 @@ init_agents2();
7275
6770
  init_drift2();
7276
6771
 
7277
6772
  // src/estate.ts
6773
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7278
6774
  init_chant_product();
7279
6775
  init_chant_config();
7280
6776
  init_config();
@@ -7284,19 +6780,12 @@ import { dirname as dirname12 } from "node:path";
7284
6780
  var estateModule;
7285
6781
  function chantEstate() {
7286
6782
  estateModule ??= (async () => {
7287
- try {
7288
- const m = await import("@intentius/chant/ci-estate");
7289
- const r = await import("@intentius/chant/pr-report-url");
7290
- if (!["readEstateLedger", "buildLedgerEstate", "writeEstate"].every((f) => typeof m[f] === "function")) return void 0;
7291
- if (typeof r.parseReportUrl !== "function" || typeof r.reportPut !== "function") return void 0;
7292
- return { ...m, parseReportUrl: r.parseReportUrl, reportPut: r.reportPut };
7293
- } catch {
7294
- return void 0;
7295
- }
6783
+ const m = await import("@intentius/chant/ci-estate");
6784
+ const r = await import("@intentius/chant/pr-report-url");
6785
+ return { readEstateLedger: m.readEstateLedger, buildLedgerEstate: m.buildLedgerEstate, writeEstate: m.writeEstate, parseReportUrl: r.parseReportUrl, reportPut: r.reportPut };
7296
6786
  })();
7297
6787
  return estateModule;
7298
6788
  }
7299
- var ESTATE_NEEDS_CHANT = `${PRODUCT} estate needs chant >= the release after 0.124.54, which carries @intentius/chant/ci-estate (chant#4612). Run '${PRODUCT} audit' for the ledger's rows on this chant.`;
7300
6789
  function estateSchedules(config2) {
7301
6790
  const drift3 = toChantSettings(config2).pipeline.drift;
7302
6791
  const driftOps = (drift3?.stacks ?? []).map(driftOpName);
@@ -7315,11 +6804,6 @@ async function estate(args, json, table, io) {
7315
6804
  }
7316
6805
  if (rest.length) return usage(`estate does not take ${rest.join(" ")}`, table, io);
7317
6806
  const chant = await chantEstate();
7318
- if (!chant) {
7319
- if (json) io.out(JSON.stringify({ command: "estate", ok: false, exitCode: table.failed, error: ESTATE_NEEDS_CHANT }));
7320
- else io.err(`${PRODUCT}: ${ESTATE_NEEDS_CHANT}`);
7321
- return table.failed;
7322
- }
7323
6807
  const path = flags["--file"] ?? findConfig(io.cwd);
7324
6808
  let config2;
7325
6809
  if (path) {
@@ -7335,8 +6819,7 @@ async function estate(args, json, table, io) {
7335
6819
  }
7336
6820
  }
7337
6821
  const { schedules, driftOps } = config2 ? estateSchedules(config2) : { schedules: [], driftOps: [] };
7338
- const product = await chantProduct();
7339
- const words = product ? product.assertProductWords(PRODUCT_WORDS) : PRODUCT_WORDS;
6822
+ const words = (await chantProduct()).assertProductWords(PRODUCT_WORDS);
7340
6823
  const root = path ? dirname12(path) : io.cwd;
7341
6824
  const load = await chant.readEstateLedger(root);
7342
6825
  const built = chant.buildLedgerEstate({ load, words, schedules, ...driftOps.length ? { driftOps } : {} });
@@ -7361,79 +6844,37 @@ async function estate(args, json, table, io) {
7361
6844
  }
7362
6845
 
7363
6846
  // src/notify.ts
6847
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7364
6848
  init_stages();
7365
6849
  init_words();
7366
- var RELAY_NEEDS_CHANT = `${PRODUCT} notify relay needs chant >= the release after 0.124.54, which carries chant notify relay (chant#4611).`;
7367
6850
  async function notify(args, json, table, io) {
7368
6851
  if (args[0] !== "relay") return usage(`unknown notify command '${args[0] ?? ""}'. Run '${PRODUCT} notify relay'.`, table, io);
7369
- return withChant(`${PRODUCT} notify relay`, table, io, (chant) => {
7370
- if (typeof chant.startRelayServer !== "function") {
7371
- if (json) io.out(JSON.stringify({ command: "notify relay", ok: false, exitCode: table.failed, error: RELAY_NEEDS_CHANT }));
7372
- else io.err(`${PRODUCT}: ${RELAY_NEEDS_CHANT}`);
7373
- return Promise.resolve(table.failed);
7374
- }
7375
- return chant.runChantInProcess(["notify", ...args], PRODUCT_WORDS);
7376
- });
6852
+ return withChant(`${PRODUCT} notify relay`, table, io, (chant) => chant.runChantInProcess(["notify", ...args], PRODUCT_WORDS));
7377
6853
  }
7378
6854
 
7379
- // src/cli.ts
7380
- init_policy();
7381
-
7382
6855
  // src/guard.ts
6856
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7383
6857
  init_stages();
7384
6858
  init_words();
7385
- import { chmodSync, copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
7386
6859
  import { join as join17 } from "node:path";
7387
- var GUARDED = {
7388
- pulumi: ["up", "update", "destroy"],
7389
- plunge: ["up", "update", "destroy"]
7390
- };
7391
- var VALUED_GLOBALS = /* @__PURE__ */ new Set(["-C", "--cwd", "-s", "--stack", "--color", "--config-file", "--logflow", "-v", "--verbose", "--tracing", "--profiling", "--memprofilerate"]);
7392
- var LAUNCHERS = /* @__PURE__ */ new Set(["env", "npx", "sudo", "command", "exec", "nohup", "time", "xargs"]);
7393
- function segments(line) {
7394
- return line.split(/\|\||&&|[;|\n`]|\$\(|\)/).map((seg) => (seg.match(/'[^']*'|"[^"]*"|\S+/g) ?? []).map((t) => t.replace(/^['"]|['"]$/g, ""))).filter((t) => t.length > 0);
7395
- }
7396
- function guardVerdict(line) {
7397
- for (const words of segments(line)) {
7398
- let i = 0;
7399
- while (i < words.length && (LAUNCHERS.has(words[i]) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i]) || words[i - 1] === "env" && words[i].startsWith("-"))) i++;
7400
- const bin = (words[i] ?? "").split("/").pop();
7401
- const refused = GUARDED[bin];
7402
- if (!refused) continue;
7403
- for (let j = i + 1; j < words.length; j++) {
7404
- const w = words[j];
7405
- if (w.startsWith("-")) {
7406
- if (VALUED_GLOBALS.has(w)) j++;
7407
- continue;
7408
- }
7409
- if (refused.includes(w)) {
7410
- return {
7411
- engine: bin,
7412
- subcommand: w,
7413
- message: `${PRODUCT} guard: an agent session does not run '${bin} ${w}'. Open a pull request; ${PRODUCT} previews it, and the update runs after a person approves the digest.`
7414
- };
7415
- }
7416
- break;
7417
- }
6860
+ import { agentGuard, installAgentGuard } from "@intentius/chant/ci-product";
6861
+ var engine2 = { refuse: ["up", "update", "destroy"], valuedFlags: ["-C", "--cwd", "-s", "--stack", "--color", "--config-file", "--logflow", "-v", "--verbose", "--tracing", "--profiling", "--memprofilerate"] };
6862
+ var GUARD = {
6863
+ words: { product: PRODUCT, refusal: `an agent session does not run '{what}'. Open a pull request; ${PRODUCT} previews it, and the update runs after a person approves the digest.` },
6864
+ rules: {
6865
+ engines: { pulumi: engine2, plunge: engine2 },
6866
+ product: { commands: [["up"], ["resume"], ["approve"], ["override"], ["cancel"], ["move"], ["rollback"], ["action"]], flagValues: [["--respond", "up"]] },
6867
+ wrapperExit: 1
7418
6868
  }
7419
- return void 0;
7420
- }
7421
- var GUARD_TEMPLATES = new URL("../templates/agent-guard/", import.meta.url);
7422
- var HOOK_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/plywood-guard.mjs"';
7423
- function withGuardHook(settings) {
7424
- const hooks = typeof settings.hooks === "object" && settings.hooks !== null ? settings.hooks : {};
7425
- const pre = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
7426
- const has = JSON.stringify(pre).includes("plywood-guard.mjs");
7427
- if (!has) pre.push({ matcher: "Bash", hooks: [{ type: "command", command: HOOK_COMMAND }] });
7428
- return { ...settings, hooks: { ...hooks, PreToolUse: pre } };
7429
- }
6869
+ };
7430
6870
  async function guard(args, json, table, io) {
7431
6871
  const [sub, ...rest] = args;
7432
6872
  if (sub === "check") {
7433
6873
  const line = (rest[0] === "--" ? rest.slice(1) : rest).join(" ");
7434
6874
  if (!line.trim()) return usage("guard check takes the command line to check", table, io);
7435
- const v = guardVerdict(line);
7436
- if (json) io.out(JSON.stringify({ command: "guard check", ok: !v, exitCode: v ? table.denied : table.done, refused: v ?? null }));
6875
+ const v = agentGuard(GUARD, { repo: io.cwd }).checkCommand(line, io.cwd);
6876
+ const refused = v ? { engine: v.bin, subcommand: v.command, message: v.message } : null;
6877
+ if (json) io.out(JSON.stringify({ command: "guard check", ok: !v, exitCode: v ? table.denied : table.done, refused }));
7437
6878
  else io.out(v ? v.message : `${PRODUCT} guard: '${line}' runs`);
7438
6879
  return v ? table.denied : table.done;
7439
6880
  }
@@ -7442,34 +6883,16 @@ async function guard(args, json, table, io) {
7442
6883
  if (dirFlag.missing) return usage("--dir takes a directory", table, io);
7443
6884
  if (dirFlag.rest.length) return usage(`guard install does not take ${dirFlag.rest.join(" ")}`, table, io);
7444
6885
  const root = dirFlag.value ? join17(io.cwd, dirFlag.value) : io.cwd;
7445
- const written = [];
7446
- const hookDir = join17(root, ".claude/hooks");
7447
- mkdirSync7(hookDir, { recursive: true });
7448
- copyFileSync(new URL("plywood-guard.mjs", GUARD_TEMPLATES), join17(hookDir, "plywood-guard.mjs"));
7449
- written.push(".claude/hooks/plywood-guard.mjs");
7450
- const settingsFile = join17(root, ".claude/settings.json");
7451
- let settings = {};
7452
- if (existsSync14(settingsFile)) {
7453
- try {
7454
- settings = JSON.parse(readFileSync15(settingsFile, "utf8"));
7455
- } catch (e) {
7456
- return usage(`.claude/settings.json is not JSON (${e.message}); fix it, then run this again`, table, io);
7457
- }
7458
- }
7459
- writeFileSync8(settingsFile, JSON.stringify(withGuardHook(settings), null, 2) + "\n");
7460
- written.push(".claude/settings.json");
7461
- const binDir = join17(root, ".plywood/agent-bin");
7462
- mkdirSync7(binDir, { recursive: true });
7463
- for (const name of Object.keys(GUARDED)) {
7464
- const to = join17(binDir, name);
7465
- copyFileSync(new URL(`bin/${name}`, GUARD_TEMPLATES), to);
7466
- chmodSync(to, 493);
7467
- written.push(`.plywood/agent-bin/${name}`);
6886
+ let written;
6887
+ try {
6888
+ written = installAgentGuard({ ...GUARD, repo: root, install: "project" }).written;
6889
+ } catch (e) {
6890
+ return usage(e.message, table, io);
7468
6891
  }
7469
6892
  if (json) io.out(JSON.stringify({ command: "guard install", ok: true, exitCode: table.done, written }));
7470
6893
  else {
7471
6894
  for (const f of written) io.err(`${PRODUCT} guard: wrote ${f}`);
7472
- io.err(`${PRODUCT} guard: an agent session that runs outside Claude Code puts .plywood/agent-bin first on PATH and sets PLYWOOD_AGENT_GUARD=1`);
6895
+ io.err(`${PRODUCT} guard: an agent session that runs outside Claude Code puts .plywood/agent-bin first on PATH`);
7473
6896
  }
7474
6897
  return table.done;
7475
6898
  }
@@ -7477,6 +6900,7 @@ async function guard(args, json, table, io) {
7477
6900
  }
7478
6901
 
7479
6902
  // src/identities.ts
6903
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7480
6904
  init_jobs2();
7481
6905
  init_chant_config();
7482
6906
  init_identity();
@@ -7484,7 +6908,7 @@ init_config();
7484
6908
  init_stacks();
7485
6909
  init_stages();
7486
6910
  init_words();
7487
- import { existsSync as existsSync15, readFileSync as readFileSync16 } from "node:fs";
6911
+ import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
7488
6912
  import { dirname as dirname13, join as join18 } from "node:path";
7489
6913
  import { targetGlobMatch } from "@intentius/chant/ci-identity";
7490
6914
  function identityRows(config2, env) {
@@ -7547,28 +6971,21 @@ function configIdentities(args, json, table, io) {
7547
6971
  else for (const line of identityLines(config2, environment, rows)) io.out(line);
7548
6972
  return table.done;
7549
6973
  }
7550
- var FORGE_CHECK_MESSAGE = `${PRODUCT} forge check holds the identities next to the forge's environments, protected branches and each cloud's trust policy. The read is chant's (@intentius/chant/forge-check, chant#4608), and the installed chant lacks it; it comes with the chant release after 0.124.54. Run '${PRODUCT} config identities' for what the jobs will hold.`;
7551
6974
  var FORGES2 = ["github", "gitlab", "forgejo"];
7552
- var FORGE_CHECK_FUNCTIONS = ["forgeCheck", "forgeCheckExit", "describeForgeCheck", "forgeCheckTarget", "pipelineContract"];
7553
6975
  var forgeCheckModule;
7554
6976
  function chantForgeCheck() {
7555
6977
  forgeCheckModule ??= (async () => {
7556
- try {
7557
- const fc = await import("@intentius/chant/forge-check");
7558
- const id = await import("@intentius/chant/ci-identity");
7559
- if (!FORGE_CHECK_FUNCTIONS.every((f) => typeof fc[f] === "function") || typeof id.resolveCloudIdentity !== "function") return void 0;
7560
- return { ...fc, resolveCloudIdentity: id.resolveCloudIdentity };
7561
- } catch {
7562
- return void 0;
7563
- }
6978
+ const fc = await import("@intentius/chant/forge-check");
6979
+ const { resolveCloudIdentity } = await import("@intentius/chant/ci-identity");
6980
+ return { ...fc, resolveCloudIdentity };
7564
6981
  })();
7565
6982
  return forgeCheckModule;
7566
6983
  }
7567
6984
  function workspaceMembers(root) {
7568
6985
  const file = join18(root, "chant.workspace.json");
7569
- if (!existsSync15(file)) return [];
6986
+ if (!existsSync14(file)) return [];
7570
6987
  try {
7571
- const doc = JSON.parse(readFileSync16(file, "utf8"));
6988
+ const doc = JSON.parse(readFileSync15(file, "utf8"));
7572
6989
  return (doc.members ?? []).map((m) => m.name).filter((n) => typeof n === "string");
7573
6990
  } catch {
7574
6991
  return [];
@@ -7597,11 +7014,6 @@ function contractPipelines(fc, cloud, env, members, runner, jobs2) {
7597
7014
  async function forgeCheck(args, json, table, io) {
7598
7015
  if (args[0] !== "check") return usage(`unknown forge command '${args[0] ?? ""}'. Run '${PRODUCT} forge check'.`, table, io);
7599
7016
  const fc = await chantForgeCheck();
7600
- if (!fc) {
7601
- if (json) io.out(JSON.stringify({ command: "forge check", ok: false, exitCode: table.failed, error: FORGE_CHECK_MESSAGE }));
7602
- else io.err(`${PRODUCT}: ${FORGE_CHECK_MESSAGE}`);
7603
- return table.failed;
7604
- }
7605
7017
  const requireClouds = args.includes("--require-clouds");
7606
7018
  let rest = args.slice(1).filter((a) => a !== "--require-clouds");
7607
7019
  const flags = {};
@@ -7671,6 +7083,7 @@ async function forgeCheck(args, json, table, io) {
7671
7083
  init_control2();
7672
7084
 
7673
7085
  // src/telemetry.ts
7086
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7674
7087
  init_chant_product();
7675
7088
  init_observe();
7676
7089
  init_config();
@@ -7699,8 +7112,7 @@ async function applyTelemetry(cwd, env = process.env) {
7699
7112
  return {};
7700
7113
  }
7701
7114
  Object.assign(env, added);
7702
- const chant = await chantProduct();
7703
- if (chant && typeof chant.configureOtlp === "function") chant.configureOtlp({ prefix: PRODUCT, env });
7115
+ (await chantProduct()).configureOtlp({ prefix: PRODUCT, env });
7704
7116
  return added;
7705
7117
  }
7706
7118
 
@@ -7708,16 +7120,16 @@ async function applyTelemetry(cwd, env = process.env) {
7708
7120
  init_init();
7709
7121
  init_stages();
7710
7122
  init_tips();
7711
- init_images();
7712
7123
  init_up();
7713
7124
 
7714
7125
  // src/upgrade.ts
7126
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7715
7127
  init_config();
7716
7128
  init_images();
7717
7129
  init_stages();
7718
7130
  init_words();
7719
7131
  import { execFile } from "node:child_process";
7720
- import { existsSync as existsSync16, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "node:fs";
7132
+ import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "node:fs";
7721
7133
  import { dirname as dirname14, join as join19, relative as relative2, resolve as resolve3 } from "node:path";
7722
7134
  var PLYWOOD_PACKAGE = "@intentius/plywood";
7723
7135
  var CHANT_PACKAGE = "@intentius/chant";
@@ -7750,9 +7162,9 @@ function findPlywoodPackage(dir) {
7750
7162
  let cur = resolve3(dir);
7751
7163
  for (let depth = 0; depth < 32; depth++) {
7752
7164
  const file = join19(cur, "package.json");
7753
- if (existsSync16(file)) {
7165
+ if (existsSync15(file)) {
7754
7166
  try {
7755
- const pkg = JSON.parse(readFileSync17(file, "utf8"));
7167
+ const pkg = JSON.parse(readFileSync16(file, "utf8"));
7756
7168
  if (DEP_FIELDS.some((f) => isObject2(pkg[f]) && typeof pkg[f][PLYWOOD_PACKAGE] === "string")) return file;
7757
7169
  } catch {
7758
7170
  }
@@ -7850,10 +7262,10 @@ async function upgrade(args, json, table, io) {
7850
7262
  const release2 = await viewRelease(exec, pkgDir, io.env, opts.version);
7851
7263
  if ("error" in release2) return fail(release2.error);
7852
7264
  const chant = opts.chant ?? release2.chant;
7853
- const pkgText = readFileSync17(pkgFile, "utf8");
7265
+ const pkgText = readFileSync16(pkgFile, "utf8");
7854
7266
  const pkgMove = movePackagePins(pkgText, rel(pkgFile), release2.version, chant);
7855
7267
  const declFile = join19(configDir, "chant.workspace.json");
7856
- const declText = existsSync16(declFile) ? readFileSync17(declFile, "utf8") : void 0;
7268
+ const declText = existsSync15(declFile) ? readFileSync16(declFile, "utf8") : void 0;
7857
7269
  let declMove;
7858
7270
  if (declText !== void 0) {
7859
7271
  try {
@@ -7863,7 +7275,7 @@ async function upgrade(args, json, table, io) {
7863
7275
  }
7864
7276
  }
7865
7277
  const notes = [...pkgMove.left];
7866
- if (existsSync16(join19(configDir, "chant.workspace.jsonc"))) notes.push(`chant.workspace.jsonc is yours: move its @intentius/chant* pins to ${chant} by hand`);
7278
+ if (existsSync15(join19(configDir, "chant.workspace.jsonc"))) notes.push(`chant.workspace.jsonc is yours: move its @intentius/chant* pins to ${chant} by hand`);
7867
7279
  if (configFile) {
7868
7280
  try {
7869
7281
  const note = imageNote(loadConfig(configFile).doc, release2.version);
@@ -7892,8 +7304,8 @@ async function upgrade(args, json, table, io) {
7892
7304
  if (json) io.out(JSON.stringify({ ...envelope, ok: true, exitCode: table.done, steps }));
7893
7305
  return table.done;
7894
7306
  }
7895
- if (pkgMove.text !== pkgText) writeFileSync9(pkgFile, pkgMove.text);
7896
- if (declMove && declText !== void 0 && declMove.text !== declText) writeFileSync9(declFile, declMove.text);
7307
+ if (pkgMove.text !== pkgText) writeFileSync8(pkgFile, pkgMove.text);
7308
+ if (declMove && declText !== void 0 && declMove.text !== declText) writeFileSync8(declFile, declMove.text);
7897
7309
  const files = [...new Set(moves.map((m) => m.file))];
7898
7310
  if (!opts.install) {
7899
7311
  say(` left to run: ${steps.install.command} (in ${steps.install.cwd}), then ${steps.ci.command} (in ${steps.ci.cwd})`);
@@ -7919,7 +7331,7 @@ ${installed2.stderr.trim()}`);
7919
7331
  for (const n of notes) say(` note: ${n}`);
7920
7332
  const ok = ci2 === void 0 || ci2.code === 0;
7921
7333
  const lockFile = join19(pkgDir, "package-lock.json");
7922
- const changed = [...files, ...existsSync16(lockFile) ? [rel(lockFile)] : []];
7334
+ const changed = [...files, ...existsSync15(lockFile) ? [rel(lockFile)] : []];
7923
7335
  if (ok) say(` Open one pull request with ${changed.join(", ")} and the files ${PRODUCT} ci wrote.`);
7924
7336
  else say(` ${PRODUCT} ci failed (exit ${ci2?.code}); the pins are moved and installed, so fix plywood.yml and run '${steps.ci.command}'`);
7925
7337
  const code = ok ? table.done : ci2?.code ?? table.failed;
@@ -7928,7 +7340,7 @@ ${installed2.stderr.trim()}`);
7928
7340
  }
7929
7341
  function installedVersion(dir) {
7930
7342
  try {
7931
- const pkg = JSON.parse(readFileSync17(join19(dir, "node_modules", PLYWOOD_PACKAGE, "package.json"), "utf8"));
7343
+ const pkg = JSON.parse(readFileSync16(join19(dir, "node_modules", PLYWOOD_PACKAGE, "package.json"), "utf8"));
7932
7344
  return typeof pkg.version === "string" ? pkg.version : void 0;
7933
7345
  } catch {
7934
7346
  return void 0;
@@ -7951,7 +7363,7 @@ var execChild = (bin, args, opts) => new Promise((done) => {
7951
7363
 
7952
7364
  // src/cli.ts
7953
7365
  init_words();
7954
- var VERSION3 = true ? "0.3.2" : "0.0.0-dev";
7366
+ var VERSION3 = true ? "0.3.5" : "0.0.0-dev";
7955
7367
  var stdio = {
7956
7368
  out: (s) => process.stdout.write(s + "\n"),
7957
7369
  err: (s) => process.stderr.write(s + "\n")
@@ -8042,7 +7454,7 @@ function helpText() {
8042
7454
  ].join("\n");
8043
7455
  }
8044
7456
  async function main(argv, io = stdio) {
8045
- const { table, source } = await exitCodes();
7457
+ const table = EXIT;
8046
7458
  const json = argv.includes("--json");
8047
7459
  const args = argv.filter((a) => a !== "--json");
8048
7460
  const verb = args[0];
@@ -8055,11 +7467,9 @@ async function main(argv, io = stdio) {
8055
7467
  return table.done;
8056
7468
  }
8057
7469
  if (verb === "--version" || verb === "-v") {
8058
- emit({ command: "version", ok: true, exitCode: table.done, version: VERSION3, exitCodes: source }, VERSION3);
7470
+ emit({ command: "version", ok: true, exitCode: table.done, version: VERSION3, exitCodes: "chant" }, VERSION3);
8059
7471
  return table.done;
8060
7472
  }
8061
- await useChantInstallLines();
8062
- await useChantPolicySource();
8063
7473
  const stageIo = { out: io.out, err: io.err, cwd: process.cwd(), env: process.env };
8064
7474
  if (verb !== "config" && verb !== "init" && verb !== "tips" && verb !== "upgrade") await applyTelemetry(stageIo.cwd, stageIo.env);
8065
7475
  if (verb === "audit") return audit2(args.slice(1), json, table, stageIo);