@codyswann/lisa 2.323.1 → 2.324.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 (102) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +71 -0
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +102 -0
  3. package/all/copy-overwrite/scripts/lisa-hooks/block-shell-json-parsing.sh +220 -0
  4. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +562 -0
  5. package/all/copy-overwrite/scripts/lisa-work-item.mjs +77 -5
  6. package/all/merge/.claude/settings.json +19 -4
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +25 -11
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/dist/standards/git-state.d.ts.map +1 -1
  11. package/dist/standards/git-state.js +10 -1
  12. package/dist/standards/git-state.js.map +1 -1
  13. package/dist/strategies/package-lisa.d.ts.map +1 -1
  14. package/dist/strategies/package-lisa.js +67 -2
  15. package/dist/strategies/package-lisa.js.map +1 -1
  16. package/package.json +1 -1
  17. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +24 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/session-start.sh +17 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +37 -0
  22. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +7 -3
  23. package/plugins/lisa/rules/eager/tracked-work.md +1 -1
  24. package/plugins/lisa/rules/reference/tracked-work.md +1 -1
  25. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +24 -0
  26. package/plugins/lisa/skills/lisa-setup-remote-env/assets/session-start.sh +17 -0
  27. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +37 -0
  28. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +7 -3
  29. package/plugins/lisa-agy/plugin.json +1 -1
  30. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +24 -0
  31. package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/session-start.sh +17 -0
  32. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +37 -0
  33. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +7 -3
  34. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  37. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-copilot/rules/eager/tracked-work.md +1 -1
  41. package/plugins/lisa-copilot/rules/reference/tracked-work.md +1 -1
  42. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +24 -0
  43. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/session-start.sh +17 -0
  44. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +37 -0
  45. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +7 -3
  46. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-cursor/rules/tracked-work-reference.mdc +1 -1
  48. package/plugins/lisa-cursor/rules/tracked-work.mdc +1 -1
  49. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +24 -0
  50. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/session-start.sh +17 -0
  51. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +37 -0
  52. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +7 -3
  53. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-expo-agy/plugin.json +1 -1
  56. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  61. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  66. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  71. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  76. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails-agy/plugin.json +1 -1
  81. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  86. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  91. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/src/base/rules/eager/tracked-work.md +1 -1
  94. package/plugins/src/base/rules/reference/tracked-work.md +1 -1
  95. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +24 -0
  96. package/plugins/src/base/skills/lisa-setup-remote-env/assets/session-start.sh +17 -0
  97. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +37 -0
  98. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +7 -3
  99. package/scripts/build-plugins.sh +32 -0
  100. package/scripts/generate-upstream-evidence-manifest.mjs +50 -11
  101. package/scripts/lisa-enforcement-fallback.sh +71 -0
  102. package/scripts/lisa-remote-env/session-start.sh +17 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.323.1",
3
+ "version": "2.324.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -2,6 +2,6 @@
2
2
 
3
3
  Before the first durable project mutation (code, tests, config, docs, committed research/plans/findings, commits, or PRs), establish exactly one live tracker leaf through `lisa-track`. Read-only discussion and orientation are exempt only while they produce no durable artifact.
4
4
 
5
- The mandatory order is: live-validate an explicit ref, or conservatively search and create exactly one valid leaf through `lisa-tracker-write` when no unique match exists; idempotently claim it through `lisa-tracker-claim`; then persist and verify the worktree-local binding with `node scripts/lisa-work-item.mjs bind <ref>`. Any tracker, claim, or binding failure blocks durable work.
5
+ The mandatory order is: live-validate an explicit ref, or conservatively search and create exactly one valid leaf through `lisa-tracker-write` when no unique match exists; idempotently claim it through `lisa-tracker-claim`; then persist and verify the worktree-local binding with `node scripts/lisa-work-item.mjs bind <ref>`. A tracker that answers no, and any claim or binding failure, blocks durable work; a tracker that cannot be reached does not block a commit — the offline checks still run, the skip is loud on stderr, and the required `Work-Item Traceability` check re-runs the live checks with credentials before anything merges.
6
6
 
