forgedock 1.40.0 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/bin/engine/outcome-codes.mjs +6 -3
  2. package/bin/engine-cli.mjs +40 -4
  3. package/bin/forgedock.mjs +22 -1
  4. package/bin/lean/batch.test.mjs +33 -1
  5. package/bin/lean/capwait.mjs +36 -0
  6. package/bin/lean/capwait.test.mjs +48 -0
  7. package/bin/lean/conformance.mjs +150 -2
  8. package/bin/lean/conformance.test.mjs +126 -0
  9. package/bin/lean/depcache.mjs +10 -2
  10. package/bin/lean/depcache.test.mjs +22 -17
  11. package/bin/lean/execution.mjs +21 -5
  12. package/bin/lean/execution.test.mjs +7 -1
  13. package/bin/lean/heavy.mjs +22 -1
  14. package/bin/lean/lanecheck.mjs +129 -0
  15. package/bin/lean/lanecheck.test.mjs +118 -0
  16. package/bin/lean/lanelock.mjs +31 -0
  17. package/bin/lean/lean.mjs +102 -30
  18. package/bin/lean/lean.test.mjs +173 -3
  19. package/bin/lean/orchestrate.mjs +42 -7
  20. package/bin/lean/phaseledger.mjs +116 -0
  21. package/bin/lean/phaseledger.test.mjs +72 -0
  22. package/bin/lean/prodwatch.mjs +12 -1
  23. package/bin/lean/prodwatch.test.mjs +32 -3
  24. package/bin/lean/prompts.mjs +19 -0
  25. package/bin/lean/render.mjs +10 -2
  26. package/bin/lean/sandbox-provider.contract.test.mjs +48 -0
  27. package/bin/lean/sandbox-remote.mjs +8 -1
  28. package/bin/lean/schedd.mjs +5 -1
  29. package/bin/lean/selftest-gate.mjs +46 -0
  30. package/bin/lean/selftest-gate.test.mjs +40 -0
  31. package/bin/lean/selftest-live.mjs +248 -0
  32. package/bin/lean/selftest-live.test.mjs +154 -0
  33. package/bin/lean/selftest-nightly.mjs +81 -0
  34. package/bin/lean/selftest-nightly.test.mjs +84 -0
  35. package/bin/lean/selftest-static.mjs +81 -0
  36. package/bin/lean/selftest-static.test.mjs +32 -0
  37. package/bin/pool/agent.mjs +16 -5
  38. package/bin/pool/router.mjs +17 -1
  39. package/bin/pool/warm.mjs +80 -0
  40. package/bin/review-staging.mjs +28 -6
  41. package/bin/stats/aggregate.mjs +2 -1
  42. package/bin/stats/efficiency.mjs +3 -1
  43. package/bin/stats/efficiency.test.mjs +4 -1
  44. package/bin/stats/github-source.mjs +2 -1
  45. package/bin/stats/local-source.mjs +1 -0
  46. package/bin/stats/render.mjs +2 -0
  47. package/commands/deploy-pr.md +3 -3
  48. package/commands/review-pr-staging.md +2 -2
  49. package/commands/work-on/build.md +2 -2
  50. package/docs/CONFIG.md +33 -0
  51. package/docs/OUTCOME-CODES.md +1 -1
  52. package/docs/PROD-WATCH.md +1 -1
  53. package/docs/RELEASE-NOTES.md +10 -0
  54. package/docs/SELFTEST.md +34 -0
  55. package/docs/eval/conformance-labels-v1.json +13 -0
  56. package/package.json +1 -1
  57. package/pool/image/Dockerfile +2 -1
  58. package/pool/image/entrypoint.sh +44 -11
  59. package/pool/image/warm-refresh.sh +14 -0
  60. package/scripts/conformance-calibrate.mjs +132 -0
  61. package/scripts/file-deferred-followups.sh +21 -8
  62. package/scripts/file-deferred-followups.test.sh +27 -6
@@ -81,6 +81,27 @@ describe("#4655 multi-requirement amendment references", () => {
81
81
  assert.match(applyAmendments(C, [{ kind: "defer", requirement: "R1/R9", why: "w" }]).problems[0], /unknown requirement R9/);
82
82
  assert.match(applyAmendments(C, [{ kind: "scope-note", requirement: "R1", why: "w" }]).problems[0], /unknown kind 'scope-note'; valid kinds are/);
83
83
  });
84
+ it("supersede-guard guards[] expands to per-guard supersedes", () => {
85
+ const a = applyAmendments(C, [{ kind: "supersede-guard", guards: ["#4879", "#4838"], why: "x" }]);
86
+ assert.deepEqual(a.problems, []);
87
+ assert.deepEqual(a.contract.supersedes, [{ guard: "#4879", why: "x" }, { guard: "#4838", why: "x" }]);
88
+ assert.equal(a.applied.length, 2);
89
+ });
90
+ it("synonym kinds and a supersedes amendment with guards[] expand alike; an invalid guard id is reported", () => {
91
+ for (const kind of ["supersede", "supersedes-guard", "supersedes"]) {
92
+ const a = applyAmendments(C, [{ kind, guards: ["4879", "#4838"], why: "x" }]);
93
+ assert.deepEqual(a.problems, [], kind);
94
+ assert.deepEqual(a.contract.supersedes.map((s) => s.guard), ["#4879", "#4838"], kind);
95
+ }
96
+ const bad = applyAmendments(C, [{ kind: "supersedes", guards: ["#4879", "abc"], why: "x" }]);
97
+ assert.equal(bad.problems.length, 1);
98
+ assert.match(bad.problems[0], /needs guard/);
99
+ assert.deepEqual(bad.contract.supersedes.map((s) => s.guard), ["#4879"]);
100
+ assert.match(applyAmendments(C, [{ kind: "supersede-guard", guards: [], why: "x" }]).problems[0], /needs guard/);
101
+ });
102
+ it("an unrelated unknown kind is still rejected with the valid-kinds message", () => {
103
+ assert.match(applyAmendments(C, [{ kind: "supersede-everything", guards: ["#1"], why: "w" }]).problems[0], /unknown kind 'supersede-everything'; valid kinds are/);
104
+ });
84
105
  });
85
106
 
