@codyswann/lisa 3.23.2 → 3.26.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.
Files changed (72) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +2 -1
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +174 -0
  3. package/dist/cli/doctor-reusable-workflow-refs.d.ts +25 -0
  4. package/dist/cli/doctor-reusable-workflow-refs.d.ts.map +1 -0
  5. package/dist/cli/doctor-reusable-workflow-refs.js +153 -0
  6. package/dist/cli/doctor-reusable-workflow-refs.js.map +1 -0
  7. package/dist/cli/doctor.d.ts.map +1 -1
  8. package/dist/cli/doctor.js +2 -0
  9. package/dist/cli/doctor.js.map +1 -1
  10. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  11. package/dist/core/lisa-owned-hash-ledger.js +4 -0
  12. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  13. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  14. package/dist/core/upstream-evidence-manifest.js +13 -3
  15. package/dist/core/upstream-evidence-manifest.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/hooks/block-managed-file-edits.sh +171 -0
  20. package/plugins/lisa-agy/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  24. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +171 -0
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +171 -0
  30. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-agy/plugin.json +1 -1
  33. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  53. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-agy/plugin.json +1 -1
  58. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  63. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  68. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/src/base/hooks/block-managed-file-edits.sh +171 -0
  71. package/scripts/build-plugins.sh +2 -1
  72. package/scripts/lisa-enforcement-fallback.sh +2 -1
package/package.json CHANGED
@@ -129,7 +129,7 @@
129
129
  }
130
130
  },
131
131
  "name": "@codyswann/lisa",
132
- "version": "3.23.2",
132
+ "version": "3.26.0",
133
133
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
134
134
  "main": "dist/index.js",
