@codyswann/lisa 4.50.5 → 4.50.6

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 (69) hide show
  1. package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts +5 -0
  2. package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts.map +1 -1
  3. package/dist/core/nightly-e2e-guard-behavior-certificate.js +9 -2
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.js +3 -3
  6. package/package.json +4 -4
  7. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +120 -14
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +43 -3
  11. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +120 -14
  12. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +43 -3
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +120 -14
  15. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +43 -3
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +120 -14
  23. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +43 -3
  24. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +120 -14
  26. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +43 -3
  27. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-agy/plugin.json +1 -1
  30. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  50. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-agy/plugin.json +1 -1
  55. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  60. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  65. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +120 -14
  68. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +43 -3
  69. package/scripts/generate-nightly-e2e-guard-certificate.mjs +10 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.50.5",
3
+ "version": "4.50.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -15,9 +15,25 @@
15
15
  * check that forgot `minVersion`, so a container verified clean against a node
16
16
  * older than the manifest demanded. One function, one verdict, no drift.
17
17
  *
18
- * **Two verdicts, not three.** `preflight-secrets` needs `unreachable` because
19
- * a vault can fail to be asked. Probing a local binary cannot: it is present or
20
- * it is not. The analogous trap — a tool whose version cannot be parsed — is
18
+ * **Three verdicts, and the third one was learned the hard way.** The original
19
+ * header here claimed two sufficed: `preflight-secrets` needs `unreachable`
20
+ * because a vault can fail to be asked, and "probing a local binary cannot: it
21
+ * is present or it is not." That sentence was true of probing and false of this
22
+ * module, because one branch of `planToolchain` never probed at all — an
23
+ * install entry with no pin for the running platform short-circuited to
24
+ * `invalid` before the binary was looked for. So a `bws` that was installed, on
25
+ * PATH, and resolving secrets in the very same session was reported under
26
+ * "These need you", next to an instruction to route the work to blocked. Four
27
+ * operator sessions believed it.
28
+ *
29
+ * The third tier is `unverified`: present and usable, but with no pin for this
30
+ * platform, so Lisa can neither reinstall it nor say where it came from. That
31
+ * is a real thing to know — an unpinned binary is a supply-chain claim nobody
32
+ * checked — and it is emphatically not a reason to stop. Availability and
33
+ * provenance are two questions, and answering the first with the second is the
34
+ * defect this tier exists to prevent recurring.
35
+ *
36
+ * The analogous trap — a tool whose version cannot be parsed — is
21
37
  * already handled correctly upstream, because `planRequired` compares
22
38
  * `found.version ?? "0"` and an unknown version loses every `minVersion`
23
39
  * comparison. It fails closed without needing a verdict of its own.
@@ -34,12 +50,44 @@ import { join } from "node:path";
34
50
  import { fileURLToPath } from "node:url";
35
51
 
36
52
  import { probe, readRemoteEnvConfig } from "./setup-remote-env.mjs";
37
- import { currentPlatform, planToolchain } from "./toolchain.mjs";
53
+ import {
54
+ compareVersions,
55
+ currentPlatform,
56
+ planToolchain,
57
+ } from "./toolchain.mjs";
38
58
  import { toolFloor } from "./tool-floor.mjs";
39
59
 
40
- /** Plan actions that mean the agent cannot use the tool right now. */
60
+ /**
61
+ * Plan actions that mean the agent cannot use the tool right now.
62
+ *
63
+ * `invalid` is here because most of its causes really are stops — an unpinned
64
+ * version, a malformed `platforms` map, an unknown install method. The one
65
+ * cause that is not, an entry with no pin for this platform whose binary is
66
+ * nonetheless on PATH, is lifted out below before this set is applied.
67
+ */
41
68
  const BLOCKING = new Set(["missing", "invalid"]);
42
69
 
