@codyswann/lisa 2.321.4 → 2.322.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 (76) hide show
  1. package/dist/core/upstream-evidence-manifest.js +4 -4
  2. package/package.json +1 -1
  3. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  4. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  5. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
  6. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +15 -14
  7. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  8. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  9. package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
  10. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +15 -14
  11. package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  12. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
  15. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +15 -14
  16. package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  17. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  18. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  21. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
  25. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +15 -14
  26. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  27. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
  30. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +15 -14
  31. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  32. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  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-setup-automations/scripts/generate-workflow.mjs +32 -13
  74. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +15 -14
  75. package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  76. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
@@ -550,7 +550,7 @@ export const UPSTREAM_EVIDENCE_MANIFEST = Object.freeze({
550
550
  "plugins/src/base/skills/lisa-sentry-access/SKILL.md": "ecaf49b42bbf399781a3ebd0b1595e5a8ac44422537832e0442b2f184da0c83c",
551
551
  "plugins/src/base/skills/lisa-setup-atlassian/SKILL.md": "3820aecb57d184fde1cfa988e7e1402d8f2a766a0b3ffbb576de5a6a72b3d5c5",
552
552
  "plugins/src/base/skills/lisa-setup-automations/SKILL.md": "3ad004148a54571a50df90f23abec71e8510ef3ea3562ac812f8e2e11beb157a",
553
- "plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs": "dd1a0999156bbbaf864fc0f1d85206db5ea2ada172ccac2ee11091303e06ff4d",
553
+ "plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs": "bfb3867cc62dd37d84367859e4d796edb7a0c046595b3e0194ddb0b6d3b63d61",
554
554
  "plugins/src/base/skills/lisa-setup-confluence/SKILL.md": "e92d762dbdaeae671c3e52dfe8e10d40a10b606b5e050589181a3898649faedc",
555
555
  "plugins/src/base/skills/lisa-setup-github-repo/SKILL.md": "b9b223a5ffad2f88e3fde5831b60fea9bdc2c981750c49d03bb046d43772a0d3",
556
556
  "plugins/src/base/skills/lisa-setup-github/SKILL.md": "75435e81130abd21c678d302b7a220da95a19868d122ca93168278628b200666",
@@ -559,10 +559,10 @@ export const UPSTREAM_EVIDENCE_MANIFEST = Object.freeze({
559
559
  "plugins/src/base/skills/lisa-setup-linear/SKILL.md": "ec76f832a6df6f58b163f7f5ba6826ed2fcbddc91e44a8372e874858e4d0c518",
560
560
  "plugins/src/base/skills/lisa-setup-notion/SKILL.md": "f5a1e9290789fd1c33675168d30461fb24a11c98a433ef777c1f536bc2f905ef",
561
561
  "plugins/src/base/skills/lisa-setup-remote-aws/SKILL.md": "80fbf157f9c562c033886c25a99b37356602edd9e61cd2d492f339769ddcf97e",
562
- "plugins/src/base/skills/lisa-setup-remote-env/SKILL.md": "313d1b96500950b566a3e22bc1620662a76635c89c6c90c2648d48e0497574b7",
562
+ "plugins/src/base/skills/lisa-setup-remote-env/SKILL.md": "0673567b71f5d3ef52b244bbd7acfe628d0cfcb7459bda5375ce81c80a1cc501",
563
563
  "plugins/src/base/skills/lisa-setup-remote-env/assets/session-start.sh": "6e3871ec2f8d56b8ebb85376ebdd3956f3ed8fa4f7b278c2ed90ca9b8845897c",
564
- "plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh": "f1b492196f41adcceb1c2a2ada048355e7f36735de3be0dfe028e472b452f28c",
565
- "plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs": "ceba67f85f1db0f390b0cbe66c63401cc03eaa8a27b254ba3d4b0c271103a53d",
564
+ "plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh": "7f7cf8a2248dbaa31f2f039fdaf147d083427a21633f7cf06ad612f38344d6c1",
565
+ "plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs": "1794aec433f1f469857123ed08156c4a74cc746cca12ca541d38edfbadc5c746",
566
566
  "plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs": "ff66d33ba41a068d09be18f81ac68988238c2afa1d4e3733ae906b73eea74324",
567
567
  "plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs": "82bc2a27a0a5afb2ff413a88365c619fd7f0eaada5de3ccabf2000938f0607a4",
568
568
  "plugins/src/base/skills/lisa-setup-sonar/SKILL.md": "53fbd8acce4b5e47e88195a7b90d5be424fa6ef7ad872f52c50467c690664c3b",
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.321.4",
118
+ "version": "2.322.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.321.4",
3
+ "version": "2.322.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.321.4",
3
+ "version": "2.322.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -41,9 +41,20 @@ export function readAutomation(name, cwd = process.cwd()) {
41
41
  `Declare the loop before generating its workflow.`
42
42
  );
43
43
  }
44
+ const surface = cfg.remoteEnv?.surfaces?.[loop.executionEnv];
45
+ const github = cfg.github ?? {};
44
46
  return {
45
47
  ...loop,
46
- repository: cfg.remoteEnv?.surfaces?.[loop.executionEnv]?.repository,
48
+ // Only used for the fork guard, so it is a question about *this* repository
49
+ // rather than about the surface. Read from the surface where one records it
50
+ // — Codex Cloud binds an environment to a repository — and otherwise from
51
+ // the project's own GitHub config. A Claude cloud environment binds no
52
+ // repository at all, and demanding one there asked for a field that cannot
53
+ // exist, which refused to generate a workflow for a correctly configured
54
+ // loop.
55
+ repository:
56
+ surface?.repository ??
57
+ (github.org && github.repo ? `${github.org}/${github.repo}` : undefined),
47
58
  // Where this project keeps its bootstrap. A workstation serving several
48
59
  // tenants gives each its own name, so the generated workflow must template
49
60
  // it rather than assume the default.
@@ -81,12 +92,28 @@ export function renderWorkflow(name, loop) {
81
92
  const bootstrap = loop.bootstrapKey ?? "BWS_ACCESS_TOKEN";
82
93
  if (!loop.repository) {
83
94
  throw new Error(
84
- `automations["${name}"] targets executionEnv "${loop.executionEnv}", ` +
85
- `but no remoteEnv.surfaces["${loop.executionEnv}"].repository is set.\n` +
86
- `Run /lisa:setup:remote-env ${loop.executionEnv} first.`
95
+ `cannot determine which repository this loop belongs to.\n` +
96
+ `The generated workflow guards on it so a fork never dispatches. Set ` +
97
+ `github.org and github.repo in .lisa.config.json.`
87
98
  );
88
99
  }
89
100
 
101
+ // Always emitted, even for a surface whose dispatcher token is not consumed
102
+ // by use. Making it conditional on secrets.rotating would mean a project that
103
+ // later declares one, without regenerating its workflow, silently loses the
104
+ // persist step and strands the replacement. One inert step is the cheaper
105
+ // failure.
106
+ const rotation = `
107
+ # The dispatcher authenticates with a credential that rotates on use, so
108
+ # the replacement must be persisted even when the dispatch itself failed.
109
+ - name: Persist any credential rotation
110
+ if: \${{ always() }}
111
+ env:
112
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
113
+ run: |
114
+ set -euo pipefail
115
+ node .claude/skills/lisa-secrets-access/scripts/rotate-secret.mjs leases`;
116
+
90
117
  const trigger = loop.enabled
91
118
  ? ` schedule:\n - cron: '${loop.schedule}'\n workflow_dispatch:`
92
119
  : ` # schedule: disabled until the production path is proven once for one\n` +
@@ -149,15 +176,7 @@ jobs:
149
176
  'executionEnv=${loop.executionEnv} ${loop.payload ?? ""}' \\
150
177
  --skill ${loop.skill ?? `lisa-${name}`}
151
178
 
152
- # The dispatcher authenticates with a credential that rotates on use, so
153
- # the replacement must be persisted even when the dispatch itself failed.
154
- - name: Persist any credential rotation
155
- if: \${{ always() }}
156
- env:
157
- ${bootstrap}: \${{ secrets.${bootstrap} }}
158
- run: |
159
- set -euo pipefail
160
- node .claude/skills/lisa-secrets-access/scripts/rotate-secret.mjs leases
179
+ ${rotation}
161
180
  `;
162
181
  }
163
182
 
@@ -10,26 +10,28 @@ Prepare a remote surface so a host project can execute there. Today that means *
10
10
 
11
11
  ## What lives where
12
12
 
13
- The remote environment's own configuration fields stay **one line into the repository, preceded by the project's dependency install**. Nothing else is pasted into a vendor UI.
13
+ The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: <install> && bash scripts/lisa-remote-env/setup.sh
17
- maintenance: <install> && bash scripts/lisa-remote-env/setup.sh
16
+ setup: bash "$HOME"/*/scripts/lisa-remote-env/setup.sh
17
+ maintenance: bash "$HOME"/*/scripts/lisa-remote-env/setup.sh
18
18
  ```
19
19
 
20
- `<install>` is whatever the project already uses`bun install`, `npm ci`, `pnpm install`. Substitute it; do not invent one.
20
+ **That line is identical for every project.** Nothing in it names the repository or the package manager, so it can be pasted unchanged into any project's environment. The glob is what locates the checkout Claude Code web runs this field from `$HOME` while the clone lands one level down — and the script then anchors itself on the repository root.
21
21
 
22
22
  They are the same command. A container may be built fresh or resumed from cache; every step is idempotent and version-aware, so running it twice is correct, and running it on resume is what picks up a rotated value, an edited note, or a changed version pin.
23
23
 
24
- The complete logic is repository-owned so it is reviewed, versioned, tested, and reusable. A large inline installer in a settings field is none of those things.
24
+ The complete logic is repository-owned so it is reviewed, versioned, tested, and reusable. A large inline installer in a settings field is none of those things — and neither is a repository name and a package manager, which is what this field used to carry.
25
25
 
26
- ### Why the install has to come first
26
+ ### The script installs the dependencies itself
27
27
 
28
28
  **A clone does not contain the skills on the harnesses that matter here.** OpenCode and Antigravity have them written into the checkout by `lisa apply`. Claude and Codex receive them as an *installed plugin*, which lives in the user's home directory — so a container that has just cloned the repository has never seen it.
29
29
 
30
30
  `node_modules/@codyswann/lisa` is therefore the only copy present on a fresh container, and it is a good one: it is the version that project pins, which is the version its setup should run. The entrypoint searches the agent directories first and falls back to it.
31
31
 
32
- Omitting the install does not degrade gracefully. The entrypoint exits before the toolchain, secrets, and hook phases have done anything, so the environment looks provisioned and fails on first dispatch.
32
+ So the install has to happen before the runner is resolved — and the entrypoint does it, rather than the settings field. Which package manager is read from the lockfile the project actually commits (`bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`), never guessed: a guessed one fails on the container's first command with an error that blames the project rather than the guess. The step is skipped when `node_modules` already exists, which is what keeps a resumed container cheap, and `LISA_SKIP_INSTALL=1` opts out entirely for a caller that has already installed.
33
+
34
+ A project with no lockfile is not fatal on its own — a checkout may carry the skill directly — so the script says so and lets the resolver decide.
33
35
 
34
36
  ## The three phases
35
37
 
@@ -134,11 +136,10 @@ When emitting, produce exactly:
134
136
  ```text
135
137
  Environment name: <project> remote executor
136
138
  Repository: <org>/<repo> (must be the default checkout)
137
- Setup script: <install> && bash scripts/lisa-remote-env/setup.sh
138
- Maintenance: <install> && bash scripts/lisa-remote-env/setup.sh
139
- (<install> is the project's own: bun install, npm ci, ...
140
- It must precede the script — on a fresh container
141
- node_modules is the only copy of the skills present.)
139
+ Setup script: bash "$HOME"/*/scripts/lisa-remote-env/setup.sh
140
+ Maintenance: bash "$HOME"/*/scripts/lisa-remote-env/setup.sh
141
+ (identical for every project the script finds the
142
+ checkout and installs from the committed lockfile)
142
143
  Environment vars: LISA_SECRETS_SURFACE=codex-cloud
143
144
  BWS_ACCESS_TOKEN=<from the provider; an environment
144
145
  variable, not a task secret — setup and cache-resume
@@ -157,11 +158,11 @@ Generate the exact text to paste:
157
158
  node scripts/setup-remote-env.mjs --emit=claude-web
158
159
  ```
159
160
 
160
- It reads the project's own install command from its lockfile and the bootstrap name from `secrets.bootstrap.key`, then emits the environment fields, the `.claude/settings.json` hook block, and the two base-image surprises worth knowing before they cost an afternoon:
161
+ It reads the bootstrap name from `secrets.bootstrap.key` and emits the environment fields, the `.claude/settings.json` hook block, and the base-image surprises worth knowing before they cost an afternoon:
161
162
 
162
163
  - **`gh` is not pre-installed.** If the project's flows shell out to it, add it to `remoteEnv.tools.install`, pinned and checksummed like anything else.
163
164
  - **A proxied credential reads as the literal string `proxy-injected`.** Tools that authenticate through the GitHub proxy work; a script that reads the variable directly gets the placeholder. The read-back asserts this rather than leaving it to be discovered against a live service.
164
- - **The setup field runs from `$HOME`, not from the checkout.** The emitted setup command starts with `cd <repo-name>` on purpose; without it the package install runs one directory above the clone and cannot find `package.json`.
165
+ - **The setup field runs from `$HOME`, not from the checkout.** That is why the field is a `$HOME` glob rather than a plain relative path: it locates the clone one level down, and the script anchors itself from there. Nothing in the emitted line names this project, so it is the same line everywhere.
165
166
  - **Trusted network access is not enough for provider CLIs.** Use Custom and include package registries, GitHub, cloud SDK hosts, and the bootstrap credential manager API.
166
167
 
167
168
  ### One environment per project, pinned locally
@@ -24,8 +24,10 @@ case "$script_dir" in
24
24
  esac
25
25
 
26
26
  # Node is the one thing that cannot be installed by the installer, since the
27
- # installer is written in it. Fail with an actionable message rather than a
28
- # "command not found" forty lines deep.
27
+ # installer is written in it. Checked before the dependency install rather than
28
+ # after: every package manager below is itself a node program, so a missing node
29
+ # would otherwise surface as that manager failing under `set -e`, and the script
30
+ # would exit on a confusing error instead of this actionable one.
29
31
  if ! command -v node >/dev/null 2>&1; then
30
32
  echo "node is required to prepare this environment but is not present." >&2
31
33
  echo "It cannot be installed by the toolchain step, because that step runs" >&2
@@ -33,6 +35,62 @@ if ! command -v node >/dev/null 2>&1; then
33
35
  exit 1
34
36
  fi
35
37
 
38
+ # Install the project's dependencies, unless the caller already did.
39
+ #
40
+ # This lives here rather than in the vendor's settings field so that field can
41
+ # be one identical line for every project. Naming the package manager there
42
+ # meant a Claude environment for an npm project and one for a bun project
43
+ # differed by a string a human had to get right, in a box with no review, no
44
+ # version history, and no test.
45
+ #
46
+ # Which manager is decided by the lockfile that is actually committed, never
47
+ # guessed: a guessed one fails on the container's first command with an error
48
+ # blaming the project rather than the guess.
49
+ #
50
+ # Skipped when node_modules already exists, which is what makes this cheap on a
51
+ # resumed container and correct to run twice.
52
+ if [ "${LISA_SKIP_INSTALL:-}" != "1" ] && [ ! -d node_modules ]; then
53
+ if [ -f bun.lock ] || [ -f bun.lockb ]; then install_cmd="bun install"
54
+ elif [ -f pnpm-lock.yaml ]; then install_cmd="pnpm install --frozen-lockfile"
55
+ elif [ -f yarn.lock ]; then
56
+ # Yarn Classic and Berry spell the same intent differently, and each
57
+ # rejects the other's flag. The lockfile itself says which is in use:
58
+ # Yarn 1 writes a "# yarn lockfile v1" header, Berry does not.
59
+ if head -5 yarn.lock | grep -q "yarn lockfile v1"; then
60
+ install_cmd="yarn install --frozen-lockfile"
61
+ else
62
+ install_cmd="yarn install --immutable"
63
+ fi
64
+ elif [ -f package-lock.json ]; then install_cmd="npm ci"
65
+ else install_cmd=""
66
+ fi
67
+
68
+ if [ -n "$install_cmd" ]; then
69
+ echo "Installing dependencies with: $install_cmd"
70
+ # CI=1 so lifecycle scripts take their automation path and leave the
71
+ # checkout alone. A remote-env setup is automation by definition, and a
72
+ # postinstall that rewrites tracked files here breaks any skill with a
73
+ # clean-checkout precondition — which the publishing skills have, because
74
+ # their diff is contractually bounded and merged without human review.
75
+ #
76
+ # Lisa's own postinstall already guards on exactly this variable, so this
77
+ # is an existing convention rather than a new one.
78
+ #
79
+ # NOT --ignore-scripts: that would also stop patch-package, so a project
80
+ # relying on patched dependencies would silently get unpatched ones — a
81
+ # quieter failure than the one being fixed.
82
+ #
83
+ # Without it the bug is cache-dependent, not deterministic: a fresh
84
+ # container installs and dirties the tree, a resumed one skips the install
85
+ # and succeeds. That reads as flakiness rather than a cause.
86
+ CI=1 $install_cmd
87
+ else
88
+ # Not fatal on its own. A project may carry no lockfile and still have the
89
+ # skill in a checkout directory, so let the resolver below decide.
90
+ echo "No lockfile found; skipping dependency install." >&2
91
+ fi
92
+ fi
93
+
36
94
  # Where the skill lives depends on how this project's harness receives it, and
37
95
  # the two delivery models differ in a way that matters here.
38
96
  #
@@ -67,9 +125,10 @@ if [ -z "$runner" ]; then
67
125
  echo "yet: Claude and Codex receive Lisa skills as an installed plugin, which" >&2
68
126
  echo "is not part of a clone, so node_modules is the only copy present." >&2
69
127
  echo >&2
70
- echo "Install dependencies before this script runs. The environment's setup" >&2
71
- echo "command should be the install and this script together, for example:" >&2
72
- echo " bun install && bash scripts/lisa-remote-env/setup.sh" >&2
128
+ echo "This script installs dependencies itself, so reaching here means the" >&2
129
+ echo "install did not produce the package, or the project has no lockfile" >&2
130
+ echo "identifying its package manager. Check that @codyswann/lisa is a" >&2
131
+ echo "dependency and that a lockfile is committed." >&2
73
132
  echo >&2
74
133
  echo "If dependencies are installed, run 'lisa apply' so the skills are" >&2
75
134
  echo "present, then re-run setup." >&2
@@ -298,6 +298,20 @@ export function installAssets(cwd = process.cwd()) {
298
298
  });
299
299
  }
300
300
 
301
+ /**
302
+ * The setup field, identical for every project.
303
+ *
304
+ * Names neither the repository nor its package manager. Claude Code web runs
305
+ * this field from `$HOME` while the checkout sits one level down, so the glob
306
+ * locates the script; the script then anchors itself on the repository root and
307
+ * installs dependencies from whichever lockfile the project commits.
308
+ *
309
+ * A field that named both was a string a human had to get right, in a settings
310
+ * box with no review, no version history and no test — and the logic it encoded
311
+ * belongs in a file that has all three.
312
+ */
313
+ const SETUP_FIELD = `bash "$HOME"/*/scripts/lisa-remote-env/setup.sh`;
314
+
301
315
  /**
302
316
  * The settings block that wires the session-start hook into a repository.
303
317
  *
@@ -366,12 +380,11 @@ export function pinEnvironment(environmentId, cwd = process.cwd()) {
366
380
  * settings page, no direct URL, and no endpoint. Emit is not a degraded option
367
381
  * here, it is the only one — so the read-back in `verify-remote-env.mjs` is
368
382
  * what makes the result trustworthy, exactly as it would be at any other tier.
369
- * @param {{bootstrapKey: string|null, install: string, repoDir?: string}} options Project details.
383
+ * @param {{bootstrapKey: string|null}} options Project details.
370
384
  * @returns {string} Text to show the operator.
371
385
  */
372
- export function emitClaudeWeb({ bootstrapKey, install, repoDir = "lisa" }) {
386
+ export function emitClaudeWeb({ bootstrapKey }) {
373
387
  const key = bootstrapKey ?? "<secrets.bootstrap.key is not configured>";
374
- const setup = `cd ${shellQuote(repoDir)} && ${install} && bash scripts/lisa-remote-env/setup.sh`;
375
388
  return [
376
389
  "Provisioning tier: EMIT — and for this surface that is the only tier.",
377
390
  " A Claude cloud environment is account-scoped configuration edited in the",
@@ -393,13 +406,17 @@ export function emitClaudeWeb({ bootstrapKey, install, repoDir = "lisa" }) {
393
406
  " is why exactly one value needs to live here.",
394
407
  "",
395
408
  " Setup script:",
396
- ` ${setup}`,
409
+ ` ${SETUP_FIELD}`,
410
+ "",
411
+ " This line is identical for every project — nothing in it names this",
412
+ " repository or its package manager. Claude runs the field from $HOME",
413
+ " while the checkout lives one level down, so the glob is what locates",
414
+ " it; the script then anchors itself and installs dependencies using",
415
+ " whichever lockfile the project actually commits.",
397
416
  "",
398
- ` Claude runs this field from $HOME, while the checkout is $HOME/${repoDir}.`,
399
- " The `cd` must come first, and the install must run before the setup",
400
- " script. On a fresh container node_modules is the only copy of the Lisa",
401
- " skills present, because Claude receives them as an installed plugin",
402
- " rather than as part of the clone.",
417
+ " Keeping it generic is the point. A field that named the repository and",
418
+ " the package manager was a string a human had to get right, in a box",
419
+ " with no review, no version history and no test.",
403
420
  "",
404
421
  "Commit to the repository",
405
422
  "------------------------",
@@ -478,57 +495,6 @@ async function detectSurface() {
478
495
  return { surface, materializeAt: mod.SURFACES[surface].materializeAt };
479
496
  }
480
497
 
481
- /**
482
- * Name the project's own install command rather than inventing one.
483
- *
484
- * The emitted setup line must begin with whatever this project already uses; a
485
- * guessed package manager produces a container that fails on its first command.
486
- * @param {string} [cwd] Repository root.
487
- * @returns {string} The install command to place before the setup script.
488
- */
489
- export function detectInstallCommand(cwd = process.cwd()) {
490
- const lockfiles = [
491
- ["bun.lockb", "bun install"],
492
- ["bun.lock", "bun install"],
493
- ["pnpm-lock.yaml", "pnpm install --frozen-lockfile"],
494
- ["yarn.lock", "yarn install --immutable"],
495
- ["package-lock.json", "npm ci"],
496
- ];
497
- for (const [file, command] of lockfiles) {
498
- if (existsSync(join(cwd, file))) return command;
499
- }
500
- return "<your install command>";
501
- }
502
-
503
- /**
504
- * Name the directory Claude creates under $HOME when it clones this repository.
505
- *
506
- * Claude cloud runs the environment setup field from $HOME, not from the
507
- * checkout. The setup line therefore has to `cd` into the clone before running
508
- * the project install. Prefer the configured GitHub repo name because it is the
509
- * thing Claude clones; fall back to the current directory for non-GitHub tests
510
- * and local dry runs.
511
- * @param {string} [cwd] Repository root.
512
- * @returns {string} Directory name to place after `cd`.
513
- */
514
- export function detectRepositoryDirectory(cwd = process.cwd()) {
515
- const path = join(cwd, ".lisa.config.json");
516
- if (existsSync(path)) {
517
- const repo = JSON.parse(readFileSync(path, "utf8")).github?.repo;
518
- if (repo && !repo.includes("/") && !repo.includes("\0")) return repo;
519
- }
520
- return basename(cwd);
521
- }
522
-
523
- /**
524
- * Quote a value for POSIX shell usage.
525
- * @param {string} value Raw value.
526
- * @returns {string} Single-quoted shell literal.
527
- */
528
- function shellQuote(value) {
529
- return "'" + String(value).replaceAll("'", "'\\''") + "'";
530
- }
531
-
532
498
  /**
533
499
  * Read the bootstrap key name, which is the one value the operator must paste.
534
500
  * @param {string} [cwd] Repository root.
@@ -577,13 +543,7 @@ async function main() {
577
543
  `Emitting is implemented for claude-web, which has no other tier.`
578
544
  );
579
545
  }
580
- console.log(
581
- emitClaudeWeb({
582
- bootstrapKey: readBootstrapKey(),
583
- install: detectInstallCommand(),
584
- repoDir: detectRepositoryDirectory(),
585
- })
586
- );
546
+ console.log(emitClaudeWeb({ bootstrapKey: readBootstrapKey() }));
587
547
  return;
588
548
  }
589
549
 
@@ -41,9 +41,20 @@ export function readAutomation(name, cwd = process.cwd()) {
41
41
  `Declare the loop before generating its workflow.`
42
42
  );
43
43
  }
44
+ const surface = cfg.remoteEnv?.surfaces?.[loop.executionEnv];
45
+ const github = cfg.github ?? {};
44
46
  return {
45
47
  ...loop,
46
- repository: cfg.remoteEnv?.surfaces?.[loop.executionEnv]?.repository,
48
+ // Only used for the fork guard, so it is a question about *this* repository
49
+ // rather than about the surface. Read from the surface where one records it
50
+ // — Codex Cloud binds an environment to a repository — and otherwise from
51
+ // the project's own GitHub config. A Claude cloud environment binds no
52
+ // repository at all, and demanding one there asked for a field that cannot
53
+ // exist, which refused to generate a workflow for a correctly configured
54
+ // loop.
55
+ repository:
56
+ surface?.repository ??
57
+ (github.org && github.repo ? `${github.org}/${github.repo}` : undefined),
47
58
  // Where this project keeps its bootstrap. A workstation serving several
48
59
  // tenants gives each its own name, so the generated workflow must template
49
60
  // it rather than assume the default.
@@ -81,12 +92,28 @@ export function renderWorkflow(name, loop) {
81
92
  const bootstrap = loop.bootstrapKey ?? "BWS_ACCESS_TOKEN";
82
93
  if (!loop.repository) {
83
94
  throw new Error(
84
- `automations["${name}"] targets executionEnv "${loop.executionEnv}", ` +
85
- `but no remoteEnv.surfaces["${loop.executionEnv}"].repository is set.\n` +
86
- `Run /lisa:setup:remote-env ${loop.executionEnv} first.`
95
+ `cannot determine which repository this loop belongs to.\n` +
96
+ `The generated workflow guards on it so a fork never dispatches. Set ` +
97
+ `github.org and github.repo in .lisa.config.json.`
87
98
  );
88
99
  }
89
100
 
101
+ // Always emitted, even for a surface whose dispatcher token is not consumed
102
+ // by use. Making it conditional on secrets.rotating would mean a project that
103
+ // later declares one, without regenerating its workflow, silently loses the
104
+ // persist step and strands the replacement. One inert step is the cheaper
105
+ // failure.
106
+ const rotation = `
107
+ # The dispatcher authenticates with a credential that rotates on use, so
108
+ # the replacement must be persisted even when the dispatch itself failed.
109
+ - name: Persist any credential rotation
110
+ if: \${{ always() }}
111
+ env:
112
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
113
+ run: |
114
+ set -euo pipefail
115
+ node .claude/skills/lisa-secrets-access/scripts/rotate-secret.mjs leases`;
116
+
90
117
  const trigger = loop.enabled
91
118
  ? ` schedule:\n - cron: '${loop.schedule}'\n workflow_dispatch:`
92
119
  : ` # schedule: disabled until the production path is proven once for one\n` +
@@ -149,15 +176,7 @@ jobs:
149
176
  'executionEnv=${loop.executionEnv} ${loop.payload ?? ""}' \\
150
177
  --skill ${loop.skill ?? `lisa-${name}`}
151
178
 
152
- # The dispatcher authenticates with a credential that rotates on use, so
153
- # the replacement must be persisted even when the dispatch itself failed.
154
- - name: Persist any credential rotation
155
- if: \${{ always() }}
156
- env:
157
- ${bootstrap}: \${{ secrets.${bootstrap} }}
158
- run: |
159
- set -euo pipefail
160
- node .claude/skills/lisa-secrets-access/scripts/rotate-secret.mjs leases
179
+ ${rotation}
161
180
  `;
162
181
  }
163
182
 
@@ -10,26 +10,28 @@ Prepare a remote surface so a host project can execute there. Today that means *
10
10
 
11
11
  ## What lives where
12
12
 
13
- The remote environment's own configuration fields stay **one line into the repository, preceded by the project's dependency install**. Nothing else is pasted into a vendor UI.
13
+ The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: <install> && bash scripts/lisa-remote-env/setup.sh
17
- maintenance: <install> && bash scripts/lisa-remote-env/setup.sh
16
+ setup: bash "$HOME"/*/scripts/lisa-remote-env/setup.sh
17
+ maintenance: bash "$HOME"/*/scripts/lisa-remote-env/setup.sh
18
18
  ```
19
19
 
20
- `<install>` is whatever the project already uses`bun install`, `npm ci`, `pnpm install`. Substitute it; do not invent one.
20
+ **That line is identical for every project.** Nothing in it names the repository or the package manager, so it can be pasted unchanged into any project's environment. The glob is what locates the checkout Claude Code web runs this field from `$HOME` while the clone lands one level down — and the script then anchors itself on the repository root.
21
21
 
22
22
  They are the same command. A container may be built fresh or resumed from cache; every step is idempotent and version-aware, so running it twice is correct, and running it on resume is what picks up a rotated value, an edited note, or a changed version pin.
23
23
 
24
- The complete logic is repository-owned so it is reviewed, versioned, tested, and reusable. A large inline installer in a settings field is none of those things.
24
+ The complete logic is repository-owned so it is reviewed, versioned, tested, and reusable. A large inline installer in a settings field is none of those things — and neither is a repository name and a package manager, which is what this field used to carry.
25
25
 
26
- ### Why the install has to come first
26
+ ### The script installs the dependencies itself
27
27
 
28
28
  **A clone does not contain the skills on the harnesses that matter here.** OpenCode and Antigravity have them written into the checkout by `lisa apply`. Claude and Codex receive them as an *installed plugin*, which lives in the user's home directory — so a container that has just cloned the repository has never seen it.
29
29
 
30
30
  `node_modules/@codyswann/lisa` is therefore the only copy present on a fresh container, and it is a good one: it is the version that project pins, which is the version its setup should run. The entrypoint searches the agent directories first and falls back to it.
31
31
 
32
- Omitting the install does not degrade gracefully. The entrypoint exits before the toolchain, secrets, and hook phases have done anything, so the environment looks provisioned and fails on first dispatch.
32
+ So the install has to happen before the runner is resolved — and the entrypoint does it, rather than the settings field. Which package manager is read from the lockfile the project actually commits (`bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`), never guessed: a guessed one fails on the container's first command with an error that blames the project rather than the guess. The step is skipped when `node_modules` already exists, which is what keeps a resumed container cheap, and `LISA_SKIP_INSTALL=1` opts out entirely for a caller that has already installed.
33
+
34
+ A project with no lockfile is not fatal on its own — a checkout may carry the skill directly — so the script says so and lets the resolver decide.
33
35
 
34
36
  ## The three phases
35
37
 
@@ -134,11 +136,10 @@ When emitting, produce exactly:
134
136
  ```text
135
137
  Environment name: <project> remote executor
136
138
  Repository: <org>/<repo> (must be the default checkout)
137
- Setup script: <install> && bash scripts/lisa-remote-env/setup.sh
138
- Maintenance: <install> && bash scripts/lisa-remote-env/setup.sh
139
- (<install> is the project's own: bun install, npm ci, ...
140
- It must precede the script — on a fresh container
141
- node_modules is the only copy of the skills present.)
139
+ Setup script: bash "$HOME"/*/scripts/lisa-remote-env/setup.sh
140
+ Maintenance: bash "$HOME"/*/scripts/lisa-remote-env/setup.sh
141
+ (identical for every project the script finds the
142
+ checkout and installs from the committed lockfile)
142
143
  Environment vars: LISA_SECRETS_SURFACE=codex-cloud
143
144
  BWS_ACCESS_TOKEN=<from the provider; an environment
144
145
  variable, not a task secret — setup and cache-resume
@@ -157,11 +158,11 @@ Generate the exact text to paste:
157
158
  node scripts/setup-remote-env.mjs --emit=claude-web
158
159
  ```
159
160
 
160
- It reads the project's own install command from its lockfile and the bootstrap name from `secrets.bootstrap.key`, then emits the environment fields, the `.claude/settings.json` hook block, and the two base-image surprises worth knowing before they cost an afternoon:
161
+ It reads the bootstrap name from `secrets.bootstrap.key` and emits the environment fields, the `.claude/settings.json` hook block, and the base-image surprises worth knowing before they cost an afternoon:
161
162
 
162
163
  - **`gh` is not pre-installed.** If the project's flows shell out to it, add it to `remoteEnv.tools.install`, pinned and checksummed like anything else.
163
164
  - **A proxied credential reads as the literal string `proxy-injected`.** Tools that authenticate through the GitHub proxy work; a script that reads the variable directly gets the placeholder. The read-back asserts this rather than leaving it to be discovered against a live service.
164
- - **The setup field runs from `$HOME`, not from the checkout.** The emitted setup command starts with `cd <repo-name>` on purpose; without it the package install runs one directory above the clone and cannot find `package.json`.
165
+ - **The setup field runs from `$HOME`, not from the checkout.** That is why the field is a `$HOME` glob rather than a plain relative path: it locates the clone one level down, and the script anchors itself from there. Nothing in the emitted line names this project, so it is the same line everywhere.
165
166
  - **Trusted network access is not enough for provider CLIs.** Use Custom and include package registries, GitHub, cloud SDK hosts, and the bootstrap credential manager API.
166
167
 
167
168
  ### One environment per project, pinned locally