@codyswann/lisa 2.316.2 → 2.317.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +139 -9
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +17 -0
  8. package/plugins/lisa/.codex-plugin/skills/lisa-automation-status/SKILL.md +23 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +33 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +17 -0
  11. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +17 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/SKILL.md +99 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +137 -51
  16. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  18. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  22. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  23. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  24. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  25. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +69 -0
  26. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  27. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +150 -0
  28. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
  29. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  30. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  32. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  33. package/plugins/lisa/commands/implement.md +5 -3
  34. package/plugins/lisa/commands/setup/remote-env.md +7 -0
  35. package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +17 -0
  36. package/plugins/lisa/skills/lisa-automation-status/SKILL.md +23 -0
  37. package/plugins/lisa/skills/lisa-doctor/SKILL.md +33 -0
  38. package/plugins/lisa/skills/lisa-implement/SKILL.md +17 -0
  39. package/plugins/lisa/skills/lisa-notion-access/SKILL.md +17 -0
  40. package/plugins/lisa/skills/lisa-remote-dispatch/SKILL.md +99 -0
  41. package/plugins/lisa/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
  42. package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  43. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +138 -52
  44. package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  45. package/plugins/lisa/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  46. package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  47. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  48. package/plugins/lisa/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  49. package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  50. package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  51. package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  52. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  53. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +69 -0
  54. package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  55. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +150 -0
  56. package/plugins/lisa/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
  57. package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  58. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  59. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  60. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  61. package/plugins/lisa-agy/commands/lisa/implement.md +5 -3
  62. package/plugins/lisa-agy/commands/lisa/setup/remote-env.md +7 -0
  63. package/plugins/lisa-agy/plugin.json +1 -1
  64. package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +17 -0
  65. package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +23 -0
  66. package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +33 -0
  67. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +17 -0
  68. package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +17 -0
  69. package/plugins/lisa-agy/skills/lisa-remote-dispatch/SKILL.md +99 -0
  70. package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  71. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +138 -52
  72. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  73. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  74. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  75. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  76. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  77. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  78. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  79. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  80. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  81. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +69 -0
  82. package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  83. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +150 -0
  84. package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  85. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  86. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  87. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  88. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  91. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-copilot/commands/lisa/implement.md +5 -3
  95. package/plugins/lisa-copilot/commands/lisa/setup/remote-env.md +7 -0
  96. package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +17 -0
  97. package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +23 -0
  98. package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +33 -0
  99. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +17 -0
  100. package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +17 -0
  101. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/SKILL.md +99 -0
  102. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  103. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +138 -52
  104. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  105. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  106. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  107. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  108. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  109. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  110. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  111. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  112. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  113. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +69 -0
  114. package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  115. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +150 -0
  116. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  117. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  118. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  119. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  120. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-cursor/commands/lisa/implement.md +5 -3
  122. package/plugins/lisa-cursor/commands/lisa/setup/remote-env.md +7 -0
  123. package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +17 -0
  124. package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +23 -0
  125. package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +33 -0
  126. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +17 -0
  127. package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +17 -0
  128. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/SKILL.md +99 -0
  129. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  130. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +138 -52
  131. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  132. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  133. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  134. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  135. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  136. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  137. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  138. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  139. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  140. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +69 -0
  141. package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  142. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +150 -0
  143. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  144. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  145. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  146. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  147. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  148. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  149. package/plugins/lisa-expo-agy/plugin.json +1 -1
  150. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  151. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  154. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  155. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  156. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  158. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  159. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  160. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  161. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  163. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  164. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  165. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  166. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  167. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  168. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  169. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  170. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  171. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  172. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  173. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  174. package/plugins/lisa-rails-agy/plugin.json +1 -1
  175. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  176. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  177. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  178. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  179. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  180. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  181. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  182. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  183. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  184. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  185. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  186. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  187. package/plugins/src/base/commands/implement.md +5 -3
  188. package/plugins/src/base/commands/setup/remote-env.md +7 -0
  189. package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +17 -0
  190. package/plugins/src/base/skills/lisa-automation-status/SKILL.md +23 -0
  191. package/plugins/src/base/skills/lisa-doctor/SKILL.md +33 -0
  192. package/plugins/src/base/skills/lisa-implement/SKILL.md +17 -0
  193. package/plugins/src/base/skills/lisa-notion-access/SKILL.md +17 -0
  194. package/plugins/src/base/skills/lisa-remote-dispatch/SKILL.md +99 -0
  195. package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  196. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +138 -52
  197. package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  198. package/plugins/src/base/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  199. package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  200. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  201. package/plugins/src/base/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  202. package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  203. package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  204. package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  205. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  206. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +69 -0
  207. package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  208. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +150 -0
  209. package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  210. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  211. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  212. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
