@codyswann/lisa 2.324.0 → 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 (73) 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/merge/.claude/settings.json +19 -4
  6. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +15 -6
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/dist/standards/git-state.d.ts.map +1 -1
  10. package/dist/standards/git-state.js +10 -1
  11. package/dist/standards/git-state.js.map +1 -1
  12. package/package.json +1 -1
  13. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa/rules/eager/tracked-work.md +1 -1
  16. package/plugins/lisa/rules/reference/tracked-work.md +1 -1
  17. package/plugins/lisa-agy/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  21. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/rules/eager/tracked-work.md +1 -1
  25. package/plugins/lisa-copilot/rules/reference/tracked-work.md +1 -1
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/rules/tracked-work-reference.mdc +1 -1
  28. package/plugins/lisa-cursor/rules/tracked-work.mdc +1 -1
  29. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-agy/plugin.json +1 -1
  32. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  52. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails-agy/plugin.json +1 -1
  57. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  62. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  67. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/src/base/rules/eager/tracked-work.md +1 -1
  70. package/plugins/src/base/rules/reference/tracked-work.md +1 -1
  71. package/scripts/build-plugins.sh +32 -0
  72. package/scripts/generate-upstream-evidence-manifest.mjs +50 -11
  73. package/scripts/lisa-enforcement-fallback.sh +16 -2