7
7
  Carry that canonical ref through the branch, every ordinary commit's `Work-Item:` trailer, the PR, usage/evidence, and `lisa-tracker-sync`. Hooks and CI never create tickets. Keep the binding through interruptions or blocked outcomes; run `node scripts/lisa-work-item.mjs clear` only after merge/deploy/verification, two-way linkage/evidence, and the tracker item have all reached true terminal completion.
@@ -11,7 +11,7 @@ Use `lisa-track` as the single entry point:
11
11
  3. Idempotently claim the resolved leaf through `lisa-tracker-claim`, which reuses the vendor build-intake claim semantics and post-read verifies the claimed-or-later state.
12
12
  4. Before any durable repository work, persist the canonical reference with `node scripts/lisa-work-item.mjs bind <ref>` and verify the worktree-local binding.
13
13
 
14
- The sequence is strict: **live validate/create -> claim -> bind -> durable work**. A failed tracker read/write, claim, or binding blocks the work. Tracker availability must be proven; tool presence or stale session text is not access.
14
+ The sequence is strict: **live validate/create -> claim -> bind -> durable work**. A tracker answer of no, and any claim or binding failure, blocks the work. A tracker that cannot be reached `gh` absent, or its credential refused — does not block a commit: the offline checks still run, the skip is loud on stderr, and the required `Work-Item Traceability` check re-runs the live checks with credentials before anything merges. Any other tracker failure still blocks. Tool presence or stale session text is not access.
15
15
 
16
16
  ## One canonical identity
17
17
 
@@ -107,6 +107,14 @@ The selection comes from the surface's `materializeAt` capability in `lisa-secre
107
107
  "url": "https://<vendor>/releases/download/bws-v2.1.0/bws-<platform>-2.1.0.zip",
108
108
  "sha256": "<sha256 published with that exact release>"
109
109
  },
110
+ {
111
+ "name": "gh",
112
+ "version": "2.83.0",
113
+ "install": "release-tar",
114
+ "url": "https://<vendor>/releases/download/v2.83.0/gh_2.83.0_linux_amd64.tar.gz",
115
+ "sha256": "<sha256 published with that exact release>",
116
+ "binary": "gh_2.83.0_linux_amd64/bin/gh"
117
+ },
110
118
  {
111
119
  "name": "codex",
112
120
  "version": "0.144.6",
@@ -133,6 +141,22 @@ The selection comes from the surface's `materializeAt` capability in `lisa-secre
133
141
 
134
142
  Expected shape for most projects: a short `require` list, an empty or near-empty `install` list, and the provider CLI as the only thing always provisioned.
135
143
 
144
+ ### The three `install` methods
145
+
146
+ | `install` | Required fields | Use it when |
147
+ | --- | --- | --- |
148
+ | `release-zip` | `url` **and** `sha256` | The vendor publishes a Linux zip of the release. |
149
+ | `release-tar` | `url` **and** `sha256` — plus `binary` in practice | The vendor publishes no zip for Linux, only a `.tar.gz`. |
150
+ | `npm-global` | `package` | The tool ships on the npm registry. |
151
+
152
+ Anything else is rejected by name at plan time, so a typo fails loudly rather than silently installing nothing.
153
+
154
+ Both archive kinds carry the **same** obligation and differ only in how they are unpacked: `url` and `sha256` are both mandatory, the checksum is verified *before* the archive is unpacked, and a version bump must move the checksum in the same reviewed commit. Neither is a weaker path than the other — `release-tar` exists because some tools worth pinning simply do not publish a zip. `gh` is the case that forced it: it ships `.deb`, `.rpm` and `.tar.gz` and nothing else, so a zip-only installer could not pin the CLI that Lisa's own commit guardrails shell out to.
155
+
156
+ **`binary` means something different for a tarball.** It defaults to the tool's `name` and is resolved relative to the unpacked directory. Release tarballs almost always nest their contents under a versioned top-level directory, so for `release-tar` it must be the path *within* the archive — `gh_2.83.0_linux_amd64/bin/gh`, not `gh`. A bare name there resolves to nothing after a download that otherwise succeeded, and the install fails at the copy step rather than at the fetch, which reads as a broken release when it is a manifest mistake. Bump the version and this path moves with it, alongside the `url` and the `sha256`.
157
+
158
+ Because `release-tar` unpacks with `tar` rather than `unzip`, a project using it should assert `tar` in its `require` list the same way a zip-installing project asserts `unzip` — the base image providing it is an assumption, not a contract.
159
+
136
160
  ### Rules
137
161
 
138
162
  - **The base image is not a contract.** A vendor can change it. A project quietly depending on a preinstalled `jq` should break loudly at setup when that happens, not mysteriously mid-task weeks later. `require` is what converts an implicit assumption into an explicit check.
@@ -27,4 +27,21 @@ if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
27
27
  fi
28
28
 
29
29
  here="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
30
+
31
+ # The toolchain first, for the same reason the secrets are here at all.
32
+ #
33
+ # It used to run only from the environment setup script, which a cached
34
+ # environment skips — so a tool added to remoteEnv.tools was invisible until the
35
+ # cache expired about a week later, or until someone edited the vendor's setup
36
+ # field to force a rebuild. Neither is a thing a project can rely on, and the
37
+ # symptom is a container missing a tool its own committed config pins.
38
+ #
39
+ # It is cheap to repeat: the plan probes each tool and installs only what is
40
+ # absent or below its pinned version, so on a warm container this is a handful
41
+ # of --version calls and nothing else.
42
+ #
43
+ # Before secrets, because materializing needs the provider CLI that this step
44
+ # installs. A failure here is fatal for the same reason: continuing would report
45
+ # a missing credential when the real fault was a missing binary.
46
+ bash "${here}/setup.sh" --phase=toolchain "$@"
30
47
  exec bash "${here}/setup.sh" --phase=secrets "$@"
@@ -153,6 +153,42 @@ function installReleaseZip(tool, binDir) {
153
153
  }
154
154
  }
