@codyswann/lisa 4.35.0 → 4.35.2

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 (96) 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/cli/apply.js +8 -8
  9. package/dist/cli/apply.js.map +1 -1
  10. package/dist/codex/scripts/_extract-edit-paths.sh +4 -0
  11. package/dist/core/apply-receipt.d.ts +17 -12
  12. package/dist/core/apply-receipt.d.ts.map +1 -1
  13. package/dist/core/apply-receipt.js +3 -3
  14. package/dist/core/apply-receipt.js.map +1 -1
  15. package/dist/core/config.d.ts +10 -6
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  19. package/dist/core/lisa-owned-hash-ledger.js +20 -0
  20. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  21. package/dist/core/lisa.d.ts +34 -24
  22. package/dist/core/lisa.d.ts.map +1 -1
  23. package/dist/core/lisa.js +49 -41
  24. package/dist/core/lisa.js.map +1 -1
  25. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  26. package/dist/core/upstream-evidence-manifest.js +10 -10
  27. package/dist/migrations/ensure-lisa-postinstall.d.ts +6 -37
  28. package/dist/migrations/ensure-lisa-postinstall.d.ts.map +1 -1
  29. package/dist/migrations/ensure-lisa-postinstall.js +20 -67
  30. package/dist/migrations/ensure-lisa-postinstall.js.map +1 -1
  31. package/dist/opencode/plugin-templates/parity-safety-net.sh +33 -12
  32. package/package.json +4 -4
  33. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa/hooks/block-direct-issue-create.sh +49 -10
  36. package/plugins/lisa/hooks/block-managed-file-edits.sh +42 -4
  37. package/plugins/lisa/hooks/parity-safety-net.sh +33 -12
  38. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +49 -10
  39. package/plugins/lisa-agy/hooks/parity-safety-net.sh +33 -12
  40. package/plugins/lisa-agy/plugin.json +1 -1
  41. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  44. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +49 -10
  48. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +42 -4
  49. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +33 -12
  50. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +49 -10
  52. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +42 -4
  53. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +33 -12
  54. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-expo-agy/plugin.json +1 -1
  57. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  62. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  67. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  72. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  77. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  81. package/plugins/lisa-rails-agy/plugin.json +1 -1
  82. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  87. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  91. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  92. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  94. package/plugins/src/base/hooks/block-direct-issue-create.sh +49 -10
  95. package/plugins/src/base/hooks/block-managed-file-edits.sh +42 -4
  96. package/plugins/src/base/hooks/parity-safety-net.sh +33 -12
@@ -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):
@@ -90,7 +90,7 @@
90
90
  # from mine", and guessing "behind" is how a fork's stronger guard gets silently
91
91
  # replaced by a weaker upstream one. Add a name here in the same commit that
92
92
  # closes a vector.
93
- # lisa-guard-capabilities: managed-path-resolution, lisaignore-precedence, generated-path-rebuild, redirect-target, tee-target, sed-in-place-all-spellings, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, direct-script-command-reach, analyzer-failure-visible
93
+ # lisa-guard-capabilities: managed-path-resolution, lisaignore-precedence, generated-path-rebuild, redirect-target, tee-target, sed-in-place-all-spellings, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, direct-script-command-reach, analyzer-failure-visible, apply-patch-parse-failure-visible, apply-patch-move-target, shell-command-string-reach
94
94
  set -euo pipefail
95
95
 
96
96
  input="$(cat)"
@@ -399,6 +399,7 @@ case "$tool_name" in
399
399
  # *** Add File: <path>
400
400
  # *** Update File: <path>
401
401
  # *** Delete File: <path>
402
+ # *** Move to: <path>
402
403
  #
403
404
  # So every header is walked and each target classified; one managed
404
405
  # target anywhere in a multi-file patch refuses the whole call, because
@@ -415,14 +416,25 @@ case "$tool_name" in
415
416
  # legitimately cannot be one file. What is NOT restated is the
416
417
  # classification: `managed_source` and `refuse` are the single copy here,
417
418
  # exactly as for every other arm.
418
- patch_text="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
419
+ if ! patch_text="$(printf '%s' "$input" | jq -er '
420
+ if ((.tool_input.command? // "") | type) == "string"
421
+ then (.tool_input.command? // "")
422
+ else error("tool_input.command must be a string")
423
+ end')"; then
424
+ printf 'block-managed-file-edits: could not parse the apply_patch command; managed-file protection refused the edit\n' >&2
425
+ exit 2
426
+ fi
419
427
  [ -n "$patch_text" ] || exit 0
420
428
  while IFS= read -r patch_line; do
421
429
  case "$patch_line" in
422
- "*** Add File: "* | "*** Update File: "* | "*** Delete File: "*) ;;
430
+ "*** Add File: "* | "*** Update File: "* | "*** Delete File: "*)
431
+ candidate="${patch_line#*File: }"
432
+ ;;
433
+ "*** Move to: "*)
434
+ candidate="${patch_line#*** Move to: }"
435
+ ;;
423
436
  *) continue ;;
424
437
  esac
425
- candidate="${patch_line#*File: }"
426
438
  [ -n "$candidate" ] || continue
427
439
  if source_path="$(managed_source "$candidate")"; then
428
440
  refuse "$candidate" "$source_path" "$(relative_path "$candidate")"
@@ -712,6 +724,29 @@ def shell_script_operand(args):
712
724
  return None
713
725
 
714
726
 
727
+ def shell_command_string(statement):
728
+ """The inline command a shell executes with `-c`, or None.
729
+
730
+ Args:
731
+ statement: One statement's tokens.
732
+
733
+ Returns:
734
+ The command string operand, or None when this is not a shell `-c` call.
735
+ """
736
+ _token, program, args = command_word(statement)
737
+ if program not in SHELL_PROGRAMS:
738
+ return None
739
+ for index, token in enumerate(args):
740
+ if token == "--":
741
+ return None
742
+ if token.startswith("-") and not token.startswith("--") and "c" in token[1:]:
743
+ command_index = index + 1
744
+ if command_index < len(args) and args[command_index] == "--":
745
+ command_index += 1
746
+ return args[command_index] if command_index < len(args) else None
747
+ return None
748
+
749
+
715
750
  def write_targets(statement):
716
751
  """Paths this statement WRITES.
717
752
 
@@ -834,6 +869,9 @@ def collect(text, depth, seen, out):
834
869
  if not statement:
835
870
  continue
836
871
  out.extend(write_targets(statement))
872
+ nested = shell_command_string(statement)
873
+ if nested is not None and depth < FOLLOW_MAX_DEPTH:
874
+ collect(nested, depth + 1, seen, out)
837
875
  operand = executed_script(statement)
838
876
  if operand is None:
839
877
  continue
@@ -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-expo",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "4.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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.35.0",
3
+ "version": "4.35.2",
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"