@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.
- 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 +99 -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 +99 -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 +99 -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 +99 -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 +99 -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 +99 -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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Surface definitions and configuration for `lisa-secrets-access`.
|
|
4
|
+
*
|
|
5
|
+
* A *provider* is where secrets live. A *surface* is where the running code
|
|
6
|
+
* lives, and it determines how secrets reach that code. These are independent
|
|
7
|
+
* axes: the same Bitwarden project serves a laptop, a CI runner, and a remote
|
|
8
|
+
* agent container, but each of the three obtains its values differently.
|
|
9
|
+
*
|
|
10
|
+
* Surfaces are declared by capability rather than by name so adding one is a
|
|
11
|
+
* single entry here rather than a new branch in every consumer. That is the
|
|
12
|
+
* whole reason this table exists.
|
|
13
|
+
* @module surfaces
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Capabilities, per surface.
|
|
22
|
+
*
|
|
23
|
+
* `materialized` — the surface obtains values from files written before the
|
|
24
|
+
* code ran, so the resolver must consult them.
|
|
25
|
+
*
|
|
26
|
+
* `mayWriteValues` — the surface is permitted to write resolved values to
|
|
27
|
+
* disk. This is normally forbidden, because a value on disk is a copy that can
|
|
28
|
+
* drift and leak. It is *required* on surfaces whose bootstrap runs before the
|
|
29
|
+
* consuming process exists and which therefore have no other channel; a remote
|
|
30
|
+
* agent container prepares itself during setup, long before any task starts.
|
|
31
|
+
*/
|
|
32
|
+
export const SURFACES = {
|
|
33
|
+
local: { materialized: false, mayWriteValues: false },
|
|
34
|
+
"github-actions": { materialized: false, mayWriteValues: false },
|
|
35
|
+
"codex-cloud": { materialized: true, mayWriteValues: true },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Config defaults when `.lisa.config.json` carries no `secrets` block. */
|
|
39
|
+
const DEFAULTS = {
|
|
40
|
+
provider: "env",
|
|
41
|
+
bootstrap: { sources: ["env"], key: null },
|
|
42
|
+
require: null,
|
|
43
|
+
rotating: [],
|
|
44
|
+
namespace: "lisa",
|
|
45
|
+
narrow: { projectIds: [], excludeKeys: [] },
|
|
46
|
+
surface: null,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Identify the surface this process is running on.
|
|
51
|
+
*
|
|
52
|
+
* An explicit value always wins, so an operator can reproduce another
|
|
53
|
+
* surface's behaviour locally when diagnosing one. Detection is otherwise
|
|
54
|
+
* ordered most-specific first.
|
|
55
|
+
* @param {string|null} [configured] Surface named in `.lisa.config.json`.
|
|
56
|
+
* @param {Record<string, string|undefined>} [env] Environment to inspect.
|
|
57
|
+
* @returns {string} A key of {@link SURFACES}.
|
|
58
|
+
*/
|
|
59
|
+
export function detectSurface(configured = null, env = process.env) {
|
|
60
|
+
const explicit = (env.LISA_SECRETS_SURFACE ?? "").trim() || configured;
|
|
61
|
+
if (explicit) {
|
|
62
|
+
if (!SURFACES[explicit]) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`unknown surface "${explicit}".\n` +
|
|
65
|
+
`Known: ${Object.keys(SURFACES).join(", ")}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return explicit;
|
|
69
|
+
}
|
|
70
|
+
if ((env.GITHUB_ACTIONS ?? "") === "true") return "github-actions";
|
|
71
|
+
if ((env.CODEX_SANDBOX ?? env.CODEX_HOME ?? "") !== "") return "codex-cloud";
|
|
72
|
+
return "local";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reject a namespace that is not exactly one safe path segment.
|
|
77
|
+
*
|
|
78
|
+
* The namespace is joined onto a config root, so anything containing a
|
|
79
|
+
* separator or a parent reference could redirect writes outside the intended
|
|
80
|
+
* directory. Validating here means every caller inherits the guard.
|
|
81
|
+
* @param {string} namespace Candidate namespace.
|
|
82
|
+
* @returns {string} The namespace, unchanged, when valid.
|
|
83
|
+
*/
|
|
84
|
+
export function assertNamespace(namespace) {
|
|
85
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(namespace) || namespace === "..") {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`secrets.namespace must be one safe path segment, got "${namespace}"`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return namespace;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Where a surface's materialized files live.
|
|
95
|
+
* @param {string} namespace Validated namespace.
|
|
96
|
+
* @param {Record<string, string|undefined>} [env] Environment to inspect.
|
|
97
|
+
* @returns {{dir: string, valuesFile: string, notesFile: string}} Paths.
|
|
98
|
+
*/
|
|
99
|
+
export function materializedPaths(namespace, env = process.env) {
|
|
100
|
+
const root = env.XDG_CONFIG_HOME || join(env.HOME || homedir(), ".config");
|
|
101
|
+
const dir = join(root, assertNamespace(namespace));
|
|
102
|
+
return {
|
|
103
|
+
dir,
|
|
104
|
+
valuesFile: join(dir, "secrets.env"),
|
|
105
|
+
notesFile: join(dir, "secret-notes.json"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Read the `secrets` block from `.lisa.config.json`.
|
|
111
|
+
*
|
|
112
|
+
* A project with no block is a supported state, not an error: the `env`
|
|
113
|
+
* provider means the environment *is* the provider. A credentials manager is
|
|
114
|
+
* the preferred path, never a required one.
|
|
115
|
+
* @param {string} [cwd] Directory to look in.
|
|
116
|
+
* @returns {object} Resolved configuration with defaults applied.
|
|
117
|
+
*/
|
|
118
|
+
export function readConfig(cwd = process.cwd()) {
|
|
119
|
+
const path = join(cwd, ".lisa.config.json");
|
|
120
|
+
if (!existsSync(path)) return withSurface(DEFAULTS);
|
|
121
|
+
let cfg;
|
|
122
|
+
try {
|
|
123
|
+
cfg = JSON.parse(readFileSync(path, "utf8")).secrets;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
throw new Error(`.lisa.config.json is not readable: ${err.message}`);
|
|
126
|
+
}
|
|
127
|
+
if (!cfg) return withSurface(DEFAULTS);
|
|
128
|
+
return withSurface({
|
|
129
|
+
provider: cfg.provider ?? DEFAULTS.provider,
|
|
130
|
+
bootstrap: { ...DEFAULTS.bootstrap, ...(cfg.bootstrap ?? {}) },
|
|
131
|
+
require: cfg.require ?? null,
|
|
132
|
+
rotating: cfg.rotating ?? [],
|
|
133
|
+
namespace: assertNamespace(cfg.namespace ?? DEFAULTS.namespace),
|
|
134
|
+
narrow: { ...DEFAULTS.narrow, ...(cfg.narrow ?? {}) },
|
|
135
|
+
surface: cfg.surface ?? null,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Attach the resolved surface and its capabilities to a configuration.
|
|
141
|
+
* @param {object} cfg Configuration without surface resolution.
|
|
142
|
+
* @returns {object} The same configuration plus `surface` and `capabilities`.
|
|
143
|
+
*/
|
|
144
|
+
function withSurface(cfg) {
|
|
145
|
+
const surface = detectSurface(cfg.surface);
|
|
146
|
+
return { ...cfg, surface, capabilities: SURFACES[surface] };
|
|
147
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate the `secrets`, `remoteEnv`, and `automations` blocks.
|
|
4
|
+
*
|
|
5
|
+
* These blocks are read by shell, by Node, and by generated workflows, so a
|
|
6
|
+
* malformed one surfaces late and somewhere unhelpful — a container that fails
|
|
7
|
+
* mid-setup, a scheduled loop that never fires, a dispatch that names a surface
|
|
8
|
+
* nobody provisioned. Checking the shape up front turns all of those into one
|
|
9
|
+
* message at `doctor` time.
|
|
10
|
+
*
|
|
11
|
+
* Structure only. Whether a credential resolves is `doctor-secrets.mjs`; this
|
|
12
|
+
* asks whether the declaration could ever be correct.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* validate-config.mjs
|
|
16
|
+
* @module validate-config
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
/** Providers with a read implementation today. */
|
|
23
|
+
const IMPLEMENTED_PROVIDERS = new Set(["bitwarden", "doppler", "env"]);
|
|
24
|
+
|
|
25
|
+
/** Providers named in the dispatch table but not yet implemented. */
|
|
26
|
+
const DECLARED_PROVIDERS = new Set(["1password", "aws", "vault"]);
|
|
27
|
+
|
|
28
|
+
/** Surfaces the resolver knows. */
|
|
29
|
+
const SURFACES = new Set(["local", "github-actions", "codex-cloud"]);
|
|
30
|
+
|
|
31
|
+
/** Install methods the toolchain runner supports. */
|
|
32
|
+
const INSTALL_METHODS = new Set(["release-zip", "npm-global"]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate the `secrets` block.
|
|
36
|
+
* @param {object|undefined} secrets The block, if present.
|
|
37
|
+
* @returns {string[]} Problems found.
|
|
38
|
+
*/
|
|
39
|
+
export function validateSecrets(secrets) {
|
|
40
|
+
if (!secrets) return [];
|
|
41
|
+
const problems = [];
|
|
42
|
+
const provider = secrets.provider ?? "env";
|
|
43
|
+
|
|
44
|
+
if (DECLARED_PROVIDERS.has(provider)) {
|
|
45
|
+
problems.push(
|
|
46
|
+
`secrets.provider "${provider}" is documented but has no read ` +
|
|
47
|
+
`implementation yet. Add one in providers.mjs rather than configuring it.`
|
|
48
|
+
);
|
|
49
|
+
} else if (!IMPLEMENTED_PROVIDERS.has(provider)) {
|
|
50
|
+
problems.push(
|
|
51
|
+
`secrets.provider "${provider}" is unknown. ` +
|
|
52
|
+
`Known: ${[...IMPLEMENTED_PROVIDERS, ...DECLARED_PROVIDERS].join(", ")}.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
secrets.namespace &&
|
|
58
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(secrets.namespace)
|
|
59
|
+
) {
|
|
60
|
+
problems.push(
|
|
61
|
+
`secrets.namespace "${secrets.namespace}" is not one safe path segment. ` +
|
|
62
|
+
`It is joined onto a config root, so a separator could redirect writes.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (secrets.surface && !SURFACES.has(secrets.surface)) {
|
|
67
|
+
problems.push(
|
|
68
|
+
`secrets.surface "${secrets.surface}" is unknown. ` +
|
|
69
|
+
`Known: ${[...SURFACES].join(", ")}.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const field of ["require", "rotating"]) {
|
|
74
|
+
const value = secrets[field];
|
|
75
|
+
if (value === undefined || value === null) continue;
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
problems.push(`secrets.${field} must be an array of exact key names`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const name of value) {
|
|
81
|
+
if (typeof name !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(name)) {
|
|
82
|
+
problems.push(
|
|
83
|
+
`secrets.${field} entry ${JSON.stringify(name)} is not an exact ` +
|
|
84
|
+
`UPPER_SNAKE_CASE environment-variable name. Lookup is never fuzzy.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ((secrets.rotating ?? []).length && !secrets.bootstrap?.key) {
|
|
91
|
+
problems.push(
|
|
92
|
+
`secrets.rotating is declared but secrets.bootstrap.key is not. A ` +
|
|
93
|
+
`rotating credential whose replacement cannot be written back is ` +
|
|
94
|
+
`stranded the first time it is used.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return problems;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate the `remoteEnv` block.
|
|
103
|
+
* @param {object|undefined} remoteEnv The block, if present.
|
|
104
|
+
* @returns {string[]} Problems found.
|
|
105
|
+
*/
|
|
106
|
+
export function validateRemoteEnv(remoteEnv) {
|
|
107
|
+
if (!remoteEnv) return [];
|
|
108
|
+
const problems = [];
|
|
109
|
+
|
|
110
|
+
for (const tool of remoteEnv.tools?.require ?? []) {
|
|
111
|
+
if (!tool.name) problems.push("remoteEnv.tools.require entry has no name");
|
|
112
|
+
}
|
|
113
|
+
for (const tool of remoteEnv.tools?.install ?? []) {
|
|
114
|
+
if (!tool.name) {
|
|
115
|
+
problems.push("remoteEnv.tools.install entry has no name");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!tool.version) {
|
|
119
|
+
problems.push(`remoteEnv install "${tool.name}" has no pinned version`);
|
|
120
|
+
}
|
|
121
|
+
if (!INSTALL_METHODS.has(tool.install)) {
|
|
122
|
+
problems.push(
|
|
123
|
+
`remoteEnv install "${tool.name}" has method ${JSON.stringify(tool.install)}. ` +
|
|
124
|
+
`Supported: ${[...INSTALL_METHODS].join(", ")}.`
|
|
125
|
+
);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (tool.install === "release-zip" && !(tool.url && tool.sha256)) {
|
|
129
|
+
problems.push(
|
|
130
|
+
`remoteEnv install "${tool.name}" needs both url and sha256. A pinned ` +
|
|
131
|
+
`version with no checksum still trusts whatever the URL serves today.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (tool.install === "npm-global" && !tool.package) {
|
|
135
|
+
problems.push(`remoteEnv install "${tool.name}" needs a package`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const [surface, block] of Object.entries(remoteEnv.surfaces ?? {})) {
|
|
140
|
+
if (!SURFACES.has(surface)) {
|
|
141
|
+
problems.push(`remoteEnv.surfaces has unknown surface "${surface}"`);
|
|
142
|
+
}
|
|
143
|
+
if (!block.repository) {
|
|
144
|
+
problems.push(`remoteEnv.surfaces["${surface}"] has no repository`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return problems;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate the `automations` block against declared surfaces.
|
|
153
|
+
* @param {object|undefined} automations The block, if present.
|
|
154
|
+
* @param {object|undefined} remoteEnv The remote-environment block.
|
|
155
|
+
* @returns {string[]} Problems found.
|
|
156
|
+
*/
|
|
157
|
+
export function validateAutomations(automations, remoteEnv) {
|
|
158
|
+
if (!automations) return [];
|
|
159
|
+
const problems = [];
|
|
160
|
+
|
|
161
|
+
for (const [name, loop] of Object.entries(automations)) {
|
|
162
|
+
if (loop.scheduler !== "github-actions") continue;
|
|
163
|
+
if (!loop.schedule) {
|
|
164
|
+
problems.push(`automations["${name}"] has no schedule`);
|
|
165
|
+
}
|
|
166
|
+
if (!loop.executionEnv) {
|
|
167
|
+
problems.push(`automations["${name}"] has no executionEnv`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!SURFACES.has(loop.executionEnv)) {
|
|
171
|
+
problems.push(
|
|
172
|
+
`automations["${name}"].executionEnv "${loop.executionEnv}" is unknown`
|
|
173
|
+
);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!remoteEnv?.surfaces?.[loop.executionEnv]?.repository) {
|
|
177
|
+
problems.push(
|
|
178
|
+
`automations["${name}"] dispatches to "${loop.executionEnv}", which is ` +
|
|
179
|
+
`not provisioned. Run /lisa:setup:remote-env ${loop.executionEnv} first.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return problems;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validate every block this plan introduced.
|
|
189
|
+
* @param {object} cfg Parsed `.lisa.config.json`.
|
|
190
|
+
* @returns {string[]} Problems found.
|
|
191
|
+
*/
|
|
192
|
+
export function validateConfig(cfg) {
|
|
193
|
+
return [
|
|
194
|
+
...validateSecrets(cfg.secrets),
|
|
195
|
+
...validateRemoteEnv(cfg.remoteEnv),
|
|
196
|
+
...validateAutomations(cfg.automations, cfg.remoteEnv),
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function main() {
|
|
201
|
+
const path = join(process.cwd(), ".lisa.config.json");
|
|
202
|
+
if (!existsSync(path)) {
|
|
203
|
+
console.log("no .lisa.config.json — nothing to validate");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const problems = validateConfig(JSON.parse(readFileSync(path, "utf8")));
|
|
207
|
+
if (!problems.length) {
|
|
208
|
+
console.log(
|
|
209
|
+
"config: secrets, remoteEnv and automations blocks are well-formed"
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const problem of problems) console.error(` ${problem}`);
|
|
214
|
+
throw new Error(`${problems.length} configuration problem(s)`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
218
|
+
try {
|
|
219
|
+
main();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error(err.message);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -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**
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs
ADDED
|
@@ -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
|
+
}
|