@codyswann/lisa 2.327.0 → 2.328.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 (109) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +30 -6
  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-detect-tooling/SKILL.md +21 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
  9. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/SKILL.md +95 -0
  11. package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/agents/openai.yaml +4 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +27 -11
  14. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
  15. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
  16. package/plugins/lisa/commands/setup/local-env.md +7 -0
  17. package/plugins/lisa/skills/lisa-detect-tooling/SKILL.md +21 -1
  18. package/plugins/lisa/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
  19. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
  20. package/plugins/lisa/skills/lisa-setup-local-env/SKILL.md +95 -0
  21. package/plugins/lisa/skills/lisa-setup-local-env/agents/openai.yaml +4 -0
  22. package/plugins/lisa/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
  23. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +27 -11
  24. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
  25. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
  26. package/plugins/lisa-agy/commands/lisa/setup/local-env.md +7 -0
  27. package/plugins/lisa-agy/plugin.json +1 -1
  28. package/plugins/lisa-agy/skills/lisa-detect-tooling/SKILL.md +21 -1
  29. package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
  30. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
  31. package/plugins/lisa-agy/skills/lisa-setup-local-env/SKILL.md +95 -0
  32. package/plugins/lisa-agy/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
  33. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +27 -11
  34. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
  35. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
  36. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  39. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-copilot/commands/lisa/setup/local-env.md +7 -0
  43. package/plugins/lisa-copilot/skills/lisa-detect-tooling/SKILL.md +21 -1
  44. package/plugins/lisa-copilot/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
  45. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
  46. package/plugins/lisa-copilot/skills/lisa-setup-local-env/SKILL.md +95 -0
  47. package/plugins/lisa-copilot/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
  48. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +27 -11
  49. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
  50. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
  51. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-cursor/commands/lisa/setup/local-env.md +7 -0
  53. package/plugins/lisa-cursor/skills/lisa-detect-tooling/SKILL.md +21 -1
  54. package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
  55. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
  56. package/plugins/lisa-cursor/skills/lisa-setup-local-env/SKILL.md +95 -0
  57. package/plugins/lisa-cursor/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
  58. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +27 -11
  59. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
  60. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
  61. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-expo-agy/plugin.json +1 -1
  64. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  69. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  74. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  79. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  84. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-rails-agy/plugin.json +1 -1
  89. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  94. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  99. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/src/base/commands/setup/local-env.md +7 -0
  102. package/plugins/src/base/skills/lisa-detect-tooling/SKILL.md +21 -1
  103. package/plugins/src/base/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
  104. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
  105. package/plugins/src/base/skills/lisa-setup-local-env/SKILL.md +95 -0
  106. package/plugins/src/base/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
  107. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +27 -11
  108. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
  109. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
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.327.0",
118
+ "version": "2.328.0",
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.327.0",
3
+ "version": "2.328.0",
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.327.0",
3
+ "version": "2.328.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -46,7 +46,27 @@ What genuinely differs is **consent**, not the list:
46
46
 
47
47
  Omitting `surfaces` means every surface, because that is true of most tools and the cost of forgetting should be a redundant check rather than a silent absence.
48
48
 
49
- A platform-specific pin is the exception worth knowing: a Linux release archive must be `surfaces: ["remote"]`, with a matching `require` entry for `local`, so a laptop asserts the tool without being offered a binary it cannot run.
49
+ Platform differences are **not** expressed with `surfaces`. A downloaded tool declares a
50
+ `platforms` map keyed `<platform>-<arch>`, each block carrying its own `install` method, `url`,
51
+ and `sha256`:
52
+
53
+ ```json
54
+ "platforms": {
55
+ "linux-x64": { "install": "release-tar", "url": "...", "sha256": "..." },
56
+ "darwin-arm64": { "install": "release-zip", "url": "...", "sha256": "..." }
57
+ }
58
+ ```
59
+
60
+ The method lives inside the block because it varies — gh ships a `.tar.gz` for Linux and a
61
+ `.zip` for macOS. A flat entry means one artifact serves everything, which is true of
62
+ `npm-global` and nothing else.
63
+
64
+ This replaces an earlier convention worth naming, because its residue may still be in a
65
+ manifest you read: a Linux archive used to be marked `surfaces: ["remote"]` with a bare
66
+ `require` entry for `local`, so a laptop asserted the tool without being offered a binary it
67
+ could not run. That kept the wrong binary off the laptop by making the tool uninstallable
68
+ there — which is how `bws` and `gh`, the two CLIs Lisa's own guardrails shell out to, came to
69
+ be required on developer machines and provisionable only in containers.
50
70
 