@@ -26,6 +26,75 @@ create them; invoke the runtime's automation tool with the spec below.
26
26
  - **Other runtimes** → use the runtime's native recurring-task mechanism. If the runtime has none,
27
27
  state that scheduling is unavailable and stop.
28
28
 
29
+ ## Unattended scheduling: `scheduler: "github-actions"`
30
+
31
+ Every option above runs on the **workstation**. That is correct when the operator is present, and
32
+ useless when the goal is "fire work off, close the laptop, go to sleep" — a native automation on a
33
+ sleeping machine does not fire.
34
+
35
+ When a loop declares `scheduler: "github-actions"`, generate a workflow instead of a local routine.
36
+ This is the right shape only in combination with `executionEnv`: the scheduled job becomes a **thin
37
+ dispatcher** that wakes, picks eligible work, submits it to a remote surface, records the identifier,
38
+ and exits. The heavy lifting happens on the remote surface, so the clock needs availability rather
39
+ than power.
40
+
41
+ ```json
42
+ {
43
+ "automations": {
44
+ "intake": {
45
+ "scheduler": "github-actions",
46
+ "schedule": "17 * * * *",
47
+ "executionEnv": "codex-cloud",
48
+ "enabled": false
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ `scheduler` is the pluggable seam. The same declaration generates a GitHub Actions workflow today and
55
+ can register a different mechanism later without touching the loop itself. **Config is the source of
56
+ truth; the workflow is generated from it** — regenerate rather than hand-edit.
57
+
58
+ ```sh
59
+ scripts/generate-workflow.mjs intake --write
60
+ ```
61
+
62
+ ### Why GitHub Actions is the default unattended clock
63
+
64
+ It is already present in every host project, already holds the bootstrap credential, and provides
65
+ cron, concurrency groups, retries, manual dispatch, and an audit trail. Cost is negligible: billing is
66
+ per minute rounded up, and a dispatch-only job finishes in well under one — a nightly loop runs a few
67
+ dollars a year. Note the rounding is **per workflow run**, so batching N dispatches into one run is
68
+ far cheaper than N triggers.
69
+
70
+ The real cost is the remote task's own consumption, against which the clock is a rounding error.
71
+
72
+ Rejected: workstation-native automation mechanisms for this purpose — they reintroduce exactly the
73
+ sleeping-laptop problem this option exists to solve.
74
+
75
+ ### Registered disabled
76
+
77
+ A recurring trigger goes live **only after the exact production path has been proven once for one
78
+ real item.** Registration therefore writes a `workflow_dispatch`-only workflow; enabling adds the
79
+ cron. The generator says so when it writes a disabled loop.
80
+
81
+ **GitHub auto-disables scheduled workflows after 60 days of repository inactivity.** For an
82
+ unattended factory that is a real trap — a quiet repository silently stops running its loops, and
83
+ nothing announces it. `lisa-automation-status` must treat a stopped clock as a finding.
84
+
85
+ ### What the generated workflow guarantees
86
+
87
+ - **Never cancels in progress.** Cancelling could kill a dispatch after the remote accepted a task
88
+ but before its identifier was recorded — irreversible work with nothing to reconcile against, where
89
+ a retry duplicates it.
90
+ - **Asserts the bootstrap before installing anything**, so a misconfigured secret fails in seconds
91
+ rather than after a toolchain download.
92
+ - **Persists credential rotation with `if: always()`.** The dispatcher authenticates with a
93
+ credential that rotates on use, so the replacement must be written back even when the dispatch
94
+ itself failed. That makes the very first artifact this produces an exercise of the rotating-credential
95
+ path in `lisa-secrets-access`, not a hypothetical one.
96
+ - **Guards on the repository**, so a fork does not inherit a live clock.
97
+
29
98
  ## Parameters
30
99
 
31
100
  - `auto-start-prds` (default **true**) — passed as `prd_ready` to the **exploratory-prds**
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate the GitHub Actions workflow that fires a registered automation loop.
4
+ *
5
+ * Once `executionEnv` exists, a scheduled loop is a thin dispatcher: it wakes,
6
+ * picks eligible work, submits it to a remote surface, records the identifier,
7
+ * and exits. The heavy lifting happens elsewhere. That makes the clock's job
8
+ * cheap and boring, and the requirements on it are availability rather than
9
+ * power.
10
+ *
11
+ * The workflow is generated from `.lisa.config.json` rather than hand-written,
12
+ * so config stays the source of truth and swapping the scheduler later is a
13
+ * config change instead of a rewrite.
14
+ *
15
+ * Usage:
16
+ * generate-workflow.mjs <loop-name> [--write]
17
+ * @module generate-workflow
18
+ */
19
+
20
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+
23
+ /** Where a generated loop workflow lands. */
24
+ export const workflowPath = name =>
25
+ join(".github", "workflows", `lisa-${name}.yml`);
26
+
27
+ /**
28
+ * Read one automation's declaration.
29
+ * @param {string} name Loop name.
30
+ * @param {string} [cwd] Repository root.
31
+ * @returns {object} The loop's configuration.
32
+ */
33
+ export function readAutomation(name, cwd = process.cwd()) {
34
+ const path = join(cwd, ".lisa.config.json");
35
+ if (!existsSync(path)) throw new Error(".lisa.config.json is missing");
36
+ const cfg = JSON.parse(readFileSync(path, "utf8"));
37
+ const loop = cfg.automations?.[name];
38
+ if (!loop) {
39
+ throw new Error(
40
+ `no automations["${name}"] in .lisa.config.json.\n` +
41
+ `Declare the loop before generating its workflow.`
42
+ );
43
+ }
44
+ return {
45
+ ...loop,
46
+ repository: cfg.remoteEnv?.surfaces?.[loop.executionEnv]?.repository,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Render the workflow YAML for one loop.
52
+ *
53
+ * Three choices here are load-bearing rather than stylistic.
54
+ *
55
+ * The `schedule` trigger is emitted **only when the loop is enabled**. A
56
+ * recurring trigger goes live after the exact production path has been proven
57
+ * once for one real item, so registration writes a manually-dispatchable
58
+ * workflow and enabling adds the clock.
59
+ *
60
+ * The concurrency group **never cancels in progress**. Cancelling could kill a
61
+ * dispatch after the remote accepted a task but before its identifier was
62
+ * recorded, which orphans irreversible work — nothing left to reconcile against,
63
+ * and a retry that duplicates it.
64
+ *
65
+ * The bootstrap credential is asserted **before** anything is installed, so a
66
+ * misconfigured secret fails in seconds rather than after a toolchain download.
67
+ * @param {string} name Loop name.
68
+ * @param {object} loop Loop configuration.
69
+ * @returns {string} Workflow YAML.
70
+ */
71
+ export function renderWorkflow(name, loop) {
72
+ if (!loop.schedule) throw new Error(`automations["${name}"] has no schedule`);
73
+ if (!loop.repository) {
74
+ throw new Error(
75
+ `automations["${name}"] targets executionEnv "${loop.executionEnv}", ` +
76
+ `but no remoteEnv.surfaces["${loop.executionEnv}"].repository is set.\n` +
77
+ `Run /lisa:setup:remote-env ${loop.executionEnv} first.`
78
+ );
79
+ }
80
+
81
+ const trigger = loop.enabled
82
+ ? ` schedule:\n - cron: '${loop.schedule}'\n workflow_dispatch:`
83
+ : ` # schedule: disabled until the production path is proven once for one\n` +
84
+ ` # real item. Enabling adds: - cron: '${loop.schedule}'\n workflow_dispatch:`;
85
+
86
+ return `# Generated by lisa-setup-automations from .lisa.config.json.
87
+ # Edit the config and regenerate; hand edits are overwritten.
88
+ name: 🔁 Lisa ${name}
89
+
90
+ on:
91
+ ${trigger}
92
+
93
+ # Never cancel in progress. A cancelled dispatch can leave a remote task that
94
+ # was accepted but whose identifier was never recorded — irreversible work with
95
+ # nothing to reconcile against, where a retry duplicates it.
96
+ concurrency:
97
+ group: lisa-${name}
98
+ cancel-in-progress: false
99
+
100
+ permissions:
101
+ contents: read
102
+
103
+ jobs:
104
+ dispatch:
105
+ name: Dispatch one ${name} cycle
106
+ if: github.repository == '${loop.repository}'
107
+ runs-on: ubuntu-latest
108
+ timeout-minutes: ${loop.timeoutMinutes ?? 15}
109
+ env:
110
+ LISA_SECRETS_SURFACE: github-actions
111
+
112
+ steps:
113
+ - uses: actions/checkout@v4
114
+ with:
115
+ persist-credentials: false
116
+
117
+ # Assert first, install second: a misconfigured bootstrap should fail in
118
+ # seconds rather than after a toolchain download.
119
+ - name: Check the bootstrap credential is configured
120
+ env:
121
+ BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
122
+ run: |
123
+ set -euo pipefail
124
+ test -n "\${BWS_ACCESS_TOKEN}" || {
125
+ echo "BWS_ACCESS_TOKEN is not configured for this repository" >&2
126
+ exit 1
127
+ }
128
+
129
+ - name: Prepare the toolchain and secrets
130
+ env:
131
+ BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
132
+ run: bash scripts/lisa-remote-env/setup.sh
133
+
134
+ - name: Run one ${name} cycle
135
+ env:
136
+ BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
137
+ run: |
138
+ set -euo pipefail
139
+ node .claude/skills/lisa-remote-dispatch/scripts/dispatch.mjs \\
140
+ 'executionEnv=${loop.executionEnv} ${loop.payload ?? ""}' \\
141
+ --skill ${loop.skill ?? `lisa-${name}`}
142
+
143
+ # The dispatcher authenticates with a credential that rotates on use, so
144
+ # the replacement must be persisted even when the dispatch itself failed.
145
+ - name: Persist any credential rotation
146
+ if: \${{ always() }}
147
+ env:
148
+ BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
149
+ run: |
150
+ set -euo pipefail
151
+ node .claude/skills/lisa-secrets-access/scripts/rotate-secret.mjs leases
152
+ `;
153
+ }
154
+
155
+ function main() {
156
+ const [name, ...flags] = process.argv.slice(2);
157
+ if (!name)
158
+ throw new Error("usage: generate-workflow.mjs <loop-name> [--write]");
159
+ const loop = readAutomation(name);
160
+ const yaml = renderWorkflow(name, loop);
161
+
162
+ if (!flags.includes("--write")) {
163
+ process.stdout.write(yaml);
164
+ return;
165
+ }
166
+ const path = workflowPath(name);
167
+ mkdirSync(dirname(path), { recursive: true });
168
+ writeFileSync(path, yaml);
169
+ console.log(`wrote ${path}`);
170
+ if (!loop.enabled) {
171
+ console.log(
172
+ "Registered DISABLED. Prove the exact production path once for one real " +
173
+ "item with a manual dispatch, then set enabled: true and regenerate."
174
+ );
175
+ }
176
+ }
177
+
178
+ if (import.meta.url === `file://${process.argv[1]}`) {
179
+ try {
180
+ main();
181
+ } catch (err) {
182
+ console.error(err.message);
183
+ process.exit(1);
184
+ }
185
+ }
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: lisa-setup-remote-env
3
+ description: "Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv."
4
+ allowed-tools: ["Bash", "Read", "Write", "Edit", "Skill"]
5
+ ---
6
+
7
+ # Setup Remote Environment: $ARGUMENTS
8
+
9
+ Prepare a remote surface so a host project can execute there. Today that means **Codex Cloud**; the shape is deliberately surface-agnostic so Claude Code web, Cursor remote, and others slot in without redesign.
10
+
11
+ ## What lives where
12
+
13
+ The remote environment's own configuration fields stay **two one-line calls into the repository**. Nothing is pasted into a vendor UI.
14
+
15
+ ```text
16
+ setup: bash scripts/lisa-remote-env/setup.sh
17
+ maintenance: bash scripts/lisa-remote-env/setup.sh
18
+ ```
19
+
20
+ They are the same script. 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.
21
+
22
+ 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.
23
+
24
+ ## The three phases
25
+
26
+ 1. **Toolchain** — assert what must already be present, install what is not.
27
+ 2. **Secrets** — materialize through `lisa-secrets-access`. This skill never reimplements any part of that contract; the single chokepoint is what makes the one-store rule enforceable.
28
+ 3. **Project hook** — an optional `setup.local.sh` the project owns.
29
+
30
+ Order matters. The toolchain comes first because the secrets step needs the provider CLI it installs. The hook comes last because it is the only part that may assume everything else is ready.
31
+
32
+ ## Toolchain manifest — two entry kinds
33
+
34
+ ```json
35
+ {
36
+ "remoteEnv": {
37
+ "tools": {
38
+ "require": [
39
+ { "name": "python3" },
40
+ { "name": "node", "minVersion": "20" },
41
+ { "name": "curl" },
42
+ { "name": "unzip" }
43
+ ],
44
+ "install": [
45
+ {
46
+ "name": "bws",
47
+ "version": "2.1.0",
48
+ "install": "release-zip",
49
+ "url": "https://<vendor>/releases/download/bws-v2.1.0/bws-<platform>-2.1.0.zip",
50
+ "sha256": "<sha256 published with that exact release>"
51
+ },
52
+ {
53
+ "name": "codex",
54
+ "version": "0.144.6",
55
+ "install": "npm-global",
56
+ "package": "@openai/codex"
57
+ }
58
+ ]
59
+ },
60
+ "hook": "scripts/lisa-remote-env/setup.local.sh",
61
+ "surfaces": {
62
+ "codex-cloud": {
63
+ "environmentId": "<id>",
64
+ "repository": "<org>/<repo>",
65
+ "branch": "main"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ **`require`** — assert present, optionally at a minimum version. For everything the base image already provides.
73
+
74
+ **`install`** — provision it, pinned and checksummed. Only for what genuinely is not there.
75
+
76
+ Expected shape for most projects: a short `require` list, an empty or near-empty `install` list, and the provider CLI as the only thing always provisioned.
77
+
78
+ ### Rules
79
+
80
+ - **The base image is not a contract.** A vendor can change it. A project quietly depending on a preinstalled `jq` should break loudly at setup when that happens, not mysteriously mid-task weeks later. `require` is what converts an implicit assumption into an explicit check.
81
+ - **Detect first, install second.** Check the installed version, skip when it matches the pin, reinstall only when the pin changed. This is what makes setup and maintenance the same script, and it is the cheap path on cache resume.
82
+ - **Version-assert, not just presence.** A base-image bump that moves off a supported Node or Python must be caught. Presence alone is not enough when a project depends on a specific version.
83
+ - **Prefer what is there.** A second Node beside the image's creates PATH ambiguity and wastes container time.
84
+ - **Pin and checksum together, in one reviewed commit.** A version bump that does not move the checksum fails before installation, not after. An archive is verified *before* it is unpacked, so unexpected contents never reach a directory on PATH.
85
+
86
+ ## Tooling is never derived from secret notes
87
+
88
+ Deliberately rejected:
89
+
90
+ - Most tooling has **no credential** — `jq`, ripgrep, browsers, a linter — so notes could only ever cover a subset, and a second mechanism would be needed anyway.
91
+ - Most credentials **imply no tool**: a webhook is curl'd, a REST key needs no CLI.
92
+ - The mapping is **guesswork** — which CLI, what version, pinned how — and is exactly the inference the secrets contract already forbids from authorising anything.
93
+ - It makes **prose load-bearing**: a note lives provider-side, is editable outside review, and is unfixable by a read-only account.
94
+ - It is a **supply-chain path** — provider write access would become arbitrary install access.
95
+
96
+ **Notes are an assertion target instead.** At verify time, a tool that declares a credential which is not materialized is an **error**; a credential materialized with no declared consumer is a **warning**. The arrow points the safe way: the repository declares intent, and the provider is checked against it.
97
+
98
+ ## Provisioning tiers
99
+
100
+ Preference order, falling back:
101
+
102
+ 1. **API** — provision programmatically.
103
+ 2. **Console** — drive the vendor's web console.
104
+ 3. **Emit** — generate exact config for a human to paste.
105
+
106
+ **For Codex Cloud today, start at tier 2 or 3.** Verified on 2026-08-01: `codex cloud` exposes only `exec | status | list | apply | diff`. There is no environment-provisioning subcommand, so there is nothing to call. Do not claim tier 1 for this surface until the CLI grows one.
107
+
108
+ When emitting, produce exactly:
109
+
110
+ ```text
111
+ Environment name: <project> remote executor
112
+ Repository: <org>/<repo> (must be the default checkout)
113
+ Setup script: bash scripts/lisa-remote-env/setup.sh
114
+ Maintenance: bash scripts/lisa-remote-env/setup.sh
115
+ Environment vars: LISA_SECRETS_SURFACE=codex-cloud
116
+ BWS_ACCESS_TOKEN=<from the provider; an environment
117
+ variable, not a task secret — setup and cache-resume
118
+ maintenance both run before task secrets exist>
119
+ ```
120
+
121
+ ## Verification is tier-independent
122
+
123
+ **Whatever tier provisioned it, the same read-back proves it.** Trust comes from the verify, not the mechanism — which is what makes emit-tier as trustworthy as API-tier.
124
+
125
+ ```sh
126
+ scripts/verify-remote-env.mjs [SECRETS_DIR]
127
+ ```
128
+
129
+ Asserts, without printing a value: each declared tool at its pinned or minimum version, the detected surface, the secrets directory at mode `0700`, both files at mode `0600`, and a clean checkout.
130
+
131
+ **Never verify against vendor UI state.** On 2026-08-01 a Codex environments table reported zero tasks for an environment that had demonstrably completed one, because the task records carried a null environment identifier and the `--env` filter was correspondingly unreliable. Reconcile through durable identifiers only.
132
+
133
+ ## Preconditions
134
+
135
+ Checked when setup runs, not at 3am:
136
+
137
+ - the environment exists and is bound to **this** repository as its default checkout;
138
+ - the bootstrap credential resolves;
139
+ - Lisa has been applied to the repository, so its skills are present in that checkout.
140
+
141
+ Fail with a message naming what is missing. Never provision-and-hope.
142
+
143
+ ## Bootstrap credential placement
144
+
145
+ On Codex Cloud the bootstrap must be an **environment variable, not a task secret**: setup runs on a new container and maintenance runs on cache resume, both before task secrets exist. The tradeoff is that the variable remains visible during the task, so compensate by keeping the machine account narrowly scoped and instructing the task never to inspect or use it.
146
+
147
+ ## Related
148
+
149
+ - `lisa-secrets-access` — owns every part of the secrets contract this skill composes with.
150
+ - `lisa-remote-dispatch` — dispatches work to an environment this skill prepared.
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Remote environment entrypoint. Installed by /lisa:setup:remote-env; the remote
4
+ # environment's setup AND maintenance fields both call this exact path.
5
+ #
6
+ # They are the same script on purpose. A container may be built fresh or resumed
7
+ # from cache, and everything below is idempotent and version-aware — so running
8
+ # it twice is correct, and running it on resume is what picks up a rotated
9
+ # value, an edited note, or a changed version pin.
10
+ #
11
+ # This file is deliberately thin. It resolves an interpreter and hands off; the
12
+ # reviewed, tested, versioned logic lives in the Lisa skill rather than here,
13
+ # and emphatically not in a vendor settings field.
14
+ set -euo pipefail
15
+
16
+ # Node is the one thing that cannot be installed by the installer, since the
17
+ # installer is written in it. Fail with an actionable message rather than a
18
+ # "command not found" forty lines deep.
19
+ if ! command -v node >/dev/null 2>&1; then
20
+ echo "node is required to prepare this environment but is not present." >&2
21
+ echo "It cannot be installed by the toolchain step, because that step runs" >&2
22
+ echo "on node. Pin a base image that provides it." >&2
23
+ exit 1
24
+ fi
25
+
26
+ # The skill ships under whichever agent directory this project uses. Search
27
+ # rather than hardcode, so one entrypoint serves every supported harness.
28
+ runner=""
29
+ for candidate in \
30
+ ".claude/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs" \
31
+ ".agents/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs" \
32
+ ".codex/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs"; do
33
+ if [ -f "$candidate" ]; then
34
+ runner="$candidate"
35
+ break
36
+ fi
37
+ done
38
+
39
+ if [ -z "$runner" ]; then
40
+ echo "Cannot find the lisa-setup-remote-env skill in this checkout." >&2
41
+ echo "Run 'lisa apply' so the skills are present, then re-run setup." >&2
42
+ exit 1
43
+ fi
44
+
45
+ exec node "$runner" "$@"
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Prepare a remote environment: toolchain, then secrets, then the project hook.
4
+ *
5
+ * This is the script a remote environment's setup **and** maintenance fields
6
+ * call. They are the same script on purpose. A container may be built fresh or
7
+ * resumed from cache, and every step here is idempotent and version-aware, so
8
+ * running it twice is correct and running it on resume is what picks up a
9
+ * rotated value, an edited note, or a changed version pin.
10
+ *
11
+ * Order matters. The toolchain comes first because the secrets step needs the
12
+ * provider CLI that the toolchain installs. The project hook comes last because
13
+ * it is the only part that may assume everything else is ready.
14
+ *
15
+ * Usage:
16
+ * setup-remote-env.mjs [--dry-run]
17
+ * @module setup-remote-env
18
+ */
19
+
20
+ import { execFileSync } from "node:child_process";
21
+ import {
22
+ chmodSync,
23
+ existsSync,
24
+ mkdirSync,
25
+ readFileSync,
26
+ rmSync,
27
+ } from "node:fs";
28
+ import { dirname, join, resolve } from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+
31
+ import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
32
+
33
+ const HERE = dirname(fileURLToPath(import.meta.url));
34
+
35
+ /**
36
+ * Locate a sibling skill's script.
37
+ *
38
+ * Secrets belong to `lisa-secrets-access`, and this script must not reimplement
39
+ * any part of that contract — the single chokepoint is what makes the one-store
40
+ * rule enforceable. Resolving relative to this file works in every install
41
+ * layout, because skills are always siblings under one `skills/` directory.
42
+ * @param {string} skill Sibling skill slug.
43
+ * @param {string} script Script filename.
44
+ * @returns {string} Absolute path.
45
+ */
46
+ function siblingScript(skill, script) {
47
+ const path = resolve(HERE, "..", "..", skill, "scripts", script);
48
+ if (!existsSync(path)) {
49
+ throw new Error(
50
+ `cannot find ${skill}/scripts/${script} beside this skill.\n` +
51
+ `Remote setup composes with that skill rather than reimplementing it.`
52
+ );
53
+ }
54
+ return path;
55
+ }
56
+
57
+ /**
58
+ * Read the `remoteEnv` block from `.lisa.config.json`.
59
+ * @param {string} [cwd] Directory to look in.
60
+ * @returns {{tools: object, hook: string|null, surfaces: object}} Configuration.
61
+ */
62
+ export function readRemoteEnvConfig(cwd = process.cwd()) {
63
+ const path = join(cwd, ".lisa.config.json");
64
+ const defaults = {
65
+ tools: { require: [], install: [] },
66
+ hook: null,
67
+ surfaces: {},
68
+ };
69
+ if (!existsSync(path)) return defaults;
70
+ const cfg = JSON.parse(readFileSync(path, "utf8")).remoteEnv;
71
+ if (!cfg) return defaults;
72
+ return {
73
+ tools: { require: [], install: [], ...(cfg.tools ?? {}) },
74
+ hook: cfg.hook ?? null,
75
+ surfaces: cfg.surfaces ?? {},
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Probe what a tool reports for its version, treating absence as not present.
81
+ * @param {string} name Executable name.
82
+ * @returns {{version: string|null, present: boolean}} Probe result.
83
+ */
84
+ function probe(name) {
85
+ try {
86
+ const out = execFileSync(name, ["--version"], {
87
+ encoding: "utf8",
88
+ stdio: ["ignore", "pipe", "ignore"],
89
+ });
90
+ return { present: true, version: extractVersion(out) };
91
+ } catch {
92
+ return { present: false, version: null };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Install a pinned archive, refusing anything whose checksum does not match.
98
+ * @param {object} tool Manifest entry.
99
+ * @param {string} binDir Directory to install into.
100
+ */
101
+ function installReleaseZip(tool, binDir) {
102
+ const temporary = join(binDir, `.${tool.name}-download`);
103
+ mkdirSync(temporary, { recursive: true });
104
+ try {
105
+ const archive = join(temporary, "download.zip");
106
+ execFileSync("curl", ["-fsSL", tool.url, "-o", archive], {
107
+ stdio: "inherit",
108
+ });
109
+ // Verify before unpacking, not after. An unexpected archive must fail
110
+ // before any of its contents reach a directory that is on PATH.
111
+ execFileSync("sha256sum", ["-c", "-"], {
112
+ input: `${tool.sha256} ${archive}\n`,
113
+ stdio: ["pipe", "ignore", "inherit"],
114
+ });
115
+ execFileSync("unzip", ["-q", "-o", archive, "-d", temporary], {
116
+ stdio: "inherit",
117
+ });
118
+ const binary = join(temporary, tool.binary ?? tool.name);
119
+ execFileSync("install", ["-m", "0755", binary, join(binDir, tool.name)], {
120
+ stdio: "inherit",
121
+ });
122
+ } finally {
123
+ rmSync(temporary, { recursive: true, force: true });
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Install a pinned global npm package.
129
+ * @param {object} tool Manifest entry.
130
+ */
131
+ function installNpmGlobal(tool) {
132
+ execFileSync(
133
+ "npm",
134
+ ["install", "--global", `${tool.package}@${tool.version}`],
135
+ {
136
+ stdio: "inherit",
137
+ }
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Execute one install decision.
143
+ * @param {object} tool Manifest entry.
144
+ * @param {string} binDir Directory for downloaded binaries.
145
+ */
146
+ function installTool(tool, binDir) {
147
+ assertPinned(tool);
148
+ if (tool.install === "release-zip") installReleaseZip(tool, binDir);
149
+ else installNpmGlobal(tool);
150
+ }
151
+
152
+ /**
153
+ * Apply the toolchain plan, reporting every decision.
154
+ * @param {object} tools Manifest.
155
+ * @param {boolean} dryRun Whether to plan only.
156
+ * @returns {Array<object>} The plan that was applied.
157
+ */
158
+ function applyToolchain(tools, dryRun) {
159
+ const plan = planToolchain(tools, probe);
160
+ const blocked = plan.filter(
161
+ p => p.action === "missing" || p.action === "invalid"
162
+ );
163
+ if (blocked.length) {
164
+ throw new Error(blocked.map(p => p.reason).join("\n\n"));
165
+ }
166
+
167
+ const binDir = join(process.env.HOME ?? "", ".local", "bin");
168
+ mkdirSync(binDir, { recursive: true, mode: 0o755 });
169
+ const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
170
+
171
+ for (const step of plan) {
172
+ console.log(` ${step.action.padEnd(8)} ${step.reason}`);
173
+ if (step.action === "install" && !dryRun)
174
+ installTool(byName.get(step.name), binDir);
175
+ }
176
+ return plan;
177
+ }
178
+
179
+ /**
180
+ * Run the project's own setup hook when it has one.
181
+ *
182
+ * A declarative manifest will never cover everything, and without an escape
183
+ * hatch a project's only option is to fork Lisa's script — at which point it
184
+ * stops receiving any of the fixes.
185
+ * @param {string|null} hook Repository-relative path.
186
+ * @param {boolean} dryRun Whether to plan only.
187
+ */
188
+ function runHook(hook, dryRun) {
189
+ if (!hook) return;
190
+ const path = resolve(process.cwd(), hook);
191
+ if (!existsSync(path)) {
192
+ throw new Error(`remoteEnv.hook points at ${hook}, which does not exist`);
193
+ }
194
+ console.log(`\nProject hook: ${hook}`);
195
+ if (dryRun) return;
196
+ chmodSync(path, 0o755);
197
+ execFileSync("bash", [path], { stdio: "inherit" });
198
+ }
199
+
200
+ function main() {
201
+ const dryRun = process.argv.includes("--dry-run");
202
+ const cfg = readRemoteEnvConfig();
203
+
204
+ console.log("Toolchain:");
205
+ applyToolchain(cfg.tools, dryRun);
206
+
207
+ console.log("\nSecrets:");
208
+ const materialize = siblingScript(
209
+ "lisa-secrets-access",
210
+ "materialize-secrets.mjs"
211
+ );
212
+ execFileSync("node", dryRun ? [materialize, "--dry-run"] : [materialize], {
213
+ stdio: "inherit",
214
+ });
215
+
216
+ runHook(cfg.hook, dryRun);
217
+ console.log(`\nRemote environment ${dryRun ? "plan complete" : "ready"}.`);
218
+ }
219
+
220
+ if (import.meta.url === `file://${process.argv[1]}`) {
221
+ try {
222
+ main();
223
+ } catch (err) {
224
+ console.error(err.message);
225
+ process.exit(1);
226
+ }
227
+ }