@codyswann/lisa 2.317.7 → 2.319.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 (77) hide show
  1. package/cdk/package-lisa/package.lisa.json +2 -2
  2. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  3. package/dist/core/upstream-evidence-manifest.js +12 -7
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/expo/package-lisa/package.lisa.json +5 -5
  6. package/nestjs/package-lisa/package.lisa.json +1 -1
  7. package/package.json +1 -1
  8. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
  11. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +12 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
  13. package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
  14. package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +12 -0
  15. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
  18. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +12 -0
  19. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
  27. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +12 -0
  28. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
  29. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
  31. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +12 -0
  32. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
  33. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-agy/plugin.json +1 -1
  36. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  56. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-agy/plugin.json +1 -1
  61. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  66. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  71. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
  74. package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +12 -0
  75. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
  76. package/scripts/check-security-floors.mjs +285 -0
  77. package/typescript/package-lisa/package.lisa.json +6 -6
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
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.317.7",
3
+ "version": "2.319.0",
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.317.7",
3
+ "version": "2.319.0",
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.317.7",
3
+ "version": "2.319.0",
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.317.7",
3
+ "version": "2.319.0",
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.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -47,6 +47,16 @@ function fingerprint(value) {
47
47
  return createHash("sha256").update(value).digest("hex").slice(0, 12);
48
48
  }
49
49
 
50
+ /**
51
+ * The value a proxied credential reads as instead of a usable token.
52
+ *
53
+ * Some surfaces keep a credential outside the sandbox entirely and substitute
54
+ * the real value at egress. The variable is then present and non-empty, which
55
+ * is exactly what makes it dangerous: a presence check passes, and only a
56
+ * consumer that reads the variable *itself* discovers it holds a placeholder.
57
+ */
58
+ const PROXY_PLACEHOLDER = "proxy-injected";
59
+
50
60
  /**
51
61
  * Assert every declared name actually resolves.
52
62
  * @param {object} cfg Resolved configuration.
@@ -56,18 +66,36 @@ function fingerprint(value) {
56
66
  */
57
67
  export function checkRequired(cfg, provider, file, report) {
58
68
  for (const name of cfg.require ?? []) {
59
- const resolves =
60
- (process.env[name] ?? "").trim() ||
61
- file.get(name) ||
62
- provider.get(name)?.value;
63
- if (resolves) report("ok", name, "resolves");
64
- else
69
+ const fromEnv = (process.env[name] ?? "").trim();
70
+ const resolves = fromEnv || file.get(name) || provider.get(name)?.value;
71
+
72
+ if (!resolves) {
65
73
  report(
66
74
  "error",
67
75
  name,
68
76
  "declared in secrets.require but resolves nowhere — a startup error, " +
69
77
  "not a late surprise"
70
78
  );
79
+ continue;
80
+ }
81
+
82
+ // Reported separately from "resolves" because the two are not the same
83
+ // claim. A tool that authenticates through the proxy works; a script that
84
+ // reads this variable and puts it in an Authorization header sends the
85
+ // literal placeholder and gets a confusing auth failure far from here.
86
+ if (fromEnv === PROXY_PLACEHOLDER) {
87
+ report(
88
+ "warn",
89
+ name,
90
+ `reads as the literal string "${PROXY_PLACEHOLDER}" — the value is ` +
91
+ `substituted at egress and never enters this environment. Tools that ` +
92
+ `authenticate through the proxy work; anything reading this variable ` +
93
+ `directly gets the placeholder, not a credential`
94
+ );
95
+ continue;
96
+ }
97
+
98
+ report("ok", name, "resolves");
71
99
  }
72
100
  }
73
101
 
@@ -28,11 +28,19 @@ import { join } from "node:path";
28
28
  * drift and leak. It is *required* on surfaces whose bootstrap runs before the
29
29
  * consuming process exists and which therefore have no other channel; a remote
30
30
  * agent container prepares itself during setup, long before any task starts.