51
71
  ## Usage
52
72
 
@@ -361,9 +361,11 @@ export function detectTooling(cwd = process.cwd()) {
361
361
  /**
362
362
  * The manifest entries an operator can paste, with the parts only they supply.
363
363
  *
364
- * Returns both lists because a platform-specific archive needs both: an
365
- * `install` entry for the surface it is built for, and a `require` entry so the
366
- * other surface still asserts the tool instead of silently ignoring it.
364
+ * A release archive is proposed as a `platforms` map rather than a single URL
365
+ * plus `surfaces: ["remote"]`. The old shape could express only one artifact, so
366
+ * keeping a Linux binary off a laptop meant excluding the tool from laptops
367
+ * altogether — which is how `bws` and `gh` came to be required on a developer
368
+ * machine and installable only in a container.
367
369
  * @param {{name: string}} proposal One detected tool.
368
370
  * @returns {{install: object[], require: object[]}} Manifest skeletons.
369
371
  */
@@ -394,23 +396,33 @@ export function proposedEntries(proposal) {
394
396
  require: [],
395
397
  };
396
398
  }
397
- // A release archive is built for one platform, so it is proposed as
398
- // remote-install plus a local `require`. Proposing only the install entry
399
- // produced a manifest that either offered a Linux binary to a laptop or
400
- // once narrowed to remote stopped checking for the tool locally at all.
401
- // Both halves or neither.
399
+ // A release archive is built for one platform, so every platform the project
400
+ // is developed or built on needs its own block — with its own install method,
401
+ // because a vendor may ship a tarball for one and a zip for another, as gh
402
+ // does. Both containers and laptops are proposed: a tool the manifest can
403
+ // only install remotely is one every developer installs by hand or does
404
+ // without.
405
+ //
406
+ // The placeholders stay unresolved on purpose. A guessed URL is an artifact
407
+ // the checksum cannot vouch for, which is the one property that makes a
408
+ // pinned entry worth reviewing.
409
+ const archive = platform => ({
410
+ install: "<release-tar or release-zip, whichever this platform ships>",
411
+ url: `<release url for this exact version, for ${platform}>`,
412
+ sha256: `<sha256 published with the ${platform} release>`,
413
+ });
402
414
  return {
403
415
  install: [
404
416
  {
405
417
  name: proposal.name,
406
418
  version: "<pin>",
407
- install: "release-tar",
408
- url: "<release url for this exact version, for the remote platform>",
409
- sha256: "<sha256 published with that release>",
410
- surfaces: ["remote"],
419
+ platforms: {
420
+ "linux-x64": archive("linux-x64"),
421
+ "darwin-arm64": archive("darwin-arm64"),
422
+ },
411
423
  },
412
424
  ],
413
- require: [{ name: proposal.name, surfaces: ["local"] }],
425
+ require: [],
414
426
  };
415
427
  }
416
428
 
@@ -73,8 +73,16 @@ function bindingsFor(surface) {
73
73
  return SURFACE_BINDINGS[surface] ?? ["repository"];
74
74
  }
75
75
 
76
- /** Install methods the toolchain runner supports. */
77
- const INSTALL_METHODS = new Set(["release-zip", "npm-global"]);
76
+ /**
77
+ * Install methods the toolchain runner supports.
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.
84
+ */
85
+ const INSTALL_METHODS = new Set(["release-zip", "release-tar", "npm-global"]);
78
86
 