70
+ /**
71
+ * Whether an unpinnable entry is nonetheless usable on this machine.
72
+ *
73
+ * Presence-only, and deliberately not a skip. Dropping the entry entirely would
74
+ * report clean for a tool that is neither pinned nor installed, which is the
75
+ * vacuous green this module refuses everywhere else. So the probe still decides
76
+ * — it just gets asked, which on this branch it previously never was.
77
+ *
78
+ * A declared `minVersion` still bites. The pin is unusable here (it names an
79
+ * artifact for another platform), but a minimum is a statement about the tool
80
+ * rather than about the download, and a binary below it cannot do the work.
81
+ * @param {object} step One `planToolchain` decision.
82
+ * @param {Record<string, string>} minVersions Declared minimums by tool name.
83
+ * @returns {boolean} Whether the tool is present and good enough to use.
84
+ */
85
+ function usableWithoutPin(step, minVersions) {
86
+ if (!step.unpinnedForPlatform || !step.found?.present) return false;
87
+ const floor = minVersions[step.name];
88
+ return !floor || compareVersions(step.found.version ?? "0", floor) >= 0;
89
+ }
90
+
43
91
  /**
44
92
  * Read the whole config, for the derivations that live outside `remoteEnv`.
45
93
  *
@@ -109,7 +157,7 @@ export function mergeFloor(tools, floor) {
109
157
  * @param {object} [remoteEnv] Parsed `remoteEnv` block.
110
158
  * @param {Function} [versionProbe] Version probe, injected for tests.
111
159
  * @param {string} [platform] Platform key, injected for tests.
112
- * @returns {{verdict: string, blocked: Array<{name: string, action: string, reason: string}>, installable: Array<{name: string, action: string, reason: string}>, reasons: Record<string, string>}}
160
+ * @returns {{verdict: string, blocked: Array<{name: string, action: string, reason: string}>, installable: Array<{name: string, action: string, reason: string}>, unverified: Array<{name: string, action: string, reason: string}>, reasons: Record<string, string>}}
113
161
  */
114
162
  export function preflightTools(
115
163
  config = readConfigRoot(),
@@ -122,19 +170,59 @@ export function preflightTools(
122
170
  toolFloor(config)
123
171
  );
124
172
  if (!(tools.require ?? []).length && !(tools.install ?? []).length) {
125
- return { verdict: "ok", blocked: [], installable: [], reasons };
173
+ return {
174
+ verdict: "ok",
175
+ blocked: [],
176
+ installable: [],
177
+ unverified: [],
178
+ reasons,
179
+ };
126
180
  }
127
181
 
128
182
  // "local" because this runs where the agent is, and it is the surface word
129
183
  // `appliesToSurface` already understands — a tool narrowed to ["remote"] is
130
184
  // correctly ignored on a laptop rather than reported as missing there.
131
185
  const plan = planToolchain(tools, versionProbe, "local", platform);
132
- const blocked = plan.filter(step => BLOCKING.has(step.action));
186
+ const minVersions = Object.fromEntries(
187
+ (tools.install ?? [])
188
+ .filter(tool => tool.minVersion)
189
+ .map(tool => [tool.name, tool.minVersion])
190
+ );
191
+ const usable = new Set(
192
+ plan
193
+ .filter(step => usableWithoutPin(step, minVersions))
194
+ .map(step => step.name)
195
+ );
196
+ // Restated in this module's own vocabulary rather than passed through.
197
+ // `resolvePlatform`'s message is written for someone repairing a manifest —
198
+ // "add a block for darwin-arm64 with its own url and sha256" — and reads as a
199
+ // demand. What the reader here needs is the availability answer first and the
200
+ // provenance caveat second, because only one of the two affects whether they
201
+ // can start work.
202
+ const unverified = plan
203
+ .filter(step => usable.has(step.name))
204
+ .map(step => ({
205
+ ...step,
206
+ action: "unverified",
207
+ reason:
208
+ `${step.name} ${step.found.version ?? "(version unknown)"} is on ` +
209
+ `PATH and usable, but the manifest pins no artifact for ${platform}, ` +
210
+ `so Lisa cannot reinstall it or say where it came from`,
211
+ }));
212
+ const blocked = plan.filter(
213
+ step => BLOCKING.has(step.action) && !usable.has(step.name)
214
+ );
133
215
  const installable = plan.filter(step => step.action === "install");
216
+ if (blocked.length || installable.length)
217
+ return { verdict: "missing", blocked, installable, unverified, reasons };
218
+ // A note is not a failure and must not exit non-zero, but it does have to be
219
+ // said — an "ok" verdict renders nothing at all, which would drop the only
220
+ // signal this tier exists to carry.
134
221
  return {
135
- verdict: blocked.length || installable.length ? "missing" : "ok",
222
+ verdict: unverified.length ? "notice" : "ok",
136
223
  blocked,
137
224
  installable,
225
+ unverified,
138
226
  reasons,
139
227
  };
140
228
  }
