@codyswann/lisa 4.45.7 → 4.45.8

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 (80) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +21 -2
  2. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +68 -0
  3. package/all/merge/.claude/settings.json +10 -0
  4. package/dist/configs/vitest/scratch-global-setup.d.ts +9 -3
  5. package/dist/configs/vitest/scratch-global-setup.d.ts.map +1 -1
  6. package/dist/configs/vitest/scratch-global-setup.js +23 -10
  7. package/dist/configs/vitest/scratch-global-setup.js.map +1 -1
  8. package/dist/configs/vitest/scratch-tmpdir-trust.d.ts +48 -0
  9. package/dist/configs/vitest/scratch-tmpdir-trust.d.ts.map +1 -0
  10. package/dist/configs/vitest/scratch-tmpdir-trust.js +194 -0
  11. package/dist/configs/vitest/scratch-tmpdir-trust.js.map +1 -0
  12. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  13. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  14. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  15. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  16. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  17. package/dist/core/upstream-evidence-manifest.js +13 -8
  18. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  19. package/package.json +5 -5
  20. package/plugins/lisa/.claude-plugin/plugin.json +9 -1
  21. package/plugins/lisa/.codex-plugin/hooks.json +8 -0
  22. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa/hooks/worktree-binding-guard.mjs +68 -0
  24. package/plugins/lisa-agy/plugin.json +1 -1
  25. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  28. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-copilot/.claude-plugin/plugin.json +9 -1
  31. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +68 -0
  32. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-cursor/hooks/hooks.json +3 -0
  34. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +68 -0
  35. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-agy/plugin.json +1 -1
  38. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  58. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails-agy/plugin.json +1 -1
  63. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  68. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  73. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/src/base/.claude-plugin/plugin.json +8 -0
  76. package/plugins/src/base/hooks/worktree-binding-guard.mjs +68 -0
  77. package/scripts/check-shell-guard-refusal-coverage.mjs +49 -3
  78. package/scripts/lib/shell-guard-trace.mjs +26 -8
  79. package/scripts/lisa-enforcement-fallback.sh +21 -2
  80. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +244 -3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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": "4.45.7",
3
+ "version": "4.45.8",
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"
@@ -293,6 +293,14 @@
293
293
  "command": "command -v entire >/dev/null 2>&1 && entire hooks claude-code session-start || true"
294
294
  }
295
295
  ]
296
+ },
297
+ {
298
+ "hooks": [
299
+ {
300
+ "type": "command",
301
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
302
+ }
303
+ ]
296
304
  }
297
305
  ],