79
87
  /**
80
88
  * Validate the `secrets` block.
@@ -143,6 +151,44 @@ export function validateSecrets(secrets) {
143
151
  return problems;
144
152
  }
145
153
 
154
+ /**
155
+ * Validate the artifact half of one install entry.
156
+ *
157
+ * Split out because the same obligations apply whether the fields sit directly
158
+ * on the entry or inside a per-platform block: a method must be one Lisa can
159
+ * execute, an archive must carry a checksum, and an npm install must name a
160
+ * package. Duplicating those three rules per shape is how one shape ends up
161
+ * quietly weaker than the other.
162
+ * @param {string} label How to name this entry in a message.
163
+ * @param {object} entry The entry or platform block.
164
+ * @param {string[]} problems Accumulator.
165
+ */
166
+ function validateInstallArtifact(label, entry, problems) {
167
+ if (!INSTALL_METHODS.has(entry.install)) {
168
+ problems.push(
169
+ `remoteEnv install ${label} has method ${JSON.stringify(entry.install)}. ` +
170
+ `Supported: ${[...INSTALL_METHODS].join(", ")}.`
171
+ );
172
+ return;
173
+ }
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
+ ) {
182
+ problems.push(
183
+ `remoteEnv install ${label} needs both url and sha256. A pinned ` +
184
+ `version with no checksum still trusts whatever the URL serves today.`
185
+ );
186
+ }
187
+ if (entry.install === "npm-global" && !entry.package) {
188
+ problems.push(`remoteEnv install ${label} needs a package`);
189
+ }
190
+ }
191
+
146
192
  /**
147
193
  * Validate the `remoteEnv` block.
148
194
  * @param {object|undefined} remoteEnv The block, if present.
@@ -163,21 +209,37 @@ export function validateRemoteEnv(remoteEnv) {
163
209
  if (!tool.version) {
164
210
  problems.push(`remoteEnv install "${tool.name}" has no pinned version`);
165
211
  }
166
- if (!INSTALL_METHODS.has(tool.install)) {
212
+ // A flat entry means one artifact serves every platform — true of npm-global
213
+ // and of nothing else. Anything downloaded is platform-specific, so those
214
+ // declare a block per platform and each block is validated in full: a
215
+ // half-filled map is the failure this shape exists to prevent, and it is
216
+ // invisible until someone runs setup on the platform that was left out.
217
+ if (tool.platforms === undefined) {
218
+ validateInstallArtifact(`"${tool.name}"`, tool, problems);
219
+ continue;
220
+ }
221
+ if (
222
+ typeof tool.platforms !== "object" ||
223
+ tool.platforms === null ||
224
+ Array.isArray(tool.platforms)
225
+ ) {
167
226
  problems.push(
168
- `remoteEnv install "${tool.name}" has method ${JSON.stringify(tool.install)}. ` +
169
- `Supported: ${[...INSTALL_METHODS].join(", ")}.`
227
+ `remoteEnv install "${tool.name}" has a platforms field that is not an ` +
228
+ `object keyed by <platform>-<arch>. Omit it when one artifact serves ` +
229
+ `every platform.`
170
230
  );
171
231
  continue;
172
232
  }
173
- if (tool.install === "release-zip" && !(tool.url && tool.sha256)) {
233
+ const platforms = Object.entries(tool.platforms);
234
+ if (platforms.length === 0) {
174
235
  problems.push(
175
- `remoteEnv install "${tool.name}" needs both url and sha256. A pinned ` +
176
- `version with no checksum still trusts whatever the URL serves today.`
236
+ `remoteEnv install "${tool.name}" declares platforms but lists none, so ` +
237
+ `it can never be installed anywhere.`
177
238
  );
239
+ continue;
178
240
  }
179
- if (tool.install === "npm-global" && !tool.package) {
180
- problems.push(`remoteEnv install "${tool.name}" needs a package`);
241
+ for (const [platform, block] of platforms) {
242
+ validateInstallArtifact(`"${tool.name}" (${platform})`, block, problems);
181
243
  }
182
244
  }
183
245
 
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: lisa-setup-local-env
3
+ description: "Bring a developer's machine in…"
4
+ allowed-tools: ["Bash", "Read", "Skill"]
5
+ ---
6
+
7
+ # Setup Local Environment: $ARGUMENTS
8
+
9
+ Make this machine match what the project says it needs.
10
+
11
+ ## Why this exists separately
12
+
13
+ `remoteEnv.tools` was always the manifest for **every** surface, and the executor always
14
+ understood `local`. What did not exist was a way to ask for it. The only path ran a file named
15
+ `setup-remote-env.mjs`, out of a directory named `lisa-remote-env`, via a skill named
16
+ `lisa-setup-remote-env` — and it only existed at all once someone had provisioned a *remote*
17
+ environment, because that is what writes it into the repository.
18
+
19
+ So the machine most likely to be missing a tool was the one with no way to ask about it.
20
+
21
+ This skill is that surface. It adds no manifest, no pins, and no installer: it imports the
22
+ ones the remote flow owns. A second installer would be a second thing to keep honest, and the
23
+ one people run least is the one that rots.
24
+
25
+ ## Usage
26
+
27
+ ```sh
28
+ node scripts/local-env.mjs # report what diverges
29
+ node scripts/local-env.mjs --install-tools # install what is missing
30
+ node scripts/local-env.mjs --json # machine-readable plan
31
+ ```
32
+
33
+ Exit status is non-zero when a tool cannot be resolved, so this is usable as a gate.
34
+
35
+ ## What differs from a container
36
+
37
+ Two things, and both are because a person is here.
38
+
39
+ **Consent.** A container is disposable and provisions itself silently — that is the whole
40
+ point of it. A laptop belongs to someone, and putting pinned binaries into their
41
+ `~/.local/bin` uninvited is not ours to decide. Nothing installs until `--install-tools`.
42
+ Reporting happens either way, because knowing the machine diverges from what the project
43
+ declares is most of the value.
44
+
45
+ **The pin is a floor, not an equality.** A container should hold exactly the pinned version;
46
+ it exists to be reproducible. A developer's machine is shared with every other project they
47
+ work on, so installing a pinned binary ahead of a **newer** one already on `PATH` is a
48
+ downgrade this project imposed on all of them. A newer tool is reported as `newer` and left
49
+ alone. Older, or absent, is an install.
50
+
51
+ ## Platform pins
52
+
53
+ An install entry either serves every platform — true of `npm-global` and nothing else — or
54
+ declares a `platforms` map keyed `<platform>-<arch>`, each block carrying its own `install`
55
+ method, `url`, and `sha256`:
56
+
57
+ ```json
58
+ {
59
+ "name": "gh",
60
+ "version": "2.83.0",
61
+ "platforms": {
62
+ "linux-x64": { "install": "release-tar", "url": "...linux_amd64.tar.gz", "sha256": "...", "binary": "gh_2.83.0_linux_amd64/bin/gh" },
63
+ "darwin-arm64": { "install": "release-zip", "url": "...macOS_arm64.zip", "sha256": "...", "binary": "gh_2.83.0_macOS_arm64/bin/gh" }
64
+ }
65
+ }
66
+ ```
67
+
68
+ The method lives **inside** each block because it varies: gh publishes a `.tar.gz` for Linux
69
+ and a `.zip` for macOS, so a single method would have forced one platform onto an archive kind
70
+ its vendor does not ship.
71
+
72
+ When a tool has no block for the running platform, this skill says so and stops there. It will
73
+ not guess a download URL — a guessed artifact is one the checksum cannot vouch for, and the
74
+ checksum is the only thing standing between a pinned entry and whatever a URL serves today.
75
+ Add the block, or install that one tool however the platform normally would.
76
+
77
+ ## Detect before you install
78
+
79
+ Run `/lisa:detect-tooling` when the report looks thin. The manifest is the only thing that puts
80
+ a binary on `PATH`, and nothing populates it automatically — so a project ships npm scripts
81
+ invoking `maestro`, wires an MCP server whose CLI it also shells out to, and configures
82
+ Playwright thresholds, while `remoteEnv.tools` stays empty and each of those fails at the
83
+ moment of use instead of at setup.
84
+
85
+ The detector proposes and a human decides. It writes nothing, so a tool still only ever reaches
86
+ a machine from a reviewed, pinned, checksummed entry.
87
+
88
+ ## What it will not do
89
+
90
+ - **No unpinned installs.** Every install goes through the same `assertPinned` gate the remote
91
+ flow uses, and the same in-process checksum verification, which refuses before unpacking.
92
+ - **No package-manager fallback.** Shelling out to Homebrew would give no version pin and no
93
+ checksum, which is the property this design exists to enforce.
94
+ - **No writes to the manifest.** Proposing entries belongs to `/lisa:detect-tooling`; deciding
95
+ belongs to a human reviewing a diff.
@@ -0,0 +1,4 @@
1
+ display_name: "Setup Local Env"
2
+ short_description: "Bring a developer's machine in…"
3
+ default_prompt:
4
+ - "Use $lisa-setup-local-env: Bring a developer's machine in…."
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Bring a developer's machine in line with the toolchain the project declares.
4
+ *
5
+ * The manifest, the planner, the pins and the installers are the remote flow's,
6
+ * unchanged and imported — this is the surface that was missing, not a second
7
+ * implementation. `remoteEnv.tools` was always surface-aware and the executor
8
+ * always understood `local`; the only way to reach either was to run a file
9
+ * called `setup-remote-env.mjs` out of a directory called `lisa-remote-env`,
10
+ * which nobody looking for local setup ever found.
11
+ *
12
+ * Two things differ from the remote flow, and both are because a person is here:
13
+ *
14
+ * consent — a container provisions itself silently because it is disposable
15
+ * and nobody is watching. A laptop belongs to someone, so nothing
16
+ * is installed until they ask with --install-tools.
17
+ *
18
+ * tolerance — a container that cannot be fully provisioned should fail before
19
+ * it half-runs, because there is no one to read the report. A
20
+ * developer can act on a list, so an unprovisionable tool is
21
+ * reported, everything else is still installed, and the exit code
22
+ * carries the failure.
23
+ * @module local-env
24
+ */
25
+
26
+ import { existsSync } from "node:fs";
27
+ import { dirname, resolve } from "node:path";
28
+ import { fileURLToPath, pathToFileURL } from "node:url";
29
+
30
+ const HERE = dirname(fileURLToPath(import.meta.url));
31
+
32
+ /**
33
+ * Resolve a script belonging to a sibling skill.
34
+ *
35
+ * Skills are always siblings under one `skills/` directory, in every install
36
+ * layout, so this holds for a plugin install, a checkout, and node_modules
37
+ * alike. Failing loudly here beats reimplementing what the sibling owns.
38
+ * @param {string} skill Sibling skill slug.
39
+ * @param {string} script Script filename.
40
+ * @returns {string} Absolute path.
41
+ */
42
+ function siblingScript(skill, script) {
43
+ const path = resolve(HERE, "..", "..", skill, "scripts", script);
44
+ if (!existsSync(path)) {
45
+ throw new Error(
46
+ `cannot find ${skill}/scripts/${script} beside this skill.\n` +
47
+ `Local setup composes with that skill rather than reimplementing it: ` +
48
+ `the pins, checksums and installers must be the same ones the remote ` +
49
+ `surface uses, or the two drift and only one gets fixed.`
50
+ );
51
+ }
52
+ return path;
53
+ }
54
+
55
+ /**
56
+ * Load the pieces the remote flow owns.
57
+ * @returns {Promise<object>} The imported helpers.
58
+ */
59
+ async function loadRemoteEnv() {
60
+ const setup = await import(
61
+ pathToFileURL(
62
+ siblingScript("lisa-setup-remote-env", "setup-remote-env.mjs")
63
+ ).href
64
+ );
65
+ const toolchain = await import(
66
+ pathToFileURL(siblingScript("lisa-setup-remote-env", "toolchain.mjs")).href
67
+ );
68
+ return { ...toolchain, ...setup };
69
+ }
70
+
71
+ /**
72
+ * Describe one blocked decision in terms a person can act on.
73
+ *
74
+ * A blocked step on a laptop is usually one of two things, and they are fixed
75
+ * differently: a tool the project requires and the machine lacks (install it),
76
+ * or a tool with no artifact pinned for this platform (add the pin, or install
77
+ * it however this platform normally would). Printing the planner's reason alone
78
+ * left the reader to work out which.
79
+ * @param {object} step A plan step.
80
+ * @param {string} platform The platform key that was resolved against.
81
+ * @returns {string} A rendered block.
82
+ */
83
+ function explainBlocked(step, platform) {
84
+ if (step.action === "invalid" && step.reason.includes("no pin for")) {
85
+ return (
86
+ ` ${step.name} — declared, but no artifact is pinned for ${platform}.\n` +
87
+ ` Lisa will not guess a download URL, because a guessed artifact is ` +
88
+ `one the checksum cannot vouch for.\n` +
89
+ ` Add a "${platform}" block under this tool's "platforms" map, or ` +
90
+ `install it yourself.`
91
+ );
92
+ }
93
+ return ` ${step.name} — ${step.reason.split("\n")[0]}`;
94
+ }
95
+
96
+ /**
97
+ * Report and optionally apply the local toolchain.
98
+ * @param {object} argv Parsed arguments.
99
+ * @returns {Promise<number>} Process exit code.
100
+ */
101
+ export async function run(argv) {
102
+ const env = await loadRemoteEnv();
103
+ // Both seams exist for tests, and both default to this machine. A platform
104
+ // that can only be exercised by running on it is one CI will never cover,
105
+ // which is how a resolution bug ships to the platform nobody tested.
106
+ const platform = argv.platform ?? env.currentPlatform();
107
+ const { tools } = env.readRemoteEnvConfig(argv.cwd);
108
+
109
+ const declared = (tools.require ?? []).length + (tools.install ?? []).length;
110
+ if (declared === 0) {
111
+ console.log(
112
+ "No tools are declared in remoteEnv.tools, so there is nothing to check.\n" +
113
+ "Run /lisa:detect-tooling — it reads npm scripts, MCP servers, " +
114
+ "credential\nnotes and quality config, and proposes pinned entries for " +
115
+ "what it finds."
116
+ );
117
+ return 0;
118
+ }
119
+
120
+ const plan = env.planToolchain(tools, env.probe, "local", platform);
121
+ const blocked = plan.filter(
122
+ step => step.action === "missing" || step.action === "invalid"
123
+ );
124
+ const installable = plan.filter(step => step.action === "install");
125
+
126
+ if (argv.json) {
127
+ console.log(JSON.stringify({ platform, plan }, null, 2));
128
+ return blocked.length ? 1 : 0;
129
+ }
130
+
131
+ console.log(`Local toolchain for ${platform}\n`);
132
+ for (const step of plan) {
133
+ console.log(` ${step.action.padEnd(8)} ${step.reason.split("\n")[0]}`);
134
+ }
135
+
136
+ if (installable.length && !argv.installTools) {
137
+ console.log(
138
+ `\n${installable.length} declared tool(s) can be installed here.\n` +
139
+ `Nothing has been installed: this machine is yours, not a container.\n` +
140
+ `Re-run with --install-tools to provision them into ~/.local/bin.`
141
+ );
142
+ }
143
+
144
+ if (installable.length && argv.installTools) {
145
+ const binDir = env.ensureBinDir();
146
+ console.log(`\nInstalling into ${binDir}`);
147
+ for (const step of installable) {
148
+ console.log(` ${step.name}`);
149
+ if (!argv.dryRun) env.installTool(step.tool, binDir);
150
+ }
151
+ }
152
+
153
+ if (blocked.length) {
154
+ console.log(`\n${blocked.length} tool(s) this command cannot resolve:\n`);
155
+ for (const step of blocked) console.log(explainBlocked(step, platform));
156
+ return 1;
157
+ }
158
+
159
+ return 0;
160
+ }
161
+
162
+ if (import.meta.url === `file://${process.argv[1]}`) {
163
+ const argv = {
164
+ installTools: process.argv.includes("--install-tools"),
165
+ dryRun: process.argv.includes("--dry-run"),
166
+ json: process.argv.includes("--json"),
167
+ };
168
+ run(argv)
169
+ .then(code => process.exit(code))
170
+ .catch(err => {
171
+ console.error(err.message);
172
+ process.exit(1);
173
+ });
174
+ }
@@ -95,6 +95,19 @@ This repository has paid for that twice: `gh` was declared nowhere and a cloud s
95
95
 
