@codyswann/lisa 4.34.21 → 4.35.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 (94) hide show
  1. package/all/copy-overwrite/scripts/check-npm-publish-landed.mjs +49 -26
  2. package/all/copy-overwrite/scripts/lib/kill-marks.mjs +1 -0
  3. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +49 -10
  4. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +42 -4
  5. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +33 -12
  6. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +9 -4
  7. package/all/copy-overwrite/scripts/lisa-work-item.mjs +9 -6
  8. package/dist/codex/scripts/_extract-edit-paths.sh +4 -0
  9. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  10. package/dist/core/lisa-owned-hash-ledger.js +20 -0
  11. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  12. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  13. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  14. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  15. package/dist/core/upstream-evidence-manifest.js +21 -10
  16. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  17. package/dist/migrations/ensure-lisa-postinstall.d.ts +6 -37
  18. package/dist/migrations/ensure-lisa-postinstall.d.ts.map +1 -1
  19. package/dist/migrations/ensure-lisa-postinstall.js +20 -67
  20. package/dist/migrations/ensure-lisa-postinstall.js.map +1 -1
  21. package/dist/opencode/plugin-templates/parity-safety-net.sh +33 -12
  22. package/package.json +4 -4
  23. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa/hooks/block-direct-issue-create.sh +49 -10
  26. package/plugins/lisa/hooks/block-managed-file-edits.sh +42 -4
  27. package/plugins/lisa/hooks/parity-safety-net.sh +33 -12
  28. package/plugins/lisa/rules/eager/measurement-integrity.md +18 -0
  29. package/plugins/lisa/rules/reference/measurement-integrity.md +109 -0
  30. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +49 -10
  31. package/plugins/lisa-agy/hooks/parity-safety-net.sh +33 -12
  32. package/plugins/lisa-agy/plugin.json +1 -1
  33. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  36. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +49 -10
  40. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +42 -4
  41. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +33 -12
  42. package/plugins/lisa-copilot/rules/eager/measurement-integrity.md +18 -0
  43. package/plugins/lisa-copilot/rules/reference/measurement-integrity.md +109 -0
  44. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +49 -10
  46. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +42 -4
  47. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +33 -12
  48. package/plugins/lisa-cursor/rules/measurement-integrity-reference.mdc +114 -0
  49. package/plugins/lisa-cursor/rules/measurement-integrity.mdc +23 -0
  50. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-expo-agy/plugin.json +1 -1
  53. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  73. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails-agy/plugin.json +1 -1
  78. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  83. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  88. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/src/base/hooks/block-direct-issue-create.sh +49 -10
  91. package/plugins/src/base/hooks/block-managed-file-edits.sh +42 -4
  92. package/plugins/src/base/hooks/parity-safety-net.sh +33 -12
  93. package/plugins/src/base/rules/eager/measurement-integrity.md +18 -0
  94. package/plugins/src/base/rules/reference/measurement-integrity.md +109 -0
@@ -179,12 +179,14 @@ EOF
179
179
  # The patch file is per-worktree: `mktemp` names it, so nothing has to invent a
180
180
  # filename, and it lives in this agent's own TMPDIR where no sibling can reach
181
181
  # it. Untracked files are unaffected by both refused operations, so `git diff`
182
- # captures everything at risk.
182
+ # captures everything at risk. `HEAD` includes staged and unstaged changes, and
183
+ # `--binary` keeps binary edits recoverable rather than emitting a text-only
184
+ # placeholder that cannot be applied.
183
185
  PRESERVE_GUIDANCE="$(
184
186
  cat <<'EOF'
185
187
  Preserve the work first, per-worktree:
186
188
 
