@codyswann/lisa 2.316.2 → 2.317.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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +139 -9
- 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-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/SKILL.md +106 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +137 -51
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- package/plugins/lisa/commands/implement.md +5 -3
- package/plugins/lisa/commands/setup/remote-env.md +7 -0
- package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-remote-dispatch/SKILL.md +106 -0
- package/plugins/lisa/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- package/plugins/lisa-agy/commands/lisa/implement.md +5 -3
- package/plugins/lisa-agy/commands/lisa/setup/remote-env.md +7 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-remote-dispatch/SKILL.md +106 -0
- package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- 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/commands/lisa/implement.md +5 -3
- package/plugins/lisa-copilot/commands/lisa/setup/remote-env.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-remote-dispatch/SKILL.md +106 -0
- package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/implement.md +5 -3
- package/plugins/lisa-cursor/commands/lisa/setup/remote-env.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-remote-dispatch/SKILL.md +106 -0
- package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- 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/commands/implement.md +5 -3
- package/plugins/src/base/commands/setup/remote-env.md +7 -0
- package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/src/base/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/src/base/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-remote-dispatch/SKILL.md +106 -0
- package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- 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
|
+
}
|