96
96
  The detector proposes and a human decides. It writes nothing, so provisioning still only ever happens from a reviewed, pinned, checksummed entry.
97
97
 
98
+ ## The same manifest provisions a laptop
99
+
100
+ `remoteEnv.tools` is not a remote-only manifest, and `/lisa:setup:local-env` is how a developer
101
+ applies it to their own machine — same pins, same checksums, same installers, differing only in
102
+ that nothing installs without `--install-tools` and that a newer tool already on `PATH` is left
103
+ alone rather than downgraded.
104
+
105
+ A downloaded tool declares a `platforms` map keyed `<platform>-<arch>`, each block carrying its
106
+ own `install` method, `url`, and `sha256`, because the artifact and the archive kind both vary
107
+ by platform. Before that existed, the only way to keep a Linux binary off a laptop was
108
+ `surfaces: ["remote"]`, which achieved it by making the tool uninstallable there — so `bws` and
109
+ `gh` were required on developer machines and provisionable only in containers.
110
+
98
111
  ## Toolchain manifest — two entry kinds
99
112
 
100
113
  ```json
@@ -108,20 +121,23 @@ The detector proposes and a human decides. It writes nothing, so provisioning st
108
121
  { "name": "unzip" }
109
122
  ],
110
123
  "install": [
111
- {
112
- "name": "bws",
113
- "version": "2.1.0",
114
- "install": "release-zip",
115
- "url": "https://<vendor>/releases/download/bws-v2.1.0/bws-<platform>-2.1.0.zip",
116
- "sha256": "<sha256 published with that exact release>"
117
- },
118
124
  {
119
125
  "name": "gh",
120
126
  "version": "2.83.0",
121
- "install": "release-tar",
122
- "url": "https://<vendor>/releases/download/v2.83.0/gh_2.83.0_linux_amd64.tar.gz",
123
- "sha256": "<sha256 published with that exact release>",
124
- "binary": "gh_2.83.0_linux_amd64/bin/gh"
127
+ "platforms": {
128
+ "linux-x64": {
129
+ "install": "release-tar",
130
+ "url": "https://<vendor>/releases/download/v2.83.0/gh_2.83.0_linux_amd64.tar.gz",
131
+ "sha256": "<sha256 published with that exact release>",
132
+ "binary": "gh_2.83.0_linux_amd64/bin/gh"
133
+ },
134
+ "darwin-arm64": {
135
+ "install": "release-zip",
136
+ "url": "https://<vendor>/releases/download/v2.83.0/gh_2.83.0_macOS_arm64.zip",
137
+ "sha256": "<sha256 published with that exact release>",
138
+ "binary": "gh_2.83.0_macOS_arm64/bin/gh"
139
+ }
140
+ }
125
141
  },
126
142
  {
127
143
  "name": "codex",