@@ -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"
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse hook for Bash: blocks commands that bypass git's verification hooks.
3
+ # Bypassing pre-commit/pre-push hooks (which exist for a reason) is blocked in
4
+ # all of its forms; the fix is to address the underlying issue, not silence the
5
+ # check. See feedback_never_no_verify in user memory.
6
+ #
7
+ # Blocked bypass vectors:
8
+ # 1. the --no-verify long flag (any subcommand, any position, incl. subshells);
9
+ # 2. HUSKY=0 / HUSKY_SKIP_HOOKS=... — disables husky-managed git hooks;
10
+ # 3. core.hooksPath pointed at /dev/null or set empty — disables ALL git hooks.
11
+ #
12
+ # Shell-token matching avoids false positives from issue bodies, heredocs, and
13
+ # commit-message prose while still catching quoted real argv values such as
14
+ # `git -c "core.hooksPath=/dev/null"`.
15
+ #
16
+ # The short `-n` form is intentionally NOT matched (see block-no-verify.agy.sh):
17
+ # grep cannot distinguish a real -n option from -n in commit-message prose or an
18
+ # unrelated piped command, and -n is far more common than --no-verify.
19
+ set -euo pipefail
20
+
21
+ input="$(cat)"
22
+
23
+ tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
24
+ if [ "$tool_name" != "Bash" ]; then
25
+ exit 0
26
+ fi
27
+
28
+ command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
29
+ if [ -z "$command_str" ]; then
30
+ exit 0
31
+ fi
32
+
33
+ command -v python3 >/dev/null 2>&1 || exit 0
34
+
35
+ if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
36
+ import os
37
+ import re
38
+ import shlex
39
+ import sys
40
+
41
+ command = os.environ.get("BLOCK_NO_VERIFY_COMMAND", "")
42
+
43
+
44
+ def strip_heredocs(text: str) -> str:
45
+ lines = text.splitlines()
46
+ output = []
47
+ pending = []
48
+ marker_pattern = re.compile(
49
+ r"<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
50
+ )
51
+ index = 0
52
+ while index < len(lines):
53
+ line = lines[index]
54
+ output.append(line)
55
+ pending.extend(
56
+ next(group for group in match.groups() if group)
57
+ for match in marker_pattern.finditer(line)
58
+ )
59
+ index += 1
60
+ while pending and index < len(lines):
61
+ if lines[index].strip() == pending[0]:
62
+ output.append(lines[index])
63
+ pending.pop(0)
64
+ index += 1
65
+ break
66
+ index += 1
67
+ return "\n".join(output)
68
+
69
+
70
+ try:
71
+ tokens = shlex.split(strip_heredocs(command), posix=True)
72
+ except ValueError:
73
+ sys.exit(0)
74
+
75
+ normalized_tokens = [token.strip("();|&") for token in tokens]
76
+
77
+ for i, token in enumerate(normalized_tokens):
78
+ if token == "--no-verify":
79
+ sys.exit(1)
80
+ if token == "HUSKY=0" or token.startswith("HUSKY_SKIP_HOOKS="):
81
+ sys.exit(1)
82
+ if token.startswith("core.hooksPath="):
83
+ value = token.split("=", 1)[1]
84
+ if value in ("", "/dev/null"):
85
+ sys.exit(1)
86
+ if token == "core.hooksPath" and i + 1 < len(normalized_tokens) and normalized_tokens[i + 1] in ("", "/dev/null"):
87
+ sys.exit(1)
88
+
89
+ sys.exit(0)
90
+ PY
91
+ then
92
+ cat >&2 <<'EOF'
93
+ Blocked: this command bypasses pre-commit/pre-push hooks (--no-verify, HUSKY=0,
94
+ or core.hooksPath disabling). Fix the underlying issue (security audit, lint,
95
+ typecheck, tests, formatting) instead. If a fix is genuinely impossible, ask the
96
+ user to make the risk-acceptance decision and add a specific documented ignore;
97
+ never bypass the hook.
98
+ EOF
99
+ exit 2
100
+ fi
101
+
102
+ exit 0
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse hook for Bash: blocks structural JSON parsing with text tools.
3
+ # Text tools (grep/sed/cut/awk) break on valid JSON — multiline values, escaped
4
+ # quotes, reordered keys, nested objects — producing silently wrong output
5
+ # instead of errors. jq is the required tool for structural JSON reads and
6
+ # writes in shell. Promoted from PROJECT_RULES.md prose to an executable
7
+ # control by the learnings gardener (issue #1787).
8
+ #
9
+ # Precision-first: this hook fires only on high-confidence STRUCTURAL parsing
10
+ # of a *.json input — never on plain text search. Blocked signatures:
11
+ # 1. `sed -i` (or an s/…/…/ program) applied to a .json file or stream;
12
+ # 2. `cut -d` with a structural delimiter (quote/colon/comma) on .json input;
13
+ # 3. `awk` field extraction (-F or a $N program) on .json input;
14
+ # 4. `grep -o` on .json input (value extraction, not search).
15
+ # A "json input" is a *.json argument, a `< file.json` redirection, or a
16
+ # pipeline stream originating from a .json file (e.g. `cat x.json | …`).
17
+ #
18
+ # Exemptions (allowed):
19
+ # - any command that invokes jq anywhere (already compliant or mixed-legit);
20
+ # - search-only usage: plain grep / grep -l / rg with no extraction signature;
21
+ # - *.jsonl targets (line-delimited streams are legitimately line-tooled);
22
+ # - heredoc payload text (stripped before classification).
23
+ set -euo pipefail
24
+
25
+ input="$(cat)"
26
+
27
+ tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
28
+ if [ "$tool_name" != "Bash" ]; then
29
+ exit 0
30
+ fi
31
+
32
+ command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
33
+ if [ -z "$command_str" ]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Fast path: no ".json" reference at all means nothing to classify.
38
+ case "$command_str" in
39
+ *.json*) ;;
40
+ *) exit 0 ;;
41
+ esac
42
+
43
+ command -v python3 >/dev/null 2>&1 || exit 0
44
+
45
+ if ! BLOCK_SHELL_JSON_COMMAND="$command_str" python3 - <<'PY'
46
+ import os
47
+ import re
48
+ import shlex
49
+ import sys
50
+
51
+ command = os.environ.get("BLOCK_SHELL_JSON_COMMAND", "")
52
+
53
+ TEXT_TOOLS = {"grep", "egrep", "fgrep", "sed", "gsed", "awk", "gawk", "cut"}
54
+ WRAPPERS = {"command", "env", "sudo", "time", "nohup"}
55
+ ASSIGNMENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
56
+
57
+
58
+ def strip_heredoc_bodies(text: str) -> str:
59
+ """Remove heredoc payload lines so prose .json mentions never classify."""
60
+ lines = text.splitlines()
61
+ output = []
62
+ marker_pattern = re.compile(
63
+ r"<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|\\?([A-Za-z_][A-Za-z0-9_]*))"
64
+ )
65
+ index = 0
66
+ while index < len(lines):
67
+ line = lines[index]
68
+ output.append(line)
69
+ markers = [
70
+ next(group for group in match.groups() if group)
71
+ for match in marker_pattern.finditer(line)
72
+ ]
73
+ index += 1
74
+ for marker in markers:
75
+ while index < len(lines):
76
+ if lines[index].strip() == marker:
77
+ index += 1
78
+ break
79
+ index += 1
80
+ return "\n".join(output)
81
+
82
+
83
+ def tokenize(text: str):
84
+ lex = shlex.shlex(text, posix=True, punctuation_chars="|&;<>")
85
+ lex.whitespace_split = True
86
+ try:
87
+ return list(lex)
88
+ except ValueError:
89
+ return None
90
+
91
+
92
+ def is_json_path(token: str) -> bool:
93
+ return token.lower().rstrip(")};").endswith(".json")
94
+
95
+
96
+ def split_pipelines(tokens):
97
+ """Yield lists of pipe-connected segments; each segment is a token list."""
98
+ pipelines, pipeline, segment = [], [], []
99
+ for token in tokens:
100
+ if token in ("|", "|&"):
101
+ pipeline.append(segment)
102
+ segment = []
103
+ elif token in (";", "&&", "||", "&", "\n"):
104
+ pipeline.append(segment)
105
+ pipelines.append(pipeline)
106
+ pipeline, segment = [], []
107
+ else:
108
+ segment.append(token)
109
+ pipeline.append(segment)
110
+ pipelines.append(pipeline)
111
+ return pipelines
112
+
113
+
114
+ def segment_parts(segment):
115
+ """Return (tool, args, reads_json) for one pipeline segment."""
116
+ reads_json = False
117
+ tool = None
118
+ args = []
119
+ index = 0
120
+ while index < len(segment):
121
+ token = segment[index]
122
+ if token == "<":
123
+ if index + 1 < len(segment) and is_json_path(segment[index + 1]):
124
+ reads_json = True
125
+ index += 2
126
+ continue
127
+ if token in (">", ">>", "<<", "<<<"):
128
+ index += 2
129
+ continue
130
+ if tool is None and (ASSIGNMENT.match(token) or token in WRAPPERS):
131
+ index += 1
132
+ continue
133
+ if tool is None:
134
+ tool = token.rsplit("/", 1)[-1]
135
+ else:
136
+ args.append(token)
137
+ if is_json_path(token):
138
+ reads_json = True
139
+ index += 1
140
+ return tool, args, reads_json
141
+
142
+
143
+ def flags_of(args):
144
+ return [a for a in args if a.startswith("-")]
145
+
146
+
147
+ def is_violation(tool, args, has_json_input):
148
+ if not has_json_input:
149
+ return False
150
+ flags = flags_of(args)
151
+ if tool in ("grep", "egrep", "fgrep"):
152
+ if any(f in ("-l", "-L", "--files-with-matches", "--files-without-match") for f in flags):
153
+ return False
154
+ return any(f == "-o" or f == "--only-matching" or ("o" in f[1:] and not f.startswith("--")) for f in flags)
155
+ if tool in ("sed", "gsed"):
156
+ if any(f.startswith("-i") or f == "--in-place" for f in flags):
157
+ return True
158
+ return any(re.match(r"^-?\d*s[/|#]", a) or a.startswith("s/") or a.startswith("s|") for a in args)
159
+ if tool == "cut":
160
+ for i, arg in enumerate(args):
161
+ if arg == "-d" and i + 1 < len(args) and args[i + 1] in ('"', "'", ":", ","):
162
+ return True
163
+ if arg.startswith("-d") and len(arg) > 2 and arg[2:] in ('"', "'", ":", ","):
164
+ return True
165
+ return False
166
+ if tool in ("awk", "gawk"):
167
+ if any(f == "-F" or f.startswith("-F") for f in flags):
168
+ return True
169
+ return any(re.search(r"\$\d", a) for a in args if not a.startswith("-"))
170
+ return False
171
+
172
+
173
+ tokens = tokenize(strip_heredoc_bodies(command))
174
+ if tokens is None:
175
+ sys.exit(0)
176
+
177
+ pipelines = split_pipelines(tokens)
178
+
179
+ # Whole-command jq exemption: any jq stage means the author is already using
180
+ # the right tool; mixed pipelines (jq | grep) are legitimate.
181
+ for pipeline in pipelines:
182
+ for segment in pipeline:
183
+ tool, _args, _reads = segment_parts(segment)
184
+ if tool == "jq":
185
+ sys.exit(0)
186
+
187
+ for pipeline in pipelines:
188
+ json_stream = False
189
+ for segment in pipeline:
190
+ tool, args, reads_json = segment_parts(segment)
191
+ if tool is None:
192
+ continue
193
+ has_json_input = reads_json or json_stream
194
+ if tool in TEXT_TOOLS and is_violation(tool, args, has_json_input):
195
+ sys.exit(1)
196
+ # Streams originating from a .json file stay json-classified through
197
+ # pass-through text stages (cat x.json | grep … | cut …).
198
+ if reads_json or (json_stream and tool in TEXT_TOOLS | {"cat", "head", "tail", "sort", "uniq", "tr", "xargs"}):
199
+ json_stream = True
200
+ else:
201
+ json_stream = False
202
+
203
+ sys.exit(0)
204
+ PY
205
+ then
206
+ cat >&2 <<'EOF'
207
+ BLOCKED: this command parses JSON with text tools (grep/sed/cut/awk). Text
208
+ tools break on valid JSON — multiline values, escaped quotes, reordered keys,
209
+ nested objects — producing silently wrong output instead of errors. Use jq for
210
+ all structural JSON reads and writes in shell. Typical fixes:
211
+ read a field: jq -r '.field' file.json
212
+ filter items: jq '.items[] | select(.name=="x")' file.json
213
+ edit in place: jq '.key="value"' file.json > tmp && mv tmp file.json
214
+ If you are only SEARCHING text inside a JSON file (not extracting values), use
215
+ `rg <pattern> file.json` with no extraction pipe and this hook will not fire.
216
+ EOF
217
+ exit 2
218
+ fi
219
+
220
+ exit 0