@codyswann/lisa 2.321.3 → 2.321.5
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.
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +3 -2
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +32 -13
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
package/package.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"brace-expansion": ">=5.0.9"
|
|
116
116
|
},
|
|
117
117
|
"name": "@codyswann/lisa",
|
|
118
|
-
"version": "2.321.
|
|
118
|
+
"version": "2.321.5",
|
|
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": {
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
`
|
|
85
|
-
`
|
|
86
|
-
`
|
|
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
|
-
|
|
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
|
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
rmSync,
|
|
38
38
|
writeFileSync,
|
|
39
39
|
} from "node:fs";
|
|
40
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
40
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
42
|
|
|
43
43
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
@@ -197,6 +197,22 @@ function applyToolchain(tools, dryRun) {
|
|
|
197
197
|
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
198
198
|
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
199
199
|
|
|
200
|
+
// Installing a binary somewhere nothing looks is the same as not installing
|
|
201
|
+
// it. `~/.local/bin` is on PATH by default on a developer workstation and is
|
|
202
|
+
// NOT on a minimal container, so the toolchain step reported `install bws`,
|
|
203
|
+
// the file landed at ~/.local/bin/bws mode 755, and the very next step died
|
|
204
|
+
// with `spawnSync bws ENOENT`.
|
|
205
|
+
//
|
|
206
|
+
// This was invisible in every local test because a workstation shell already
|
|
207
|
+
// exports the directory — the environment doing the hiding was the one used
|
|
208
|
+
// to verify the fix.
|
|
209
|
+
//
|
|
210
|
+
// Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
211
|
+
// whatever an image happens to ship under the same name.
|
|
212
|
+
if (!pathContains(binDir)) {
|
|
213
|
+
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
for (const step of plan) {
|
|
201
217
|
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
202
218
|
if (step.action === "install" && !dryRun)
|
|
@@ -205,6 +221,19 @@ function applyToolchain(tools, dryRun) {
|
|
|
205
221
|
return plan;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether a directory is already on PATH.
|
|
226
|
+
*
|
|
227
|
+
* Compared entry by entry rather than by substring: a substring test would
|
|
228
|
+
* consider `/root/.local/bin` present because `/root/.local/bin/extra` is, and
|
|
229
|
+
* would then skip the prepend that makes the tool findable.
|
|
230
|
+
* @param {string} dir Directory to look for.
|
|
231
|
+
* @returns {boolean} True when PATH already contains exactly that entry.
|
|
232
|
+
*/
|
|
233
|
+
export function pathContains(dir, path = process.env.PATH ?? "") {
|
|
234
|
+
return path.split(delimiter).filter(Boolean).includes(dir);
|
|
235
|
+
}
|
|
236
|
+
|
|
208
237
|
/**
|
|
209
238
|
* Run the project's own setup hook when it has one.
|
|
210
239
|
*
|
|
@@ -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
|
-
|
|
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
|
-
`
|
|
85
|
-
`
|
|
86
|
-
`
|
|
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
|
-
|
|
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
|
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
rmSync,
|
|
38
38
|
writeFileSync,
|
|
39
39
|
} from "node:fs";
|
|
40
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
40
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
42
|
|
|
43
43
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
@@ -197,6 +197,22 @@ function applyToolchain(tools, dryRun) {
|
|
|
197
197
|
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
198
198
|
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
199
199
|
|
|
200
|
+
// Installing a binary somewhere nothing looks is the same as not installing
|
|
201
|
+
// it. `~/.local/bin` is on PATH by default on a developer workstation and is
|
|
202
|
+
// NOT on a minimal container, so the toolchain step reported `install bws`,
|
|
203
|
+
// the file landed at ~/.local/bin/bws mode 755, and the very next step died
|
|
204
|
+
// with `spawnSync bws ENOENT`.
|
|
205
|
+
//
|
|
206
|
+
// This was invisible in every local test because a workstation shell already
|
|
207
|
+
// exports the directory — the environment doing the hiding was the one used
|
|
208
|
+
// to verify the fix.
|
|
209
|
+
//
|
|
210
|
+
// Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
211
|
+
// whatever an image happens to ship under the same name.
|
|
212
|
+
if (!pathContains(binDir)) {
|
|
213
|
+
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
for (const step of plan) {
|
|
201
217
|
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
202
218
|
if (step.action === "install" && !dryRun)
|
|
@@ -205,6 +221,19 @@ function applyToolchain(tools, dryRun) {
|
|
|
205
221
|
return plan;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether a directory is already on PATH.
|
|
226
|
+
*
|
|
227
|
+
* Compared entry by entry rather than by substring: a substring test would
|
|
228
|
+
* consider `/root/.local/bin` present because `/root/.local/bin/extra` is, and
|
|
229
|
+
* would then skip the prepend that makes the tool findable.
|
|
230
|
+
* @param {string} dir Directory to look for.
|
|
231
|
+
* @returns {boolean} True when PATH already contains exactly that entry.
|
|
232
|
+
*/
|
|
233
|
+
export function pathContains(dir, path = process.env.PATH ?? "") {
|
|
234
|
+
return path.split(delimiter).filter(Boolean).includes(dir);
|
|
235
|
+
}
|
|
236
|
+
|
|
208
237
|
/**
|
|
209
238
|
* Run the project's own setup hook when it has one.
|
|
210
239
|
*
|
|
@@ -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
|
-
|
|
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
|
-
`
|
|
85
|
-
`
|
|
86
|
-
`
|
|
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
|
-
|
|
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
|
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
rmSync,
|
|
38
38
|
writeFileSync,
|
|
39
39
|
} from "node:fs";
|
|
40
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
40
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
42
|
|
|
43
43
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
@@ -197,6 +197,22 @@ function applyToolchain(tools, dryRun) {
|
|
|
197
197
|
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
198
198
|
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
199
199
|
|
|
200
|
+
// Installing a binary somewhere nothing looks is the same as not installing
|
|
201
|
+
// it. `~/.local/bin` is on PATH by default on a developer workstation and is
|
|
202
|
+
// NOT on a minimal container, so the toolchain step reported `install bws`,
|
|
203
|
+
// the file landed at ~/.local/bin/bws mode 755, and the very next step died
|
|
204
|
+
// with `spawnSync bws ENOENT`.
|
|
205
|
+
//
|
|
206
|
+
// This was invisible in every local test because a workstation shell already
|
|
207
|
+
// exports the directory — the environment doing the hiding was the one used
|
|
208
|
+
// to verify the fix.
|
|
209
|
+
//
|
|
210
|
+
// Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
211
|
+
// whatever an image happens to ship under the same name.
|
|
212
|
+
if (!pathContains(binDir)) {
|
|
213
|
+
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
for (const step of plan) {
|
|
201
217
|
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
202
218
|
if (step.action === "install" && !dryRun)
|
|
@@ -205,6 +221,19 @@ function applyToolchain(tools, dryRun) {
|
|
|
205
221
|
return plan;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether a directory is already on PATH.
|
|
226
|
+
*
|
|
227
|
+
* Compared entry by entry rather than by substring: a substring test would
|
|
228
|
+
* consider `/root/.local/bin` present because `/root/.local/bin/extra` is, and
|
|
229
|
+
* would then skip the prepend that makes the tool findable.
|
|
230
|
+
* @param {string} dir Directory to look for.
|
|
231
|
+
* @returns {boolean} True when PATH already contains exactly that entry.
|
|
232
|
+
*/
|
|
233
|
+
export function pathContains(dir, path = process.env.PATH ?? "") {
|
|
234
|
+
return path.split(delimiter).filter(Boolean).includes(dir);
|
|
235
|
+
}
|
|
236
|
+
|
|
208
237
|
/**
|
|
209
238
|
* Run the project's own setup hook when it has one.
|
|
210
239
|
*
|
|
@@ -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
|
-
|
|
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
|
-
`
|
|
85
|
-
`
|
|
86
|
-
`
|
|
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
|
-
|
|
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
|
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
rmSync,
|
|
38
38
|
writeFileSync,
|
|
39
39
|
} from "node:fs";
|
|
40
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
40
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
42
|
|
|
43
43
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
@@ -197,6 +197,22 @@ function applyToolchain(tools, dryRun) {
|
|
|
197
197
|
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
198
198
|
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
199
199
|
|
|
200
|
+
// Installing a binary somewhere nothing looks is the same as not installing
|
|
201
|
+
// it. `~/.local/bin` is on PATH by default on a developer workstation and is
|
|
202
|
+
// NOT on a minimal container, so the toolchain step reported `install bws`,
|
|
203
|
+
// the file landed at ~/.local/bin/bws mode 755, and the very next step died
|
|
204
|
+
// with `spawnSync bws ENOENT`.
|
|
205
|
+
//
|
|
206
|
+
// This was invisible in every local test because a workstation shell already
|
|
207
|
+
// exports the directory — the environment doing the hiding was the one used
|
|
208
|
+
// to verify the fix.
|
|
209
|
+
//
|
|
210
|
+
// Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
211
|
+
// whatever an image happens to ship under the same name.
|
|
212
|
+
if (!pathContains(binDir)) {
|
|
213
|
+
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
for (const step of plan) {
|
|
201
217
|
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
202
218
|
if (step.action === "install" && !dryRun)
|
|
@@ -205,6 +221,19 @@ function applyToolchain(tools, dryRun) {
|
|
|
205
221
|
return plan;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether a directory is already on PATH.
|
|
226
|
+
*
|
|
227
|
+
* Compared entry by entry rather than by substring: a substring test would
|
|
228
|
+
* consider `/root/.local/bin` present because `/root/.local/bin/extra` is, and
|
|
229
|
+
* would then skip the prepend that makes the tool findable.
|
|
230
|
+
* @param {string} dir Directory to look for.
|
|
231
|
+
* @returns {boolean} True when PATH already contains exactly that entry.
|
|
232
|
+
*/
|
|
233
|
+
export function pathContains(dir, path = process.env.PATH ?? "") {
|
|
234
|
+
return path.split(delimiter).filter(Boolean).includes(dir);
|
|
235
|
+
}
|
|
236
|
+
|
|
208
237
|
/**
|
|
209
238
|
* Run the project's own setup hook when it has one.
|
|
210
239
|
*
|