155
155
 
156
+ /**
157
+ * Install a pinned tarball, refusing anything whose checksum does not match.
158
+ *
159
+ * Same contract as the zip path and the same ordering — verify, then unpack —
160
+ * because an unexpected archive must fail before any of its contents reach a
161
+ * directory that is on PATH. It exists because several tools worth pinning
162
+ * publish no zip for Linux: gh ships .deb, .rpm and .tar.gz and nothing else.
163
+ * @param {object} tool Manifest entry.
164
+ * @param {string} binDir Directory to install into.
165
+ */
166
+ function installReleaseTar(tool, binDir) {
167
+ const temporary = join(binDir, `.${tool.name}-download`);
168
+ mkdirSync(temporary, { recursive: true });
169
+ try {
170
+ const archive = join(temporary, "download.tar.gz");
171
+ execFileSync("curl", ["-fsSL", tool.url, "-o", archive], {
172
+ stdio: "inherit",
173
+ });
174
+ execFileSync("sha256sum", ["-c", "-"], {
175
+ input: `${tool.sha256} ${archive}\n`,
176
+ stdio: ["pipe", "ignore", "inherit"],
177
+ });
178
+ execFileSync("tar", ["-xzf", archive, "-C", temporary], {
179
+ stdio: "inherit",
180
+ });
181
+ // Release tarballs usually nest under a versioned directory, so `binary` is
182
+ // a path within the archive rather than a bare name.
183
+ const binary = join(temporary, tool.binary ?? tool.name);
184
+ execFileSync("install", ["-m", "0755", binary, join(binDir, tool.name)], {
185
+ stdio: "inherit",
186
+ });
187
+ } finally {
188
+ rmSync(temporary, { recursive: true, force: true });
189
+ }
190
+ }
191
+
156
192
  /**
157
193
  * Install a pinned global npm package.
158
194
  * @param {object} tool Manifest entry.
@@ -175,6 +211,7 @@ function installNpmGlobal(tool) {
175
211
  function installTool(tool, binDir) {
176
212
  assertPinned(tool);
177
213
  if (tool.install === "release-zip") installReleaseZip(tool, binDir);
214
+ else if (tool.install === "release-tar") installReleaseTar(tool, binDir);
178
215
  else installNpmGlobal(tool);
179
216
  }
180
217
 
@@ -148,10 +148,14 @@ export function planToolchain(tools, probe) {
148
148
  * @param {object} tool Manifest entry.
149
149
  */