86
107
  describe("#4574 amendment slips", () => {
@@ -141,6 +162,9 @@ describe("precheck (deterministic, no reviewer)", () => {
141
162
  assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, ciVerified: true, validateRuns: { V1: { rc: 127, output: "bash: pytest: command not found" } } })), [], "CI-verified head: like a validate left to CI");
142
163
  assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, validateRuns: { V1: { rc: 0, output: "" } } })), [], "a passing rerun counts as passed");
143
164
  assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, validateRuns: { V1: { rc: 1, output: "AssertionError: 1 !== 2" } } })), ["conformance.unimplemented"]);
165
+ // #4906: a rerun that timed out (rc 124) is infrastructure, never conformance.unimplemented
166
+ assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, validateRuns: { V1: { rc: 124, output: "(timed out)" } } })), ["env.validate_timeout"]);
167
+ assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, ciVerified: true, validateRuns: { V1: { rc: 124, output: "" } } })), [], "CI-verified head covers a timed-out rerun");
144
168
  const local = precheck({ contract: C, result: failed, changedFiles: files, repoHas: repoTracks(["src/leak/__init__.py"]), validateRuns: { V1: { rc: 1, output: "ModuleNotFoundError: No module named 'leak.core'" } } });
145
169
  assert.deepEqual(codes(local), ["conformance.unimplemented"], "a repo module the change did not create is a code defect");
146
170
  });
@@ -866,3 +890,105 @@ describe("implied docs-freshness deliverable", () => {
866
890
  assert.deepEqual(impliedDocDeliverables({ before: [...base, "docs/guide.md"], after, contract: C, wasFailing: true, nowOk: true }), []);
867
891
  });
868
892
  });