298
306
  "SubagentStart": [
@@ -547,6 +547,63 @@ function claimedRoot(toolInput, cwd) {
547
547
  return null;
548
548
  }
549
549
 
550
+ /**
551
+ * Record where the session STARTED, so its first guarded call is checked
552
+ * against a baseline rather than establishing one.
553
+ *
554
+ * ## Why the first call could not be checked before
555
+ *
556
+ * It established the baseline instead of testing one, which is
557
+ * trust-on-first-use — and TOFU is only as safe as its first use. Here the
558
+ * first use is assigned by the harness, unasked (CodySwannGT/lisa#3864), so the
559
+ * one moment the guard trusted was the moment the session had least control
560
+ * over. A session already sitting in a foreign worktree when it first acted
561
+ * bound to that tree and was never refused, and every later call was then
562
+ * checked faithfully against the wrong baseline — defending the session INTO
563
+ * the wrong worktree rather than out of it (CodySwannGT/lisa#3955).
564
+ *
565
+ * ## Why an earlier observation separates what a cleverer check cannot
566
+ *
567
+ * At the first guarded call, "I was displaced before I acted" and "I
568
+ * legitimately started here" present identically: one session id, one observed
569
+ * root, no prior state. No comparison can tell them apart, because the
570
+ * information is not in the hook's inputs at that instant.
571
+ *
572
+ * At session start it is. The displacement mechanism actually observed is a
573
+ * PEER's `EnterWorktree` moving this session (CodySwannGT/lisa#3712), which
574
+ * happens during the session — after it has started. A baseline taken at start
575
+ * is therefore recorded BEFORE the event that moves it, and the first guarded
576
+ * call becomes an ordinary comparison against a baseline that already exists.
577
+ * It reaches the same refusal a later displacement produces, because by then it
578
+ * is one.
579
+ *
580
+ * ## What this does NOT achieve
581
+ *
582
+ * #3955 asks for a baseline from the ASSIGNER rather than from observation.
583
+ * That is not what this is and Lisa cannot build it: Lisa does not own
584
+ * `EnterWorktree`, and nothing the harness writes names a session's assigned
585
+ * worktree in a form a hook can read. This is still an observation, just an
586
+ * earlier one. It closes the window between session start and the first guarded
587
+ * call; it leaves open a session LAUNCHED already displaced, where start
588
+ * observes the foreign tree and trust-on-first-use still applies.
589
+ *
590
+ * Returns nothing on purpose, like its sibling below: SessionStart has no call
591
+ * to refuse, so every path through it allows.
592
+ * @param sessionId - The session this baseline belongs to
593
+ * @param observed - The worktree root the session started in
594
+ */
595
+ function recordBaseline(sessionId, observed) {
596
+ // Never overwrite. A resumed session, a reconnect, or anything else that
597
+ // fires the event twice must not be able to launder a displaced binding into
598
+ // a fresh baseline — which is the whole failure this exists to close.
599
+ if (readState(sessionId)?.boundRoot) return;
600
+ writeState(sessionId, {
601
+ boundRoot: observed,
602
+ claimedRoot: null,
603
+ updatedAt: new Date().toISOString(),
604
+ });
605
+ }
606
+
550
607
  /**
551
608
  * Record what `EnterWorktree` just claimed, for the next tool call to check.
552
609
  *
@@ -648,6 +705,10 @@ function evaluate(payload) {
648
705
  const observed = worktreeRoot(cwd);
649
706
  if (!observed) return 0;
650
707
 
708
+ if (payload.hook_event_name === "SessionStart") {
709
+ recordBaseline(sessionId, observed);
710
+ return 0;
711
+ }
651
712
  if (payload.tool_name === "EnterWorktree") {
652
713
  recordClaim(payload, observed);
653
714
  return 0;
@@ -659,6 +720,13 @@ function evaluate(payload) {
659
720
 
660
721
  const state = readState(sessionId);
661
722
  if (!state?.boundRoot) {
723
+ // No baseline means SessionStart did not run for this session — an older
724
+ // install, a runtime that fires no such event, a harness that skips it.
725
+ // Absence of a baseline is absence of evidence, not evidence of
726
+ // displacement, so this records one exactly as it always did rather than
727
+ // refusing. Treating it as a refusal would turn a floor into a wall on
728
+ // every surface where the event does not fire, and a wall gets switched
729
+ // off, which costs the whole guard.
662
730
  writeState(sessionId, {
663
731
  boundRoot: observed,
664
732
  claimedRoot: null,
@@ -28,9 +28,13 @@
28
28
  *
29
29
  * CLI:
30
30
  * node scripts/check-shell-guard-refusal-coverage.mjs [--json] [--trace FILE]
31
+ * [--trace-out FILE]
31
32
  *
32
33
  * --trace FILE judge an existing trace instead of producing one. Use it in
33
34
  * CI when the full suite already ran with the tracer imported.
35
+ * --trace-out FILE
36
+ * also write the judged trace to FILE, so a later step in the
37
+ * same job can read what this run observed. Never commit it.
34
38
  *
35
39
  * Exit codes (mirroring the sibling check-* scripts):
36
40
  * 0 — every guard the run drove was observed refusing and allowing.
@@ -41,7 +45,7 @@
41
45
  * @module scripts/check-shell-guard-refusal-coverage
42
46
  */
43
47
  import { execFileSync, spawnSync } from "node:child_process";
44
- import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
48
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
45
49
  import { tmpdir } from "node:os";
46
50
  import path from "node:path";
47
51
  import process from "node:process";
@@ -56,6 +60,9 @@ import {
56
60
  tracerIndex,
57
61
  } from "./lib/shell-guard-refusal-coverage.mjs";
58
62
 
63
+ /** The flag naming a file the produced trace should also be written to. */
64
+ const TRACE_OUT_FLAG = "--trace-out";
65
+
59
66
  /** Wall-clock ceiling for the traced vitest run. */
60
67
  const RUN_TIMEOUT_MS = 45 * 60_000;
61
68
 
@@ -185,16 +192,54 @@ function produceTrace(root, population) {
185
192
  };
186
193
  }
187
194
 
