@codyswann/lisa 2.332.0 → 2.332.2

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 (72) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +5 -3
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -14
  8. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +39 -3
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +19 -1
  10. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -14
  11. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +39 -3
  12. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +19 -1
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -14
  15. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +39 -3
  16. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +19 -1
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -14
  24. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +39 -3
  25. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +19 -1
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -14
  28. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +39 -3
  29. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +19 -1
  30. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-agy/plugin.json +1 -1
  33. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  53. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-agy/plugin.json +1 -1
  58. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  63. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  68. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -14
  71. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +39 -3
  72. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +19 -1
package/package.json CHANGED
@@ -115,7 +115,7 @@
115
115
  "brace-expansion": ">=5.0.9"
116
116
  },
117
117
  "name": "@codyswann/lisa",
118
- "version": "2.332.0",
118
+ "version": "2.332.2",
119
119
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
120
120
  "main": "dist/index.js",
121
121
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -76,13 +76,37 @@ function bindingsFor(surface) {
76
76
  /**
77
77
  * Install methods the toolchain runner supports.
78
78
  *
79
- * `release-tar` was missing here while the runner had supported it for as long
80
- * as gh has been pinned so this validator would have rejected the project's
81
- * own manifest, and the only reason nobody hit it is that nothing ran the two
82
- * against each other. Kept in step with `assertPinned` in `toolchain.mjs`,
83
- * which is the list that actually decides.
79
+ * This is a deliberate copy of `INSTALL_METHODS` in `toolchain.mjs`, which is
80
+ * the list that actually decides. Skills are distributed as self-contained
81
+ * directories and nothing imports across a skill boundary, so importing the
82
+ * authority here would couple two skills' distribution for one array.
83
+ *
84
+ * The copy has drifted twice. First `release-tar` was missing while the runner
85
+ * had supported it for as long as gh has been pinned. Then `release-tree` and
86
+ * `release-binary` were both missing, which rejected a valid `jq` pin. Each
87
+ * time the only safeguard was a comment asking the next author to remember.
88
+ * A conformance test now compares the two sets directly, so adding a kind on
89
+ * either side fails until both know it.
84
90
  */
85
- const INSTALL_METHODS = new Set(["release-zip", "release-tar", "npm-global"]);
91
+ const INSTALL_METHODS = new Set([
92
+ "release-zip",
93
+ "release-tar",
94
+ "release-tree",
95
+ "release-binary",
96
+ "npm-global",
97
+ ]);
98
+
99
+ /**
100
+ * Download kinds — every method that fetches a URL, so every one that needs a
101
+ * checksum. Spelled as "not npm-global" rather than as a list, because the next
102
+ * download kind should inherit the obligation by default; the drift above came
103
+ * from lists that had to be remembered.
104
+ * @param {string} method An install method.
105
+ * @returns {boolean} Whether the method fetches a pinned artifact.
106
+ */
107
+ function isDownloadKind(method) {
108
+ return INSTALL_METHODS.has(method) && method !== "npm-global";
109
+ }
86
110
 
87
111
  /**
88
112
  * Validate the `secrets` block.
@@ -171,19 +195,28 @@ function validateInstallArtifact(label, entry, problems) {
171
195
  );
172
196
  return;
173
197
  }
174
- // Both archive kinds, not just zip. A tarball pinned without a checksum
175
- // trusts whatever the URL serves today exactly as much as a zip does, and
176
- // checking only one of them meant the tool Lisa's guardrails shell out to —
177
- // gh, which ships a tarball on Linux — was the one going unverified.
178
- if (
179
- (entry.install === "release-zip" || entry.install === "release-tar") &&
180
- !(entry.url && entry.sha256)
181
- ) {
198
+ // Every download kind, not an enumerated few. A tarball pinned without a
199
+ // checksum trusts whatever the URL serves today exactly as much as a zip
200
+ // does, and checking only one of them meant the tool Lisa's guardrails shell
201
+ // out to — gh, which ships a tarball on Linux — was the one going unverified.
202
+ // A `release-binary` matters most of all: the artifact is directly
203
+ // executable, so a wrong one needs no unpacking step to run.
204
+ if (isDownloadKind(entry.install) && !(entry.url && entry.sha256)) {
182
205
  problems.push(
183
206
  `remoteEnv install ${label} needs both url and sha256. A pinned ` +
184
207
  `version with no checksum still trusts whatever the URL serves today.`
185
208
  );
186
209
  }
210
+ // A tree is the one kind with no sane default entry point: the archive root
211
+ // is a directory, so an omitted `binary` would install a directory as a
212
+ // command. The runner refuses it, and a validator that accepted it would just
213
+ // move the failure from review to provisioning.
214
+ if (entry.install === "release-tree" && !entry.binary) {
215
+ problems.push(
216
+ `remoteEnv install ${label} needs "binary" — the path to the entry ` +
217
+ `point INSIDE the archive, such as "maestro/bin/maestro".`
218
+ );
219
+ }
187
220
  if (entry.install === "npm-global" && !entry.package) {
188
221
  problems.push(`remoteEnv install ${label} needs a package`);
189
222
  }
@@ -914,9 +914,45 @@ async function main() {
914
914
  "lisa-secrets-access",
915
915
  "materialize-secrets.mjs"
916
916
  );
917
- execFileSync("node", dryRun ? [materialize, "--dry-run"] : [materialize], {
918
- stdio: "inherit",
919
- });
917
+ // A failure here is fatal only when nothing else will try again.
918
+ //
919
+ // On a surface that materializes at BOTH setup and session start, this run
920
+ // is the floor and the hook is the authority. The two run in different
921
+ // environments: a cloud vendor can expose its configured variables to the
922
+ // agent session while the setup phase — which runs earlier, during image
923
+ // construction — never sees them. That is not an error state, it is the
924
+ // normal shape of `materializeAt: "both"`, and the hook materializes
925
+ // correctly moments later.
926
+ //
927
+ // Treating it as fatal cost an entire environment: setup.sh `exec`s this
928
+ // script, so a non-zero exit fails the vendor's setup step and Claude Code
929
+ // never starts at all. Trading "secrets arrive one phase later" for "the
930
+ // container will not boot" is a bad trade, and it regressed a case that
931
+ // used to work — before this surface materialized at setup, setup simply
932
+ // never attempted it.
933
+ //
934
+ // Every other case keeps the hard failure: an explicit `--phase=secrets`
935
+ // (the hook's own authoritative run) and a setup-only surface such as
936
+ // codex-cloud both have no second chance, so a silent pass there would
937
+ // hand back a session with no credentials and call it ready.
938
+ const retried = !requested && materializesAtSessionStart(materializeAt);
939
+ try {
940
+ execFileSync(
941
+ "node",
942
+ dryRun ? [materialize, "--dry-run"] : [materialize],
943
+ { stdio: "inherit" }
944
+ );
945
+ } catch (err) {
946
+ if (!retried) throw err;
947
+ console.log(
948
+ `\n Secrets were not materialized during setup, and that is not fatal ` +
949
+ `here.\n This surface also materializes at session start, which runs ` +
950
+ `with the\n session's own environment — the usual reason setup cannot ` +
951
+ `see the\n bootstrap credential. The session-start hook will retry.\n` +
952
+ ` If secrets are still missing INSIDE a session, that run is the one ` +
953
+ `to debug.`
954
+ );
955
+ }
920
956
  } else if (!requested) {
921
957
  console.log(
922
958
  `\nSecrets: materialized from a session-start hook on "${surface}", ` +
@@ -301,6 +301,24 @@ export function planToolchain(
301
301
  return plan;
302
302
  }
303
303
 
304
+ /**
305
+ * Every install method the runner can execute — the authoritative list.
306
+ *
307
+ * `validate-config.mjs` maintains its own copy because skills are distributed
308
+ * as self-contained directories and nothing else imports across a skill
309
+ * boundary. A comment saying "keep these in step" has now failed twice — first
310
+ * for `release-tar`, then for `release-tree` and `release-binary` together — so
311
+ * the pairing is enforced by a conformance test against this export instead.
312
+ * Adding a kind here fails that test until the validator learns it too.
313
+ */
314
+ export const INSTALL_METHODS = Object.freeze([
315
+ "release-zip",
316
+ "release-tar",
317
+ "release-tree",
318
+ "release-binary",
319
+ "npm-global",
320
+ ]);
321
+
304
322
  /**
305
323
  * Reject a manifest entry that could install something unverifiable.
306
324
  *
@@ -373,6 +391,6 @@ export function assertPinned(tool) {
373
391
  }
374
392
  throw new Error(
375
393
  `${tool.name}: unknown install method "${tool.install}".\n` +
376
- `Supported: release-zip, release-tar, release-tree, release-binary, npm-global.`
394
+ `Supported: ${INSTALL_METHODS.join(", ")}.`
377
395
  );
378
396
  }
@@ -76,13 +76,37 @@ function bindingsFor(surface) {
76
76
  /**
77
77
  * Install methods the toolchain runner supports.
78
78
  *
79
- * `release-tar` was missing here while the runner had supported it for as long
80
- * as gh has been pinned so this validator would have rejected the project's
81
- * own manifest, and the only reason nobody hit it is that nothing ran the two
82
- * against each other. Kept in step with `assertPinned` in `toolchain.mjs`,
83
- * which is the list that actually decides.
79
+ * This is a deliberate copy of `INSTALL_METHODS` in `toolchain.mjs`, which is
80
+ * the list that actually decides. Skills are distributed as self-contained
81
+ * directories and nothing imports across a skill boundary, so importing the
82
+ * authority here would couple two skills' distribution for one array.
83
+ *
84
+ * The copy has drifted twice. First `release-tar` was missing while the runner
85
+ * had supported it for as long as gh has been pinned. Then `release-tree` and
86
+ * `release-binary` were both missing, which rejected a valid `jq` pin. Each
87
+ * time the only safeguard was a comment asking the next author to remember.
88
+ * A conformance test now compares the two sets directly, so adding a kind on
89
+ * either side fails until both know it.
84
90
  */
85
- const INSTALL_METHODS = new Set(["release-zip", "release-tar", "npm-global"]);
91
+ const INSTALL_METHODS = new Set([
92
+ "release-zip",
93
+ "release-tar",
94
+ "release-tree",
95
+ "release-binary",
96
+ "npm-global",
97
+ ]);
98
+
99
+ /**
100
+ * Download kinds — every method that fetches a URL, so every one that needs a
101
+ * checksum. Spelled as "not npm-global" rather than as a list, because the next
102
+ * download kind should inherit the obligation by default; the drift above came
103
+ * from lists that had to be remembered.
104
+ * @param {string} method An install method.
105
+ * @returns {boolean} Whether the method fetches a pinned artifact.
106
+ */
107
+ function isDownloadKind(method) {
108
+ return INSTALL_METHODS.has(method) && method !== "npm-global";
109
+ }
86
110
 
87
111
  /**
88
112
  * Validate the `secrets` block.
@@ -171,19 +195,28 @@ function validateInstallArtifact(label, entry, problems) {
171
195
  );
172
196
  return;
173
197
  }
174
- // Both archive kinds, not just zip. A tarball pinned without a checksum
175
- // trusts whatever the URL serves today exactly as much as a zip does, and
176
- // checking only one of them meant the tool Lisa's guardrails shell out to —
177
- // gh, which ships a tarball on Linux — was the one going unverified.
178
- if (
179
- (entry.install === "release-zip" || entry.install === "release-tar") &&
180
- !(entry.url && entry.sha256)
181
- ) {
198
+ // Every download kind, not an enumerated few. A tarball pinned without a
199
+ // checksum trusts whatever the URL serves today exactly as much as a zip
200
+ // does, and checking only one of them meant the tool Lisa's guardrails shell
201
+ // out to — gh, which ships a tarball on Linux — was the one going unverified.
202
+ // A `release-binary` matters most of all: the artifact is directly
203
+ // executable, so a wrong one needs no unpacking step to run.
204
+ if (isDownloadKind(entry.install) && !(entry.url && entry.sha256)) {
182
205
  problems.push(
183
206
  `remoteEnv install ${label} needs both url and sha256. A pinned ` +
184
207
  `version with no checksum still trusts whatever the URL serves today.`
185
208
  );
186
209
  }
210
+ // A tree is the one kind with no sane default entry point: the archive root
211
+ // is a directory, so an omitted `binary` would install a directory as a
212
+ // command. The runner refuses it, and a validator that accepted it would just
213
+ // move the failure from review to provisioning.
214
+ if (entry.install === "release-tree" && !entry.binary) {
215
+ problems.push(
216
+ `remoteEnv install ${label} needs "binary" — the path to the entry ` +
217
+ `point INSIDE the archive, such as "maestro/bin/maestro".`
218
+ );
219
+ }
187
220
  if (entry.install === "npm-global" && !entry.package) {
188
221
  problems.push(`remoteEnv install ${label} needs a package`);
189
222
  }
@@ -914,9 +914,45 @@ async function main() {
914
914
  "lisa-secrets-access",
915
915
  "materialize-secrets.mjs"
916
916
  );
917
- execFileSync("node", dryRun ? [materialize, "--dry-run"] : [materialize], {
918
- stdio: "inherit",
919
- });
917
+ // A failure here is fatal only when nothing else will try again.
918
+ //
919
+ // On a surface that materializes at BOTH setup and session start, this run
920
+ // is the floor and the hook is the authority. The two run in different
921
+ // environments: a cloud vendor can expose its configured variables to the
922
+ // agent session while the setup phase — which runs earlier, during image
923
+ // construction — never sees them. That is not an error state, it is the
924
+ // normal shape of `materializeAt: "both"`, and the hook materializes
925
+ // correctly moments later.
926
+ //
927
+ // Treating it as fatal cost an entire environment: setup.sh `exec`s this
928
+ // script, so a non-zero exit fails the vendor's setup step and Claude Code
929
+ // never starts at all. Trading "secrets arrive one phase later" for "the
930
+ // container will not boot" is a bad trade, and it regressed a case that
931
+ // used to work — before this surface materialized at setup, setup simply
932
+ // never attempted it.
933
+ //
934
+ // Every other case keeps the hard failure: an explicit `--phase=secrets`
935
+ // (the hook's own authoritative run) and a setup-only surface such as
936
+ // codex-cloud both have no second chance, so a silent pass there would
937
+ // hand back a session with no credentials and call it ready.
938
+ const retried = !requested && materializesAtSessionStart(materializeAt);
939
+ try {
940
+ execFileSync(
941
+ "node",
942
+ dryRun ? [materialize, "--dry-run"] : [materialize],
943
+ { stdio: "inherit" }
944
+ );
945
+ } catch (err) {
946
+ if (!retried) throw err;
947
+ console.log(
948
+ `\n Secrets were not materialized during setup, and that is not fatal ` +
949
+ `here.\n This surface also materializes at session start, which runs ` +
950
+ `with the\n session's own environment — the usual reason setup cannot ` +
951
+ `see the\n bootstrap credential. The session-start hook will retry.\n` +
952
+ ` If secrets are still missing INSIDE a session, that run is the one ` +
953
+ `to debug.`
954
+ );
955
+ }
920
956
  } else if (!requested) {
921
957
  console.log(
922
958
  `\nSecrets: materialized from a session-start hook on "${surface}", ` +
@@ -301,6 +301,24 @@ export function planToolchain(
301
301
  return plan;
302
302
  }
303
303
 
304
+ /**
305
+ * Every install method the runner can execute — the authoritative list.
306
+ *
307
+ * `validate-config.mjs` maintains its own copy because skills are distributed
308
+ * as self-contained directories and nothing else imports across a skill
309
+ * boundary. A comment saying "keep these in step" has now failed twice — first
310
+ * for `release-tar`, then for `release-tree` and `release-binary` together — so
311
+ * the pairing is enforced by a conformance test against this export instead.
312
+ * Adding a kind here fails that test until the validator learns it too.
313
+ */
314
+ export const INSTALL_METHODS = Object.freeze([
315
+ "release-zip",
316
+ "release-tar",
317
+ "release-tree",
318
+ "release-binary",
319
+ "npm-global",
320
+ ]);
321
+
304
322
  /**
305
323
  * Reject a manifest entry that could install something unverifiable.
306
324
  *
@@ -373,6 +391,6 @@ export function assertPinned(tool) {
373
391
  }
374
392
  throw new Error(
375
393
  `${tool.name}: unknown install method "${tool.install}".\n` +
376
- `Supported: release-zip, release-tar, release-tree, release-binary, npm-global.`
394
+ `Supported: ${INSTALL_METHODS.join(", ")}.`
377
395
  );
378
396
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -76,13 +76,37 @@ function bindingsFor(surface) {
76
76
  /**
77
77
  * Install methods the toolchain runner supports.
78
78
  *
79
- * `release-tar` was missing here while the runner had supported it for as long
80
- * as gh has been pinned so this validator would have rejected the project's
81
- * own manifest, and the only reason nobody hit it is that nothing ran the two
82
- * against each other. Kept in step with `assertPinned` in `toolchain.mjs`,
83
- * which is the list that actually decides.
79
+ * This is a deliberate copy of `INSTALL_METHODS` in `toolchain.mjs`, which is
80
+ * the list that actually decides. Skills are distributed as self-contained
81
+ * directories and nothing imports across a skill boundary, so importing the
82
+ * authority here would couple two skills' distribution for one array.
83
+ *
84
+ * The copy has drifted twice. First `release-tar` was missing while the runner
85
+ * had supported it for as long as gh has been pinned. Then `release-tree` and
86
+ * `release-binary` were both missing, which rejected a valid `jq` pin. Each
87
+ * time the only safeguard was a comment asking the next author to remember.
88
+ * A conformance test now compares the two sets directly, so adding a kind on
89
+ * either side fails until both know it.
84
90
  */
85
- const INSTALL_METHODS = new Set(["release-zip", "release-tar", "npm-global"]);
91
+ const INSTALL_METHODS = new Set([
92
+ "release-zip",
93
+ "release-tar",
94
+ "release-tree",
95
+ "release-binary",
96
+ "npm-global",
97
+ ]);
98
+
99
+ /**
100
+ * Download kinds — every method that fetches a URL, so every one that needs a
101
+ * checksum. Spelled as "not npm-global" rather than as a list, because the next
102
+ * download kind should inherit the obligation by default; the drift above came
103
+ * from lists that had to be remembered.
104
+ * @param {string} method An install method.
105
+ * @returns {boolean} Whether the method fetches a pinned artifact.
106
+ */
107
+ function isDownloadKind(method) {
108
+ return INSTALL_METHODS.has(method) && method !== "npm-global";
109
+ }
86
110
 
87
111
  /**
88
112
  * Validate the `secrets` block.
@@ -171,19 +195,28 @@ function validateInstallArtifact(label, entry, problems) {
171
195
  );
172
196
  return;
173
197
  }
174
- // Both archive kinds, not just zip. A tarball pinned without a checksum
175
- // trusts whatever the URL serves today exactly as much as a zip does, and
176
- // checking only one of them meant the tool Lisa's guardrails shell out to —
177
- // gh, which ships a tarball on Linux — was the one going unverified.
178
- if (
179
- (entry.install === "release-zip" || entry.install === "release-tar") &&
180
- !(entry.url && entry.sha256)
181
- ) {
198
+ // Every download kind, not an enumerated few. A tarball pinned without a
199
+ // checksum trusts whatever the URL serves today exactly as much as a zip
200
+ // does, and checking only one of them meant the tool Lisa's guardrails shell
201
+ // out to — gh, which ships a tarball on Linux — was the one going unverified.
202
+ // A `release-binary` matters most of all: the artifact is directly
203
+ // executable, so a wrong one needs no unpacking step to run.
204
+ if (isDownloadKind(entry.install) && !(entry.url && entry.sha256)) {
182
205
  problems.push(
183
206
  `remoteEnv install ${label} needs both url and sha256. A pinned ` +
184
207
  `version with no checksum still trusts whatever the URL serves today.`
185
208
  );
186
209
  }
210
+ // A tree is the one kind with no sane default entry point: the archive root
211
+ // is a directory, so an omitted `binary` would install a directory as a
212
+ // command. The runner refuses it, and a validator that accepted it would just
213
+ // move the failure from review to provisioning.
214
+ if (entry.install === "release-tree" && !entry.binary) {
215
+ problems.push(
216
+ `remoteEnv install ${label} needs "binary" — the path to the entry ` +
217
+ `point INSIDE the archive, such as "maestro/bin/maestro".`
218
+ );
219
+ }
187
220
  if (entry.install === "npm-global" && !entry.package) {
188
221
  problems.push(`remoteEnv install ${label} needs a package`);
189
222
  }
@@ -914,9 +914,45 @@ async function main() {
914
914
  "lisa-secrets-access",
915
915
  "materialize-secrets.mjs"
916
916
  );
917
- execFileSync("node", dryRun ? [materialize, "--dry-run"] : [materialize], {
918
- stdio: "inherit",
919
- });
917
+ // A failure here is fatal only when nothing else will try again.
918
+ //
919
+ // On a surface that materializes at BOTH setup and session start, this run
920
+ // is the floor and the hook is the authority. The two run in different
921
+ // environments: a cloud vendor can expose its configured variables to the
922
+ // agent session while the setup phase — which runs earlier, during image
923
+ // construction — never sees them. That is not an error state, it is the
924
+ // normal shape of `materializeAt: "both"`, and the hook materializes
925
+ // correctly moments later.
926
+ //
927
+ // Treating it as fatal cost an entire environment: setup.sh `exec`s this
928
+ // script, so a non-zero exit fails the vendor's setup step and Claude Code
929
+ // never starts at all. Trading "secrets arrive one phase later" for "the
930
+ // container will not boot" is a bad trade, and it regressed a case that
931
+ // used to work — before this surface materialized at setup, setup simply
932
+ // never attempted it.
933
+ //
934
+ // Every other case keeps the hard failure: an explicit `--phase=secrets`
935
+ // (the hook's own authoritative run) and a setup-only surface such as
936
+ // codex-cloud both have no second chance, so a silent pass there would
937
+ // hand back a session with no credentials and call it ready.
938
+ const retried = !requested && materializesAtSessionStart(materializeAt);
939
+ try {
940
+ execFileSync(
941
+ "node",
942
+ dryRun ? [materialize, "--dry-run"] : [materialize],
943
+ { stdio: "inherit" }
944
+ );
945
+ } catch (err) {
946
+ if (!retried) throw err;
947
+ console.log(
948
+ `\n Secrets were not materialized during setup, and that is not fatal ` +
949
+ `here.\n This surface also materializes at session start, which runs ` +
950
+ `with the\n session's own environment — the usual reason setup cannot ` +
951
+ `see the\n bootstrap credential. The session-start hook will retry.\n` +
952
+ ` If secrets are still missing INSIDE a session, that run is the one ` +
953
+ `to debug.`
954
+ );
955
+ }
920
956
  } else if (!requested) {
921
957
  console.log(
922
958
  `\nSecrets: materialized from a session-start hook on "${surface}", ` +
@@ -301,6 +301,24 @@ export function planToolchain(
301
301
  return plan;
302
302
  }
303
303
 
304
+ /**
305
+ * Every install method the runner can execute — the authoritative list.
306
+ *
307
+ * `validate-config.mjs` maintains its own copy because skills are distributed
308
+ * as self-contained directories and nothing else imports across a skill
309
+ * boundary. A comment saying "keep these in step" has now failed twice — first
310
+ * for `release-tar`, then for `release-tree` and `release-binary` together — so
311
+ * the pairing is enforced by a conformance test against this export instead.
312
+ * Adding a kind here fails that test until the validator learns it too.
313
+ */
314
+ export const INSTALL_METHODS = Object.freeze([
315
+ "release-zip",
316
+ "release-tar",
317
+ "release-tree",
318
+ "release-binary",
319
+ "npm-global",
320
+ ]);
321
+
304
322
  /**
305
323
  * Reject a manifest entry that could install something unverifiable.
306
324
  *
@@ -373,6 +391,6 @@ export function assertPinned(tool) {
373
391
  }
374
392
  throw new Error(
375
393
  `${tool.name}: unknown install method "${tool.install}".\n` +
376
- `Supported: release-zip, release-tar, release-tree, release-binary, npm-global.`
394
+ `Supported: ${INSTALL_METHODS.join(", ")}.`
377
395
  );
378
396
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.332.0",
3
+ "version": "2.332.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"