@@ -150,11 +238,14 @@ export function reportTools(result) {
150
238
  // The header tracks the exit code. Only a blocked tool is a failure — one
151
239
  // Lisa can install is an action with a command attached, and calling that
152
240
  // "FAILED" while exiting zero teaches readers that the word means nothing.
153
- const lines = [
154
- result.blocked.length
155
- ? "Tooling preflight FAILED."
156
- : "Tooling preflight — action available.",
157
- ];
241
+ const header = () => {
242
+ if (result.blocked.length) return "Tooling preflight FAILED.";
243
+ if (result.installable.length)
244
+ return "Tooling preflight — action available.";
245
+ // Nothing to do and nothing to fix; the note below is the whole message.
246
+ return "Tooling preflight passed, with a note.";
247
+ };
248
+ const lines = [header()];
158
249
  if (result.installable.length) {
159
250
  lines.push(
160
251
  ``,
@@ -183,6 +274,21 @@ export function reportTools(result) {
183
274
  `blocked with this reason rather than claiming it and stopping partway.`
184
275
  );
185
276
  }
277
+ if (result.unverified.length) {
278
+ lines.push(
279
+ ``,
280
+ `Installed, but Lisa did not put them there and cannot vouch for them:`,
281
+ ``
282
+ );
283
+ for (const step of result.unverified) {
284
+ lines.push(` ${step.name} — ${step.reason}`);
285
+ }
286
+ lines.push(
287
+ ``,
288
+ `Nothing here blocks your work — these tools are usable right now. Add a`,
289
+ `pin for this platform when you want Lisa able to verify or replace them.`
290
+ );
291
+ }
186
292
  return lines.join("\n");
187
293
  }
188
294
 
@@ -146,6 +146,18 @@ function planInstallable(tool, found, pinIsFloor = false) {
146
146
  /** Surfaces a manifest entry may name. */
147
147
  const KNOWN_SURFACES = new Set(["local", "remote"]);
148
148
 
149
+ /**
150
+ * Marks the one resolution failure that is a question about *this machine*.
151
+ *
152
+ * A `platforms` map that is not an object is a broken declaration, and nothing
153
+ * on PATH redeems it. A map that simply has no block for the running platform
154
+ * is a different statement: the manifest cannot say what Lisa should place
155
+ * here, which says nothing at all about what is already here. Callers that need
156
+ * to tell those apart get a code rather than a regex over the message, because
157
+ * a message is prose written for an operator and will be rewritten.
158
+ */
159
+ export const NO_PIN_FOR_PLATFORM = "no-pin-for-platform";
160
+
149
161
  /**
150
162
  * The platform key a manifest entry is resolved against.
151
163
  *
@@ -197,13 +209,15 @@ export function resolvePlatform(tool, platform = currentPlatform()) {
197
209
  const block = platforms[platform];
198
210
  if (!block) {
199
211
  const known = Object.keys(platforms).sort().join(", ");
200
- throw new Error(
212
+ const err = new Error(
201
213
  `${tool.name}: no pin for ${platform}.\n` +
202
214
  `Declared platforms: ${known || "(none)"}.\n` +
203
215
  `Add a block for ${platform} with its own url and sha256, or drop the ` +
204
216
  `tool from this surface. Guessing an artifact would defeat the ` +
205
217
  `checksum.`
206
218
  );
219
+ err.code = NO_PIN_FOR_PLATFORM;
220
+ throw err;
207
221
  }
208
222
  // The platform block wins over the shared fields, and `platforms` itself is
209
223
  // dropped so a resolved entry is indistinguishable from a flat one — that is
@@ -265,11 +279,17 @@ export function appliesToSurface(tool, surface) {
265
279
  * `require` entries are deliberately not resolved: they carry a name and a
266
280
  * minimum version, nothing platform-specific, and inventing a per-platform shape
267
281
  * for them would be ceremony with no artifact behind it.
282
+ *
283
+ * Every entry is probed, including one with no pin for this platform. That
284
+ * entry's verdict is still `invalid`, because provisioning genuinely cannot
285
+ * proceed — but `found` and `unpinnedForPlatform` travel with it so a caller
286
+ * asking about availability rather than provisioning has the answer instead of
287
+ * having to infer one from the other.
268
288
  * @param {{require?: object[], install?: object[]}} tools Manifest.
269
289
  * @param {(name: string) => {version: string|null, present: boolean}} probe Version probe.
270
290
  * @param {string} [surface] Surface being provisioned.
271
291
  * @param {string} [platform] Platform key to resolve install entries against.
272
- * @returns {Array<{name: string, action: string, reason: string, tool?: object}>} Ordered decisions.
292
+ * @returns {Array<{name: string, action: string, reason: string, tool?: object, found?: {version: string|null, present: boolean}, unpinnedForPlatform?: boolean}>} Ordered decisions.
273
293
  */
274
294
  export function planToolchain(
275
295
  tools,
@@ -287,7 +307,27 @@ export function planToolchain(
287
307
  try {
288
308
  resolved = resolvePlatform(tool, platform);
289
309
  } catch (err) {
290
- plan.push({ name: tool.name, action: "invalid", reason: err.message });
310
+ // Ask the probe anyway, and carry its answer alongside the verdict.
311
+ //
312
+ // The verdict itself is right and does not change: Lisa cannot place an
313
+ // artifact it has no pin for. But "can Lisa provision this here?" and
314
+ // "can the agent use this right now?" are different questions, and
315
+ // skipping the probe on this branch left the second one unanswerable —
316
+ // so `preflight-tools` answered it with the first, and reported a `bws`
317
+ // that was installed, on PATH, and resolving secrets in the same session
318
+ // as a blocker that work had to be routed around.
319
+ //
320
+ // Only the missing-pin case gets a probe result. A malformed `platforms`
321
+ // map is a broken declaration rather than a fact about this machine, and
322
+ // a binary happening to be on PATH does not redeem it.
323
+ plan.push({
324
+ name: tool.name,
325
+ action: "invalid",
326
+ reason: err.message,
327
+ ...(err.code === NO_PIN_FOR_PLATFORM
328
+ ? { unpinnedForPlatform: true, found: probe(tool.name) }
329
+ : {}),
330
+ });
291
331
  continue;
292
332
  }
293
333
  // The resolved entry travels with the decision so the installer never has to
@@ -51,6 +51,16 @@ export const RETAINED_RELEASES = Object.freeze([
51
51
  // refusing to execute bytes it trusted yesterday. Rereading the tag is the
52
52
  // only sanctioned way to keep it: the digest is derived, never copied.
53
53
  "v4.26.1",
54
+ // The 1.7.0 guard, as the 50 releases from `4.22.13` through `4.26.0`
55
+ // published it. Commit 48c936bb9 moved the workspace guard and regeneration
56
+ // replaced the outgoing row instead of adding beside it, so the digest those
57
+ // 50 shipped was revoked and nothing noticed. Re-measured: 50 tags carrying
58
+ // it, bounded on both sides — v4.22.12 differs, v4.26.1 differs.
59
+ //
60
+ // `v4.17.16` above also certifies a 1.7.0 guard; the two are DIFFERENT bytes
61
+ // under the same contract version, which is why retention is a ref list and
62
+ // not a contract list.
63
+ "v4.26.0",
54
64
  ]);
55
65
 
56
66
  const digest = bytes => createHash("sha256").update(bytes).digest("hex");