195
+ /**
196
+ * Persist the trace this run produced, when the caller asked for it.
197
+ *
198
+ * The trace is the only artefact that can tell a LATER step — the mutation
199
+ * gate's uninstrumentable branch, which until CodySwannGT/lisa#3931 asserted a
200
+ * search it never ran — which shell guards this run actually drove. Written
201
+ * only on request, and never committed: a checked-in trace would report a guard
202
+ * as evidenced long after its driving test was deleted, which is the same false
203
+ * claim with a longer half-life.
204
+ *
205
+ * A write that fails is announced and otherwise ignored. This check's verdict
206
+ * does not depend on the copy, and refusing the run because a convenience file
207
+ * could not be written would trade a real signal for a bookkeeping one.
208
+ * @param {string} root - Repository root.
209
+ * @param {readonly string[]} args - CLI arguments.
210
+ * @param {string} trace - The raw JSONL this run produced.
211
+ * @returns {void}
212
+ */
213
+ function keepTrace(root, args, trace) {
214
+ const at = args.indexOf(TRACE_OUT_FLAG);
215
+ const target = at === -1 ? undefined : args[at + 1];
216
+ if (target === undefined) return;
217
+ const resolved = path.isAbsolute(target) ? target : path.join(root, target);
218
+ try {
219
+ mkdirSync(path.dirname(resolved), { recursive: true });
220
+ writeFileSync(resolved, trace);
221
+ } catch (error) {
222
+ console.error(
223
+ `check:shell-guard-refusal-coverage: could not write ${target}: ${
224
+ error instanceof Error ? error.message : String(error)
225
+ }`
226
+ );
227
+ }
228
+ }
229
+
188
230
  /**
189
231
  * CLI entry point.
190
232
  * @returns {void}
191
233
  */
