@codyswann/lisa 2.326.3 → 2.328.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 +55 -29
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/sync/lifecycle-defaults.d.ts +61 -0
- package/dist/sync/lifecycle-defaults.d.ts.map +1 -0
- package/dist/sync/lifecycle-defaults.js +75 -0
- package/dist/sync/lifecycle-defaults.js.map +1 -0
- package/dist/sync/registry.d.ts.map +1 -1
- package/dist/sync/registry.js +15 -23
- package/dist/sync/registry.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-confluence-to-tracker/SKILL.md +3 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +20 -9
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +76 -66
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-claim/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-evidence/SKILL.md +7 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-journey/SKILL.md +2 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-sync/SKILL.md +34 -30
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +6 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +8 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-notion-to-tracker/SKILL.md +3 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +59 -12
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-sync/SKILL.md +2 -2
- package/plugins/lisa/agents/linear-agent.md +6 -6
- package/plugins/lisa/agents/linear-build-intake.md +3 -3
- package/plugins/lisa/commands/setup/local-env.md +7 -0
- package/plugins/lisa/rules/eager/rejection-detection.md +2 -2
- package/plugins/lisa/rules/reference/config-resolution.md +37 -9
- package/plugins/lisa/rules/reference/rejection-detection.md +2 -2
- package/plugins/lisa/skills/lisa-confluence-to-tracker/SKILL.md +3 -3
- package/plugins/lisa/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-linear-access/SKILL.md +20 -9
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +77 -67
- package/plugins/lisa/skills/lisa-linear-claim/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-linear-evidence/SKILL.md +7 -7
- package/plugins/lisa/skills/lisa-linear-journey/SKILL.md +2 -2
- package/plugins/lisa/skills/lisa-linear-sync/SKILL.md +34 -30
- package/plugins/lisa/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +6 -6
- package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +8 -7
- package/plugins/lisa/skills/lisa-notion-to-tracker/SKILL.md +3 -3
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa/skills/lisa-setup-linear/SKILL.md +59 -12
- package/plugins/lisa/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa/skills/lisa-setup-local-env/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/lisa/skills/lisa-tracker-build-intake/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-tracker-sync/SKILL.md +2 -2
- package/plugins/lisa-agy/agents/linear-agent.md +6 -6
- package/plugins/lisa-agy/agents/linear-build-intake.md +3 -3
- package/plugins/lisa-agy/commands/lisa/setup/local-env.md +7 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-confluence-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-agy/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +20 -9
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +77 -67
- package/plugins/lisa-agy/skills/lisa-linear-claim/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-linear-evidence/SKILL.md +7 -7
- package/plugins/lisa-agy/skills/lisa-linear-journey/SKILL.md +2 -2
- package/plugins/lisa-agy/skills/lisa-linear-sync/SKILL.md +34 -30
- package/plugins/lisa-agy/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +6 -6
- package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +8 -7
- package/plugins/lisa-agy/skills/lisa-notion-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa-agy/skills/lisa-setup-linear/SKILL.md +59 -12
- package/plugins/lisa-agy/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa-agy/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/lisa-agy/skills/lisa-tracker-build-intake/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-tracker-sync/SKILL.md +2 -2
- 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/agents/linear-agent.agent.md +6 -6
- package/plugins/lisa-copilot/agents/linear-build-intake.agent.md +3 -3
- package/plugins/lisa-copilot/commands/lisa/setup/local-env.md +7 -0
- package/plugins/lisa-copilot/rules/eager/rejection-detection.md +2 -2
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +37 -9
- package/plugins/lisa-copilot/rules/reference/rejection-detection.md +2 -2
- package/plugins/lisa-copilot/skills/lisa-confluence-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-copilot/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa-copilot/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +20 -9
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +77 -67
- package/plugins/lisa-copilot/skills/lisa-linear-claim/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-linear-evidence/SKILL.md +7 -7
- package/plugins/lisa-copilot/skills/lisa-linear-journey/SKILL.md +2 -2
- package/plugins/lisa-copilot/skills/lisa-linear-sync/SKILL.md +34 -30
- package/plugins/lisa-copilot/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +6 -6
- package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +8 -7
- package/plugins/lisa-copilot/skills/lisa-notion-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa-copilot/skills/lisa-setup-linear/SKILL.md +59 -12
- package/plugins/lisa-copilot/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa-copilot/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/lisa-copilot/skills/lisa-tracker-build-intake/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-tracker-sync/SKILL.md +2 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/agents/linear-agent.md +6 -6
- package/plugins/lisa-cursor/agents/linear-build-intake.md +3 -3
- package/plugins/lisa-cursor/commands/lisa/setup/local-env.md +7 -0
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +37 -9
- package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +2 -2
- package/plugins/lisa-cursor/rules/rejection-detection.mdc +2 -2
- package/plugins/lisa-cursor/skills/lisa-confluence-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-cursor/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +20 -9
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +77 -67
- package/plugins/lisa-cursor/skills/lisa-linear-claim/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-linear-evidence/SKILL.md +7 -7
- package/plugins/lisa-cursor/skills/lisa-linear-journey/SKILL.md +2 -2
- package/plugins/lisa-cursor/skills/lisa-linear-sync/SKILL.md +34 -30
- package/plugins/lisa-cursor/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +6 -6
- package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +8 -7
- package/plugins/lisa-cursor/skills/lisa-notion-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa-cursor/skills/lisa-setup-linear/SKILL.md +59 -12
- package/plugins/lisa-cursor/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa-cursor/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/lisa-cursor/skills/lisa-tracker-build-intake/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-tracker-sync/SKILL.md +2 -2
- 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/agents/linear-agent.md +6 -6
- package/plugins/src/base/agents/linear-build-intake.md +3 -3
- package/plugins/src/base/commands/setup/local-env.md +7 -0
- package/plugins/src/base/rules/eager/rejection-detection.md +2 -2
- package/plugins/src/base/rules/reference/config-resolution.md +37 -9
- package/plugins/src/base/rules/reference/rejection-detection.md +2 -2
- package/plugins/src/base/skills/lisa-confluence-to-tracker/SKILL.md +3 -3
- package/plugins/src/base/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/src/base/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-linear-access/SKILL.md +20 -9
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +77 -67
- package/plugins/src/base/skills/lisa-linear-claim/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-linear-evidence/SKILL.md +7 -7
- package/plugins/src/base/skills/lisa-linear-journey/SKILL.md +2 -2
- package/plugins/src/base/skills/lisa-linear-sync/SKILL.md +34 -30
- package/plugins/src/base/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +6 -6
- package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +8 -7
- package/plugins/src/base/skills/lisa-notion-to-tracker/SKILL.md +3 -3
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/src/base/skills/lisa-setup-linear/SKILL.md +59 -12
- package/plugins/src/base/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/src/base/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/src/base/skills/lisa-tracker-build-intake/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-tracker-sync/SKILL.md +2 -2
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bring a developer's machine in line with the toolchain the project declares.
|
|
4
|
+
*
|
|
5
|
+
* The manifest, the planner, the pins and the installers are the remote flow's,
|
|
6
|
+
* unchanged and imported — this is the surface that was missing, not a second
|
|
7
|
+
* implementation. `remoteEnv.tools` was always surface-aware and the executor
|
|
8
|
+
* always understood `local`; the only way to reach either was to run a file
|
|
9
|
+
* called `setup-remote-env.mjs` out of a directory called `lisa-remote-env`,
|
|
10
|
+
* which nobody looking for local setup ever found.
|
|
11
|
+
*
|
|
12
|
+
* Two things differ from the remote flow, and both are because a person is here:
|
|
13
|
+
*
|
|
14
|
+
* consent — a container provisions itself silently because it is disposable
|
|
15
|
+
* and nobody is watching. A laptop belongs to someone, so nothing
|
|
16
|
+
* is installed until they ask with --install-tools.
|
|
17
|
+
*
|
|
18
|
+
* tolerance — a container that cannot be fully provisioned should fail before
|
|
19
|
+
* it half-runs, because there is no one to read the report. A
|
|
20
|
+
* developer can act on a list, so an unprovisionable tool is
|
|
21
|
+
* reported, everything else is still installed, and the exit code
|
|
22
|
+
* carries the failure.
|
|
23
|
+
* @module local-env
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { dirname, resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29
|
+
|
|
30
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a script belonging to a sibling skill.
|
|
34
|
+
*
|
|
35
|
+
* Skills are always siblings under one `skills/` directory, in every install
|
|
36
|
+
* layout, so this holds for a plugin install, a checkout, and node_modules
|
|
37
|
+
* alike. Failing loudly here beats reimplementing what the sibling owns.
|
|
38
|
+
* @param {string} skill Sibling skill slug.
|
|
39
|
+
* @param {string} script Script filename.
|
|
40
|
+
* @returns {string} Absolute path.
|
|
41
|
+
*/
|
|
42
|
+
function siblingScript(skill, script) {
|
|
43
|
+
const path = resolve(HERE, "..", "..", skill, "scripts", script);
|
|
44
|
+
if (!existsSync(path)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`cannot find ${skill}/scripts/${script} beside this skill.\n` +
|
|
47
|
+
`Local setup composes with that skill rather than reimplementing it: ` +
|
|
48
|
+
`the pins, checksums and installers must be the same ones the remote ` +
|
|
49
|
+
`surface uses, or the two drift and only one gets fixed.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load the pieces the remote flow owns.
|
|
57
|
+
* @returns {Promise<object>} The imported helpers.
|
|
58
|
+
*/
|
|
59
|
+
async function loadRemoteEnv() {
|
|
60
|
+
const setup = await import(
|
|
61
|
+
pathToFileURL(
|
|
62
|
+
siblingScript("lisa-setup-remote-env", "setup-remote-env.mjs")
|
|
63
|
+
).href
|
|
64
|
+
);
|
|
65
|
+
const toolchain = await import(
|
|
66
|
+
pathToFileURL(siblingScript("lisa-setup-remote-env", "toolchain.mjs")).href
|
|
67
|
+
);
|
|
68
|
+
return { ...toolchain, ...setup };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Describe one blocked decision in terms a person can act on.
|
|
73
|
+
*
|
|
74
|
+
* A blocked step on a laptop is usually one of two things, and they are fixed
|
|
75
|
+
* differently: a tool the project requires and the machine lacks (install it),
|
|
76
|
+
* or a tool with no artifact pinned for this platform (add the pin, or install
|
|
77
|
+
* it however this platform normally would). Printing the planner's reason alone
|
|
78
|
+
* left the reader to work out which.
|
|
79
|
+
* @param {object} step A plan step.
|
|
80
|
+
* @param {string} platform The platform key that was resolved against.
|
|
81
|
+
* @returns {string} A rendered block.
|
|
82
|
+
*/
|
|
83
|
+
function explainBlocked(step, platform) {
|
|
84
|
+
if (step.action === "invalid" && step.reason.includes("no pin for")) {
|
|
85
|
+
return (
|
|
86
|
+
` ${step.name} — declared, but no artifact is pinned for ${platform}.\n` +
|
|
87
|
+
` Lisa will not guess a download URL, because a guessed artifact is ` +
|
|
88
|
+
`one the checksum cannot vouch for.\n` +
|
|
89
|
+
` Add a "${platform}" block under this tool's "platforms" map, or ` +
|
|
90
|
+
`install it yourself.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return ` ${step.name} — ${step.reason.split("\n")[0]}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Report and optionally apply the local toolchain.
|
|
98
|
+
* @param {object} argv Parsed arguments.
|
|
99
|
+
* @returns {Promise<number>} Process exit code.
|
|
100
|
+
*/
|
|
101
|
+
export async function run(argv) {
|
|
102
|
+
const env = await loadRemoteEnv();
|
|
103
|
+
// Both seams exist for tests, and both default to this machine. A platform
|
|
104
|
+
// that can only be exercised by running on it is one CI will never cover,
|
|
105
|
+
// which is how a resolution bug ships to the platform nobody tested.
|
|
106
|
+
const platform = argv.platform ?? env.currentPlatform();
|
|
107
|
+
const { tools } = env.readRemoteEnvConfig(argv.cwd);
|
|
108
|
+
|
|
109
|
+
const declared = (tools.require ?? []).length + (tools.install ?? []).length;
|
|
110
|
+
if (declared === 0) {
|
|
111
|
+
console.log(
|
|
112
|
+
"No tools are declared in remoteEnv.tools, so there is nothing to check.\n" +
|
|
113
|
+
"Run /lisa:detect-tooling — it reads npm scripts, MCP servers, " +
|
|
114
|
+
"credential\nnotes and quality config, and proposes pinned entries for " +
|
|
115
|
+
"what it finds."
|
|
116
|
+
);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const plan = env.planToolchain(tools, env.probe, "local", platform);
|
|
121
|
+
const blocked = plan.filter(
|
|
122
|
+
step => step.action === "missing" || step.action === "invalid"
|
|
123
|
+
);
|
|
124
|
+
const installable = plan.filter(step => step.action === "install");
|
|
125
|
+
|
|
126
|
+
if (argv.json) {
|
|
127
|
+
console.log(JSON.stringify({ platform, plan }, null, 2));
|
|
128
|
+
return blocked.length ? 1 : 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`Local toolchain for ${platform}\n`);
|
|
132
|
+
for (const step of plan) {
|
|
133
|
+
console.log(` ${step.action.padEnd(8)} ${step.reason.split("\n")[0]}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (installable.length && !argv.installTools) {
|
|
137
|
+
console.log(
|
|
138
|
+
`\n${installable.length} declared tool(s) can be installed here.\n` +
|
|
139
|
+
`Nothing has been installed: this machine is yours, not a container.\n` +
|
|
140
|
+
`Re-run with --install-tools to provision them into ~/.local/bin.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (installable.length && argv.installTools) {
|
|
145
|
+
const binDir = env.ensureBinDir();
|
|
146
|
+
console.log(`\nInstalling into ${binDir}`);
|
|
147
|
+
for (const step of installable) {
|
|
148
|
+
console.log(` ${step.name}`);
|
|
149
|
+
if (!argv.dryRun) env.installTool(step.tool, binDir);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (blocked.length) {
|
|
154
|
+
console.log(`\n${blocked.length} tool(s) this command cannot resolve:\n`);
|
|
155
|
+
for (const step of blocked) console.log(explainBlocked(step, platform));
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
163
|
+
const argv = {
|
|
164
|
+
installTools: process.argv.includes("--install-tools"),
|
|
165
|
+
dryRun: process.argv.includes("--dry-run"),
|
|
166
|
+
json: process.argv.includes("--json"),
|
|
167
|
+
};
|
|
168
|
+
run(argv)
|
|
169
|
+
.then(code => process.exit(code))
|
|
170
|
+
.catch(err => {
|
|
171
|
+
console.error(err.message);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -95,6 +95,19 @@ This repository has paid for that twice: `gh` was declared nowhere and a cloud s
|
|
|
95
95
|
|
|
96
96
|
The detector proposes and a human decides. It writes nothing, so provisioning still only ever happens from a reviewed, pinned, checksummed entry.
|
|
97
97
|
|
|
98
|
+
## The same manifest provisions a laptop
|
|
99
|
+
|
|
100
|
+
`remoteEnv.tools` is not a remote-only manifest, and `/lisa:setup:local-env` is how a developer
|
|
101
|
+
applies it to their own machine — same pins, same checksums, same installers, differing only in
|
|
102
|
+
that nothing installs without `--install-tools` and that a newer tool already on `PATH` is left
|
|
103
|
+
alone rather than downgraded.
|
|
104
|
+
|
|
105
|
+
A downloaded tool declares a `platforms` map keyed `<platform>-<arch>`, each block carrying its
|
|
106
|
+
own `install` method, `url`, and `sha256`, because the artifact and the archive kind both vary
|
|
107
|
+
by platform. Before that existed, the only way to keep a Linux binary off a laptop was
|
|
108
|
+
`surfaces: ["remote"]`, which achieved it by making the tool uninstallable there — so `bws` and
|
|
109
|
+
`gh` were required on developer machines and provisionable only in containers.
|
|
110
|
+
|
|
98
111
|
## Toolchain manifest — two entry kinds
|
|
99
112
|
|
|
100
113
|
```json
|
|
@@ -108,20 +121,23 @@ The detector proposes and a human decides. It writes nothing, so provisioning st
|
|
|
108
121
|
{ "name": "unzip" }
|
|
109
122
|
],
|
|
110
123
|
"install": [
|
|
111
|
-
{
|
|
112
|
-
"name": "bws",
|
|
113
|
-
"version": "2.1.0",
|
|
114
|
-
"install": "release-zip",
|
|
115
|
-
"url": "https://<vendor>/releases/download/bws-v2.1.0/bws-<platform>-2.1.0.zip",
|
|
116
|
-
"sha256": "<sha256 published with that exact release>"
|
|
117
|
-
},
|
|
118
124
|
{
|
|
119
125
|
"name": "gh",
|
|
120
126
|
"version": "2.83.0",
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
"platforms": {
|
|
128
|
+
"linux-x64": {
|
|
129
|
+
"install": "release-tar",
|
|
130
|
+
"url": "https://<vendor>/releases/download/v2.83.0/gh_2.83.0_linux_amd64.tar.gz",
|
|
131
|
+
"sha256": "<sha256 published with that exact release>",
|
|
132
|
+
"binary": "gh_2.83.0_linux_amd64/bin/gh"
|
|
133
|
+
},
|
|
134
|
+
"darwin-arm64": {
|
|
135
|
+
"install": "release-zip",
|
|
136
|
+
"url": "https://<vendor>/releases/download/v2.83.0/gh_2.83.0_macOS_arm64.zip",
|
|
137
|
+
"sha256": "<sha256 published with that exact release>",
|
|
138
|
+
"binary": "gh_2.83.0_macOS_arm64/bin/gh"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
125
141
|
},
|
|
126
142
|
{
|
|
127
143
|
"name": "codex",
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { execFileSync } from "node:child_process";
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
32
33
|
import {
|
|
33
34
|
chmodSync,
|
|
34
35
|
existsSync,
|
|
@@ -37,7 +38,7 @@ import {
|
|
|
37
38
|
rmSync,
|
|
38
39
|
writeFileSync,
|
|
39
40
|
} from "node:fs";
|
|
40
|
-
import {
|
|
41
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
41
42
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
43
|
|
|
43
44
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
@@ -122,6 +123,34 @@ export function probe(name, exec = execFileSync) {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Refuse an archive whose contents are not exactly what was pinned.
|
|
128
|
+
*
|
|
129
|
+
* Hashed in-process rather than by shelling out. `sha256sum` is a GNU coreutils
|
|
130
|
+
* program: it is not present on a stock macOS, where the equivalent is
|
|
131
|
+
* `shasum -a 256`, and it was never in the manifest's `require` list either — so
|
|
132
|
+
* the verification step depended on a tool nothing asserted, on every surface.
|
|
133
|
+
* Doing it here removes the dependency instead of adding a second name to probe
|
|
134
|
+
* for, and the check can then be tested without a real archive or a real binary.
|
|
135
|
+
* @param {string} archive Path to the downloaded file.
|
|
136
|
+
* @param {string} expected Pinned lowercase hex digest.
|
|
137
|
+
* @param {string} name Tool name, for the message.
|
|
138
|
+
*/
|
|
139
|
+
export function verifyChecksum(archive, expected, name) {
|
|
140
|
+
const actual = createHash("sha256")
|
|
141
|
+
.update(readFileSync(archive))
|
|
142
|
+
.digest("hex");
|
|
143
|
+
if (actual !== String(expected).trim().toLowerCase()) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`${name}: checksum mismatch — refusing to install.\n` +
|
|
146
|
+
` expected ${expected}\n` +
|
|
147
|
+
` actual ${actual}\n` +
|
|
148
|
+
`The URL served something other than the reviewed artifact. Do not ` +
|
|
149
|
+
`update the pin to match without establishing why it changed.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
125
154
|
/**
|
|
126
155
|
* Install a pinned archive, refusing anything whose checksum does not match.
|
|
127
156
|
* @param {object} tool Manifest entry.
|
|
@@ -137,10 +166,7 @@ function installReleaseZip(tool, binDir) {
|
|
|
137
166
|
});
|
|
138
167
|
// Verify before unpacking, not after. An unexpected archive must fail
|
|
139
168
|
// before any of its contents reach a directory that is on PATH.
|
|
140
|
-
|
|
141
|
-
input: `${tool.sha256} ${archive}\n`,
|
|
142
|
-
stdio: ["pipe", "ignore", "inherit"],
|
|
143
|
-
});
|
|
169
|
+
verifyChecksum(archive, tool.sha256, tool.name);
|
|
144
170
|
execFileSync("unzip", ["-q", "-o", archive, "-d", temporary], {
|
|
145
171
|
stdio: "inherit",
|
|
146
172
|
});
|
|
@@ -171,10 +197,7 @@ function installReleaseTar(tool, binDir) {
|
|
|
171
197
|
execFileSync("curl", ["-fsSL", tool.url, "-o", archive], {
|
|
172
198
|
stdio: "inherit",
|
|
173
199
|
});
|
|
174
|
-
|
|
175
|
-
input: `${tool.sha256} ${archive}\n`,
|
|
176
|
-
stdio: ["pipe", "ignore", "inherit"],
|
|
177
|
-
});
|
|
200
|
+
verifyChecksum(archive, tool.sha256, tool.name);
|
|
178
201
|
execFileSync("tar", ["-xzf", archive, "-C", temporary], {
|
|
179
202
|
stdio: "inherit",
|
|
180
203
|
});
|
|
@@ -205,16 +228,46 @@ function installNpmGlobal(tool) {
|
|
|
205
228
|
|
|
206
229
|
/**
|
|
207
230
|
* Execute one install decision.
|
|
208
|
-
*
|
|
231
|
+
*
|
|
232
|
+
* Exported because the local-environment flow installs from the same manifest
|
|
233
|
+
* with the same pins and the same checksum refusal. A second installer would be
|
|
234
|
+
* a second thing to keep honest, and the one people run least would rot.
|
|
235
|
+
* @param {object} tool Manifest entry, already resolved to this platform.
|
|
209
236
|
* @param {string} binDir Directory for downloaded binaries.
|
|
210
237
|
*/
|
|
211
|
-
function installTool(tool, binDir) {
|
|
238
|
+
export function installTool(tool, binDir) {
|
|
212
239
|
assertPinned(tool);
|
|
213
240
|
if (tool.install === "release-zip") installReleaseZip(tool, binDir);
|
|
214
241
|
else if (tool.install === "release-tar") installReleaseTar(tool, binDir);
|
|
215
242
|
else installNpmGlobal(tool);
|
|
216
243
|
}
|
|
217
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Create the directory installs land in, and make sure PATH will find it.
|
|
247
|
+
*
|
|
248
|
+
* Installing a binary somewhere nothing looks is the same as not installing it.
|
|
249
|
+
* `~/.local/bin` is on PATH by default on a developer workstation and is NOT on
|
|
250
|
+
* a minimal container, so the toolchain step reported `install bws`, the file
|
|
251
|
+
* landed at ~/.local/bin/bws mode 755, and the very next step died with
|
|
252
|
+
* `spawnSync bws ENOENT`.
|
|
253
|
+
*
|
|
254
|
+
* This was invisible in every local test because a workstation shell already
|
|
255
|
+
* exports the directory — the environment doing the hiding was the one used to
|
|
256
|
+
* verify the fix.
|
|
257
|
+
*
|
|
258
|
+
* Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
259
|
+
* whatever an image happens to ship under the same name.
|
|
260
|
+
* @returns {string} The directory installs are written to.
|
|
261
|
+
*/
|
|
262
|
+
export function ensureBinDir() {
|
|
263
|
+
const binDir = join(process.env.HOME ?? "", ".local", "bin");
|
|
264
|
+
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
265
|
+
if (!pathContains(binDir)) {
|
|
266
|
+
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
267
|
+
}
|
|
268
|
+
return binDir;
|
|
269
|
+
}
|
|
270
|
+
|
|
218
271
|
/**
|
|
219
272
|
* Apply the toolchain plan, reporting every decision.
|
|
220
273
|
* @param {object} tools Manifest.
|
|
@@ -231,8 +284,7 @@ function applyToolchain(tools, dryRun, options = {}) {
|
|
|
231
284
|
throw new Error(blocked.map(p => p.reason).join("\n\n"));
|
|
232
285
|
}
|
|
233
286
|
|
|
234
|
-
const binDir =
|
|
235
|
-
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
287
|
+
const binDir = ensureBinDir();
|
|
236
288
|
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
237
289
|
|
|
238
290
|
// Installing is a different act on a laptop than in a container. A container
|
|
@@ -263,26 +315,14 @@ function applyToolchain(tools, dryRun, options = {}) {
|
|
|
263
315
|
return plan;
|
|
264
316
|
}
|
|
265
317
|
|
|
266
|
-
// Installing a binary somewhere nothing looks is the same as not installing
|
|
267
|
-
// it. `~/.local/bin` is on PATH by default on a developer workstation and is
|
|
268
|
-
// NOT on a minimal container, so the toolchain step reported `install bws`,
|
|
269
|
-
// the file landed at ~/.local/bin/bws mode 755, and the very next step died
|
|
270
|
-
// with `spawnSync bws ENOENT`.
|
|
271
|
-
//
|
|
272
|
-
// This was invisible in every local test because a workstation shell already
|
|
273
|
-
// exports the directory — the environment doing the hiding was the one used
|
|
274
|
-
// to verify the fix.
|
|
275
|
-
//
|
|
276
|
-
// Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
277
|
-
// whatever an image happens to ship under the same name.
|
|
278
|
-
if (!pathContains(binDir)) {
|
|
279
|
-
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
318
|
for (const step of plan) {
|
|
283
319
|
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
320
|
+
// The planner already resolved this entry to the running platform, so use
|
|
321
|
+
// what it decided rather than looking the raw entry up again. `byName` is
|
|
322
|
+
// the pre-platform shape and would hand the installer a url that belongs to
|
|
323
|
+
// whichever platform happened to be written first.
|
|
284
324
|
if (step.action === "install" && !dryRun)
|
|
285
|
-
installTool(byName.get(step.name), binDir);
|
|
325
|
+
installTool(step.tool ?? byName.get(step.name), binDir);
|
|
286
326
|
}
|
|
287
327
|
return plan;
|
|
288
328
|
}
|
|
@@ -100,7 +100,7 @@ function planRequired(tool, found) {
|
|
|
100
100
|
* @param {{version: string|null, present: boolean}} found Probe result.
|
|
101
101
|
* @returns {{name: string, action: string, reason: string}} The decision.
|
|
102
102
|
*/
|
|
103
|
-
function planInstallable(tool, found) {
|
|
103
|
+
function planInstallable(tool, found, pinIsFloor = false) {
|
|
104
104
|
if (!tool.version) {
|
|
105
105
|
return {
|
|
106
106
|
name: tool.name,
|
|
@@ -115,6 +115,25 @@ function planInstallable(tool, found) {
|
|
|
115
115
|
reason: `${tool.name} ${tool.version} already installed`,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
// On a laptop the pin is a floor, not an equality. A container is disposable
|
|
119
|
+
// and reproducible by construction, so an exact match is right there. A
|
|
120
|
+
// developer's machine is shared with every other project they work on, and
|
|
121
|
+
// installing a pinned binary into ~/.local/bin ahead of a NEWER one already on
|
|
122
|
+
// PATH is a downgrade this project imposed on all of them — for gh, pinned at
|
|
123
|
+
// 2.83.0 against a workstation running 2.96.0, that is the likely case rather
|
|
124
|
+
// than the exotic one.
|
|
125
|
+
if (
|
|
126
|
+
pinIsFloor &&
|
|
127
|
+
found.present &&
|
|
128
|
+
found.version &&
|
|
129
|
+
compareVersions(found.version, tool.version) > 0
|
|
130
|
+
) {
|
|
131
|
+
return {
|
|
132
|
+
name: tool.name,
|
|
133
|
+
action: "newer",
|
|
134
|
+
reason: `${tool.name} ${found.version} is newer than the pinned ${tool.version} — leaving it alone`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
118
137
|
return {
|
|
119
138
|
name: tool.name,
|
|
120
139
|
action: "install",
|
|
@@ -127,6 +146,72 @@ function planInstallable(tool, found) {
|
|
|
127
146
|
/** Surfaces a manifest entry may name. */
|
|
128
147
|
const KNOWN_SURFACES = new Set(["local", "remote"]);
|
|
129
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The platform key a manifest entry is resolved against.
|
|
151
|
+
*
|
|
152
|
+
* `<platform>-<arch>` rather than either alone, because both halves change the
|
|
153
|
+
* artifact: an Apple Silicon laptop and an Intel one run different builds of the
|
|
154
|
+
* same release, and so do an arm64 container and an amd64 one.
|
|
155
|
+
* @param {{platform: string, arch: string}} [runtime] Injectable, for tests.
|
|
156
|
+
* @returns {string} A key such as "darwin-arm64" or "linux-x64".
|
|
157
|
+
*/
|
|
158
|
+
export function currentPlatform(runtime = process) {
|
|
159
|
+
return `${runtime.platform}-${runtime.arch}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Collapse a manifest entry to the artifact for one platform.
|
|
164
|
+
*
|
|
165
|
+
* A download URL is platform-specific and a checksum doubly so, which the
|
|
166
|
+
* single-URL shape could not express: the only way to stop a laptop being handed
|
|
167
|
+
* a Linux binary was `surfaces: ["remote"]`, which bought that safety by making
|
|
168
|
+
* the tool uninstallable on the laptop entirely. So `bws` and `gh` — the two
|
|
169
|
+
* tools Lisa's own guardrails shell out to — were declared, required, and
|
|
170
|
+
* unprovisionable on the machine most likely to be missing them.
|
|
171
|
+
*
|
|
172
|
+
* A `platforms` map fixes the cause instead of the symptom. `install` lives
|
|
173
|
+
* inside each block rather than beside it, because the method varies too: gh
|
|
174
|
+
* publishes a .tar.gz for Linux and a .zip for macOS, so a single install method
|
|
175
|
+
* would have forced one platform onto an archive kind its vendor does not ship.
|
|
176
|
+
*
|
|
177
|
+
* A flat entry is still valid and means "identical everywhere" — true of every
|
|
178
|
+
* `npm-global` install, which is genuinely platform-independent.
|
|
179
|
+
* @param {object} tool Manifest entry.
|
|
180
|
+
* @param {string} [platform] Platform key to resolve for.
|
|
181
|
+
* @returns {object} The entry with its platform block merged in.
|
|
182
|
+
*/
|
|
183
|
+
export function resolvePlatform(tool, platform = currentPlatform()) {
|
|
184
|
+
const { platforms } = tool;
|
|
185
|
+
if (platforms === undefined) return tool;
|
|
186
|
+
if (
|
|
187
|
+
typeof platforms !== "object" ||
|
|
188
|
+
platforms === null ||
|
|
189
|
+
Array.isArray(platforms)
|
|
190
|
+
) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`${tool.name}: platforms must be an object keyed by <platform>-<arch>, ` +
|
|
193
|
+
`got ${Array.isArray(platforms) ? "an array" : typeof platforms}.\n` +
|
|
194
|
+
`Omit it when one artifact serves every platform.`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const block = platforms[platform];
|
|
198
|
+
if (!block) {
|
|
199
|
+
const known = Object.keys(platforms).sort().join(", ");
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${tool.name}: no pin for ${platform}.\n` +
|
|
202
|
+
`Declared platforms: ${known || "(none)"}.\n` +
|
|
203
|
+
`Add a block for ${platform} with its own url and sha256, or drop the ` +
|
|
204
|
+
`tool from this surface. Guessing an artifact would defeat the ` +
|
|
205
|
+
`checksum.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
// The platform block wins over the shared fields, and `platforms` itself is
|
|
209
|
+
// dropped so a resolved entry is indistinguishable from a flat one — that is
|
|
210
|
+
// what lets assertPinned and the installers stay unaware of any of this.
|
|
211
|
+
const { platforms: _discarded, ...shared } = tool;
|
|
212
|
+
return { ...shared, ...block };
|
|
213
|
+
}
|
|
214
|
+
|
|
130
215
|
/**
|
|
131
216
|
* Whether a manifest entry applies to the surface being provisioned.
|
|
132
217
|
*
|
|
@@ -171,18 +256,48 @@ export function appliesToSurface(tool, surface) {
|
|
|
171
256
|
|
|
172
257
|
/**
|
|
173
258
|
* Produce the complete plan for a toolchain manifest.
|
|
259
|
+
*
|
|
260
|
+
* Install entries are resolved to the running platform here rather than at
|
|
261
|
+
* install time, so a tool with no artifact for this machine is reported
|
|
262
|
+
* alongside every other problem instead of aborting the run at the first one.
|
|
263
|
+
* An operator fixing a manifest wants the whole list.
|
|
264
|
+
*
|
|
265
|
+
* `require` entries are deliberately not resolved: they carry a name and a
|
|
266
|
+
* minimum version, nothing platform-specific, and inventing a per-platform shape
|
|
267
|
+
* for them would be ceremony with no artifact behind it.
|
|
174
268
|
* @param {{require?: object[], install?: object[]}} tools Manifest.
|
|
175
269
|
* @param {(name: string) => {version: string|null, present: boolean}} probe Version probe.
|
|
176
|
-
* @
|
|
270
|
+
* @param {string} [surface] Surface being provisioned.
|
|
271
|
+
* @param {string} [platform] Platform key to resolve install entries against.
|
|
272
|
+
* @returns {Array<{name: string, action: string, reason: string, tool?: object}>} Ordered decisions.
|
|
177
273
|
*/
|
|
178
|
-
export function planToolchain(
|
|
274
|
+
export function planToolchain(
|
|
275
|
+
tools,
|
|
276
|
+
probe,
|
|
277
|
+
surface = "remote",
|
|
278
|
+
platform = currentPlatform()
|
|
279
|
+
) {
|
|
179
280
|
const plan = [];
|
|
180
281
|
for (const tool of tools.require ?? [])
|
|
181
282
|
if (appliesToSurface(tool, surface))
|
|
182
283
|
plan.push(planRequired(tool, probe(tool.name)));
|
|
183
|
-
for (const tool of tools.install ?? [])
|
|
184
|
-
if (appliesToSurface(tool, surface))
|
|
185
|
-
|
|
284
|
+
for (const tool of tools.install ?? []) {
|
|
285
|
+
if (!appliesToSurface(tool, surface)) continue;
|
|
286
|
+
let resolved;
|
|
287
|
+
try {
|
|
288
|
+
resolved = resolvePlatform(tool, platform);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
plan.push({ name: tool.name, action: "invalid", reason: err.message });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// The resolved entry travels with the decision so the installer never has to
|
|
294
|
+
// resolve a second time — two resolutions are two chances to disagree, and
|
|
295
|
+
// the one that installs would be the one nothing tested.
|
|
296
|
+
plan.push({
|
|
297
|
+
...planInstallable(resolved, probe(tool.name), surface === "local"),
|
|
298
|
+
tool: resolved,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
186
301
|
return plan;
|
|
187
302
|
}
|
|
188
303
|
|
|
@@ -195,6 +310,17 @@ export function planToolchain(tools, probe, surface = "remote") {
|
|
|
195
310
|
* @param {object} tool Manifest entry.
|
|
196
311
|
*/
|
|
197
312
|
export function assertPinned(tool) {
|
|
313
|
+
// An unresolved entry reaching here means a caller skipped resolvePlatform and
|
|
314
|
+
// is about to read a url and sha256 that belong to no platform in particular.
|
|
315
|
+
// Refusing is the point: the failure this whole change exists to prevent is
|
|
316
|
+
// exactly "downloaded the wrong platform's artifact", and a silent pass here
|
|
317
|
+
// would reintroduce it one call site at a time.
|
|
318
|
+
if (tool.platforms !== undefined) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`${tool.name}: platform-specific entry was not resolved before install.\n` +
|
|
321
|
+
`Call resolvePlatform() first — the shared fields alone do not name an artifact.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
198
324
|
// Both archive kinds carry the same obligation, and differ only in how they
|
|
199
325
|
// are unpacked. gh, for one, publishes no zip for Linux at all — only .deb,
|
|
200
326
|
// .rpm and .tar.gz — so a zip-only installer could not pin the CLI that
|
|
@@ -21,7 +21,7 @@ See the `config-resolution` rule for configuration and dispatch table.
|
|
|
21
21
|
- Anything else → stop and report `"Unknown tracker '<value>' in .lisa.config.json. Expected 'jira', 'github', or 'linear'."`
|
|
22
22
|
3. Pass through the output.
|
|
23
23
|
|
|
24
|
-
`$ARGUMENTS` is forwarded verbatim, including the optional `--rollup` flag (see "Parent status rollup" below), `--update-label
|
|
24
|
+
`$ARGUMENTS` is forwarded verbatim, including the optional `--rollup` flag (see "Parent status rollup" below), the vendor lane-write flag (`--update-label` on GitHub, `--update-state` on Linear), `pr_url=<url>`, and `merge_sha=<sha>`. The shim never interprets these — the vendor skill does.
|
|
25
25
|
|
|
26
26
|
If `$ARGUMENTS` is empty, all vendor skills auto-detect a ticket reference from the active plan file (most recently modified `.md` in `plans/`).
|
|
27
27
|
|
|
@@ -60,6 +60,6 @@ This is the reverse half of `lisa-git-submit-pr`'s PR body linkage. A PR that me
|
|
|
60
60
|
## Rules
|
|
61
61
|
|
|
62
62
|
- Idempotent updates — running sync at the same milestone twice should not produce duplicate comments. Vendor skills enforce this.
|
|
63
|
-
- Never auto-transition the
|
|
63
|
+
- Never auto-transition a lane the caller did not ask for. On GitHub the canonical signal is the `status:*` label, written only with `--update-label`; on Linear it is the native workflow state, written only with `--update-state`. Without the flag, every vendor arm only suggests.
|
|
64
64
|
- Parent rollup derives state from children per the `leaf-only-lifecycle` rule; it never sets a parent to `ready` and never resolves a dev/staging `done` in this single-environment repo.
|
|
65
65
|
- Pull request backlinks are mandatory when `pr_url=<url>` is present: native first, managed-comment fallback, never silently dropped.
|
|
@@ -47,7 +47,7 @@ Use the `linear-verify` skill to check the item against organizational standards
|
|
|
47
47
|
|
|
48
48
|
**Gating behavior — this is the one place auto-transitioning is allowed:**
|
|
49
49
|
|
|
50
|
-
Resolve build labels from `.lisa.config.json` `linear.
|
|
50
|
+
Resolve build labels from `.lisa.config.json` `linear.workflow.*` (defaults: `Todo` / `In Progress` / `In Review`); resolve the `blocked` label from the same section (`linear.workflow.blocked`, default `Blocked`) and the `human_needed` marker label from the same section (`linear.labels.build.human_needed`, default `human-needed`).
|
|
51
51
|
|
|
52
52
|
If `linear-verify` returns `FAIL` on any of the above, do NOT continue to build. **Draft the missing spec content first, then block for confirmation** — never bounce a raw "go write all this" checklist back to the creator:
|
|
53
53
|
1. **Best-effort autofill (before blocking).** Run `pre-flight-autofill` for every work item. Preserve a human bare configured key or `Confirmed: <env>`; automation writes `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, `Assumption: <env> — remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces the annotation with the bare key or `Confirmed: <env>`. For a legacy bare value, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence stops for confirmation. Resolve one exact `deploy.branches` key from the human-authored title, body, and reproduction steps or URL hostname; exclude the complete `Target Backend Environment` section and other machine-authored metadata/draft blocks so annotations cannot become evidence. A reported bug environment is an example, not a restriction. Evidence supersedes only `Assumption:`. Normalize `prod` ↔ `production` only when exactly one is configured; no other aliases exist. Conflicts stop; never infer from arbitrary branch text, URL paths/query strings, or substrings. Write through `linear-write-issue`, never overwrite human prose, then re-run `linear-verify`.
|
|
@@ -116,13 +116,13 @@ Use the `linear-evidence` skill to:
|
|
|
116
116
|
|
|
117
117
|
### 8. Suggest Status Transition
|
|
118
118
|
|
|
119
|
-
Based on the milestone, suggest (but don't auto-transition the native Linear `state`). Label role names are resolved from `.lisa.config.json` `linear.
|
|
119
|
+
Based on the milestone, suggest (but don't auto-transition the native Linear `state`). Label role names are resolved from `.lisa.config.json` `linear.workflow.*`:
|
|
120
120
|
|
|
121
121
|
| Milestone | Suggested role | Default label |
|
|
122
122
|
|-----------|----------------|---------------|
|
|
123
|
-
| Plan created | `claimed` | `
|
|
124
|
-
| PR ready | `review` | `
|
|
125
|
-
| PR merged | `done` (env-aware; build-intake performs if dispatched via that flow) | env-keyed variant per `linear.
|
|
123
|
+
| Plan created | `claimed` | `In Progress` |
|
|
124
|
+
| PR ready | `review` | `In Review` |
|
|
125
|
+
| PR merged | `done` (env-aware; build-intake performs if dispatched via that flow) | env-keyed variant per `linear.workflow.done` |
|
|
126
126
|
|
|
127
127
|
Note: `done` may be a string or an env-keyed map (`{ dev, staging, production }`). When suggesting the PR-merged transition, the env is implied by the PR's base branch via `deploy.branches` — surface the resolved label name; do not auto-transition.
|
|
128
128
|
|
|
@@ -132,7 +132,7 @@ The label transitions ARE the canonical signal. The native `state` field stays a
|
|
|
132
132
|
|
|
133
133
|
- Never auto-transition the native Linear `state`, with one explicit exception: when `linear-verify` returns `FAIL` for the pre-flight gate (Step 2), first run the `pre-flight-autofill` draft-then-block procedure (draft the authorable missing sections into the description as labeled assumptions), then update labels to the configured `blocked` label, add the configured `human_needed` marker label (`linear.labels.build.human_needed`, default `human-needed`), and reassign to the creator with a confirmation comment. Every other status change remains a label-driven suggestion.
|
|
134
134
|
- Always read the full item graph via `linear-read-issue` before determining intent — don't rely on type labels alone.
|
|
135
|
-
- Never create or materially edit an item by calling MCP write tools directly — always delegate to `linear-write-issue` so relationships, Gherkin criteria, and metadata gates are enforced. Two explicit exceptions are permitted: (1) the Step 2 pre-flight failure path (when `linear-verify` returns `FAIL`) may call `lisa-linear-access operation: save-issue` and `lisa-linear-access operation: save-comment` directly to set `
|
|
135
|
+
- Never create or materially edit an item by calling MCP write tools directly — always delegate to `linear-write-issue` so relationships, Gherkin criteria, and metadata gates are enforced. Two explicit exceptions are permitted: (1) the Step 2 pre-flight failure path (when `linear-verify` returns `FAIL`) may call `lisa-linear-access operation: save-issue` and `lisa-linear-access operation: save-comment` directly to set `Blocked`, add the configured `human_needed` marker label, and reassign to the creator — this narrow exception is already granted by the rule above; (2) the Step 3 triage path may call `lisa-linear-access operation: save-comment` to post triage findings and `lisa-linear-access operation: save-issue` to add the `claude-triaged-{repo}` label — these are lightweight metadata updates that do not create or materially edit ticket content and therefore do not need to route through `linear-write-issue`.
|
|
136
136
|
- If sign-in credentials are in the item, extract and pass them to the flow. If the item touches an authenticated surface and credentials are missing, that is a Step 2 failure — block and reassign rather than guessing.
|
|
137
137
|
- If the item has a Validation Journey section, pass it to the verifier agent. The Validation Journey's local-verification step must point at the target backend environment named in the description.
|
|
138
138
|
- The environment handoff for every work item uses the same durable grammar: human bare configured key or `Confirmed: <env>`; automated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, `Assumption: <env> — remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. For legacy bare values, managed draft markers and current ticket content decide provenance; provider edit history is not required. A marker proves automation; otherwise unknown provenance plus conflicting evidence stops for confirmation. Human-confirmed wins, then validated `Inferred:`; otherwise one exact `deploy.branches` key from the human-authored title, body, and reproduction steps or URL hostname drives the implementation base branch. Exclude the complete `Target Backend Environment` section and other machine-authored metadata/draft blocks so annotations cannot become evidence. Evidence supersedes only `Assumption:`. Normalize `prod` ↔ `production` only when exactly one is configured; no other aliases exist. Conflicts stop. Never infer from arbitrary branch text, URL paths/query strings, or substrings. With no signal, use the remote default and record the applicable assumption without blocking on a non-unique reverse-map. Require any selected mapping and remote branch. A reported bug environment is an example of the all-work-type rule. Non-integration fixes still require the linked forward cherry-pick.
|