@codyswann/lisa 4.17.5 → 4.17.6

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 (57) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +10 -0
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +3 -2
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa-agy/plugin.json +1 -1
  8. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  11. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-expo-agy/plugin.json +1 -1
  18. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  23. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  28. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  33. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  38. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-rails-agy/plugin.json +1 -1
  43. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  48. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  53. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  55. package/scripts/check-shell-guard-refusal-coverage.mjs +233 -0
  56. package/scripts/lib/shell-guard-refusal-coverage.mjs +315 -0
  57. package/scripts/lib/shell-guard-trace.mjs +256 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * shell-guard-trace — record which shipped `.sh` guards a test run actually
3
+ * EXECUTES, and with what exit status (CodySwannGT/lisa#3190).
4
+ *
5
+ * @remarks
6
+ * ## Why a runtime trace rather than a scan of the tests
7
+ *
8
+ * The question this module answers is "which guard did the suite run", and
9
+ * every static spelling of that question has been tried here and is wrong in
10
+ * the direction that produces silence:
11
+ *
12
+ * - **By filename.** `foo.test.ts` next to `foo.sh` proves nothing. A test
13
+ * named after a guard that never runs it is the deceptive case
14
+ * CodySwannGT/lisa#3190 was filed about, and a scan keyed on names reports
15
+ * it as covered.
16
+ * - **By the path appearing in the test source.** Measured on this tree:
17
+ * 258 of 259 tracked `.sh` files are named somewhere inside a test file that
18
+ * also spawns something — inventories, manifests, and parity tables mention
19
+ * every shipped artifact. The signal is noise.
20
+ * - **By resolving the spawn argument statically.** Precise where it works
21
+ * (26 guards found) and blind exactly where this repository's suites are
22
+ * most interesting: the dominant idiom READS a guard's bytes, writes them
23
+ * into a fixture project under a different name, and executes THAT. The
24
+ * shipped path never reaches a spawn argument at all.
25
+ *
26
+ * The trace has none of those failure modes because it observes the process
27
+ * table instead of arguing about it, and it handles the write-a-copy idiom by
28
+ * matching on **content hash**: the copy is byte-identical to the guard, so it
29
+ * is the guard.
30
+ *
31
+ * ## What it does not see, stated rather than hidden
32
+ *
33
+ * - A helper reached by `source` rather than by `bash` is not a child process
34
+ * and never appears here. `lisa-edit-gate.sh` is the live example — every
35
+ * on-edit hook sources it. Such a helper enters the population only when a
36
+ * suite executes it directly, which is how CodySwannGT/lisa#3190's roster
37
+ * covers it.
38
+ * - Asynchronous child starts are not patched. Every child start in this
39
+ * test tree is synchronous by rule — see `tests/helpers/unbounded-spawn-scan`
40
+ * — so patching the three synchronous entry points covers the population,
41
+ * and a suite that started a guard asynchronously would be a rule violation
42
+ * caught by that scan first.
43
+ * - A suite that mocks `node:child_process` wholesale never reaches this
44
+ * module, and nothing real ran, so there is nothing to record.
45
+ *
46
+ * ## Loaded with `--import`, not as a vitest setup file
47
+ *
48
+ * `--import` runs before any user module links, so the patch lands on the CJS
49
+ * `child_process` exports that node's ESM facade for the builtin is later
50
+ * generated from. Measured: a vitest case using `import { spawnSync } from
51
+ * "node:child_process"` is intercepted. A vitest `setupFiles` entry runs after
52
+ * linking and would miss every already-bound import, and it would also collide
53
+ * with the suites that `vi.mock` the module.
54
+ *
55
+ * It NEVER throws and never changes a return value: a tracer that can break the
56
+ * suite it observes would be turned off, and then this whole control is prose.
57
+ *
58
+ * @module scripts/lib/shell-guard-trace
59
+ */
60
+ import { appendFileSync, readFileSync, statSync } from "node:fs";
61
+ import { createHash } from "node:crypto";
62
+ import { createRequire } from "node:module";
63
+ import path from "node:path";
64
+ import process from "node:process";
65
+
66
+ /** Largest file the tracer will hash, in bytes. */
67
+ const MAX_HASHED_BYTES = 2_000_000;
68
+
69
+ /** Longest argv element the tracer will treat as a possible path. */
70
+ const MAX_PATH_LENGTH = 4096;
71
+
72
+ const traceFile = process.env["LISA_SHELL_GUARD_TRACE"];
73
+ const indexFile = process.env["LISA_SHELL_GUARD_INDEX"];
74
+
75
+ if (traceFile && indexFile) install(traceFile, indexFile);
76
+
77
+ /**
78
+ * Patch the synchronous child starts so every guard execution is recorded.
79
+ * @param {string} trace - Path of the JSONL trace to append to.
80
+ * @param {string} index - Path of the guard index JSON to read.
81
+ * @returns {void}
82
+ */
83
+ function install(trace, index) {
84
+ /** @type {{ byHash: Record<string, string[]>, sizes: number[] }} */
85
+ let inventory;
86
+ try {
87
+ inventory = JSON.parse(readFileSync(index, "utf8"));
88
+ } catch {
89
+ return;
90
+ }
91
+ const sizes = new Set(inventory.sizes);
92
+ const seen = new Set();
93
+
94
+ /**
95
+ * Append one observation, at most once per guard/status pair per process.
96
+ * @param {string} script - Repository-relative guard path.
97
+ * @param {number | null} status - The guard's exit status.
98
+ * @param {string} origin - Test frame that started it.
99
+ * @returns {void}
100
+ */
101
+ const record = (script, status, origin) => {
102
+ const key = `${script}:${String(status)}`;
103
+ if (seen.has(key)) return;
104
+ seen.add(key);
105
+ try {
106
+ appendFileSync(trace, `${JSON.stringify({ script, status, origin })}\n`);
107
+ } catch {
108
+ /* A trace that cannot be written must not fail the suite. */
109
+ }
110
+ };
111
+
112
+ /**
113
+ * Whether one child start RUNS its shell operand rather than inspecting it.
114
+ *
115
+ * This distinction is load-bearing and was found by measurement, not by
116
+ * reasoning. Without it, `bash -n script.sh` — a syntax check that never
117
+ * executes a line — reported fourteen `scripts/*.sh` as "driven, only ever
118
+ * exit 0", which is a false accusation of exactly the defect this control
119
+ * looks for. `shellcheck`, `cat` and `cp` name a guard in argv for the same
120
+ * reason and run none of it.
121
+ *
122
+ * So the operand counts only under an interpreter that will execute it, with
123
+ * POSIX `-n` (noexec) absent.
124
+ * @param {string} command - The command the child start was given.
125
+ * @param {string[]} argv - Its argument vector.
126
+ * @returns {boolean} Whether the operand is being executed.
127
+ */
128
+ const executes = (command, argv) => {
129
+ const runner = path.basename(command.split(/\s+/u)[0] ?? "");
130
+ if (!["bash", "sh", "dash", "ksh", "zsh", "env"].includes(runner)) {
131
+ return false;
132
+ }
133
+ return !argv.includes("-n");
134
+ };
135
+
136
+ /**
137
+ * Every tracked guard whose bytes appear in one child start's argv.
138
+ * @param {unknown} command - First argument of the child start.
139
+ * @param {unknown} args - Second argument: an argv array, or the options.
140
+ * @param {unknown} third - Third argument, when the second was an argv array.
141
+ * @returns {string[]} Repository-relative guard paths.
142
+ */
143
+ const matched = (command, args, third) => {
144
+ if (typeof command !== "string") return [];
145
+ const argv = Array.isArray(args)
146
+ ? args.filter(entry => typeof entry === "string")
147
+ : [];
148
+ // A guard invoked as the command itself is executing itself; anything else
149
+ // is executing only when the interpreter says so.
150
+ const tokens = executes(command, argv)
151
+ ? [...command.split(/\s+/u), ...argv]
152
+ : [command];
153
+ const options = Array.isArray(args) ? third : args;
154
+ const cwd =
155
+ options !== null &&
156
+ typeof options === "object" &&
157
+ typeof (/** @type {{cwd?: unknown}} */ (options).cwd) === "string"
158
+ ? /** @type {{cwd: string}} */ (options).cwd
159
+ : process.cwd();
160
+ const hits = new Set();
161
+ for (const token of tokens) {
162
+ if (!token || token.length > MAX_PATH_LENGTH) continue;
163
+ const absolute = path.isAbsolute(token)
164
+ ? token
165
+ : path.resolve(cwd, token);
166
+ let size = -1;
167
+ try {
168
+ const stats = statSync(absolute);
169
+ if (!stats.isFile()) continue;
170
+ size = stats.size;
171
+ } catch {
172
+ continue;
173
+ }
174
+ // Hash only what could be a guard. Every tracked guard's size is known,
175
+ // so a `git`, a `node`, or a fixture source is rejected by an integer
176
+ // comparison rather than by reading it.
177
+ if (!sizes.has(size) || size > MAX_HASHED_BYTES) continue;
178
+ try {
179
+ const digest = createHash("sha256")
180
+ .update(readFileSync(absolute))
181
+ .digest("hex");
182
+ for (const guard of inventory.byHash[digest] ?? []) hits.add(guard);
183
+ } catch {
184
+ continue;
185
+ }
186
+ }
187
+ return [...hits];
188
+ };
189
+
190
+ /**
191
+ * The first stack frame inside the test tree, for the failure report.
192
+ * @returns {string} A `file:line` frame, or an empty string.
193
+ */
194
+ const origin = () => {
195
+ const frames = (new Error("trace").stack ?? "")
196
+ .split("\n")
197
+ .filter(line => line.includes("/tests/"));
198
+ // The suite, not the helper it reached through. Every bounded spawn in this
199
+ // tree goes through `tests/helpers/io-latency-budget`, so naming the first
200
+ // `tests/` frame would name that helper for nearly every observation and
201
+ // the report would say nothing about where to look.
202
+ const frame = frames.find(line => line.includes(".test.ts")) ?? frames[0];
203
+ return (frame ?? "").trim().replace(/^at\s+/u, "");
204
+ };
205
+
206
+ const require = createRequire(import.meta.url);
207
+ const childProcess = require("node:child_process");
208
+
209
+ const originalSpawnSync = childProcess.spawnSync;
210
+ childProcess.spawnSync = function tracedSpawnSync(command, args, options) {
211
+ const result = originalSpawnSync.call(this, command, args, options);
212
+ try {
213
+ const guards = matched(command, args, options);
214
+ if (guards.length > 0) {
215
+ const where = origin();
216
+ for (const guard of guards) record(guard, result.status, where);
217
+ }
218
+ } catch {
219
+ /* Never let observation change the observed. */
220
+ }
221
+ return result;
222
+ };
223
+
224
+ for (const name of ["execFileSync", "execSync"]) {
225
+ const original = childProcess[name];
226
+ childProcess[name] = function tracedExec(command, args, options) {
227
+ /**
228
+ * Record one outcome of the wrapped call.
229
+ * @param {number | null} status - Its exit status.
230
+ * @returns {void}
231
+ */
232
+ const observe = status => {
233
+ try {
234
+ const guards = matched(command, args, options);
235
+ if (guards.length === 0) return;
236
+ const where = origin();
237
+ for (const guard of guards) record(guard, status, where);
238
+ } catch {
239
+ /* Never let observation change the observed. */
240
+ }
241
+ };
242
+ try {
243
+ const result = original.call(this, command, args, options);
244
+ observe(0);
245
+ return result;
246
+ } catch (error) {
247
+ observe(
248
+ typeof (/** @type {{status?: unknown}} */ (error).status) === "number"
249
+ ? /** @type {{status: number}} */ (error).status
250
+ : null
251
+ );
252
+ throw error;
253
+ }
254
+ };
255
+ }
256
+ }