150
150
  export function assertPinned(tool) {
151
- if (tool.install === "release-zip") {
151
+ // Both archive kinds carry the same obligation, and differ only in how they
152
+ // are unpacked. gh, for one, publishes no zip for Linux at all — only .deb,
153
+ // .rpm and .tar.gz — so a zip-only installer could not pin the CLI that
154
+ // Lisa's own guardrails shell out to.
155
+ if (tool.install === "release-zip" || tool.install === "release-tar") {
152
156
  if (!tool.url || !tool.sha256) {
153
157
  throw new Error(
154
- `${tool.name}: a release-zip install needs both url and sha256.\n` +
158
+ `${tool.name}: a ${tool.install} install needs both url and sha256.\n` +
155
159
  `A version bump must move the checksum in the same reviewed commit.`
156
160
  );
157
161
  }
@@ -164,6 +168,6 @@ export function assertPinned(tool) {
164
168
  }
165
169
  throw new Error(
166
170
  `${tool.name}: unknown install method "${tool.install}".\n` +
167
- `Supported: release-zip, npm-global.`
171
+ `Supported: release-zip, release-tar, npm-global.`
168
172
  );
169
173
  }
@@ -90,6 +90,38 @@ if [ -f "$SRC_DIR/base/hooks/threshold-ratchet.mjs" ]; then
90
90
  done
91
91
  fi
92
92
 
93
+ # Enforcement guards for host projects.
94
+ #
95
+ # These three are declared in the Lisa plugin, so a container whose plugin
96
+ # install fails runs with no PreToolUse enforcement at all — silently. Lisa
97
+ # closes that with a repository hook, which reaches a cloud session because it
98
+ # is part of the clone; a host project needs the same thing, and has no
99
+ # plugins/ directory to fall back on.
100
+ #
101
+ # Synced rather than hand-copied, for the same reason the ratchet above is: a
102
+ # unit test asserts byte-equality, so the shipped guard can never drift from the
103
+ # reviewed one.
104
+ HOST_GUARD_DIR="$ROOT_DIR/all/copy-overwrite/scripts/lisa-hooks"
105
+ if [ -d "$SRC_DIR/base/hooks" ]; then
106
+ mkdir -p "$HOST_GUARD_DIR"
107
+ fi
108
+ for guard in block-no-verify parity-safety-net block-shell-json-parsing; do
109
+ if [ -f "$SRC_DIR/base/hooks/$guard.sh" ]; then
110
+ cp "$SRC_DIR/base/hooks/$guard.sh" "$HOST_GUARD_DIR/$guard.sh"
111
+ chmod +x "$HOST_GUARD_DIR/$guard.sh"
112
+ fi
113
+ done
114
+ # The dispatcher itself, so a host project gets the identical entry point.
115
+ # Guarded like the ratchet above: this script is also run against isolated
116
+ # fixtures that carry a source tree but none of the repository's own scripts,
117
+ # and an unconditional copy fails the whole build there.
118
+ if [ -f "$ROOT_DIR/scripts/lisa-enforcement-fallback.sh" ]; then
119
+ mkdir -p "$ROOT_DIR/all/copy-overwrite/scripts"
120
+ cp "$ROOT_DIR/scripts/lisa-enforcement-fallback.sh" \
121
+ "$ROOT_DIR/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh"
122
+ chmod +x "$ROOT_DIR/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh"
123
+ fi
124
+
93
125
  # Stack-specific plugins (NO base copy)
94
126
  STACKS=(typescript expo nestjs cdk harper-fabric phaser rails)
95
127
  for stack in "${STACKS[@]}"; do
@@ -42,18 +42,57 @@ const forbiddenBasenames = new Set([
42
42
  ".mcp.json",
43
43
  "audit.local",
44
44
  ]);
45
- const canonicalOriginUrls = new Set([
46
- "https://github.com/CodySwannGT/lisa.git",
47
- "https://github.com/CodySwannGT/lisa",
48
- "git@github.com:CodySwannGT/lisa.git",
49
- ]);
45
+ /** The repository this manifest may be generated from. Not a URL — see below. */
46
+ const canonicalRepository = "CodySwannGT/lisa";
50
47
 
51
- const originUrl = execFileSync("git", ["remote", "get-url", "origin"], {
52
- cwd: repoRoot,
53
- encoding: "utf8",
54
- }).trim();
55
- if (!canonicalOriginUrls.has(originUrl)) {
56
- throw new Error(`Refusing non-canonical Lisa origin: ${originUrl}`);
48
+ /**
49
+ * The `owner/repo` an origin URL points at, or null if it names none.
50
+ *
51
+ * The guard exists to refuse hash-pinning from a fork, which is a question
52
+ * about **identity**. It used to compare the whole origin URL against a list of
53
+ * spellings, which answers a question about **transport** instead — and the two
54
+ * come apart in ordinary situations, not just exotic ones. `remote.origin.url`
55
+ * and `remote.origin.pushurl` can differ, `url.<base>.insteadOf` rewrites what
56
+ * `git remote get-url` returns, and a cloud container clones through a local
57
+ * git proxy. All three are proxied checkouts of the real repository, and all
58
+ * three were refused before a single hash was checked.
59
+ *
60
+ * Comparing the path keeps the property that matters: a fork is
61
+ * `someone-else/lisa` and is still refused, while `https://…`, `git@…` and
62
+ * `http://local_proxy@127.0.0.1:PORT/git/…` all resolve to the same identity.
63
+ * @param {string} url Whatever `git remote get-url` returned.
64
+ * @returns {string|null} `owner/repo`, lowercased, or null.
65
+ */
66
+ function repositoryPath(url) {
67
+ // scp-style (git@host:owner/repo.git) is not a URL, so it is handled first.
68
+ const scp = /^[^/]+@[^:/]+:(?<path>.+)$/u.exec(url);
69
+ const raw =
70
+ scp?.groups?.path ?? (URL.canParse(url) ? new URL(url).pathname : null);
71
+ if (raw === null) return null;
72
+ const segments = raw
73
+ .replace(/\.git$/u, "")
74
+ .split("/")
75
+ .filter(Boolean);
76
+ if (segments.length < 2) return null;
77
+ return segments.slice(-2).join("/").toLowerCase();
78
+ }
79
+
80
+ // The configured value rather than the resolved one, for the same reason the
81
+ // comparison is by path: `url.<base>.insteadOf` rewrites what `git remote
82
+ // get-url` returns, so it answers "how do I reach this" rather than "what is
83
+ // this". Falls back to the resolved URL only if no value is configured.
84
+ const originUrl = execFileSync(
85
+ "git",
86
+ ["config", "--get", "remote.origin.url"],
87
+ { cwd: repoRoot, encoding: "utf8" }
88
+ ).trim();
89
+ if (repositoryPath(originUrl) !== canonicalRepository.toLowerCase()) {
90
+ throw new Error(
91
+ `Refusing to generate from a non-canonical repository.\n` +
92
+ `origin resolves to: ${repositoryPath(originUrl) ?? "(no owner/repo)"}\n` +
93
+ `expected: ${canonicalRepository}\n` +
94
+ `(origin URL was: ${originUrl})`
95
+ );
57
96
  }
58
97
 
59
98
  const tracked = execFileSync("git", ["ls-files", "-z"], {
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Run Lisa's Bash enforcement guards when the plugin that normally provides them
4
+ # is not installed.
5
+ #
6
+ # Every PreToolUse guard — block-no-verify, parity-safety-net,
7
+ # block-shell-json-parsing — is declared in the Lisa plugin. A cloud session
8
+ # installs plugins at session start from the marketplace the repository
9
+ # declares, and when that does not happen the container runs with
10
+ # `installed_plugins.json` empty and no enforcement whatsoever.
11
+ #
12
+ # That is how a dispatched session committed with `--no-verify`: not by evading
13
+ # a guard, but in an environment where none existed. The guards failed open, and
14
+ # silently, which is the worst of the three ways they could fail.
15
+ #
16
+ # A repository hook is the delivery that cannot fail this way. `.claude/settings.json`
17
+ # is part of the clone, so it reaches a cloud session whether or not a plugin
18
+ # ever installs.
19
+ set -uo pipefail
20
+
21
+ payload="$(cat)"
22
+
23
+ repo_root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
24
+ [ -n "$repo_root" ] || exit 0
25
+
26
+ # Skip when the plugin is installed, so a developer machine does not run every
27
+ # guard twice and print every refusal twice. Absence is the interesting case and
28
+ # the only one this exists for.
29
+ #
30
+ # Read from the plugin registry rather than from CLAUDE_PLUGIN_ROOT: that
31
+ # variable is set for plugin hooks, and this hook is by definition not one.
32
+ installed="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/installed_plugins.json"
33
+ if [ -f "$installed" ] && grep -q '"lisa@lisa"' "$installed" 2>/dev/null; then
34
+ exit 0
35
+ fi
36
+
37
+ # Where the guards live depends on which repository this is.
38
+ #
39
+ # `plugins/lisa/hooks/` exists only in the Lisa monorepo. A host project gets
40
+ # the same three scripts written into its checkout by `lisa apply`, because a
41
+ # host project whose plugin install fails has exactly the same hole and no
42
+ # `plugins/` directory to fall back on.
43
+ status=0
44
+ for guard in block-no-verify parity-safety-net block-shell-json-parsing; do
45
+ script=""
46
+ for candidate in \
47
+ "$repo_root/scripts/lisa-hooks/$guard.sh" \
48
+ "$repo_root/plugins/lisa/hooks/$guard.sh"; do
49
+ if [ -f "$candidate" ]; then
50
+ script="$candidate"
51
+ break
52
+ fi
53
+ done
54
+ [ -n "$script" ] || continue
55
+ # Each guard reads the tool payload on stdin and signals a refusal with exit
56
+ # 2. The payload is replayed to every one of them, and the strongest refusal
57
+ # is returned — a guard that declines must not be able to clear one that did
58
+ # not.
59
+ #
60
+ # The status is captured from the pipeline directly rather than through `if !`,
61
+ # where `$?` is the negation and every refusal read as success: the guard
62
+ # printed its objection and the command ran anyway, which is the same
63
+ # fail-open this file exists to close.
64
+ printf '%s' "$payload" | bash "$script"
65
+ guard_status=$?
66
+ if [ "$guard_status" -gt "$status" ]; then
67
+ status="$guard_status"
68
+ fi
69
+ done
70
+
71
+ exit "$status"
@@ -27,4 +27,21 @@ if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
27
27
  fi
28
28
 
29
29
  here="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
30
+
31
+ # The toolchain first, for the same reason the secrets are here at all.
32
+ #
33
+ # It used to run only from the environment setup script, which a cached
34
+ # environment skips — so a tool added to remoteEnv.tools was invisible until the
35
+ # cache expired about a week later, or until someone edited the vendor's setup
36
+ # field to force a rebuild. Neither is a thing a project can rely on, and the
37
+ # symptom is a container missing a tool its own committed config pins.
38
+ #
39
+ # It is cheap to repeat: the plan probes each tool and installs only what is
40
+ # absent or below its pinned version, so on a warm container this is a handful
41
+ # of --version calls and nothing else.
42
+ #
43
+ # Before secrets, because materializing needs the provider CLI that this step
44
+ # installs. A failure here is fatal for the same reason: continuing would report
45
+ # a missing credential when the real fault was a missing binary.
46
+ bash "${here}/setup.sh" --phase=toolchain "$@"
30
47
  exec bash "${here}/setup.sh" --phase=secrets "$@"