187
- git diff [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
189
+ git diff --binary HEAD [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
188
190
 
189
191
  and restore it later with `git apply <that file>`.
190
192
 
@@ -444,6 +446,7 @@ follow_refuse() {
444
446
  # so peak process count never rises. The latency was the whole problem.
445
447
  follow_unwrapped=""
446
448
  follow_operand_index=0
449
+ follow_located=""
447
450
 
448
451
  # Strip substitution, quote and alias wrappers off one token. A sibling of
449
452
  # strip_subst_wrappers() below rather than a call to it: that one is defined
@@ -568,6 +571,7 @@ follow_add_base() {
568
571
  # this hook cannot evaluate.
569
572
  follow_locate() {
570
573
  local token="$1" home="${HOME:-}" tmp="${TMPDIR:-/tmp}" proj="${CLAUDE_PROJECT_DIR:-$PWD}" base
574
+ follow_located=""
571
575
  tmp="${tmp%/}"
572
576
  [ -n "$tmp" ] || tmp="/tmp"
573
577
  # The tilde arms below are LITERAL text: this is the unexpanded spelling the
@@ -589,14 +593,14 @@ follow_locate() {
589
593
  "" | *'$'* | *'*'* | *'?'* | *'`'* | *'{'* | *'['*) return 1 ;;
590
594
  /*)
591
595
  [ -f "$token" ] && [ -r "$token" ] || return 1
592
- printf '%s' "$token"
596
+ follow_located="$token"
593
597
  return 0
594
598
  ;;
595
599
  esac
596
600
  while IFS= read -r base; do
597
601
  [ -n "$base" ] || continue
598
602
  if [ -f "$base/$token" ] && [ -r "$base/$token" ]; then
599
- printf '%s' "$base/$token"
603
+ follow_located="$base/$token"
600
604
  return 0
601
605
  fi
602
606
  done <<<"$PWD"$'\n'"$proj$follow_bases"
@@ -641,12 +645,13 @@ follow_take() {
641
645
  follow_target() {
642
646
  local token="$1" depth="$2" path=""
643
647
  [ -n "$token" ] || return 0
644
- if ! path="$(follow_locate "$token")"; then
648
+ if ! follow_locate "$token"; then
645
649
  if [ "$depth" -eq 0 ]; then
646
650
  follow_refuse "$token" "no readable file could be resolved for it"
647
651
  fi
648
652
  return 0
649
653
  fi
654
+ path="$follow_located"
650
655
  follow_take "$path" "$depth"
651
656
  }
652
657
 
@@ -667,11 +672,18 @@ follow_target() {
667
672
  # Accepted residual: a shebang script whose relative path resolves only from a
668
673
  # working directory this hook does not share is not followed.
669
674
  follow_direct() {
670
- local token="$1" depth="$2" path="" first=""
671
- if ! path="$(follow_locate "$token")"; then
675
+ local token="$1" depth="$2" path="" first="" raw="" char="" k=0
676
+ local LC_ALL=C
677
+ if ! follow_locate "$token"; then
672
678
  return 0
673
679
  fi
674
- first="$(LC_ALL=C head -c 256 -- "$path" 2>/dev/null | LC_ALL=C head -n 1 2>/dev/null | LC_ALL=C tr -dc '[:print:]' 2>/dev/null)" || first=""
680
+ path="$follow_located"
681
+ IFS= read -r -n 256 raw <"$path" 2>/dev/null || true
682
+ while [ "$k" -lt "${#raw}" ]; do
683
+ char="${raw:k:1}"
684
+ case "$char" in [[:print:]]) first="$first$char" ;; esac
685
+ k=$((k + 1))
686
+ done
675
687
  case "$first" in
676
688
  '#!'*[/[:space:]]bash* | '#!'*[/[:space:]]sh* | '#!'*[/[:space:]]zsh* \
677
689
  | '#!'*[/[:space:]]ksh* | '#!'*[/[:space:]]dash* | '#!'*[/[:space:]]ash*) ;;
@@ -701,7 +713,7 @@ follow_next_operand() {
701
713
  follow_scan() {
702
714
  local text="$1" depth="$2"
703
715
  local split_out line sep stmt t tj i j n cmd_pos eval_mode inline arg_exec
704
- local heredoc redirected pipe_cat prev_cat pending_operand
716
+ local heredoc redirected pipe_cat prev_cat pending_operand stdin_mode
705
717
  local find_mode dispatch_pending
706
718
  local -a toks=()
707
719
  split_out="$(follow_split "$text")"
@@ -888,6 +900,7 @@ follow_scan() {
888
900
  inline=0
889
901
  redirected=""
890
902
  heredoc=0
903
+ stdin_mode=0
891
904
  j=$((i + 1))
892
905
  while [ "$j" -lt "$n" ]; do
893
906
  follow_unwrap "${toks[j]}"
@@ -920,6 +933,7 @@ follow_scan() {
920
933
  ;;
921
934
  --*) j=$((j + 1)) ;;
922
935
  -*c*) inline=1; j=$((j + 1)) ;;
936
+ -s | -[a-zA-Z]*s[a-zA-Z]*) stdin_mode=1; j=$((j + 1)) ;;
923
937
  -*) j=$((j + 1)) ;;
924
938
  *) break ;;
925
939
  esac
@@ -944,6 +958,14 @@ follow_scan() {
944
958
  i="$j"
945
959
  continue
946
960
  fi
961
+ if [ "$stdin_mode" -eq 1 ]; then
962
+ # `sh -s -- -y` reads its program from stdin; tokens after `--` are
963
+ # positional arguments to that program, never a script path.
964
+ [ -z "$pipe_cat" ] || follow_target "$pipe_cat" "$depth"
965
+ cmd_pos=0
966
+ i="$n"
967
+ continue
968
+ fi
947
969
  if [ "$j" -ge "$n" ]; then
948
970
  # A dispatcher supplies the script from ANOTHER command's output, so
949
971
  # the operand exists but is unreadable here. That is the fail-closed
@@ -1806,15 +1828,14 @@ readonly SQL_TRUNCATE_EVIDENCE='\btruncate[[:space:]]+(table[[:space:]]+[[:alnum
1806
1828
  # the same command must also invoke a command-line database client. Prose in a
1807
1829
  # tracker filing or a commit message invokes none, which is exactly the
1808
1830
  # difference the text scan could not see on its own.
1809
- readonly SQL_CLIENT='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(psql|pgcli|mysql|mysqladmin|mysqlsh|mariadb|mycli|sqlite3|sqlcmd|cqlsh|clickhouse-client|cockroach|mongosh|duckdb|usql)([[:space:]]|$)'
1810
- readonly SQL_TRUNCATE_BARE='\btruncate[[:space:]]+(only[[:space:]]+)?[[:alnum:]_."`]'
1831
+ readonly SQL_CLIENT_TRUNCATE='(^|[;&|()][[:space:]]*)([[:alnum:]_./-]*/)?(psql|pgcli|mysql|mysqladmin|mysqlsh|mariadb|mycli|sqlite3|sqlcmd|cqlsh|clickhouse-client|cockroach|mongosh|duckdb|usql)([[:space:]]+[^;&|()]*)?[[:space:]]+(-e|-c|--command|--eval)(=|[[:space:]]+)[^;&|()]*\btruncate[[:space:]]+(only[[:space:]]+)?[[:alnum:]_."`]'
1811
1832
  # Residual, accepted and stated: a bare `TRUNCATE <name>` with no terminator,
1812
1833
  # no qualification and no recognized client in the same command goes unmatched
1813
1834
  # — the same trade the `drop` arm has always made for `drop the ball`. No DROP
1814
1835
  # shape is weakened, and the guard still fails in the safe direction.
1815
1836
  if matches "$SQL_DROP" \
1816
1837
  || matches "$SQL_TRUNCATE_EVIDENCE" \
1817
- || { matches "$SQL_CLIENT" && matches "$SQL_TRUNCATE_BARE"; }; then
1838
+ || matches "$SQL_CLIENT_TRUNCATE"; then
1818
1839
  block "destructive SQL (DROP/TRUNCATE) detected"
1819
1840
  fi
1820
1841
 
@@ -0,0 +1,18 @@
1
+ # Measurement Integrity (load-bearing)
2
+
3
+ **Before you report a count, a population, or "N items affected", run the checks — because every way this goes wrong produces output that looks exactly like an answer.** No exception, no warning, no partial flag: the wrong number is indistinguishable from the right one by reading it.
4
+
5
+ That is why "be careful about scope" is not a remedy. **Every check below is performed or not performed**, never noticed:
6
+
7
+ 1. **State the population boundary** before enumerating — what is in, and what is adjacent and out.
8
+ 2. **Prove the enumeration complete** — paginate, and *assert* the end-of-pages signal rather than trusting it. A silent cap looks exactly like a population.
9
+ 3. **Validate the detector against known-true items.** If it cannot find the examples you already have, its total means nothing.
10
+ 4. **Treat search results as candidates, never members** — read each one directly and confirm the literal.
11
+ 5. **Control the instrument in both directions** — it must report a real absence, and must not invent one.
12
+ 6. **Report pattern-derived totals as floors**, and **state the unit** beside any count whose argument rests on how many *independent* observations you have.
13
+
14
+ No single check covers the others: validating against known items is blind to truncation, and reading every candidate can only ever see what enumeration handed it.
15
+
16
+ **And one that fires while reading rather than measuring:** a filter built from the failure shapes you already expect cannot show you a shape you did not. Read gate and tool output **unfiltered** on a refusal — the diagnosis is usually already in it.
17
+
18
+ Full method (six modes in two families, the membership test for adding a seventh, and which check is blind to what): [reference/measurement-integrity.md](../reference/measurement-integrity.md).
@@ -0,0 +1,109 @@
1
+ # Measurement Integrity
2
+
3
+ How an audit, sweep, or population count arrives at a confident wrong number, and the mechanical steps that catch each way.
4
+
5
+ ## The unifying property
6
+
7
+ **Every failure here produces output that looks exactly like an answer.** None throws, none warns, none reports partial. The wrong number is indistinguishable from the right one by inspecting the result, which is why it survives review and gets cited.
8
+
9
+ That has a direct consequence for remedies: **a step that requires the auditor to notice something has already failed.** Every recommendation below is shaped as an action that is either performed or not — validate the detector against known-true items, assert the end-of-pages signal, pair a negative with a positive control. "Be careful about scope" is not one of these.
10
+
11
+ ## The membership test
12
+
13
+ This is a method, not a bestiary. A failure belongs here when **all three** hold:
14
+
15
+ 1. **The output is well formed.** No exception, no warning, no truncation marker, no partial flag.
16
+ 2. **The error is invisible from the artifact alone.** Reading the result — the number, the list, the report — cannot distinguish it from a correct one.
17
+ 3. **A mechanical step would have caught it.** Something performed-or-not, not a judgement. If no such step exists, it is a hazard worth documenting but it is not a mode here, because the entry earns its place by supplying the check.
18
+
19
+ **The list is open.** It is currently six modes in two families plus one adjacent family. An earlier four-item version of a related list felt complete at four and was not; a taxonomy is the artifact most likely to be believed finished, and its own characteristic failure is a reader who stops looking once their case appears covered. Condition 3 is what lets you add to it rather than merely consult it.
20
+
21
+ ## Family A — the instrument mis-measures
22
+
23
+ Six modes fall out of two axes: **direction** (does it under- or over-report?) and **stage** (does it fail while *enumerating* candidates, or while *matching* them?). All four cells are occupied, which is a reason to believe the family is close to complete and not a reason to stop looking.
24
+
25
+ | | under-reports | over-reports |
26
+ |---|---|---|
27
+ | **enumeration** | class exclusion · truncation | scope inflation |
28
+ | **matching** | pattern blindness | tokenization · alternation order |
29
+
30
+ ### A1. Class exclusion — under, enumeration
31
+
32
+ An entire corpus sits outside the query's scope and nothing says so. `gh search issues` does not return pull requests; run alone against a mostly-PR population it reported **10 items where the answer was 45**, and omitted the two items already known to be members. A list that misses the examples you started from still reads as authoritative.
33
+
34
+ **Catch it:** validate the detector against known-true items before believing any count.
35
+
36
+ ### A2. Truncation — under, enumeration
37
+
38
+ Real members past the first page are dropped with no truncation signal. `gh issue list` and `gh pr list` default to 30 and cap silently at whatever `--limit` is passed: one audit reported **37 items where the answer was 513**, a 14× undercount. A paging API returning **249 of 845 rows with `hasNextPage: false`** is the same mode on another substrate.
39
+
40
+ **Catch it:** paginate to exhaustion and assert the end-of-pages signal rather than trusting it.
41
+
42
+ ### A3. Pattern blindness — under, matching
43
+
44
+ Members the pattern cannot express are never matched. A prose form of an organisation's name appeared in three bodies and was matched by no pattern in the audit that was looking for exactly that; it was caught only because a person added the spelling by hand.
45
+
46
+ **Catch it:** nothing automatic. Report every pattern-derived total as a **floor**, never a total — saying so is the honest form, and it is a step that is either performed or not.
47
+
48
+ ### A4. Scope inflation — over, enumeration
49
+
50
+ Candidates are enumerated from a root that reaches beyond the population. A tree walk asserting a repository ships exactly one copy of a script found **21 additional copies**, one per agent worktree nested inside the checkout, and failed. The walk was correct; its root was wrong.
51
+
52
+ Note the trap this mode sets for A1's remedy: **path reachability is not membership.** "The search reached it" and "it belongs to the population" are different claims, and conflating them is how an over-report is defended as thoroughness.
53
+
54
+ **Catch it:** state the population boundary before enumerating, and assert that a known non-member is excluded — the positive control's mirror.
55
+
56
+ ### A5. Tokenization — over, matching
57
+
58
+ The query is split into terms, so unrelated items match. Searching for a URL-shaped needle matched issues containing only its bare final word in ordinary prose — one read *"There is no account, token, or session."* Two of 47 candidates were phantoms.
59
+
60
+ **Catch it:** treat search results as **candidates, never members**; read each one directly and confirm the literal.
61
+
62
+ ### A6. Alternation order — over, matching, and it fabricates
63
+
64
+ A regex alternative matches early and produces a value with no referent. Extracting paths with `\.(mjs|js|sh|json)` matched `js` inside `package.json` before reaching `json`, producing a phantom `package.js` and reporting a live exposure that did not exist.
65
+
66
+ This is the most expensive mode. A4 and A5 return a *real item for a wrong reason* and die on a direct read; this returns an **artifact that does not exist**, so a direct read cannot disconfirm it — there is nothing to read — and it sends someone chasing a break that was never there.
67
+
68
+ **Catch it:** order alternations longest-first, or anchor the match end. Control it with an input crafted to trip the partial match; asserting the regex matches what it should is satisfied by the broken version.
69
+
70
+ ## Family B — the instrument is right and the report is wrong
71
+
72
+ ### B1. Unit error
73
+
74
+ The measurement is correct and the **reported unit** is not. "Three separate branches" versus *three events across two branches* is not a count error — the count was three either way. It inflates the **independence** of the sample, which is the property the argument rested on: repeated observations on one branch are weaker evidence of a general flake than the same number spread across branches.
75
+
76
+ No amount of instrument validation catches this. The detector was never involved.
77
+
78
+ **Catch it:** state the unit next to the number, and ask whether the claim rests on *how many* or on *how many independent*. Where it rests on independence, report both.
79
+
80
+ ## Family C — adjacent, and deliberately not a mode here
81
+
82
+ ### C1. Filter blindness
83
+
84
+ **A filter built from the failure shapes you already expect cannot show you a shape you did not.** An agent grep-filtered push output for failure markers and discarded the gate's own line naming the cause; the gate had diagnosed itself perfectly and the filter threw the diagnosis away.
85
+
86
+ This is listed and **excluded from the taxonomy on purpose.** It fails condition 2 in a different place: the artifact was complete and correct, and the loss happened while *reading* it. Forcing it into Family A would dilute the membership test, which is the part of this document that does work. It is here because it is the same shape one layer out, and because the remedy is one line: **read gate and tool output unfiltered on a refusal.**
87
+
88
+ ## Composition: no single check catches all of them
89
+
90
+ This is the payload. Each check is blind to modes the others catch.
91
+
92
+ | check | catches | blind to |
93
+ |---|---|---|
94
+ | validate the detector against known-true items | A1, A3 | A2 when the knowns sit on page one · A4 · A5 · A6 · B1 |
95
+ | read every candidate directly | A5, A6 | A1 · A2 · A3 — it only ever sees what enumeration handed it |
96
+ | paginate and assert end-of-pages | A2 | everything else |
97
+ | assert a known non-member is excluded | A4 | A1 · A2 · A3 |
98
+ | state the unit beside the number | B1 | all of Family A |
99
+
100
+ **Validating against known-true items** loses to truncation, because your known examples are usually recent and therefore on the first page. **Reading every candidate** kills both over-reports and cannot see what was never enumerated. They are complements, not alternatives.
101
+
102
+ ## The procedure
103
+
104
+ 1. **State the population boundary** before enumerating — what is in, what is adjacent and out.
105
+ 2. **Enumerate candidates and prove the enumeration complete** — paginate, assert the end-of-pages signal, and check the query's scope covers every corpus (issues *and* pull requests, open *and* closed, bodies *and* comments).
106
+ 3. **Validate the detector against known-true items.** If it cannot find the examples you already have, its total means nothing.
107
+ 4. **Read every candidate directly** and confirm the literal. Search results are candidates, never members.
108
+ 5. **Control the instrument in both directions** — it must report a real absence, and must not invent one.
109
+ 6. **Report pattern-derived totals as floors**, and **state the unit** beside any count whose argument rests on independence.
@@ -1426,7 +1426,11 @@ EXEC_WRAPPERS = {
1426
1426
  "nohup": (frozenset(), 0),
1427
1427
  "setsid": (frozenset(), 0),
1428
1428
  "stdbuf": (frozenset({"-i", "-o", "-e"}), 0),
1429
- "sudo": (frozenset({"-u", "--user", "-g", "--group", "-C", "--close-from"}), 0),
1429
+ "sudo": (frozenset({
1430
+ "-u", "--user", "-g", "--group", "-C", "--close-from",
1431
+ "-p", "--prompt", "-h", "--host", "-r", "--role",
1432
+ "-t", "--type", "-U", "--other-user",
1433
+ }), 0),
1430
1434
  "time": (frozenset(), 0),
1431
1435
  "timeout": (frozenset({"-k", "--kill-after", "-s", "--signal"}), 1),
1432
1436
  }
@@ -1447,6 +1451,31 @@ NO_SCRIPT_OPTIONS = {
1447
1451
  "ruby": {"-e"},
1448
1452
  }
1449
1453
 
1454
+ # Interpreter switches whose value is a separate token, followed by the real
1455
+ # script operand. Treating the value as the script makes the guard inspect a
1456
+ # preload or option value and never reach what the interpreter executes.
1457
+ INTERPRETER_VALUE_OPTIONS = {
1458
+ "bash": {"-o", "--option"},
1459
+ "dash": {"-o"},
1460
+ "deno": {"-c", "--config"},
1461
+ "ksh": {"-o"},
1462
+ "node": {"-r", "--require"},
1463
+ "nodejs": {"-r", "--require"},
1464
+ "sh": {"-o"},
1465
+ "zsh": {"-o"},
1466
+ }
1467
+
1468
+ # Runtime subcommands that introduce the script path rather than name it.
1469
+ INTERPRETER_SUBCOMMANDS = {
1470
+ "bun": {"run"},
1471
+ "deno": {"run"},
1472
+ }
1473
+
1474
+
1475
+ def is_assignment_word(token):
1476
+ """Whether token is a POSIX shell assignment in command-prefix position."""
1477
+ return re.match(r"^[A-Za-z_][A-Za-z0-9_]*\+?=", token) is not None
1478
+
1450
1479
  # Programs that READ their operands and never execute them as programs.
1451
1480
  #
1452
1481
  # This is an allowlist of readers rather than the inverse, and the asymmetry is
@@ -1501,7 +1530,7 @@ def executing_command(argv):
1501
1530
  index = 0
1502
1531
  while index < len(argv):
1503
1532
  token = argv[index]
1504
- if "=" in token and not token.startswith(("=", "-")):
1533
+ if is_assignment_word(token):
1505
1534
  index += 1
1506
1535
  continue
1507
1536
  program = basename(token)
@@ -1568,16 +1597,20 @@ def executed_operand(argv):
1568
1597
  # shebang — `./create.sh` names no interpreter and runs all the same.
1569
1598
  if program not in EXECUTING_INTERPRETERS and resolve_operand(token) is not None:
1570
1599
  return token
1571
- # `bash < create.sh` runs that file as surely as `bash create.sh` does, and
1572
- # `<` is not a segment boundary, so the pair survives in one argv.
1573
- for index, argument in enumerate(args):
1574
- if argument == "<" and index + 1 < len(args):
1575
- return args[index + 1]
1576
1600
  # Reached by a known interpreter AND by a program this parser cannot name.
1577
1601
  # The unknown case is followed on purpose — see READ_ONLY_PROGRAMS.
1578
- for argument in args:
1579
- if argument == "--":
1602
+ redirected = None
1603
+ index = 0
1604
+ while index < len(args):
1605
+ argument = args[index]
1606
+ if argument == "<":
1607
+ if index + 1 < len(args):
1608
+ redirected = args[index + 1]
1609
+ index += 2
1580
1610
  continue
1611
+ if argument == "--":
1612
+ index += 1
1613
+ return args[index] if index < len(args) else redirected
1581
1614
  if argument.startswith("-"):
1582
1615
  # A command string or a module name; no script file follows.
1583
1616
  head = argument.split("=", 1)[0]
@@ -1588,9 +1621,15 @@ def executed_operand(argv):
1588
1621
  return None
1589
1622
  elif head in NO_SCRIPT_OPTIONS.get(program, set()):
1590
1623
  return None
1624
+ takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
1625
+ index += 2 if takes_value and "=" not in argument else 1
1626
+ continue
1627
+ if argument in INTERPRETER_SUBCOMMANDS.get(program, set()):
1628
+ index += 1
1591
1629
  continue
1592
1630
  return argument
1593
- return None
1631
+ # `bash < create.sh` runs stdin only when no explicit script was named.
1632
+ return redirected
1594
1633
 
1595
1634
 
1596
1635
  def file_operands(argv):
@@ -179,12 +179,14 @@ EOF
179
179
  # The patch file is per-worktree: `mktemp` names it, so nothing has to invent a
180
180
  # filename, and it lives in this agent's own TMPDIR where no sibling can reach
181
181
  # it. Untracked files are unaffected by both refused operations, so `git diff`
182
- # captures everything at risk.
182
+ # captures everything at risk. `HEAD` includes staged and unstaged changes, and
183
+ # `--binary` keeps binary edits recoverable rather than emitting a text-only
184
+ # placeholder that cannot be applied.
183
185
  PRESERVE_GUIDANCE="$(
184
186
  cat <<'EOF'
185
187
  Preserve the work first, per-worktree:
186
188
 
187
- git diff [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
189
+ git diff --binary HEAD [-- <path>] > "$(mktemp "${TMPDIR:-/tmp}/lisa-preserve-XXXXXX.patch")"
188
190
 
189
191
  and restore it later with `git apply <that file>`.
190
192
 
@@ -444,6 +446,7 @@ follow_refuse() {
444
446
  # so peak process count never rises. The latency was the whole problem.
445
447
  follow_unwrapped=""
446
448
  follow_operand_index=0
449
+ follow_located=""
447
450
 
448
451
  # Strip substitution, quote and alias wrappers off one token. A sibling of
449
452
  # strip_subst_wrappers() below rather than a call to it: that one is defined
@@ -568,6 +571,7 @@ follow_add_base() {
568
571
  # this hook cannot evaluate.
569
572
  follow_locate() {
570
573
  local token="$1" home="${HOME:-}" tmp="${TMPDIR:-/tmp}" proj="${CLAUDE_PROJECT_DIR:-$PWD}" base
574
+ follow_located=""
571
575
  tmp="${tmp%/}"
572
576
  [ -n "$tmp" ] || tmp="/tmp"
573
577
  # The tilde arms below are LITERAL text: this is the unexpanded spelling the
@@ -589,14 +593,14 @@ follow_locate() {
589
593
  "" | *'$'* | *'*'* | *'?'* | *'`'* | *'{'* | *'['*) return 1 ;;
590
594
  /*)
591
595
  [ -f "$token" ] && [ -r "$token" ] || return 1
592
- printf '%s' "$token"
596
+ follow_located="$token"
593
597
  return 0
594
598
  ;;
595
599
  esac
596
600
  while IFS= read -r base; do
597
601
  [ -n "$base" ] || continue
598
602
  if [ -f "$base/$token" ] && [ -r "$base/$token" ]; then
599
- printf '%s' "$base/$token"
603
+ follow_located="$base/$token"
600
604
  return 0
601
605
  fi
602
606
  done <<<"$PWD"$'\n'"$proj$follow_bases"
@@ -641,12 +645,13 @@ follow_take() {
641
645
  follow_target() {
642
646
  local token="$1" depth="$2" path=""
643
647
  [ -n "$token" ] || return 0
644
- if ! path="$(follow_locate "$token")"; then
648
+ if ! follow_locate "$token"; then
645
649
  if [ "$depth" -eq 0 ]; then
646
650
  follow_refuse "$token" "no readable file could be resolved for it"
647
651
  fi
648
652
  return 0
649
653
  fi
654
+ path="$follow_located"
650
655
  follow_take "$path" "$depth"
651
656
  }
652
657
 
@@ -667,11 +672,18 @@ follow_target() {
667
672
  # Accepted residual: a shebang script whose relative path resolves only from a
668
673
  # working directory this hook does not share is not followed.
669
674
  follow_direct() {
670
- local token="$1" depth="$2" path="" first=""
671
- if ! path="$(follow_locate "$token")"; then
675
+ local token="$1" depth="$2" path="" first="" raw="" char="" k=0
676
+ local LC_ALL=C
677
+ if ! follow_locate "$token"; then
672
678
  return 0
673
679
  fi
674
- first="$(LC_ALL=C head -c 256 -- "$path" 2>/dev/null | LC_ALL=C head -n 1 2>/dev/null | LC_ALL=C tr -dc '[:print:]' 2>/dev/null)" || first=""
680
+ path="$follow_located"
681
+ IFS= read -r -n 256 raw <"$path" 2>/dev/null || true
682
+ while [ "$k" -lt "${#raw}" ]; do
683
+ char="${raw:k:1}"
684
+ case "$char" in [[:print:]]) first="$first$char" ;; esac
685
+ k=$((k + 1))
686
+ done
675
687
  case "$first" in
676
688
  '#!'*[/[:space:]]bash* | '#!'*[/[:space:]]sh* | '#!'*[/[:space:]]zsh* \
677
689
  | '#!'*[/[:space:]]ksh* | '#!'*[/[:space:]]dash* | '#!'*[/[:space:]]ash*) ;;
@@ -701,7 +713,7 @@ follow_next_operand() {
701
713
  follow_scan() {
702
714
  local text="$1" depth="$2"
703
715
  local split_out line sep stmt t tj i j n cmd_pos eval_mode inline arg_exec
704
- local heredoc redirected pipe_cat prev_cat pending_operand
716
+ local heredoc redirected pipe_cat prev_cat pending_operand stdin_mode
705
717
  local find_mode dispatch_pending
706
718
  local -a toks=()
707
719
  split_out="$(follow_split "$text")"
@@ -888,6 +900,7 @@ follow_scan() {
888
900
  inline=0
889
901
  redirected=""
890
902
  heredoc=0
903
+ stdin_mode=0
891
904
  j=$((i + 1))
892
905
  while [ "$j" -lt "$n" ]; do
893
906
  follow_unwrap "${toks[j]}"
@@ -920,6 +933,7 @@ follow_scan() {
920
933
  ;;
921
934
  --*) j=$((j + 1)) ;;
922
935
  -*c*) inline=1; j=$((j + 1)) ;;
936
+ -s | -[a-zA-Z]*s[a-zA-Z]*) stdin_mode=1; j=$((j + 1)) ;;
923
937
  -*) j=$((j + 1)) ;;
924
938
  *) break ;;
925
939
  esac
@@ -944,6 +958,14 @@ follow_scan() {
944
958
  i="$j"
945
959
  continue
946
960
  fi
961
+ if [ "$stdin_mode" -eq 1 ]; then
962
+ # `sh -s -- -y` reads its program from stdin; tokens after `--` are
963
+ # positional arguments to that program, never a script path.
964
+ [ -z "$pipe_cat" ] || follow_target "$pipe_cat" "$depth"
965
+ cmd_pos=0
966
+ i="$n"
967
+ continue
968
+ fi
947
969
  if [ "$j" -ge "$n" ]; then
948
970
  # A dispatcher supplies the script from ANOTHER command's output, so
949
971
  # the operand exists but is unreadable here. That is the fail-closed
@@ -1806,15 +1828,14 @@ readonly SQL_TRUNCATE_EVIDENCE='\btruncate[[:space:]]+(table[[:space:]]+[[:alnum
1806
1828
  # the same command must also invoke a command-line database client. Prose in a
1807
1829
  # tracker filing or a commit message invokes none, which is exactly the
1808
1830
  # difference the text scan could not see on its own.
1809
- readonly SQL_CLIENT='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(psql|pgcli|mysql|mysqladmin|mysqlsh|mariadb|mycli|sqlite3|sqlcmd|cqlsh|clickhouse-client|cockroach|mongosh|duckdb|usql)([[:space:]]|$)'
1810
- readonly SQL_TRUNCATE_BARE='\btruncate[[:space:]]+(only[[:space:]]+)?[[:alnum:]_."`]'
1831
+ readonly SQL_CLIENT_TRUNCATE='(^|[;&|()][[:space:]]*)([[:alnum:]_./-]*/)?(psql|pgcli|mysql|mysqladmin|mysqlsh|mariadb|mycli|sqlite3|sqlcmd|cqlsh|clickhouse-client|cockroach|mongosh|duckdb|usql)([[:space:]]+[^;&|()]*)?[[:space:]]+(-e|-c|--command|--eval)(=|[[:space:]]+)[^;&|()]*\btruncate[[:space:]]+(only[[:space:]]+)?[[:alnum:]_."`]'
1811
1832
  # Residual, accepted and stated: a bare `TRUNCATE <name>` with no terminator,
1812
1833
  # no qualification and no recognized client in the same command goes unmatched
1813
1834
  # — the same trade the `drop` arm has always made for `drop the ball`. No DROP
1814
1835
  # shape is weakened, and the guard still fails in the safe direction.
1815
1836
  if matches "$SQL_DROP" \
1816
1837
  || matches "$SQL_TRUNCATE_EVIDENCE" \
1817
- || { matches "$SQL_CLIENT" && matches "$SQL_TRUNCATE_BARE"; }; then
1838
+ || matches "$SQL_CLIENT_TRUNCATE"; then
1818
1839
  block "destructive SQL (DROP/TRUNCATE) detected"
1819
1840
  fi
1820
1841
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.34.21",
3
+ "version": "4.35.1",
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": "4.34.21",
3
+ "version": "4.35.1",
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": "4.34.21",
3
+ "version": "4.35.1",
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": "4.34.21",
3
+ "version": "4.35.1",
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": "4.34.21",
3
+ "version": "4.35.1",
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": "4.34.21",
3
+ "version": "4.35.1",
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": "4.34.21",
3
+ "version": "4.35.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"