135
135
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse hook: refuse agent writes to files Lisa overwrites on every apply.
3
+ #
4
+ # Lisa ships templates in three modes, and only one of them destroys host edits:
5
+ #
6
+ # copy-overwrite — replaced wholesale on every `lisa apply`. An edit here is
7
+ # gone at the next `bun install`, silently.
8
+ # copy-contents — Lisa APPENDS its lines; host content survives.
9
+ # create-only — skipped when the file exists; the host owns it outright.
10
+ #
11
+ # So this guard covers copy-overwrite and nothing else. Blocking the other two
12
+ # would stop agents editing files they are supposed to own, which is worse than
13
+ # the problem being solved.
14
+ #
15
+ # Measured, not hypothetical. Nothing enforced this, so downstream copies were
16
+ # edited and then silently diverged: `classify-maestro-failures.mjs` reached
17
+ # 36,061 bytes in one fleet repo against 29,586 shipped, and five gate files in
18
+ # another stopped receiving upstream fixes — one over roughly 138 bytes of
19
+ # cosmetic change. The edits were made in good faith; nothing told anyone the
20
+ # file was not theirs.
21
+ #
22
+ # ## Why the path, not a banner comment
23
+ #
24
+ # 103 of Lisa's 145 copy-overwrite files carry a "managed by Lisa" header, and
25
+ # the other 42 CANNOT: 30 are `.json` (no comment syntax), 8 are `.gitkeep` /
26
+ # `.keep` placeholders, and the rest are bare-value files like `.nvmrc`. A guard
27
+ # keyed on the banner would miss every one of them, so this resolves the path
28
+ # against the installed package instead and covers all 145 regardless of format.
29
+ #
30
+ # Blocked signatures:
31
+ # 1. Write / Edit / MultiEdit / NotebookEdit whose target resolves to a
32
+ # copy-overwrite template in the installed Lisa package;
33
+ # 2. Bash output redirection (`>`, `>>`, `>|`), `tee`, or `sed -i` aimed at
34
+ # one. Reads never fire.
35
+ #
36
+ # Exemptions (allowed):
37
+ # - `LISA_ALLOW_MANAGED_FILE_WRITE` set — the operator's explicit override,
38
+ # named in the refusal;
39
+ # - paths under `node_modules/` or `dist/` — vendored copies, not the host's;
40
+ # - the Lisa source repository itself, where these files are the originals and
41
+ # editing them is the entire point.
42
+ set -euo pipefail
43
+
44
+ input="$(cat)"
45
+
46
+ command -v jq >/dev/null 2>&1 || exit 0
47
+
48
+ # The operator's override, checked first so it is always the cheapest way out.
49
+ if [ -n "${LISA_ALLOW_MANAGED_FILE_WRITE:-}" ]; then
50
+ exit 0
51
+ fi
52
+
53
+ tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
54
+ [ -n "$tool_name" ] || exit 0
55
+
56
+ project_root="${CLAUDE_PROJECT_DIR:-$PWD}"
57
+ package_root="$project_root/node_modules/@codyswann/lisa"
58
+
59
+ # No installed Lisa means nothing to classify against. Silent, because a project
60
+ # that has not installed Lisa is not doing anything wrong.
61
+ [ -d "$package_root" ] || exit 0
62
+
63
+ # In Lisa's own repository these files ARE the originals. Blocking here would
64
+ # make the templates uneditable by the only agents that should edit them.
65
+ if [ -f "$project_root/package.json" ] &&
66
+ grep -q '"name": *"@codyswann/lisa"' "$project_root/package.json" 2>/dev/null; then
67
+ exit 0
68
+ fi
69
+
70
+ # Whether a host-relative path is shipped as a copy-overwrite template.
71
+ #
72
+ # A few stat calls rather than an enumeration: the same relative path is probed
73
+ # under each stack's copy-overwrite tree, so cost does not grow with the 145
74
+ # templates.
75
+ managed_source() {
76
+ local candidate="$1"
77
+ case "$candidate" in
78
+ */node_modules/* | node_modules/* | */dist/* | dist/*) return 1 ;;
79
+ esac
80
+ # Normalise to a project-relative path so an absolute target still matches.
81
+ local rel="${candidate#"$project_root"/}"
82
+ rel="${rel#./}"
83
+ [ -n "$rel" ] || return 1
84
+ local stack
85
+ for stack in "$package_root"/*/copy-overwrite; do
86
+ [ -d "$stack" ] || continue
87
+ if [ -e "$stack/$rel" ]; then
88
+ printf '%s' "${stack#"$package_root"/}/$rel"
89
+ return 0
90
+ fi
91
+ done
92
+ return 1
93
+ }
94
+
95
+ refuse() {
96
+ local target="$1"
97
+ local source="$2"
98
+ cat >&2 <<EOF
99
+ BLOCKED: refusing to write \`$target\`.
100
+
101
+ WHY: this file is Lisa-managed. It is shipped as a **copy-overwrite** template
102
+ (\`$source\` in the installed package) and is replaced wholesale on every
103
+ \`lisa apply\` — which runs on every \`bun install\`. Your edit would survive
104
+ until the next install and then vanish, with nothing reporting that it had.
105
+
106
+ That is not hypothetical: downstream copies edited this way diverged silently
107
+ until they stopped receiving upstream fixes entirely.
108
+
109
+ WHERE IT GOES INSTEAD — take the first one that fits:
110
+
111
+ 1. The change should apply everywhere. Edit the template upstream in Lisa and
112
+ release it — \`/lisa:cross-pollinate\`, or an issue on CodySwannGT/lisa.
113
+ That is the only edit that survives an install.
114
+
115
+ 2. Only this project needs to differ. Look for the local escape hatch beside the
116
+ file — Lisa ships \`.local\` variants for the configs that support one
117
+ (\`eslint.config.local.ts\`, \`tsconfig.local.json\`, \`audit.ignore.local.json\`,
118
+ and others). Those are yours and are never overwritten.
119
+
120
+ 3. The behaviour is configurable rather than hardcoded. Check \`.lisa.config.json\`
121
+ — the \`gates\` block in particular decides which checks run and what command
122
+ proves each one, so a project can substitute its own task without touching a
123
+ shipped file.
124
+
125
+ 4. You believe this file should not be Lisa-managed at all. That is a real
126
+ argument and it belongs upstream, not in a local edit that will be erased.
127
+
128
+ If a human explicitly asked for this edit, they can re-run with
129
+ \`LISA_ALLOW_MANAGED_FILE_WRITE=1\` set, which bypasses this guard.
130
+ EOF
131
+ exit 2
132
+ }
133
+
134
+ case "$tool_name" in
135
+ Write | Edit | MultiEdit | NotebookEdit | Update)
136
+ paths="$(printf '%s' "$input" | jq -r '
137
+ [ .tool_input.file_path?,
138
+ .tool_input.path?,
139
+ .tool_input.notebook_path?,
140
+ (.tool_input.edits? // [] | .[].file_path?)
141
+ ] | map(select(. != null and . != "")) | .[]' 2>/dev/null || true)"
142
+ while IFS= read -r candidate; do
143
+ [ -n "$candidate" ] || continue
144
+ if source_path="$(managed_source "$candidate")"; then
145
+ refuse "$candidate" "$source_path"
146
+ fi
147
+ done <<EOF
148
+ $paths
149
+ EOF
150
+ ;;
151
+ Bash)
152
+ command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
153
+ [ -n "$command_str" ] || exit 0
154
+ # Write signatures only. A bare mention is a read and stays allowed — the
155
+ # path has to be the target of a redirection, a tee, or an in-place sed.
156
+ # `>>?\|?` covers `>`, `>>`, and the noclobber override `>|`.
157
+ while IFS= read -r token; do
158
+ [ -n "$token" ] || continue
159
+ if source_path="$(managed_source "$token")"; then
160
+ refuse "$token" "$source_path"
161
+ fi
162
+ done <<EOF
163
+ $(printf '%s' "$command_str" |
164
+ grep -Eo ">>?\|?[[:space:]]*['\"]?[^ |;&'\"]+|tee([[:space:]]+-[a-zA-Z]+)*[[:space:]]+['\"]?[^ |;&'\"]+|sed[[:space:]]+[^|;&]*-i[^|;&]*[[:space:]]['\"]?[^ |;&'\"]+" 2>/dev/null |
165
+ sed -E "s/^(>>?\|?|tee([[:space:]]+-[a-zA-Z]+)*|sed[[:space:]]+[^|;&]*-i[^|;&]*)[[:space:]]*//" |
166
+ tr -d "\"'" || true)
167
+ EOF
168
+ ;;
169
+ esac
170
+
171
+ exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse hook: refuse agent writes to files Lisa overwrites on every apply.
3
+ #
4
+ # Lisa ships templates in three modes, and only one of them destroys host edits:
5
+ #
6
+ # copy-overwrite — replaced wholesale on every `lisa apply`. An edit here is
7
+ # gone at the next `bun install`, silently.
8
+ # copy-contents — Lisa APPENDS its lines; host content survives.
9
+ # create-only — skipped when the file exists; the host owns it outright.
10
+ #
11
+ # So this guard covers copy-overwrite and nothing else. Blocking the other two
12
+ # would stop agents editing files they are supposed to own, which is worse than
13
+ # the problem being solved.
14
+ #
15
+ # Measured, not hypothetical. Nothing enforced this, so downstream copies were
16
+ # edited and then silently diverged: `classify-maestro-failures.mjs` reached
17
+ # 36,061 bytes in one fleet repo against 29,586 shipped, and five gate files in
18
+ # another stopped receiving upstream fixes — one over roughly 138 bytes of
19
+ # cosmetic change. The edits were made in good faith; nothing told anyone the
20
+ # file was not theirs.
21
+ #
22
+ # ## Why the path, not a banner comment
23
+ #
24
+ # 103 of Lisa's 145 copy-overwrite files carry a "managed by Lisa" header, and
25
+ # the other 42 CANNOT: 30 are `.json` (no comment syntax), 8 are `.gitkeep` /
26
+ # `.keep` placeholders, and the rest are bare-value files like `.nvmrc`. A guard
27
+ # keyed on the banner would miss every one of them, so this resolves the path
28
+ # against the installed package instead and covers all 145 regardless of format.
29
+ #
30
+ # Blocked signatures:
31
+ # 1. Write / Edit / MultiEdit / NotebookEdit whose target resolves to a
32
+ # copy-overwrite template in the installed Lisa package;
33
+ # 2. Bash output redirection (`>`, `>>`, `>|`), `tee`, or `sed -i` aimed at
34
+ # one. Reads never fire.
35
+ #
36
+ # Exemptions (allowed):
37
+ # - `LISA_ALLOW_MANAGED_FILE_WRITE` set — the operator's explicit override,
38
+ # named in the refusal;
39
+ # - paths under `node_modules/` or `dist/` — vendored copies, not the host's;
40
+ # - the Lisa source repository itself, where these files are the originals and
41
+ # editing them is the entire point.
42
+ set -euo pipefail
43
+
44
+ input="$(cat)"
45
+
46
+ command -v jq >/dev/null 2>&1 || exit 0
47
+
48
+ # The operator's override, checked first so it is always the cheapest way out.
49
+ if [ -n "${LISA_ALLOW_MANAGED_FILE_WRITE:-}" ]; then
50
+ exit 0
51
+ fi
52
+
53
+ tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
54
+ [ -n "$tool_name" ] || exit 0
55
+
56
+ project_root="${CLAUDE_PROJECT_DIR:-$PWD}"
57
+ package_root="$project_root/node_modules/@codyswann/lisa"
58
+
59
+ # No installed Lisa means nothing to classify against. Silent, because a project
60
+ # that has not installed Lisa is not doing anything wrong.
61
+ [ -d "$package_root" ] || exit 0
62
+
63
+ # In Lisa's own repository these files ARE the originals. Blocking here would
64
+ # make the templates uneditable by the only agents that should edit them.
65
+ if [ -f "$project_root/package.json" ] &&
66
+ grep -q '"name": *"@codyswann/lisa"' "$project_root/package.json" 2>/dev/null; then
67
+ exit 0
68
+ fi
69
+
70
+ # Whether a host-relative path is shipped as a copy-overwrite template.
71
+ #
72
+ # A few stat calls rather than an enumeration: the same relative path is probed
73
+ # under each stack's copy-overwrite tree, so cost does not grow with the 145
74
+ # templates.
75
+ managed_source() {
76
+ local candidate="$1"
77
+ case "$candidate" in
78
+ */node_modules/* | node_modules/* | */dist/* | dist/*) return 1 ;;
79
+ esac
80
+ # Normalise to a project-relative path so an absolute target still matches.
81
+ local rel="${candidate#"$project_root"/}"
82
+ rel="${rel#./}"
83
+ [ -n "$rel" ] || return 1
84
+ local stack
85
+ for stack in "$package_root"/*/copy-overwrite; do
86
+ [ -d "$stack" ] || continue
87
+ if [ -e "$stack/$rel" ]; then
88
+ printf '%s' "${stack#"$package_root"/}/$rel"
89
+ return 0
90
+ fi
91
+ done
92
+ return 1
93
+ }
94
+
95
+ refuse() {
96
+ local target="$1"
97
+ local source="$2"
98
+ cat >&2 <<EOF
99
+ BLOCKED: refusing to write \`$target\`.
100
+
101
+ WHY: this file is Lisa-managed. It is shipped as a **copy-overwrite** template
102
+ (\`$source\` in the installed package) and is replaced wholesale on every
103
+ \`lisa apply\` — which runs on every \`bun install\`. Your edit would survive
104
+ until the next install and then vanish, with nothing reporting that it had.
105
+
106
+ That is not hypothetical: downstream copies edited this way diverged silently
107
+ until they stopped receiving upstream fixes entirely.
108
+
109
+ WHERE IT GOES INSTEAD — take the first one that fits:
110
+
111
+ 1. The change should apply everywhere. Edit the template upstream in Lisa and
112
+ release it — \`/lisa:cross-pollinate\`, or an issue on CodySwannGT/lisa.
113
+ That is the only edit that survives an install.
114
+
115
+ 2. Only this project needs to differ. Look for the local escape hatch beside the
116
+ file — Lisa ships \`.local\` variants for the configs that support one
117
+ (\`eslint.config.local.ts\`, \`tsconfig.local.json\`, \`audit.ignore.local.json\`,
118
+ and others). Those are yours and are never overwritten.
119
+
120
+ 3. The behaviour is configurable rather than hardcoded. Check \`.lisa.config.json\`
121
+ — the \`gates\` block in particular decides which checks run and what command
122
+ proves each one, so a project can substitute its own task without touching a
123
+ shipped file.
124
+
125
+ 4. You believe this file should not be Lisa-managed at all. That is a real
126
+ argument and it belongs upstream, not in a local edit that will be erased.
127
+
128
+ If a human explicitly asked for this edit, they can re-run with
129
+ \`LISA_ALLOW_MANAGED_FILE_WRITE=1\` set, which bypasses this guard.
130
+ EOF
131
+ exit 2
132
+ }
133
+
134
+ case "$tool_name" in
135
+ Write | Edit | MultiEdit | NotebookEdit | Update)
136
+ paths="$(printf '%s' "$input" | jq -r '
137
+ [ .tool_input.file_path?,
138
+ .tool_input.path?,
139
+ .tool_input.notebook_path?,
140
+ (.tool_input.edits? // [] | .[].file_path?)
141
+ ] | map(select(. != null and . != "")) | .[]' 2>/dev/null || true)"
142
+ while IFS= read -r candidate; do
143
+ [ -n "$candidate" ] || continue
144
+ if source_path="$(managed_source "$candidate")"; then
145
+ refuse "$candidate" "$source_path"
146
+ fi
147
+ done <<EOF
148
+ $paths
149
+ EOF
150
+ ;;
151
+ Bash)
152
+ command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
153
+ [ -n "$command_str" ] || exit 0
154
+ # Write signatures only. A bare mention is a read and stays allowed — the
155
+ # path has to be the target of a redirection, a tee, or an in-place sed.
156
+ # `>>?\|?` covers `>`, `>>`, and the noclobber override `>|`.
157
+ while IFS= read -r token; do
158
+ [ -n "$token" ] || continue
159
+ if source_path="$(managed_source "$token")"; then
160
+ refuse "$token" "$source_path"
161
+ fi
162
+ done <<EOF
163
+ $(printf '%s' "$command_str" |
164
+ grep -Eo ">>?\|?[[:space:]]*['\"]?[^ |;&'\"]+|tee([[:space:]]+-[a-zA-Z]+)*[[:space:]]+['\"]?[^ |;&'\"]+|sed[[:space:]]+[^|;&]*-i[^|;&]*[[:space:]]['\"]?[^ |;&'\"]+" 2>/dev/null |
165
+ sed -E "s/^(>>?\|?|tee([[:space:]]+-[a-zA-Z]+)*|sed[[:space:]]+[^|;&]*-i[^|;&]*)[[:space:]]*//" |
166
+ tr -d "\"'" || true)
167
+ EOF
168
+ ;;
169
+ esac
170
+
171
+ exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse hook: refuse agent writes to files Lisa overwrites on every apply.
3
+ #
4
+ # Lisa ships templates in three modes, and only one of them destroys host edits:
5
+ #
6
+ # copy-overwrite — replaced wholesale on every `lisa apply`. An edit here is
7
+ # gone at the next `bun install`, silently.
8
+ # copy-contents — Lisa APPENDS its lines; host content survives.
9
+ # create-only — skipped when the file exists; the host owns it outright.
10
+ #
11
+ # So this guard covers copy-overwrite and nothing else. Blocking the other two
12
+ # would stop agents editing files they are supposed to own, which is worse than
13
+ # the problem being solved.
14
+ #
15
+ # Measured, not hypothetical. Nothing enforced this, so downstream copies were
16
+ # edited and then silently diverged: `classify-maestro-failures.mjs` reached
17
+ # 36,061 bytes in one fleet repo against 29,586 shipped, and five gate files in
18
+ # another stopped receiving upstream fixes — one over roughly 138 bytes of
19
+ # cosmetic change. The edits were made in good faith; nothing told anyone the
20
+ # file was not theirs.
21
+ #
22
+ # ## Why the path, not a banner comment
23
+ #
24
+ # 103 of Lisa's 145 copy-overwrite files carry a "managed by Lisa" header, and
25
+ # the other 42 CANNOT: 30 are `.json` (no comment syntax), 8 are `.gitkeep` /
26
+ # `.keep` placeholders, and the rest are bare-value files like `.nvmrc`. A guard
27
+ # keyed on the banner would miss every one of them, so this resolves the path
28
+ # against the installed package instead and covers all 145 regardless of format.
29
+ #
30
+ # Blocked signatures:
31
+ # 1. Write / Edit / MultiEdit / NotebookEdit whose target resolves to a
32
+ # copy-overwrite template in the installed Lisa package;
33
+ # 2. Bash output redirection (`>`, `>>`, `>|`), `tee`, or `sed -i` aimed at
34
+ # one. Reads never fire.
35
+ #
36
+ # Exemptions (allowed):
37
+ # - `LISA_ALLOW_MANAGED_FILE_WRITE` set — the operator's explicit override,
38
+ # named in the refusal;
39
+ # - paths under `node_modules/` or `dist/` — vendored copies, not the host's;
40
+ # - the Lisa source repository itself, where these files are the originals and
41
+ # editing them is the entire point.
42
+ set -euo pipefail
43
+
44
+ input="$(cat)"
45
+
46
+ command -v jq >/dev/null 2>&1 || exit 0
47
+
48
+ # The operator's override, checked first so it is always the cheapest way out.
49
+ if [ -n "${LISA_ALLOW_MANAGED_FILE_WRITE:-}" ]; then
50
+ exit 0
51
+ fi
52
+
53
+ tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
54
+ [ -n "$tool_name" ] || exit 0
55
+
56
+ project_root="${CLAUDE_PROJECT_DIR:-$PWD}"
57
+ package_root="$project_root/node_modules/@codyswann/lisa"
58
+
59
+ # No installed Lisa means nothing to classify against. Silent, because a project
60
+ # that has not installed Lisa is not doing anything wrong.
61
+ [ -d "$package_root" ] || exit 0
62
+
63
+ # In Lisa's own repository these files ARE the originals. Blocking here would
64
+ # make the templates uneditable by the only agents that should edit them.
65
+ if [ -f "$project_root/package.json" ] &&
66
+ grep -q '"name": *"@codyswann/lisa"' "$project_root/package.json" 2>/dev/null; then
67
+ exit 0
68
+ fi
69
+
70
+ # Whether a host-relative path is shipped as a copy-overwrite template.
71
+ #
72
+ # A few stat calls rather than an enumeration: the same relative path is probed
73
+ # under each stack's copy-overwrite tree, so cost does not grow with the 145
74
+ # templates.
75
+ managed_source() {
76
+ local candidate="$1"
77
+ case "$candidate" in
78
+ */node_modules/* | node_modules/* | */dist/* | dist/*) return 1 ;;
79
+ esac
80
+ # Normalise to a project-relative path so an absolute target still matches.
81
+ local rel="${candidate#"$project_root"/}"
82
+ rel="${rel#./}"
83
+ [ -n "$rel" ] || return 1
84
+ local stack
85
+ for stack in "$package_root"/*/copy-overwrite; do
86
+ [ -d "$stack" ] || continue
87
+ if [ -e "$stack/$rel" ]; then
88
+ printf '%s' "${stack#"$package_root"/}/$rel"
89
+ return 0
90
+ fi
91
+ done
92
+ return 1
93
+ }
94
+
95
+ refuse() {
96
+ local target="$1"
97
+ local source="$2"
98
+ cat >&2 <<EOF
99
+ BLOCKED: refusing to write \`$target\`.
100
+
101
+ WHY: this file is Lisa-managed. It is shipped as a **copy-overwrite** template
102
+ (\`$source\` in the installed package) and is replaced wholesale on every
103
+ \`lisa apply\` — which runs on every \`bun install\`. Your edit would survive
104
+ until the next install and then vanish, with nothing reporting that it had.
105
+
106
+ That is not hypothetical: downstream copies edited this way diverged silently
107
+ until they stopped receiving upstream fixes entirely.
108
+
109
+ WHERE IT GOES INSTEAD — take the first one that fits:
110
+
111
+ 1. The change should apply everywhere. Edit the template upstream in Lisa and
112
+ release it — \`/lisa:cross-pollinate\`, or an issue on CodySwannGT/lisa.
113
+ That is the only edit that survives an install.
114
+
115
+ 2. Only this project needs to differ. Look for the local escape hatch beside the
116
+ file — Lisa ships \`.local\` variants for the configs that support one
117
+ (\`eslint.config.local.ts\`, \`tsconfig.local.json\`, \`audit.ignore.local.json\`,
118
+ and others). Those are yours and are never overwritten.
119
+
120
+ 3. The behaviour is configurable rather than hardcoded. Check \`.lisa.config.json\`
121
+ — the \`gates\` block in particular decides which checks run and what command
122
+ proves each one, so a project can substitute its own task without touching a
123
+ shipped file.
124
+
125
+ 4. You believe this file should not be Lisa-managed at all. That is a real
126
+ argument and it belongs upstream, not in a local edit that will be erased.
127
+
128
+ If a human explicitly asked for this edit, they can re-run with
129
+ \`LISA_ALLOW_MANAGED_FILE_WRITE=1\` set, which bypasses this guard.
130
+ EOF
131
+ exit 2
132
+ }
133
+
134
+ case "$tool_name" in
135
+ Write | Edit | MultiEdit | NotebookEdit | Update)
136
+ paths="$(printf '%s' "$input" | jq -r '
137
+ [ .tool_input.file_path?,
138
+ .tool_input.path?,
139
+ .tool_input.notebook_path?,
140
+ (.tool_input.edits? // [] | .[].file_path?)
141
+ ] | map(select(. != null and . != "")) | .[]' 2>/dev/null || true)"
142
+ while IFS= read -r candidate; do
143
+ [ -n "$candidate" ] || continue
144
+ if source_path="$(managed_source "$candidate")"; then
145
+ refuse "$candidate" "$source_path"
146
+ fi
147
+ done <<EOF
148
+ $paths
149
+ EOF
150
+ ;;
151
+ Bash)
152
+ command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
153
+ [ -n "$command_str" ] || exit 0
154
+ # Write signatures only. A bare mention is a read and stays allowed — the
155
+ # path has to be the target of a redirection, a tee, or an in-place sed.
156
+ # `>>?\|?` covers `>`, `>>`, and the noclobber override `>|`.
157
+ while IFS= read -r token; do
158
+ [ -n "$token" ] || continue
159
+ if source_path="$(managed_source "$token")"; then
160
+ refuse "$token" "$source_path"
161
+ fi
162
+ done <<EOF
163
+ $(printf '%s' "$command_str" |
164
+ grep -Eo ">>?\|?[[:space:]]*['\"]?[^ |;&'\"]+|tee([[:space:]]+-[a-zA-Z]+)*[[:space:]]+['\"]?[^ |;&'\"]+|sed[[:space:]]+[^|;&]*-i[^|;&]*[[:space:]]['\"]?[^ |;&'\"]+" 2>/dev/null |
165
+ sed -E "s/^(>>?\|?|tee([[:space:]]+-[a-zA-Z]+)*|sed[[:space:]]+[^|;&]*-i[^|;&]*)[[:space:]]*//" |
166
+ tr -d "\"'" || true)
167
+ EOF
168
+ ;;
169
+ esac
170
+
171
+ exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "3.23.2",
3
+ "version": "3.26.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"