31
+ *
32
+ * The two remote surfaces share the capability but not the timing, and a reader
33
+ * adding a third should not assume otherwise. `codex-cloud` re-runs its setup
34
+ * script when a container resumes, so materializing there picks up a rotated
35
+ * value. `claude-web` *skips* its setup script whenever a filesystem cache
36
+ * exists, so materializing there would strand a rotated value until the cache
37
+ * expired — its materialize step runs from a session-start hook instead.
31
38
  */
32
39
  export const SURFACES = {
33
40
  local: { materialized: false, mayWriteValues: false },
34
41
  "github-actions": { materialized: false, mayWriteValues: false },
35
42
  "codex-cloud": { materialized: true, mayWriteValues: true },
43
+ "claude-web": { materialized: true, mayWriteValues: true },
36
44
  };
37
45
 
38
46
  /** Config defaults when `.lisa.config.json` carries no `secrets` block. */
@@ -68,6 +76,10 @@ export function detectSurface(configured = null, env = process.env) {
68
76
  return explicit;
69
77
  }
70
78
  if ((env.GITHUB_ACTIONS ?? "") === "true") return "github-actions";
79
+ // Compared to the exact string rather than tested for presence: the variable
80
+ // is documented as carrying "true" in a cloud session and never being true
81
+ // locally, so a presence test would misread a shell that exports it empty.
82
+ if ((env.CLAUDE_CODE_REMOTE ?? "") === "true") return "claude-web";
71
83
  if ((env.CODEX_SANDBOX ?? env.CODEX_HOME ?? "") !== "") return "codex-cloud";
72
84
  return "local";
73
85
  }