192
234
  export function main() {
193
235
  const args = process.argv.slice(2);
194
- const known = new Set(["--json", "--trace"]);
236
+ const known = new Set(["--json", "--trace", TRACE_OUT_FLAG]);
237
+ const valueFlags = new Set(["--trace", TRACE_OUT_FLAG]);
195
238
  const unknown = args.find(
196
239
  (arg, at) =>
197
- arg.startsWith("--") && !known.has(arg) && args[at - 1] !== "--trace"
240
+ arg.startsWith("--") &&
241
+ !known.has(arg) &&
242
+ !valueFlags.has(args[at - 1] ?? "")
198
243
  );
199
244
  if (unknown) {
200
245
  console.error(
@@ -216,6 +261,7 @@ export function main() {
216
261
  process.exitCode = 2;
217
262
  return;
218
263
  }
264
+ keepTrace(root, args, produced.trace);
219
265
  const report = judge({ population, observed: parseTrace(produced.trace) });
220
266
  console.log(
221
267
  args.includes("--json")
@@ -55,6 +55,14 @@
55
55
  * It NEVER throws and never changes a return value: a tracer that can break the
56
56
  * suite it observes would be turned off, and then this whole control is prose.
57
57
  *
58
+ * Each record carries the `sha256` that matched, not only the guard path. A
59
+ * consumer reading a trace some other step produced cannot otherwise tell an
60
+ * observation of THIS guard from an observation of the version it replaced, and
61
+ * a guard that inherits its predecessor's coverage is the stale-evidence shape
62
+ * this repository keeps closing (CodySwannGT/lisa#3931). The judge in
63
+ * `shell-guard-refusal-coverage.mjs` does not need it — it hashes the tree
64
+ * itself in the same process — so the field is additive and ignored there.
65
+ *
58
66
  * @module scripts/lib/shell-guard-trace
59
67
  */
60
68
  import { appendFileSync, readFileSync, statSync } from "node:fs";
@@ -98,12 +106,15 @@ function install(trace, index) {
98
106
  * @param {string} origin - Test frame that started it.
99
107
  * @returns {void}
100
108
  */
101
- const record = (script, status, origin) => {
109
+ const record = (script, status, origin, sha256) => {
102
110
  const key = `${script}:${String(status)}`;
103
111
  if (seen.has(key)) return;
104
112
  seen.add(key);
105
113
  try {
106
- appendFileSync(trace, `${JSON.stringify({ script, status, origin })}\n`);
114
+ appendFileSync(
115
+ trace,
116
+ `${JSON.stringify({ script, status, origin, sha256 })}\n`
117
+ );
107
118
  } catch {
108
119
  /* A trace that cannot be written must not fail the suite. */
109
120
  }
@@ -138,7 +149,7 @@ function install(trace, index) {
138
149
  * @param {unknown} command - First argument of the child start.
139
150
  * @param {unknown} args - Second argument: an argv array, or the options.
140
151
  * @param {unknown} third - Third argument, when the second was an argv array.
141
- * @returns {string[]} Repository-relative guard paths.
152
+ * @returns {{guard: string, sha256: string}[]} Guards, each with the digest that matched.
142
153
  */
143
154
  const matched = (command, args, third) => {
144
155
  if (typeof command !== "string") return [];
@@ -157,7 +168,8 @@ function install(trace, index) {
157
168
  typeof (/** @type {{cwd?: unknown}} */ (options).cwd) === "string"
158
169
  ? /** @type {{cwd: string}} */ (options).cwd
159
170
  : process.cwd();
160
- const hits = new Set();
171
+ /** @type {Map<string, string>} */
172
+ const hits = new Map();
161
173
  for (const token of tokens) {
162
174
  if (!token || token.length > MAX_PATH_LENGTH) continue;
163
175
  const absolute = path.isAbsolute(token)
@@ -179,12 +191,14 @@ function install(trace, index) {
179
191
  const digest = createHash("sha256")
180
192
  .update(readFileSync(absolute))
181
193
  .digest("hex");
182
- for (const guard of inventory.byHash[digest] ?? []) hits.add(guard);
194
+ for (const guard of inventory.byHash[digest] ?? []) {
195
+ hits.set(guard, digest);
196
+ }
183
197
  } catch {
184
198
  continue;
185
199
  }
186
200
  }
187
- return [...hits];
201
+ return [...hits].map(([guard, sha256]) => ({ guard, sha256 }));
188
202
  };
189
203
 
190
204
  /**
@@ -213,7 +227,9 @@ function install(trace, index) {
213
227
  const guards = matched(command, args, options);
214
228
  if (guards.length > 0) {
215
229
  const where = origin();
216
- for (const guard of guards) record(guard, result.status, where);
230
+ for (const hit of guards) {
231
+ record(hit.guard, result.status, where, hit.sha256);
232
+ }
217
233
  }
218
234
  } catch {
219
235
  /* Never let observation change the observed. */
@@ -234,7 +250,9 @@ function install(trace, index) {
234
250
  const guards = matched(command, args, options);
235
251
  if (guards.length === 0) return;
236
252
  const where = origin();
237
- for (const guard of guards) record(guard, status, where);
253
+ for (const hit of guards) {
254
+ record(hit.guard, status, where, hit.sha256);
255
+ }
238
256
  } catch {
239
257
  /* Never let observation change the observed. */
240
258
  }
@@ -480,6 +480,25 @@ notice_marker=""
480
480
  # non-sticky shared TMPDIR: another user could replace that parent between the
481
481
  # checks below. Root- or caller-owned parents are acceptable; any parent that
482
482
  # grants group or other write access must also carry the sticky bit.
483
+ #
484
+ # The two `stat` spellings must be asked for the SAME twelve bits, and getting
485
+ # that wrong is invisible because each platform only ever runs its own branch.
486
+ # BSD `%Lp` renders the low NINE bits and drops setuid/setgid/sticky entirely:
487
+ #
488
+ # stat -f '%Lp' <a 1777 dir> -> 777 <- sticky gone
489
+ # stat -f '%p' <a 1777 dir> -> 41777 <- sticky present, plus file type
490
+ # stat -c '%a' <a 1777 dir> -> 1777 <- sticky present (GNU includes it)
491
+ #
492
+ # So under `%Lp` the sticky test below is `777 & 1000`, which is zero for every
493
+ # directory on the machine — the exemption was unreachable on macOS and the
494
+ # whole `&& [ ... 8#1000 ] -eq 0` clause was dead code there. The same logical
495
+ # root was therefore trusted on Linux and rejected on macOS, and since CI is
496
+ # Linux the stricter platform was the one nobody specified and nobody tests on
497
+ # (CodySwannGT/lisa#3691).
498
+ #
499
+ # `%p` restores the bit and adds the file type above it, so the mask normalizes
500
+ # both spellings to one value and the arithmetic below has a single meaning:
501
+ # `41777 & 7777` and `1777 & 7777` are both 1777.
483
502
  notice_directory_trusted() {
484
503
  local directory="$1"
485
504
  local directory_stat=""
@@ -487,7 +506,7 @@ notice_directory_trusted() {
487
506
  local directory_mode=""
488
507
  local directory_mode_value=0
489
508
 
490
- directory_stat="$(stat -f '%u %Lp' "$directory" 2>/dev/null)" || \
509
+ directory_stat="$(stat -f '%u %p' "$directory" 2>/dev/null)" || \
491
510
  directory_stat="$(stat -c '%u %a' "$directory" 2>/dev/null)" || return 1
492
511
  directory_owner="${directory_stat%% *}"
493
512
  directory_mode="${directory_stat#* }"
@@ -495,7 +514,7 @@ notice_directory_trusted() {
495
514
  *[!0-9:]* | :* | *: ) return 1 ;;
496
515
  esac
497
516
  [ "$directory_owner" = "0" ] || [ "$directory_owner" = "$notice_uid" ] || return 1
498
- directory_mode_value=$((8#$directory_mode))
517
+ directory_mode_value=$((8#$directory_mode & 8#7777))
499
518
  if [ $((directory_mode_value & 8#0022)) -ne 0 ] && \
500
519
  [ $((directory_mode_value & 8#1000)) -eq 0 ]; then
501
520
  return 1