893
+
894
+ import { normalizeKindModes, blockingFailures, unverifiedRequirements, KIND_MODES } from "./conformance.mjs";
895
+
896
+ describe("per-kind modes and requirement evidence", () => {
897
+ const f = (code) => ({ code });
898
+ const ALLOW = ["conformance.unimplemented"];
899
+ it("normalizeKindModes keeps valid entries from a map or the flat forge.yaml string", () => {
900
+ assert.deepEqual(KIND_MODES, ["enforce", "shadow"]);
901
+ assert.deepEqual(normalizeKindModes({ "a.b": "shadow", c: "bogus", d: "enforce" }), { "a.b": "shadow", d: "enforce" });
902
+ assert.deepEqual(normalizeKindModes("a.b=shadow, c.d = enforce, x=nope"), { "a.b": "shadow", "c.d": "enforce" });
903
+ assert.deepEqual(normalizeKindModes(null), {});
904
+ });
905
+ it("a shadow kind is recorded and an enforce kind blocks, in either lane mode", () => {
906
+ const kinds = { "k.shadow": "shadow", "k.enforce": "enforce" };
907
+ for (const shadow of [true, false]) {
908
+ const r = blockingFailures([f("k.shadow"), f("k.enforce")], { shadow, kinds });
909
+ assert.deepEqual([r.block.map((x) => x.code), r.record.map((x) => x.code)], [["k.enforce"], ["k.shadow"]]);
910
+ }
911
+ });
912
+ it("kinds without their own mode follow the lane: enforce blocks all, shadow only the allowlist", () => {
913
+ assert.equal(blockingFailures([f("x"), f("conformance.unimplemented")], { shadow: false }).block.length, 2);
914
+ const r = blockingFailures([f("x"), f("conformance.unimplemented")], { shadow: true, allow: ALLOW });
915
+ assert.deepEqual([r.block.map((x) => x.code), r.record.map((x) => x.code)], [["conformance.unimplemented"], ["x"]]);
916
+ assert.equal(blockingFailures([f("conformance.unimplemented")], { shadow: true, allow: ALLOW, kinds: { "conformance.unimplemented": "shadow" } }).block.length, 0);
917
+ });
918
+ it("a deliver requirement with no executed evidence test is unverified, with its id", () => {
919
+ const c = {
920
+ requirements: [{ id: "R1", disposition: "deliver" }, { id: "R2", disposition: "deliver" }, { id: "R3", disposition: "deliver" }, { id: "R4", disposition: "defer" }, { id: "R5", disposition: "deliver", verify: "out_of_band" }],
921
+ deliverables: [{ id: "D1", path: "src/a.js", requirements: ["R1"] }, { id: "D2", path: "test/a.test.js", requirements: ["R1"] }, { id: "D3", path: "test/b.test.js", requirements: ["R2"] }],
922
+ validate: [{ id: "V1", command: "node --test test/a.test.js" }, { id: "V2", command: "node --test test/b.test.js" }],
923
+ };
924
+ const u = (o) => unverifiedRequirements(c, o).map((x) => x.id);
925
+ assert.deepEqual(u({ runs: [{ id: "V1", exit: 0 }, { id: "V2", exit: 1 }] }), ["R2", "R3"]);
926
+ assert.deepEqual(u({ runs: [{ id: "V1", exit: 1 }], reruns: { V1: { rc: 0 }, V2: { rc: 0 } } }), ["R3"]);
927
+ assert.deepEqual(u({ runs: [], ciCovered: ["V1", "V2"] }), ["R3"]);
928
+ assert.deepEqual(u({ runs: [{ id: "V1", exit: 0 }, { id: "V2", exit: 0 }], reruns: { V2: { rc: 1 } } }), ["R2", "R3"]);
929
+ });
930
+ it("formatShadowSummary adds per-kind shares only when rows carry kinds", () => {
931
+ const rows = [{ conformance: { mode: "shadow", would: "nonconforming", kinds: ["a.b"] } }, { conformance: { mode: "shadow", would: "conforms", kinds: [] } }];
932
+ assert.match(formatShadowSummary(rows), /; per kind: a\.b 1\/2 \(50%\)$/);
933
+ assert.doesNotMatch(formatShadowSummary([{ conformance: { mode: "shadow", would: "conforms" } }]), /per kind/);
934
+ });
935
+ });
936
+
937
+ import { mutationTargets, mutationFailures, runMutationCheck, MUTATION_SURVIVED } from "./conformance.mjs";
938
+
939
+ describe("mutation check (#4913)", () => {
940
+ const c = {
941
+ requirements: [{ id: "R1", quote: "q1", disposition: "deliver" }, { id: "R2", quote: "q2", disposition: "deliver" }, { id: "R3", quote: "q3", disposition: "deliver", verify: "out_of_band", evidence: "job" }],
942
+ deliverables: [{ id: "D1", path: "test/a.test.js", requirements: ["R1", "R3"] }, { id: "D2", path: "src/b.js", requirements: ["R2"] }],
943
+ validate: [{ id: "V1", command: "node --test test/a.test.js" }],
944
+ };
945
+ const green = { runs: [{ id: "V1", exit: 0 }] };
946
+ it("targets only deliver requirements whose evidence test ran green", () => {
947
+ assert.deepEqual(mutationTargets(c, green), [{ id: "R1", commands: [{ id: "V1", command: "node --test test/a.test.js" }] }]);
948
+ assert.deepEqual(mutationTargets(c, { runs: [{ id: "V1", exit: 1 }] }), []);
949
+ });
950
+ it("only a survivor fails; killed and skipped never do", () => {
951
+ const f = mutationFailures([{ id: "R1", command: "x", status: "survived" }, { id: "R2", command: "y", status: "killed" }, { id: "R4", command: "z", status: "skipped" }]);
952
+ assert.equal(f.length, 1); assert.equal(f[0].code, MUTATION_SURVIVED); assert.match(f[0].detail, /^R1 /);
953
+ });
954
+ const harness = (o = {}) => {
955
+ const calls = [], fs = { writeFileSync() {}, rmSync() {} };
956
+ let restored = false;
957
+ const git = async (a) => { calls.push(a.join(" ")); if (a[0] === "reset") restored = true; if (a[0] === "status") return restored ? (o.stillDirty ?? "") : (o.dirty ?? ""); if (a[0] === "apply" && o.applyFails) throw new Error("no"); return "patch"; };
958
+ const runCommand = async () => { calls.push("run"); if (o.throws) throw new Error("boom"); return { rc: o.rc ?? 0 }; };
959
+ const targets = mutationTargets(c, green);
960
+ return { calls, go: () => runMutationCheck({ targets, files: ["src/b.js", "test/a.test.js"], base: "origin/staging", git, runCommand, cwd: "/w", fs, tmp: "/t" }) };
961
+ };
962
+ it("reverts only non-test files, reruns, then restores from HEAD (reset + clean of those paths)", async () => {
963
+ const h = harness(); const r = await h.go();
964
+ assert.deepEqual(r.results.map((x) => x.status), ["survived"]);
965
+ assert.ok(h.calls.some((x) => x.startsWith("diff --binary origin/staging...HEAD -- src/b.js") && !x.includes("test/a")));
966
+ assert.deepEqual(h.calls.filter((x) => /^(apply|run|reset|clean)/.test(x)), ["apply -R --whitespace=nowarn /t/mutation-" + process.pid + ".patch", "run", "reset -q --hard HEAD", "clean -fdq -- src/b.js"]);
967
+ });
968
+ it("a failing rerun is killed; the tree is restored even when the command throws", async () => {
969
+ assert.equal((await harness({ rc: 1 }).go()).results[0].status, "killed");
970
+ const h = harness({ throws: true }); assert.equal((await h.go()).results[0].status, "skipped");
971
+ assert.equal(h.calls.at(-1), "status --porcelain -- src/b.js");
972
+ });
973
+ it("a dirty tree or a failed revert skips without running anything or restoring", async () => {
974
+ for (const o of [{ dirty: " M src/b.js" }, { applyFails: true }]) { const h = harness(o); const r = await h.go(); assert.deepEqual(r.results, []); assert.ok(r.skipped); assert.ok(!h.calls.includes("run") && !h.calls.some((x) => x.startsWith("reset"))); }
975
+ });
976
+ it("#4931: a tree still dirty after the restore throws instead of returning (never push the reverted change)", async () => {
977
+ await assert.rejects(harness({ stillDirty: "?? src/b.js" }).go(), /could not restore the worktree: \?\? src\/b\.js/);
978
+ });
979
+ it("#4931: a real repo whose change deleted a non-test file is fully restored", async () => {
980
+ const { execFileSync } = await import("node:child_process");
981
+ const { writeFileSync, existsSync, readFileSync } = await import("node:fs");
982
+ const { join } = await import("node:path");
983
+ const { tmpDir } = await import("../tests/_tmp.mjs");
984
+ const d = tmpDir("mut-");
985
+ const g = (...a) => execFileSync("git", ["-C", d, "-c", "user.email=t@t", "-c", "user.name=t", ...a], { encoding: "utf8" });
986
+ g("init", "-q", "-b", "main"); writeFileSync(join(d, "a.js"), "a1\n"); writeFileSync(join(d, "b.js"), "b\n"); g("add", "."); g("commit", "-qm", "base");
987
+ g("checkout", "-qb", "lane"); writeFileSync(join(d, "a.js"), "a2\n"); g("rm", "-q", "b.js"); g("add", "a.js"); g("commit", "-qm", "change");
988
+ const r = await runMutationCheck({ targets: [{ id: "R1", commands: [{ id: "V1", command: "t" }] }], files: ["a.js", "b.js"], base: "main",
989
+ git: async (a) => g(...a), runCommand: async () => ({ rc: 1 }), cwd: d, tmp: d });
990
+ assert.equal(r.skipped, "", r.skipped); assert.equal(r.results[0].status, "killed");
991
+ assert.equal(readFileSync(join(d, "a.js"), "utf8"), "a2\n"); assert.equal(existsSync(join(d, "b.js")), false);
992
+ assert.equal(g("status", "--porcelain").trim(), "");
993
+ });
994
+ });
@@ -146,7 +146,15 @@ export function makeDepCache({ root = depcacheRoot(), exec, fs = nodeFs, now = (
146
146
 
147
147
  // ---- trees ----
148
148
  const boundary = (p) => new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?=$|[/\\\\'\"\\s:;])", "g");
149
- /** Hardlink `src` into `dst`; symlinks recreated; files in bin dirs that name `from` are copied with `to` instead. */
149
+ /** Hardlink a file; across filesystems (EXDEV, #4909) copy it with its mode so the cache is reused instead of reinstalled. */
150
+ function linkOrCopy(sp, dp) {
151
+ try { fs.linkSync(sp, dp); } catch (e) {
152
+ if (e?.code !== "EXDEV") throw e;
153
+ fs.copyFileSync(sp, dp);
154
+ fs.chmodSync(dp, fs.statSync(sp).mode & 0o7777);
155
+ }
156
+ }
157
+ /** Hardlink `src` into `dst` (copy on EXDEV); symlinks recreated; files in bin dirs that name `from` are copied with `to` instead. */
150
158
  function linkTree(src, dst, { from = null, to = null, binDir = () => false } = {}) {
151
159
  const re = from && to && from !== to ? boundary(from) : null;
152
160
  const walk = (s, d, rel) => {
@@ -165,7 +173,7 @@ export function makeDepCache({ root = depcacheRoot(), exec, fs = nodeFs, now = (
165
173
  continue;
166
174
  }
167
175
  }
168
- fs.linkSync(sp, dp);
176
+ linkOrCopy(sp, dp);
169
177
  }
170
178
  }
171
179
  };
@@ -272,23 +272,12 @@ describe("depcache (#4220): shared dependency installs keyed by lock hash", () =
272
272
  assert.ok(!existsSync(join(wt, "node_modules")));
273
273
  assert.equal(snapDirs(s.root, "pnpm").length, 0);
274
274
 
275
- const s2 = setup("npm");
276
- const exdev = { ...fs, linkSync: () => { throw Object.assign(new Error("EXDEV: cross-device link"), { code: "EXDEV" }); } };
277
- const c2 = makeDepCache({ root: s2.root, exec: s2.f.exec, env: {}, fs: exdev });
278
- const wt2 = s2.lane("y");
279
- const [r2] = await c2.prepare({ wt: wt2, repoKey: "r" });
280
- assert.equal(r2.status, "miss-installed");
281
- assert.equal(r2.published, false);
282
- assert.match(r2.reason, /EXDEV/);
283
- assert.ok(existsSync(join(wt2, "node_modules/left-pad/index.js")), "the lane keeps its own install");
284
- assert.equal(snapDirs(s2.root, "npm").length, 0);
285
- assert.deepEqual(readdirSync(join(s2.root, "r")), [], "no tmp or lock left");
286
- // A published snapshot that cannot be linked (EXDEV) is skipped and leaves nothing half-linked.
287
- await s2.cache.prepare({ wt: s2.lane("z"), repoKey: "r" });
288
- const wt3 = s2.lane("w");
289
- const [r3] = await c2.prepare({ wt: wt3, repoKey: "r" });
290
- assert.equal(r3.status, "skipped");
291
- assert.ok(!existsSync(join(wt3, "node_modules")));
275
+ const s4x = setup("npm");
276
+ const noLink = makeDepCache({ root: s4x.root, exec: s4x.f.exec, env: {}, fs: { ...fs, linkSync: () => { throw Object.assign(new Error("EACCES"), { code: "EACCES" }); } } });
277
+ const [rx] = await noLink.prepare({ wt: s4x.lane("y"), repoKey: "r" });
278
+ assert.equal(rx.status, "miss-installed");
279
+ assert.equal(rx.published, false);
280
+ assert.equal(snapDirs(s4x.root, "npm").length, 0);
292
281
 
293
282
  const s4 = setup("npm");
294
283
  const c4 = makeDepCache({ root: s4.root, exec: async (cmd, args) => { if (cmd === "git" || args[0] === "--version") return s4.f.exec(cmd, args, { cwd: s4.base }); throw Object.assign(new Error("spawn npm ENOENT"), { code: "ENOENT" }); }, env: {} });
@@ -297,6 +286,22 @@ describe("depcache (#4220): shared dependency installs keyed by lock hash", () =
297
286
  assert.match(r4.reason, /tool not found/);
298
287
  });
299
288
 
289
+ it("with the cache on another filesystem (EXDEV) a second lane with the same lock hash copies the snapshot: deps hit, no install (#4909)", async () => {
290
+ const s = setup("npm");
291
+ const exdev = { ...fs, linkSync: () => { throw Object.assign(new Error("EXDEV: cross-device link"), { code: "EXDEV" }); } };
292
+ const c = makeDepCache({ root: s.root, exec: s.f.exec, env: {}, fs: exdev });
293
+ const [r1] = await c.prepare({ wt: s.lane("a"), repoKey: "r" });
294
+ assert.equal(r1.status, "miss-installed");
295
+ assert.equal(r1.published, true, "published by copy");
296
+ const installs = () => s.f.calls.filter((x) => x.cmd !== "git" && x.args[0] !== "--version").length;
297
+ const before = installs();
298
+ const wt2 = s.lane("b");
299
+ const [r2] = await c.prepare({ wt: wt2, repoKey: "r" });
300
+ assert.equal(r2.status, "hit");
301
+ assert.equal(installs(), before, "no install on the second lane");
302
+ assert.ok(existsSync(join(wt2, "node_modules/left-pad/index.js")));
303
+ });
304
+
300
305
  it("no lockfile, yarn Plug'n'Play, an existing node_modules and FORGE_DEPCACHE=0 are skipped untouched", async () => {
301
306
  const s = setup("npm");
302
307
  const empty = join(s.base, "empty"); mkdirSync(empty);
@@ -79,6 +79,15 @@ export function isTransportError(e) {
79
79
  if (e.name === "AbortError" || e.name === "TimeoutError") return true;
80
80
  return /fetch failed|ECONNREFUSED|ECONNRESET|ETIMEDOUT|socket hang up|network error|timed? ?out/i.test(String(e.message ?? ""));
81
81
  }
82
+ const CLONE_RE = /\bclone\b[^\n]*\b(time[ds]?\s?out|timeout|fail\w*|refused|reset|unreachable|early eof|could not)|\b(time[ds]?\s?out|timeout|fail\w*)\b[^\n]*\bclone\b/i;
83
+ /** #4866: text of a sandbox clone failure (timeout, transport) as seen in an error or a lane result's detail. */
84
+ export const isCloneFailure = (text) => CLONE_RE.test(String(text ?? ""));
85
+ const CLONE_RETRY_S = 120;
86
+ /** Outcome code of a retryable sandbox clone failure; local (not in OUTCOME_CODES), like the preflight codes in lanecheck.mjs. */
87
+ export const SANDBOX_CLONE_CODE = "sandbox.clone";
88
+ /** #4866: a sandbox clone failure is a retryable `deferred` `sandbox.clone`, never an engine-error (so the breaker never sees it). */
89
+ const cloneDeferred = (id, detail) => ({ terminalReason: "deferred", code: SANDBOX_CLONE_CODE, retryInSeconds: CLONE_RETRY_S, detail: `sandbox ${id} could not clone the repo: ${detail}; retry in ${CLONE_RETRY_S}s` });
90
+ const errLine = (t) => String(t ?? "").split("\n")[0].slice(0, 300);
82
91
  const errText = (e) => String(e?.stderr || e?.message || e).split("\n")[0].slice(0, 300);
83
92
 
84
93
  /**
@@ -115,10 +124,12 @@ export function parseExecution(sec = {}) {
115
124
  const patRaw = Array.isArray(s.host_capability_patterns) ? s.host_capability_patterns : str(s.host_capability_patterns) ? str(s.host_capability_patterns).split(",") : [];
116
125
  const hostCapabilityPatterns = patRaw.map((x) => { const t = str(x), i = t.indexOf(":"); return { name: t.slice(0, i).trim().toLowerCase(), pattern: t.slice(i + 1).trim() }; })
117
126
  .filter((x) => CAPABILITY_RE.test(x.name) && x.pattern && safeRegExp(x.pattern));
127
+ const wsN = Number(str(s.warm_sandboxes));
128
+ const warmSandboxes = str(s.warm_sandboxes) !== "" && Number.isInteger(wsN) && wsN > 0 ? wsN : 0; // #4912: warm sandboxes the pool agent keeps ready; 0 = off
118
129
  const relay = Object.fromEntries(Object.entries(parseRelayCommands(s.relay_commands)).filter(([c]) => CAPABILITY_RE.test(c))); // #4778
119
130
  const uw = Number(str(s.unreachable_window_min));
120
131
  const unreachableWindowMin = str(s.unreachable_window_min) !== "" && Number.isFinite(uw) && uw >= 0 ? uw : null; // unset: providerLanes defaults to 5
121
- return { mode, maxRemote, provider, image, secrets, budget, ...(hostCapabilities.length ? { hostCapabilities } : {}), ...(hostCapabilityPatterns.length ? { hostCapabilityPatterns } : {}), ...(endpoint ? { endpoint } : {}), ...(tokenEnv ? { tokenEnv } : {}), ...(cacheStore ? { cacheStore } : {}), ...(allowActions ? { allowActions } : {}), ...(fallbackLocal ? { fallbackLocal } : {}), ...(heavy !== null ? { heavy } : {}), ...(unreachableWindowMin !== null ? { unreachableWindowMin } : {}), ...(Object.keys(relay).length ? { relay } : {}) };
132
+ return { mode, maxRemote, provider, image, secrets, budget, ...(hostCapabilities.length ? { hostCapabilities } : {}), ...(hostCapabilityPatterns.length ? { hostCapabilityPatterns } : {}), ...(endpoint ? { endpoint } : {}), ...(tokenEnv ? { tokenEnv } : {}), ...(cacheStore ? { cacheStore } : {}), ...(allowActions ? { allowActions } : {}), ...(fallbackLocal ? { fallbackLocal } : {}), ...(heavy !== null ? { heavy } : {}), ...(unreachableWindowMin !== null ? { unreachableWindowMin } : {}), ...(Object.keys(relay).length ? { relay } : {}), ...(warmSandboxes ? { warmSandboxes } : {}) };
122
133
  }
123
134
 
124
135
  const round1 = (x) => Math.round(x * 10) / 10;
@@ -525,7 +536,7 @@ export function providerByName(name, opts) {
525
536
  * returns that sandbox's ledger (ledger.mjs `makeLedger`).
526
537
  */
527
538
  export function providerLanes({ provider, laneArgs, ledgerFor, env = process.env, secrets = [], pollMs = 5000, sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
528
- costPerMinute = 0, now = () => Date.now(), unreachableWindowMs = 300000 }) {
539
+ costPerMinute = 0, now = () => Date.now(), unreachableWindowMs = 300000, logTail = null }) {
529
540
  const cpm = nonNeg(Number(costPerMinute)) ?? 0;
530
541
  // #4255: the sandbox's usage: the provider's `usage()`, else the billable minutes it put on the handle (#4254), else wall
531
542
  // clock since the handle's startedAt; cost is the provider's, else minutes x cost_per_minute.
@@ -560,6 +571,8 @@ export function providerLanes({ provider, laneArgs, ledgerFor, env = process.env
560
571
  };
561
572
  const unreachable = (h, e) => ({ terminalReason: "deferred", code: OUTCOME_CODES.SANDBOX_UNREACHABLE, retryInSeconds: 60,
562
573
  detail: `sandbox ${h.id} ${errText(e)}; kept for adopt on resume` });
574
+ const tailOf = async (h) => { try { return typeof logTail === "function" ? String(await logTail(h) ?? "") : ""; } catch { return ""; } };
575
+ const settleClone = (r, id) => (r?.terminalReason === "engine-error" && r.code === undefined && isCloneFailure(r.detail) ? cloneDeferred(id, errLine(r.detail)) : r);
563
576
  const wait = async (h, g = guard({ waited: 0, tries: 0 })) => {
564
577
  for (;;) {
565
578
  const r = await g(() => provider.fetchResult(h));
@@ -567,7 +580,9 @@ export function providerLanes({ provider, laneArgs, ledgerFor, env = process.env
567
580
  const st = await g(() => provider.status(h));
568
581
  if (st !== "running") {
569
582
  const last = await g(() => provider.fetchResult(h)); // it may have written the result just before exiting
570
- return last ?? { terminalReason: "engine-error", detail: `sandbox ${h.id} ${st} without a result` };
583
+ if (last) return last;
584
+ const why = `sandbox ${h.id} ${st} without a result`;
585
+ return isCloneFailure(await tailOf(h)) ? cloneDeferred(h.id, why) : { terminalReason: "engine-error", detail: why };
571
586
  }
572
587
  await sleep(pollMs);
573
588
  }
@@ -578,6 +593,7 @@ export function providerLanes({ provider, laneArgs, ledgerFor, env = process.env
578
593
  ? { terminalReason: "deferred", code: OUTCOME_CODES.SANDBOX_AUTH, retryInSeconds: 300, detail: `sandbox ${id} could not start (auth): ${errText(e)}; check the pool token (COMPUTESHARE_TOKEN or ~/.config/computeshare/forgedock.token)` }
579
594
  : e?.retryable === true
580
595
  ? { terminalReason: "deferred", code: OUTCOME_CODES.SANDBOX_CAPACITY, detail: `sandbox ${id} could not start (capacity): ${errText(e)}`, ...(Number(e.retryInSeconds) > 0 ? { retryInSeconds: Number(e.retryInSeconds) } : {}) }
596
+ : isCloneFailure(errText(e)) ? cloneDeferred(id, errText(e))
581
597
  : { terminalReason: "needs-human", code: OUTCOME_CODES.SANDBOX_START_FAILED, detail: `sandbox ${id} failed to start: ${errText(e)}` });
582
598
  return {
583
599
  // #4612: a graceful stop of a live sandbox (the provider's checkpoint stop); false when it has none.
@@ -600,10 +616,10 @@ export function providerLanes({ provider, laneArgs, ledgerFor, env = process.env
600
616
  if (!Number.isFinite(h.startedAt)) h.startedAt = base.startedAt;
601
617
  started = true;
602
618
  ctx.onStart?.({ ...(Number.isInteger(h.pid) ? { pid: h.pid } : {}), handle: h, remote: true });
603
- r = await wait(h);
619
+ r = settleClone(await wait(h), base.id);
604
620
  } catch (e) {
605
621
  if (started && e?.unreachable) return withUsage(unreachable(h, e), h); // #4672: no tear, the ledger keeps it
606
- r = started ? { terminalReason: "engine-error", detail: `sandbox ${base.id}: ${errText(e)}` } : startFailed(base.id, e);
622
+ r = started ? settleClone({ terminalReason: "engine-error", detail: `sandbox ${base.id}: ${errText(e)}` }, base.id) : startFailed(base.id, e);
607
623
  }
608
624
  await tear(led, h); // every path, after the result is in (tear never throws)
609
625
  return started ? withUsage(r, h) : r; // a sandbox that never started used nothing
@@ -7,7 +7,7 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { tmpDir } from "../tests/_tmp.mjs";
9
9
  import { renderContract } from "./render.mjs";
10
- import { parseExecution, SANDBOX_PROVIDERS, detectNeeds, hostCapabilityBlock, placementText, contractHostNeeds, placement, placeLane, validationWeight, hostHeadroom, scopedEnv, providerLanes, isTransportError, containerProvider, providerByName, openExecutionBudget } from "./execution.mjs";
10
+ import { parseExecution, SANDBOX_PROVIDERS, detectNeeds, hostCapabilityBlock, placementText, contractHostNeeds, placement, placeLane, validationWeight, hostHeadroom, scopedEnv, providerLanes, isTransportError, isCloneFailure, containerProvider, providerByName, openExecutionBudget } from "./execution.mjs";
11
11
  import { makeLedger, countLiveLanes } from "./ledger.mjs";
12
12
  import { openBatch } from "./batch.mjs";
13
13
  import { orchestrate, formatTable, formatSandboxSummary } from "./orchestrate.mjs";
@@ -43,6 +43,10 @@ const NO_BUDGET = { batchMinutes: null, batchUsd: null, dayMinutes: null, dayUsd
43
43
  const ledgerIn = (root, { pid = process.pid, host = HOST } = {}) => (n) => makeLedger({ root, batch: "bt", lane: `sandbox-${n}`, owner: "orch_test", issue: n, host, pid });
44
44
 
45
45
  describe("execution config (#4253)", () => {
46
+ it("warm_sandboxes: positive integer kept, default 0 omitted (#4912)", () => {
47
+ assert.equal(parseExecution({ mode: "remote", warm_sandboxes: "2" }).warmSandboxes, 2);
48
+ for (const v of [undefined, "0", "-1", "x", "1.5"]) assert.ok(!("warmSandboxes" in parseExecution({ mode: "remote", ...(v === undefined ? {} : { warm_sandboxes: v }) })));
49
+ });
46
50
  it("pool is a known sandbox provider (#4546)", () => {
47
51
  assert.ok(SANDBOX_PROVIDERS.includes("pool"));
48
52
  assert.equal(parseExecution({ mode: "hybrid", max_remote: "1", provider: "pool" }).provider, "pool");
@@ -127,6 +131,8 @@ describe("providerLanes: an unreachable provider is retried, then deferred witho
127
131
  it("unreachable: classifies transport errors and parses the window key", () => {
128
132
  for (const e of [new TypeError("fetch failed"), Object.assign(new Error("x"), { code: "ECONNRESET" }), Object.assign(new Error("x"), { status: 502 }), Object.assign(new Error("x"), { name: "AbortError" })]) assert.equal(isTransportError(e), true);
129
133
  for (const e of [new Error("boom"), Object.assign(new Error("HTTP 404"), { status: 404 })]) assert.equal(isTransportError(e), false);
134
+ for (const t of ["fatal: git clone timed out after 300s", "git clone failed: early EOF"]) assert.equal(isCloneFailure(t), true); // #4866
135
+ for (const t of ["clone of repo ok", "", null]) assert.equal(isCloneFailure(t), false);
130
136
  assert.equal(parseExecution({}).unreachableWindowMin, undefined); // providerLanes defaults it to 5 min
131
137
  assert.equal(parseExecution({ unreachable_window_min: "2" }).unreachableWindowMin, 2);
132
138
  });
@@ -20,7 +20,7 @@
20
20
  import { spawn } from "node:child_process";
21
21
  import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeSync, appendFileSync, writeFileSync, chmodSync } from "node:fs";
22
22
  import { cpus, freemem } from "node:os";
23
- import { dirname, join } from "node:path";
23
+ import { delimiter, dirname, join } from "node:path";
24
24
  import { fileURLToPath, pathToFileURL } from "node:url";
25
25
  import { writeAtomic } from "./batch.mjs";
26
26
  import { schedDir, pidAlive, DEFAULT_PRIORITY } from "./sched.mjs";
@@ -310,6 +310,27 @@ export function sumWaits(text, waits = {}) {
310
310
  return { waits, queueMax };
311
311
  }
312
312
 
313
+ /**
314
+ * Admission-time form of the guardChain loop (#4866): every PATH dir holding a pass-through wrapper for a heavy-shimmed tool
315
+ * with no real binary behind it (so the wrapper's own `command -v` lands back on the shim). Returns [{name, dir}].
316
+ */
317
+ export function findShimLoops(path, { names = HEAVY_BINS, find = which, read = head, skip = [] } = {}) {
318
+ const out = [];
319
+ const dirs = String(path ?? "").split(delimiter).filter(Boolean);
320
+ for (const name of names) {
321
+ const wrappers = [];
322
+ for (const d of dirs) {
323
+ if (skip.includes(d)) continue;
324
+ const f = find(name, d, []);
325
+ if (f && isPassThroughWrapper(read(f), name)) wrappers.push(d);
326
+ }
327
+ if (!wrappers.length) continue;
328
+ const real = find(name, path, [...skip, ...wrappers]);
329
+ if (!real) for (const dir of wrappers) out.push({ name, dir });
330
+ }
331
+ return out;
332
+ }
333
+
313
334
  export const SHIM_LOOP_MAX = 4;
314
335
  /**
315
336
  * Re-entry guard (#4680): FORGE_HEAVY_CHAIN lists `name:cmd` per shim run. A repeat of the same name+cmd where cmd is a
@@ -0,0 +1,129 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ /**
4
+ * forge#4866: lane preflight. Host and environment faults (no pool token, a stale pool agent, a PATH wrapper that loops back
5
+ * to the heavy shim, a gh without a --json field the engine reads) used to surface inside lanes as 401s, fork loops or
6
+ * untyped engine-errors. Each check here returns `{code, ok, detail, scope}`; `scope` is "remote" (only remote lanes are held)
7
+ * or "all". The orchestrator holds admission while any check fails and re-checks each pass (never the breaker); `forgedock
8
+ * doctor --lanes` prints every check. Pure: all I/O is injected (`io`), so a fixed cause clears on the next call.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { findShimLoops } from "./heavy.mjs";
12
+
13
+ /** The running CLI's package version. */
14
+ export const cliVersion = () => JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
15
+ export const UPGRADE_COMMAND = "npx forgedock pool upgrade";
16
+ /** The `gh <cmd> --json` fields the engine reads (keep next to the call sites' lists in engine-cli.mjs and lean/*.mjs). */
17
+ export const GH_FIELDS = Object.freeze({
18
+ "issue view": ["number", "title", "body", "state", "stateReason", "labels", "comments", "milestone", "author", "createdAt", "updatedAt"],
19
+ "pr view": ["number", "state", "mergeable", "mergeStateStatus"],
20
+ "repo view": ["nameWithOwner", "defaultBranchRef"],
21
+ });
22
+ const pass = (code, detail, scope) => ({ code, ok: true, detail, scope });
23
+ const fail = (code, detail, scope) => ({ code, ok: false, detail, scope });
24
+ /** Preflight codes. Local to this module, not in OUTCOME_CODES: they are admission holds that never reach a run's terminal outcome. */
25
+ export const PREFLIGHT_CODES = Object.freeze({
26
+ PREFLIGHT_POOL_AUTH: "preflight.pool_auth", PREFLIGHT_POOL_VERSION: "preflight.pool_version",
27
+ PREFLIGHT_PATH_LOOP: "preflight.path_loop", PREFLIGHT_GH_FIELDS: "preflight.gh_fields",
28
+ });
29
+ const C = PREFLIGHT_CODES;
30
+
31
+ /** A remote or hybrid execution is configured (the only case the pool checks apply to). */
32
+ export const usesPool = (exec) => Boolean(exec) && (exec.mode === "remote" || exec.mode === "hybrid") && ["pool", "remote"].includes(exec.provider ?? "pool");
33
+
34
+ /** The pool token resolves (env/keychain `token_env`, else the router's `forgedock_token_file`) and the router does not answer 401/403 to an authenticated ping. */
35
+ export async function checkPoolAuth({ exec, router = null, credential = () => null, ping = null }) {
36
+ const code = C.PREFLIGHT_POOL_AUTH;
37
+ if (!usesPool(exec)) return pass(code, "skipped: no remote or hybrid execution", "remote");
38
+ const name = exec.tokenEnv ?? "COMPUTESHARE_TOKEN";
39
+ const fromEnv = credential(name);
40
+ const token = fromEnv || router?.token || null;
41
+ if (!token) return fail(code, `pool token does not resolve: tried env/keychain ${name}, ${router ? "the router's forgedock_token_file" : "no router config (COMPUTESHARE_CONFIG / ~/.config/computeshare/router.json)"}`, "remote");
42
+ const endpoint = exec.endpoint ?? router?.endpoint ?? null;
43
+ if (!endpoint || typeof ping !== "function") return pass(code, `token resolved from ${fromEnv ? name : "forgedock_token_file"}`, "remote");
44
+ let r = null;
45
+ try { r = await ping(endpoint, token); } catch { r = null; }
46
+ if ([401, 403].includes(r?.status)) return fail(code, `router ${endpoint} answered HTTP ${r.status} to the token from ${fromEnv ? name : "forgedock_token_file"}`, "remote");
47
+ return pass(code, `token resolved from ${fromEnv ? name : "forgedock_token_file"}${r ? "" : "; router unreachable (not an auth failure)"}`, "remote");
48
+ }
49
+
50
+ /** Every online pool agent runs the CLI's version. `agents` is the router's `GET /agents` list (each spreads versionOf: version, image). */
51
+ export function checkPoolVersion({ exec, agents = null, cliVersion }) {
52
+ const code = C.PREFLIGHT_POOL_VERSION;
53
+ if (!usesPool(exec)) return pass(code, "skipped: no remote or hybrid execution", "remote");
54
+ if (!Array.isArray(agents)) return pass(code, "skipped: pool agents not readable", "remote");
55
+ const bad = agents.filter((a) => a?.ok !== false && typeof a?.version === "string" && a.version !== cliVersion);
56
+ if (!bad.length) return pass(code, `${agents.length} agent(s) at ${cliVersion}`, "remote");
57
+ return fail(code, bad.map((a) => `agent ${a.name ?? "?"} runs image version ${a.version}, CLI is ${cliVersion}`).join("; ") + `; upgrade: ${UPGRADE_COMMAND}`, "remote");
58
+ }
59
+
60
+ /** No PATH wrapper dir resolves a heavy-shimmed tool back to the shim. */
61
+ export function checkPathLoop({ env = {}, loops = findShimLoops }) {
62
+ const code = C.PREFLIGHT_PATH_LOOP;
63
+ const skip = env.FORGE_HEAVY_SHIM_DIR ? [env.FORGE_HEAVY_SHIM_DIR] : [];
64
+ const found = loops(env.PATH ?? "", { skip });
65
+ if (!found.length) return pass(code, "no PATH wrapper loops back to the heavy shim", "all");
66
+ return fail(code, found.map((l) => `${l.dir} wraps ${l.name} and resolves back to the heavy shim`).join("; "), "all");
67
+ }
68
+
69
+ /** Parse the field names `gh <cmd> --json bogus` lists as available (null when the output has no list). */
70
+ export function availableFields(text) {
71
+ const m = /Available fields:\s*([\s\S]*)$/i.exec(String(text ?? ""));
72
+ if (!m) return null;
73
+ const f = m[1].split(/\s+/).map((x) => x.trim()).filter((x) => /^[A-Za-z]\w*$/.test(x));
74
+ return f.length ? f : null;
75
+ }
76
+
77
+ /** The installed gh offers every `--json` field the engine uses; `exec(args) -> {stdout, stderr}`. An unreadable gh passes (not this check's fault). */
78
+ export async function checkGhFields({ exec, fields = GH_FIELDS }) {
79
+ const code = C.PREFLIGHT_GH_FIELDS;
80
+ let version = "unknown";
81
+ try { version = /gh version (\S+)/.exec(String((await exec(["--version"]))?.stdout ?? ""))?.[1] ?? "unknown"; } catch { return pass(code, "skipped: gh not runnable", "all"); }
82
+ const probes = await Promise.all(Object.entries(fields).map(async ([cmd, want]) => {
83
+ try { return [want, cmd, await exec([...cmd.split(" "), "--json", "forgedock_probe"])]; } catch { return null; }
84
+ }));
85
+ const missing = [];
86
+ for (const pr of probes) {
87
+ if (!pr) continue;
88
+ const [want, cmd, r] = pr;
89
+ const have = availableFields(`${r?.stderr ?? ""}\n${r?.stdout ?? ""}`);
90
+ if (!have) continue;
91
+ for (const f of want) if (!have.includes(f)) missing.push(`${f} (gh ${cmd} --json)`);
92
+ }
93
+ return missing.length ? fail(code, `gh ${version} lacks --json field ${missing.join(", ")}`, "all") : pass(code, `gh ${version} has every --json field the engine uses`, "all");
94
+ }
95
+
96
+ /** Run every check; never throws (a check that throws passes with the error as its detail). */
97
+ export async function runLaneChecks({ exec = null, env = process.env, cliVersion: cliV = cliVersion(), io = {} } = {}) {
98
+ const safe = async (code, scope, fn) => { try { return await fn(); } catch (e) { return pass(code, `check errored: ${String(e?.message ?? e).split("\n")[0]}`, scope); } };
99
+ let router = null;
100
+ try { router = io.router?.() ?? null; } catch { router = null; }
101
+ const ping = io.ping ?? null;
102
+ const agentsOf = async () => { try { return (await io.agents?.(exec?.endpoint ?? router?.endpoint, exec?.tokenEnv ? (io.credential?.(exec.tokenEnv) ?? router?.token) : router?.token)) ?? null; } catch { return null; } };
103
+ return Promise.all([
104
+ safe(C.PREFLIGHT_POOL_AUTH, "remote", () => checkPoolAuth({ exec, router, credential: io.credential, ping })),
105
+ safe(C.PREFLIGHT_POOL_VERSION, "remote", async () => checkPoolVersion({ exec, agents: usesPool(exec) ? await agentsOf() : null, cliVersion: cliV })),
106
+ safe(C.PREFLIGHT_PATH_LOOP, "all", () => checkPathLoop({ env: { ...env, PATH: env.PATH ?? "" }, ...(io.loops ? { loops: io.loops } : {}) })),
107
+ safe(C.PREFLIGHT_GH_FIELDS, "all", () => (io.ghExec ? checkGhFields({ exec: io.ghExec }) : pass(C.PREFLIGHT_GH_FIELDS, "skipped: no gh", "all"))),
108
+ ]);
109
+ }
110
+
111
+ /** One line per check: `PASS|FAIL code detail`. */
112
+ export const formatLaneChecks = (checks) => checks.map((c) => `${c.ok ? "PASS" : "FAIL"} ${c.code} ${c.detail}`).join("\n");
113
+
114
+ /** Real-host io for the daemon, the batch and `doctor --lanes`. gh output is cached per process (it cannot change under a running engine). */
115
+ export async function hostLaneIo({ env = process.env } = {}) {
116
+ const { poolRouterConfig } = await import("./execution.mjs");
117
+ const { resolveCredential } = await import("./credentials.mjs");
118
+ const { execFile } = await import("node:child_process");
119
+ const call = (url, token, timeoutMs = 5000) => fetch(url, { headers: token ? { authorization: `Bearer ${token}` } : {}, signal: AbortSignal.timeout(timeoutMs) });
120
+ const run = (args) => new Promise((res, rej) => execFile("gh", args, { timeout: 15000, env }, (e, stdout, stderr) => (e && e.code === "ENOENT" ? rej(e) : res({ stdout, stderr }))));
121
+ const memo = new Map();
122
+ return {
123
+ router: () => poolRouterConfig({ env }),
124
+ credential: (n) => resolveCredential(n, { env }),
125
+ ping: async (endpoint, token) => ({ status: (await call(`${endpoint}/status`, token)).status }),
126
+ agents: async (endpoint, token) => { const r = await call(`${endpoint}/agents`, token); return r.ok ? (await r.json())?.agents ?? null : null; },
127
+ ghExec: (args) => { const k = args.join(" "); if (!memo.has(k)) memo.set(k, run(args)); return memo.get(k); },
128
+ };
129
+ }