@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.
Files changed (79) hide show
  1. package/all/copy-contents/gitignore +6 -0
  2. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  3. package/dist/core/upstream-evidence-manifest.js +7 -5
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/package.json +1 -1
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +35 -14
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  11. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  12. package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
  13. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +35 -14
  14. package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  15. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
  18. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +35 -14
  19. package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  20. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  21. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  24. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
  28. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +35 -14
  29. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  30. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  31. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
  33. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +35 -14
  34. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  35. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
  36. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo-agy/plugin.json +1 -1
  39. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  59. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-agy/plugin.json +1 -1
  64. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  69. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  74. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +206 -10
  77. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +35 -14
  78. package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +64 -5
  79. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +27 -67
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.321.5",
3
+ "version": "2.322.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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 missing = [];
105
- if (!block.environmentId) missing.push("environmentId");
106
- if (!block.repository) missing.push("repository");
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
- function main() {
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
- try {
259
- main();
260
- } catch (err) {
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, 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: 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
- `<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 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
- ### Why the install has to come first
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
- 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.
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: <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.)
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 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:
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.** 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`.
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