@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.
Files changed (212) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +139 -9
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +17 -0
  8. package/plugins/lisa/.codex-plugin/skills/lisa-automation-status/SKILL.md +23 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +33 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +17 -0
  11. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +17 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/SKILL.md +106 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +137 -51
  16. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  18. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  22. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  23. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  24. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  25. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +69 -0
  26. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  27. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +150 -0
  28. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
  29. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  30. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  32. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  33. package/plugins/lisa/commands/implement.md +5 -3
  34. package/plugins/lisa/commands/setup/remote-env.md +7 -0
  35. package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +17 -0
  36. package/plugins/lisa/skills/lisa-automation-status/SKILL.md +23 -0
  37. package/plugins/lisa/skills/lisa-doctor/SKILL.md +33 -0
  38. package/plugins/lisa/skills/lisa-implement/SKILL.md +17 -0
  39. package/plugins/lisa/skills/lisa-notion-access/SKILL.md +17 -0
  40. package/plugins/lisa/skills/lisa-remote-dispatch/SKILL.md +106 -0
  41. package/plugins/lisa/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
  42. package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  43. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +138 -52
  44. package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  45. package/plugins/lisa/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  46. package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  47. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  48. package/plugins/lisa/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  49. package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  50. package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  51. package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  52. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  53. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +69 -0
  54. package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  55. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +150 -0
  56. package/plugins/lisa/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
  57. package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  58. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  59. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  60. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  61. package/plugins/lisa-agy/commands/lisa/implement.md +5 -3
  62. package/plugins/lisa-agy/commands/lisa/setup/remote-env.md +7 -0
  63. package/plugins/lisa-agy/plugin.json +1 -1
  64. package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +17 -0
  65. package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +23 -0
  66. package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +33 -0
  67. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +17 -0
  68. package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +17 -0
  69. package/plugins/lisa-agy/skills/lisa-remote-dispatch/SKILL.md +106 -0
  70. package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  71. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +138 -52
  72. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  73. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  74. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  75. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  76. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  77. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  78. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  79. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  80. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  81. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +69 -0
  82. package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  83. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +150 -0
  84. package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  85. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  86. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  87. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  88. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  91. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-copilot/commands/lisa/implement.md +5 -3
  95. package/plugins/lisa-copilot/commands/lisa/setup/remote-env.md +7 -0
  96. package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +17 -0
  97. package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +23 -0
  98. package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +33 -0
  99. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +17 -0
  100. package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +17 -0
  101. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/SKILL.md +106 -0
  102. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  103. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +138 -52
  104. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  105. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  106. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  107. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  108. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  109. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  110. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  111. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  112. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  113. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +69 -0
  114. package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  115. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +150 -0
  116. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  117. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  118. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  119. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  120. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-cursor/commands/lisa/implement.md +5 -3
  122. package/plugins/lisa-cursor/commands/lisa/setup/remote-env.md +7 -0
  123. package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +17 -0
  124. package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +23 -0
  125. package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +33 -0
  126. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +17 -0
  127. package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +17 -0
  128. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/SKILL.md +106 -0
  129. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  130. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +138 -52
  131. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  132. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  133. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  134. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  135. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  136. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  137. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  138. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  139. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  140. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +69 -0
  141. package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  142. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +150 -0
  143. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  144. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  145. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  146. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  147. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  148. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  149. package/plugins/lisa-expo-agy/plugin.json +1 -1
  150. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  151. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  154. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  155. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  156. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  158. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  159. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  160. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  161. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  163. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  164. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  165. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  166. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  167. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  168. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  169. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  170. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  171. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  172. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  173. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  174. package/plugins/lisa-rails-agy/plugin.json +1 -1
  175. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  176. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  177. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  178. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  179. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  180. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  181. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  182. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  183. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  184. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  185. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  186. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  187. package/plugins/src/base/commands/implement.md +5 -3
  188. package/plugins/src/base/commands/setup/remote-env.md +7 -0
  189. package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +17 -0
  190. package/plugins/src/base/skills/lisa-automation-status/SKILL.md +23 -0
  191. package/plugins/src/base/skills/lisa-doctor/SKILL.md +33 -0
  192. package/plugins/src/base/skills/lisa-implement/SKILL.md +17 -0
  193. package/plugins/src/base/skills/lisa-notion-access/SKILL.md +17 -0
  194. package/plugins/src/base/skills/lisa-remote-dispatch/SKILL.md +106 -0
  195. package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  196. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +138 -52
  197. package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  198. package/plugins/src/base/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  199. package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  200. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  201. package/plugins/src/base/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  202. package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  203. package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  204. package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  205. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  206. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +69 -0
  207. package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  208. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +150 -0
  209. package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  210. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  211. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  212. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
@@ -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**
@@ -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
+ }