@@ -19,14 +19,59 @@
19
19
  import { existsSync, readFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
 
22
+ import { SURFACES as SURFACE_CAPABILITIES } from "./surfaces.mjs";
23
+
22
24
  /** Providers with a read implementation today. */
23
25
  const IMPLEMENTED_PROVIDERS = new Set(["bitwarden", "doppler", "env"]);
24
26
 
25
27
  /** Providers named in the dispatch table but not yet implemented. */
26
28
  const DECLARED_PROVIDERS = new Set(["1password", "aws", "vault"]);
27
29
 
28
- /** Surfaces the resolver knows. */
29
- const SURFACES = new Set(["local", "github-actions", "codex-cloud"]);
30
+ /**
31
+ * Surfaces the resolver knows.
32
+ *
33
+ * Derived from the resolver's own table rather than restated here. The two
34
+ * lists previously drifted apart by construction: adding a surface meant
35
+ * remembering to edit a second file, and forgetting produced a config that
36
+ * resolved correctly at runtime while `doctor` called it unknown.
37
+ */
38
+ const SURFACES = new Set(Object.keys(SURFACE_CAPABILITIES));
39
+
40
+ /**
41
+ * What a provisioned surface must record before anything dispatches to it.
42
+ *
43
+ * Deliberately not uniform, because these surfaces do not bind the same way. A
44
+ * Codex Cloud environment is bound to one repository, so naming the repository
45
+ * is part of proving the environment is the right one. A Claude cloud
46
+ * environment has no repository at all — it is account-scoped configuration
47
+ * (network policy, variables, setup script) and the repository arrives per
48
+ * session — so its durable handle is the routine that dispatch fires.
49
+ *
50
+ * Requiring `repository` of every surface, as this file used to, would demand
51
+ * a field that cannot be true of `claude-web` in any meaningful sense.
52
+ *
53
+ * `repository` stays the default so every existing surface keeps its current
54
+ * contract. This file checks structure only — whether a declaration *could* be
55
+ * correct — so it deliberately does not restate the fuller preconditions that
56
+ * `lisa-remote-dispatch` enforces at the moment it actually dispatches.
57
+ */
58
+ const SURFACE_BINDINGS = {
59
+ "claude-web": ["routineId", "fireUrl"],
60
+ };
61
+
62
+ /**
63
+ * The fields a surface must record, with the default applied.
64
+ *
65
+ * Read through one helper rather than at each call site, because the two
66
+ * callers answer different questions — "could this declaration be correct" and
67
+ * "may an automation dispatch to it" — and a fallback that drifted between them
68
+ * would let those answers disagree about the very same config.
69
+ * @param {string} surface Surface name.
70
+ * @returns {string[]} Field names that must be present.
71
+ */
72
+ function bindingsFor(surface) {
73
+ return SURFACE_BINDINGS[surface] ?? ["repository"];
74
+ }
30
75
 
31
76
  /** Install methods the toolchain runner supports. */
32
77
  const INSTALL_METHODS = new Set(["release-zip", "npm-global"]);
@@ -139,15 +184,30 @@ export function validateRemoteEnv(remoteEnv) {
139
184
  for (const [surface, block] of Object.entries(remoteEnv.surfaces ?? {})) {
140
185
  if (!SURFACES.has(surface)) {
141
186
  problems.push(`remoteEnv.surfaces has unknown surface "${surface}"`);
187
+ continue;
142
188
  }
143
- if (!block.repository) {
144
- problems.push(`remoteEnv.surfaces["${surface}"] has no repository`);
189
+ for (const field of bindingsFor(surface)) {
190
+ if (!block[field]) {
191
+ problems.push(`remoteEnv.surfaces["${surface}"] has no ${field}`);
192
+ }
145
193
  }
146
194
  }
147
195
 
148
196
  return problems;
149
197
  }
150
198
 
199
+ /**
200
+ * Report whether a surface has been provisioned far enough to dispatch to.
201
+ * @param {object|undefined} remoteEnv The remote-environment block.
202
+ * @param {string} surface Surface name.
203
+ * @returns {boolean} Whether every binding field is recorded.
204
+ */
205
+ export function isProvisioned(remoteEnv, surface) {
206
+ const block = remoteEnv?.surfaces?.[surface];
207
+ if (!block) return false;
208
+ return bindingsFor(surface).every(field => Boolean(block[field]));
209
+ }
210
+
151
211
  /**
152
212
  * Validate the `automations` block against declared surfaces.
153
213
  * @param {object|undefined} automations The block, if present.
@@ -173,7 +233,7 @@ export function validateAutomations(automations, remoteEnv) {
173
233
  );
174
234
  continue;
175
235
  }
176
- if (!remoteEnv?.surfaces?.[loop.executionEnv]?.repository) {
236
+ if (!isProvisioned(remoteEnv, loop.executionEnv)) {
177
237
  problems.push(
178
238
  `automations["${name}"] dispatches to "${loop.executionEnv}", which is ` +
179
239
  `not provisioned. Run /lisa:setup:remote-env ${loop.executionEnv} first.`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -47,6 +47,16 @@ function fingerprint(value) {
47
47
  return createHash("sha256").update(value).digest("hex").slice(0, 12);
48
48
  }
49
49
 
50
+ /**
51
+ * The value a proxied credential reads as instead of a usable token.
52
+ *
53
+ * Some surfaces keep a credential outside the sandbox entirely and substitute
54
+ * the real value at egress. The variable is then present and non-empty, which
55
+ * is exactly what makes it dangerous: a presence check passes, and only a
56
+ * consumer that reads the variable *itself* discovers it holds a placeholder.
57
+ */
58
+ const PROXY_PLACEHOLDER = "proxy-injected";
59
+
50
60
  /**
51
61
  * Assert every declared name actually resolves.
52
62
  * @param {object} cfg Resolved configuration.
@@ -56,18 +66,36 @@ function fingerprint(value) {
56
66
  */
57
67
  export function checkRequired(cfg, provider, file, report) {
58
68
  for (const name of cfg.require ?? []) {
59
- const resolves =
60
- (process.env[name] ?? "").trim() ||
61
- file.get(name) ||
62
- provider.get(name)?.value;
63
- if (resolves) report("ok", name, "resolves");
64
- else
69
+ const fromEnv = (process.env[name] ?? "").trim();
70
+ const resolves = fromEnv || file.get(name) || provider.get(name)?.value;
71
+
72
+ if (!resolves) {
65
73
  report(
66
74
  "error",
67
75
  name,
68
76
  "declared in secrets.require but resolves nowhere — a startup error, " +
69
77
  "not a late surprise"
70
78
  );
79
+ continue;
80
+ }
81
+
82
+ // Reported separately from "resolves" because the two are not the same
83
+ // claim. A tool that authenticates through the proxy works; a script that
84
+ // reads this variable and puts it in an Authorization header sends the
85
+ // literal placeholder and gets a confusing auth failure far from here.
86
+ if (fromEnv === PROXY_PLACEHOLDER) {
87
+ report(
88
+ "warn",
89
+ name,
90
+ `reads as the literal string "${PROXY_PLACEHOLDER}" — the value is ` +
91
+ `substituted at egress and never enters this environment. Tools that ` +
92
+ `authenticate through the proxy work; anything reading this variable ` +
93
+ `directly gets the placeholder, not a credential`
94
+ );
95
+ continue;
96
+ }
97
+
98
+ report("ok", name, "resolves");
71
99
  }
72
100
  }
73
101
 
@@ -28,11 +28,19 @@ import { join } from "node:path";
28
28
  * drift and leak. It is *required* on surfaces whose bootstrap runs before the
29
29
  * consuming process exists and which therefore have no other channel; a remote
30
30
  * agent container prepares itself during setup, long before any task starts.
31
+ *
32
+ * The two remote surfaces share the capability but not the timing, and a reader
33
+ * adding a third should not assume otherwise. `codex-cloud` re-runs its setup
34
+ * script when a container resumes, so materializing there picks up a rotated
35
+ * value. `claude-web` *skips* its setup script whenever a filesystem cache
36
+ * exists, so materializing there would strand a rotated value until the cache
37
+ * expired — its materialize step runs from a session-start hook instead.
31
38
  */
32
39
  export const SURFACES = {
33
40
  local: { materialized: false, mayWriteValues: false },
34
41
  "github-actions": { materialized: false, mayWriteValues: false },
35
42
  "codex-cloud": { materialized: true, mayWriteValues: true },
43
+ "claude-web": { materialized: true, mayWriteValues: true },
36
44
  };
37
45
 
38
46
  /** Config defaults when `.lisa.config.json` carries no `secrets` block. */
@@ -68,6 +76,10 @@ export function detectSurface(configured = null, env = process.env) {
68
76
  return explicit;
69
77
  }
70
78
  if ((env.GITHUB_ACTIONS ?? "") === "true") return "github-actions";
79
+ // Compared to the exact string rather than tested for presence: the variable
80
+ // is documented as carrying "true" in a cloud session and never being true
81
+ // locally, so a presence test would misread a shell that exports it empty.
82
+ if ((env.CLAUDE_CODE_REMOTE ?? "") === "true") return "claude-web";
71
83
  if ((env.CODEX_SANDBOX ?? env.CODEX_HOME ?? "") !== "") return "codex-cloud";
72
84
  return "local";
73
85
  }
@@ -19,14 +19,59 @@
19
19
  import { existsSync, readFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
 
22
+ import { SURFACES as SURFACE_CAPABILITIES } from "./surfaces.mjs";
23
+
22
24
  /** Providers with a read implementation today. */
23
25
  const IMPLEMENTED_PROVIDERS = new Set(["bitwarden", "doppler", "env"]);
24
26
 
25
27
  /** Providers named in the dispatch table but not yet implemented. */
26
28
  const DECLARED_PROVIDERS = new Set(["1password", "aws", "vault"]);
27
29
 
28
- /** Surfaces the resolver knows. */
29
- const SURFACES = new Set(["local", "github-actions", "codex-cloud"]);
30
+ /**
31
+ * Surfaces the resolver knows.
32
+ *
33
+ * Derived from the resolver's own table rather than restated here. The two
34
+ * lists previously drifted apart by construction: adding a surface meant
35
+ * remembering to edit a second file, and forgetting produced a config that
36
+ * resolved correctly at runtime while `doctor` called it unknown.
37
+ */
38
+ const SURFACES = new Set(Object.keys(SURFACE_CAPABILITIES));
39
+
40
+ /**
41
+ * What a provisioned surface must record before anything dispatches to it.
42
+ *
43
+ * Deliberately not uniform, because these surfaces do not bind the same way. A
44
+ * Codex Cloud environment is bound to one repository, so naming the repository
45
+ * is part of proving the environment is the right one. A Claude cloud
46
+ * environment has no repository at all — it is account-scoped configuration
47
+ * (network policy, variables, setup script) and the repository arrives per
48
+ * session — so its durable handle is the routine that dispatch fires.
49
+ *
50
+ * Requiring `repository` of every surface, as this file used to, would demand
51
+ * a field that cannot be true of `claude-web` in any meaningful sense.
52
+ *
53
+ * `repository` stays the default so every existing surface keeps its current
54
+ * contract. This file checks structure only — whether a declaration *could* be
55
+ * correct — so it deliberately does not restate the fuller preconditions that
56
+ * `lisa-remote-dispatch` enforces at the moment it actually dispatches.
57
+ */
58
+ const SURFACE_BINDINGS = {
59
+ "claude-web": ["routineId", "fireUrl"],
60
+ };
61
+
62
+ /**
63
+ * The fields a surface must record, with the default applied.
64
+ *
65
+ * Read through one helper rather than at each call site, because the two
66
+ * callers answer different questions — "could this declaration be correct" and
67
+ * "may an automation dispatch to it" — and a fallback that drifted between them
68
+ * would let those answers disagree about the very same config.
69
+ * @param {string} surface Surface name.
70
+ * @returns {string[]} Field names that must be present.
71
+ */
72
+ function bindingsFor(surface) {
73
+ return SURFACE_BINDINGS[surface] ?? ["repository"];
74
+ }
30
75
 
31
76
  /** Install methods the toolchain runner supports. */
32
77
  const INSTALL_METHODS = new Set(["release-zip", "npm-global"]);
@@ -139,15 +184,30 @@ export function validateRemoteEnv(remoteEnv) {
139
184
  for (const [surface, block] of Object.entries(remoteEnv.surfaces ?? {})) {
140
185
  if (!SURFACES.has(surface)) {
141
186
  problems.push(`remoteEnv.surfaces has unknown surface "${surface}"`);
187
+ continue;
142
188
  }
143
- if (!block.repository) {
144
- problems.push(`remoteEnv.surfaces["${surface}"] has no repository`);
189
+ for (const field of bindingsFor(surface)) {
190
+ if (!block[field]) {
191
+ problems.push(`remoteEnv.surfaces["${surface}"] has no ${field}`);
192
+ }
145
193
  }
146
194
  }
147
195
 
148
196
  return problems;
149
197
  }
150
198
 
199
+ /**
200
+ * Report whether a surface has been provisioned far enough to dispatch to.
201
+ * @param {object|undefined} remoteEnv The remote-environment block.
202
+ * @param {string} surface Surface name.
203
+ * @returns {boolean} Whether every binding field is recorded.
204
+ */
205
+ export function isProvisioned(remoteEnv, surface) {
206
+ const block = remoteEnv?.surfaces?.[surface];
207
+ if (!block) return false;
208
+ return bindingsFor(surface).every(field => Boolean(block[field]));
209
+ }
210
+
151
211
  /**
152
212
  * Validate the `automations` block against declared surfaces.
153
213
  * @param {object|undefined} automations The block, if present.
@@ -173,7 +233,7 @@ export function validateAutomations(automations, remoteEnv) {
173
233
  );
174
234
  continue;
175
235
  }
176
- if (!remoteEnv?.surfaces?.[loop.executionEnv]?.repository) {
236
+ if (!isProvisioned(remoteEnv, loop.executionEnv)) {
177
237
  problems.push(
178
238
  `automations["${name}"] dispatches to "${loop.executionEnv}", which is ` +
179
239
  `not provisioned. Run /lisa:setup:remote-env ${loop.executionEnv} first.`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.317.7",
3
+ "version": "2.319.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"