@codyswann/lisa 2.321.5 → 2.322.1
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/all/copy-contents/gitignore +6 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +7 -5
- 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-remote-dispatch/scripts/dispatch.mjs +206 -10
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +35 -14
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
- package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
- package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +35 -14
- package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +35 -14
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
- 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-remote-dispatch/scripts/dispatch.mjs +206 -10
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +35 -14
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +35 -14
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
- 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-remote-dispatch/scripts/dispatch.mjs +206 -10
- package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +35 -14
- package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
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.
|
|
118
|
+
"version": "2.322.1",
|
|
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": {
|
|
@@ -21,14 +21,53 @@
|
|
|
21
21
|
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
23
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
-
import { join } from "node:path";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
25
26
|
|
|
26
27
|
/** Surfaces this dispatcher knows how to reach. `local` means "do not dispatch". */
|
|
27
|
-
export const EXECUTION_ENVS = new Set(["local", "codex-cloud"]);
|
|
28
|
+
export const EXECUTION_ENVS = new Set(["local", "codex-cloud", "claude-web"]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What each surface must have recorded before anything may dispatch to it.
|
|
32
|
+
*
|
|
33
|
+
* Not uniform, because the two surfaces do not bind the same way. A Codex Cloud
|
|
34
|
+
* environment is bound to one repository, so naming both is what proves the
|
|
35
|
+
* environment is the right one. A Claude cloud environment binds no repository
|
|
36
|
+
* at all — it is account-scoped configuration and the repository arrives per
|
|
37
|
+
* session — so its durable handle is the routine that dispatch fires.
|
|
38
|
+
*/
|
|
39
|
+
const SURFACE_PRECONDITIONS = {
|
|
40
|
+
"codex-cloud": ["environmentId", "repository"],
|
|
41
|
+
"claude-web": ["routineId", "fireUrl"],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The beta this endpoint ships under.
|
|
46
|
+
*
|
|
47
|
+
* Dated and rotating: the two most recent previous versions keep working, so a
|
|
48
|
+
* bump here is a migration with a window rather than a break. Stated once so
|
|
49
|
+
* there is a single place to move it.
|
|
50
|
+
*/
|
|
51
|
+
const ROUTINE_BETA = "experimental-cc-routine-2026-04-01";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* How long to wait for the routine to accept the dispatch.
|
|
55
|
+
*
|
|
56
|
+
* `fetch` has no timeout of its own, so a routine endpoint that accepts the
|
|
57
|
+
* connection and then says nothing leaves dispatch waiting forever — the one
|
|
58
|
+
* failure mode that never reaches the error path below and never reaches the
|
|
59
|
+
* operator either. Generous, because the endpoint only has to *accept* the
|
|
60
|
+
* dispatch: the session's own work happens long after this call returns, so
|
|
61
|
+
* this bounds a handshake rather than a run.
|
|
62
|
+
*/
|
|
63
|
+
const FIRE_TIMEOUT_MS = 30_000;
|
|
28
64
|
|
|
29
65
|
/** Where dispatched work is recorded so a later session can find it. */
|
|
30
66
|
const LEDGER = join(".lisa", "remote-dispatch.json");
|
|
31
67
|
|
|
68
|
+
/** This file's directory, for locating the sibling secrets skill. */
|
|
69
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
70
|
+
|
|
32
71
|
/**
|
|
33
72
|
* Split `key=value` parameters from the rest of an invocation.
|
|
34
73
|
*
|
|
@@ -101,9 +140,11 @@ export function readSurfaceConfig(surface, cwd = process.cwd()) {
|
|
|
101
140
|
* @param {string} surface Execution surface.
|
|
102
141
|
*/
|
|
103
142
|
export function assertPreconditions(block, surface) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
143
|
+
const required = SURFACE_PRECONDITIONS[surface] ?? [
|
|
144
|
+
"environmentId",
|
|
145
|
+
"repository",
|
|
146
|
+
];
|
|
147
|
+
const missing = required.filter(field => !block[field]);
|
|
107
148
|
if (missing.length) {
|
|
108
149
|
throw new Error(
|
|
109
150
|
`remoteEnv.surfaces["${surface}"] is missing: ${missing.join(", ")}.\n` +
|
|
@@ -230,7 +271,152 @@ export function splitSkillFlag(argv) {
|
|
|
230
271
|
return { skill, raw: rest.join(" ") };
|
|
231
272
|
}
|
|
232
273
|
|
|
233
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Read the response a routine returns when it accepts a dispatch.
|
|
276
|
+
*
|
|
277
|
+
* Kept separate from the request so the shape can be exercised against a
|
|
278
|
+
* recorded body. The identifier is a field here rather than something scraped
|
|
279
|
+
* out of console output, which is the one respect in which this surface is
|
|
280
|
+
* easier to reconcile than the other.
|
|
281
|
+
* @param {string} body Raw response body.
|
|
282
|
+
* @returns {{sessionId: string, url: string}} The accepted session.
|
|
283
|
+
*/
|
|
284
|
+
export function readFireResponse(body) {
|
|
285
|
+
let parsed;
|
|
286
|
+
try {
|
|
287
|
+
parsed = JSON.parse(body);
|
|
288
|
+
} catch {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`routine returned a body that is not JSON:\n${String(body).slice(0, 400)}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const sessionId = parsed.claude_code_session_id;
|
|
294
|
+
const url = parsed.claude_code_session_url;
|
|
295
|
+
if (!sessionId) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`routine accepted the request but returned no session identifier.\n` +
|
|
298
|
+
`${JSON.stringify(parsed).slice(0, 400)}\n` +
|
|
299
|
+
`Refusing to report success: without the identifier nothing can ` +
|
|
300
|
+
`reconcile this run, and a retry would duplicate it.`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return { sessionId, url: url ?? "" };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Fire a routine and record the session it created.
|
|
308
|
+
*
|
|
309
|
+
* The payload is deliberately the work item and nothing else. It arrives on the
|
|
310
|
+
* far side wrapped in a block the platform marks as untrusted data, and a
|
|
311
|
+
* routine acts on it only because its saved prompt says to — which is the
|
|
312
|
+
* boundary this plan wanted anyway, enforced by the platform rather than by
|
|
313
|
+
* convention. Nothing here can widen the remote run's authority.
|
|
314
|
+
*
|
|
315
|
+
* The bearer token is resolved through the secrets chokepoint at the moment of
|
|
316
|
+
* use and never stored in configuration.
|
|
317
|
+
* Its three side effects — resolving a credential, making a request, writing the
|
|
318
|
+
* ledger — are all injectable, so the accept and refuse paths can be exercised
|
|
319
|
+
* without a token, a network, or a write into the working repository.
|
|
320
|
+
* @param {object} block Surface configuration.
|
|
321
|
+
* @param {string} prompt The thin skill invocation.
|
|
322
|
+
* @param {string} payload The caller's original payload, for the record.
|
|
323
|
+
* @param {{post?: Function, getToken?: Function, cwd?: string}} [options] Seams for tests.
|
|
324
|
+
* @returns {Promise<{sessionId: string, url: string}>} The accepted session.
|
|
325
|
+
*/
|
|
326
|
+
export async function dispatchClaudeWeb(block, prompt, payload, options = {}) {
|
|
327
|
+
const {
|
|
328
|
+
post = fetch,
|
|
329
|
+
getToken = resolveBearerToken,
|
|
330
|
+
cwd = process.cwd(),
|
|
331
|
+
} = options;
|
|
332
|
+
const token = getToken(block);
|
|
333
|
+
let response;
|
|
334
|
+
try {
|
|
335
|
+
response = await post(block.fireUrl, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: {
|
|
338
|
+
authorization: `Bearer ${token}`,
|
|
339
|
+
"anthropic-beta": ROUTINE_BETA,
|
|
340
|
+
"anthropic-version": "2023-06-01",
|
|
341
|
+
"content-type": "application/json",
|
|
342
|
+
},
|
|
343
|
+
body: JSON.stringify({ text: prompt }),
|
|
344
|
+
signal: AbortSignal.timeout(FIRE_TIMEOUT_MS),
|
|
345
|
+
});
|
|
346
|
+
} catch (err) {
|
|
347
|
+
// A timeout arrives here as an abort rather than a network error, and
|
|
348
|
+
// "could not reach" is the true and useful thing to say about both. It is
|
|
349
|
+
// named separately so the operator can tell a silent endpoint from a
|
|
350
|
+
// refused connection, since only one of those is worth retrying.
|
|
351
|
+
const reason =
|
|
352
|
+
err.name === "TimeoutError"
|
|
353
|
+
? `no response within ${FIRE_TIMEOUT_MS / 1000}s`
|
|
354
|
+
: err.message;
|
|
355
|
+
throw new Error(`could not reach the routine endpoint: ${reason}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const body = await response.text();
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
// The token is the usual cause and the usual thing to leak, so the message
|
|
361
|
+
// names the status and the routine rather than echoing the request.
|
|
362
|
+
throw new Error(
|
|
363
|
+
`routine ${block.routineId} refused the dispatch (HTTP ${response.status}).\n` +
|
|
364
|
+
`${body.slice(0, 400)}\n` +
|
|
365
|
+
`A 401 means the bearer token is wrong, revoked, or regenerated.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { sessionId, url } = readFireResponse(body);
|
|
370
|
+
record(
|
|
371
|
+
{
|
|
372
|
+
taskId: sessionId,
|
|
373
|
+
surface: "claude-web",
|
|
374
|
+
routineId: block.routineId,
|
|
375
|
+
sessionUrl: url,
|
|
376
|
+
prompt,
|
|
377
|
+
payload,
|
|
378
|
+
dispatchedAt: new Date().toISOString(),
|
|
379
|
+
},
|
|
380
|
+
cwd
|
|
381
|
+
);
|
|
382
|
+
return { sessionId, url };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Resolve the dispatcher's own credential through the secrets chokepoint.
|
|
387
|
+
*
|
|
388
|
+
* Never read from configuration: the token authorises starting work on someone
|
|
389
|
+
* else's infrastructure, and it can be regenerated and revoked, so it belongs
|
|
390
|
+
* in the credential manager and in `secrets.rotating` alongside it.
|
|
391
|
+
* @param {object} block Surface configuration.
|
|
392
|
+
* @returns {string} The bearer token.
|
|
393
|
+
*/
|
|
394
|
+
function resolveBearerToken(block) {
|
|
395
|
+
const name = block.tokenKey ?? "CLAUDE_ROUTINE_TOKEN";
|
|
396
|
+
const resolver = resolve(
|
|
397
|
+
HERE,
|
|
398
|
+
"..",
|
|
399
|
+
"..",
|
|
400
|
+
"lisa-secrets-access",
|
|
401
|
+
"scripts",
|
|
402
|
+
"resolve-secret.mjs"
|
|
403
|
+
);
|
|
404
|
+
try {
|
|
405
|
+
return execFileSync("node", [resolver, "get", name], {
|
|
406
|
+
encoding: "utf8",
|
|
407
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
408
|
+
}).trim();
|
|
409
|
+
} catch (err) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`could not resolve ${name} through lisa-secrets-access.\n` +
|
|
412
|
+
`${String(err.stderr ?? err.message).trim()}\n` +
|
|
413
|
+
`It is the dispatcher's own credential; store it in the provider ` +
|
|
414
|
+
`rather than in .lisa.config.json.`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function main() {
|
|
234
420
|
const { skill, raw } = splitSkillFlag(process.argv.slice(2));
|
|
235
421
|
const { params, rest } = parseInvocation(raw);
|
|
236
422
|
const surface = resolveExecutionEnv(params);
|
|
@@ -247,6 +433,15 @@ function main() {
|
|
|
247
433
|
// the repository-local skill, so an interactive run, a scheduled run, and a
|
|
248
434
|
// recovery run all execute one contract.
|
|
249
435
|
const prompt = `$${skill} ${rest}`.trim();
|
|
436
|
+
|
|
437
|
+
if (surface === "claude-web") {
|
|
438
|
+
const { sessionId, url } = await dispatchClaudeWeb(block, prompt, rest);
|
|
439
|
+
console.log(`dispatched: ${sessionId}`);
|
|
440
|
+
if (url) console.log(url);
|
|
441
|
+
console.log(`recorded in ${LEDGER}; not polling — this process is done.`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
250
445
|
const taskId = dispatchCodexCloud(block, prompt, rest);
|
|
251
446
|
|
|
252
447
|
console.log(`dispatched: ${taskId}`);
|
|
@@ -255,10 +450,11 @@ function main() {
|
|
|
255
450
|
}
|
|
256
451
|
|
|
257
452
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
453
|
+
// Awaited rather than called bare: one dispatch path is async, so a synchronous
|
|
454
|
+
// try/catch would let a rejection escape as an unhandled rejection and exit 0 —
|
|
455
|
+
// reporting dispatched work that was never accepted.
|
|
456
|
+
main().catch(err => {
|
|
261
457
|
console.error(err.message);
|
|
262
458
|
process.exit(1);
|
|
263
|
-
}
|
|
459
|
+
});
|
|
264
460
|
}
|
|
@@ -10,26 +10,48 @@ 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
|
|
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:
|
|
17
|
-
maintenance:
|
|
16
|
+
setup: for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] && exec bash "$f"; done; echo "lisa-remote-env entrypoint not found under $PWD" >&2; exit 1
|
|
17
|
+
maintenance: for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] && exec bash "$f"; done; echo "lisa-remote-env entrypoint not found under $PWD" >&2; exit 1
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
**That line is identical for every project AND every surface.** Nothing in it names the
|
|
21
|
+
repository, the package manager, or a home directory, so it can be pasted unchanged anywhere.
|
|
22
|
+
|
|
23
|
+
Locating the checkout is the only hard part, because the surfaces disagree about where the
|
|
24
|
+
field runs:
|
|
25
|
+
|
|
26
|
+
| Surface | cwd when the field runs | Checkout |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| Codex Cloud | **is** the checkout | `/workspace/<repo>` |
|
|
29
|
+
| Claude Code web | `$HOME` | `$HOME/<repo>` |
|
|
30
|
+
|
|
31
|
+
So both candidates are tried **relative to cwd** — the checkout itself, then one level down —
|
|
32
|
+
and neither mentions `$HOME`. An earlier version of this field used
|
|
33
|
+
`bash "$HOME"/*/scripts/...`, which works on Claude Code web and fails on Codex Cloud, where
|
|
34
|
+
the checkout is not under `$HOME` at all: the glob matches nothing and bash reports
|
|
35
|
+
`No such file or directory` for a path still containing a literal `*`.
|
|
36
|
+
|
|
37
|
+
`exec` on the first hit means no subshell and no `ls` output to parse, and the explicit
|
|
38
|
+
`exit 1` means a missing entrypoint says so rather than the field silently succeeding. The
|
|
39
|
+
script then anchors itself on the repository root, so it behaves the same however it was
|
|
40
|
+
reached.
|
|
21
41
|
|
|
22
42
|
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
43
|
|
|
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.
|
|
44
|
+
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
45
|
|
|
26
|
-
###
|
|
46
|
+
### The script installs the dependencies itself
|
|
27
47
|
|
|
28
48
|
**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
49
|
|
|
30
50
|
`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
51
|
|
|
32
|
-
|
|
52
|
+
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.
|
|
53
|
+
|
|
54
|
+
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
55
|
|
|
34
56
|
## The three phases
|
|
35
57
|
|
|
@@ -134,11 +156,10 @@ When emitting, produce exactly:
|
|
|
134
156
|
```text
|
|
135
157
|
Environment name: <project> remote executor
|
|
136
158
|
Repository: <org>/<repo> (must be the default checkout)
|
|
137
|
-
Setup script:
|
|
138
|
-
Maintenance:
|
|
139
|
-
(
|
|
140
|
-
|
|
141
|
-
node_modules is the only copy of the skills present.)
|
|
159
|
+
Setup script: for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] && exec bash "$f"; done; echo "lisa-remote-env entrypoint not found under $PWD" >&2; exit 1
|
|
160
|
+
Maintenance: for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] && exec bash "$f"; done; echo "lisa-remote-env entrypoint not found under $PWD" >&2; exit 1
|
|
161
|
+
(identical for every project — the script finds the
|
|
162
|
+
checkout and installs from the committed lockfile)
|
|
142
163
|
Environment vars: LISA_SECRETS_SURFACE=codex-cloud
|
|
143
164
|
BWS_ACCESS_TOKEN=<from the provider; an environment
|
|
144
165
|
variable, not a task secret — setup and cache-resume
|
|
@@ -157,11 +178,11 @@ Generate the exact text to paste:
|
|
|
157
178
|
node scripts/setup-remote-env.mjs --emit=claude-web
|
|
158
179
|
```
|
|
159
180
|
|
|
160
|
-
It reads the
|
|
181
|
+
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
182
|
|
|
162
183
|
- **`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
184
|
- **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.**
|
|
185
|
+
- **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
186
|
- **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
187
|
|
|
167
188
|
### 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.
|
|
28
|
-
#
|
|
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 "
|
|
71
|
-
echo "
|
|
72
|
-
echo "
|
|
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
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs
CHANGED
|
@@ -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
|
|
383
|
+
* @param {{bootstrapKey: string|null}} options Project details.
|
|
370
384
|
* @returns {string} Text to show the operator.
|
|
371
385
|
*/
|
|
372
|
-
export function emitClaudeWeb({ bootstrapKey
|
|
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
|
-
` ${
|
|
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
|
-
|
|
399
|
-
"
|
|
400
|
-
"
|
|
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
|
|