@codyswann/lisa 4.71.3 → 4.71.5

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 (86) hide show
  1. package/all/copy-overwrite/scripts/lib/npm-update-classifier-cache.mjs +94 -0
  2. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +1 -0
  3. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +9 -2
  4. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +122 -8
  5. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +12 -3
  6. package/dist/cli/hook-refresh-files.d.ts +86 -0
  7. package/dist/cli/hook-refresh-files.d.ts.map +1 -0
  8. package/dist/cli/hook-refresh-files.js +209 -0
  9. package/dist/cli/hook-refresh-files.js.map +1 -0
  10. package/dist/cli/index.d.ts +3 -0
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +3 -0
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/refresh-hooks-cmd.d.ts +27 -0
  15. package/dist/cli/refresh-hooks-cmd.d.ts.map +1 -0
  16. package/dist/cli/refresh-hooks-cmd.js +156 -0
  17. package/dist/cli/refresh-hooks-cmd.js.map +1 -0
  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/nightly-e2e-guard-behavior-certificate.js +2 -2
  22. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  23. package/dist/core/upstream-evidence-manifest.js +21 -9
  24. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  25. package/dist/opencode/plugin-templates/parity-safety-net.sh +122 -8
  26. package/package.json +4 -4
  27. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa/hooks/parity-safety-net.sh +122 -8
  30. package/plugins/lisa-agy/hooks/parity-safety-net.sh +122 -8
  31. package/plugins/lisa-agy/plugin.json +1 -1
  32. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  35. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +122 -8
  39. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +122 -8
  41. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-agy/plugin.json +1 -1
  44. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  64. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-agy/plugin.json +1 -1
  69. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  74. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  79. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/src/base/hooks/parity-safety-net.sh +122 -8
  82. package/rails/copy-overwrite/scripts/lisa-scratch-run.sh +54 -9
  83. package/scripts/build-plugins.sh +3 -0
  84. package/scripts/generate-scratch-supervisor-profile.mjs +75 -0
  85. package/scripts/two-channel-couplings.json +1 -1
  86. package/typescript/copy-contents/.husky/commit-msg +28 -31
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -527,6 +527,94 @@ follow_names=""
527
527
  follow_count=0
528
528
  follow_bytes=0
529
529
  followed_text=""
530
+ followed_rm_text=""
531
+ # Generated from the canonical reviewed supervisor, never from a host manifest.
532
+ # Regenerate through scripts/generate-scratch-supervisor-profile.mjs.
533
+ readonly SCRATCH_SUPERVISOR_SHA256='6fdec1ad0441d7636dc6749c44a23167baf7b75004f156b8a7d4ce1c18c790b7'
534
+ readonly SCRATCH_SUPERVISOR_CLEANUP_COUNT='7'
535
+
536
+ # An authenticated public supervisor owns its fixed cleanup sites. Its original
537
+ # source remains visible to every other policy; its actual payload is followed.
538
+ follow_supervisor() {
539
+ local token="$1" depth="$2" statement="$3" path="" profile="" payload="" rm_source="" original=""
540
+ case "$token" in *lisa-scratch-run.sh) ;; *) return 1 ;; esac
541
+ follow_locate "$token" || follow_refuse "$token" "supervisor source could not be resolved"
542
+ path="$follow_located"
543
+ profile="$(python3 - "$path" "$SCRATCH_SUPERVISOR_SHA256" "$statement" "$token" "$SCRATCH_SUPERVISOR_CLEANUP_COUNT" <<'PY'
544
+ import hashlib
545
+ import json
546
+ import os
547
+ import re
548
+ import shlex
549
+ import stat
550
+ import sys
551
+
552
+ path, digest, statement, operand, expected_count = sys.argv[1:]
553
+ def refuse():
554
+ sys.exit(2)
555
+
556
+ try:
557
+ info = os.lstat(path)
558
+ if not stat.S_ISREG(info.st_mode) or info.st_size > 262144:
559
+ refuse()
560
+ raw = open(path, 'rb').read(262145)
561
+ if len(raw) != info.st_size or not re.fullmatch(r'[0-9a-f]{64}', digest):
562
+ refuse()
563
+ if hashlib.sha256(raw).hexdigest() != digest:
564
+ refuse()
565
+ tokens = shlex.split(statement)
566
+ indices = [index for index, token in enumerate(tokens) if token == operand]
567
+ if len(indices) != 1 or '$' in operand or '`' in operand:
568
+ refuse()
569
+ index = indices[0]
570
+ if index < 1 or tokens[index - 1] not in ('sh', 'bash', '/bin/sh', '/bin/bash'):
571
+ refuse()
572
+ prefix = tokens[:index - 1]
573
+ if prefix[:1] == ['env']:
574
+ prefix = prefix[1:]
575
+ assignments = {}
576
+ for token in prefix:
577
+ if '=' not in token or re.search(r'[$`\r\n]', token):
578
+ refuse()
579
+ key, value = token.split('=', 1)
580
+ if key not in ('LISA_SCRATCH_BASE', 'LISA_SCRATCH_TRACE', 'TMPDIR') or key in assignments:
581
+ refuse()
582
+ assignments[key] = value
583
+ # Match actual shell :- precedence, including inherited and
584
+ # explicitly empty values. TMPDIR cannot override a nonempty inherited base.
585
+ effective = dict(os.environ, **assignments)
586
+ base = effective.get('LISA_SCRATCH_BASE') or effective.get('TMPDIR') or '/tmp'
587
+ if not base or not os.path.isabs(base) or re.search(r'[$`\r\n]', base):
588
+ refuse()
589
+ real_base = os.path.realpath(base)
590
+ roots = ['/tmp', '/var/tmp', os.environ.get('TMPDIR', '/tmp')]
591
+ if not any(os.path.commonpath([real_base, os.path.realpath(root)]) == os.path.realpath(root) for root in roots if root and os.path.isabs(root)):
592
+ refuse()
593
+ args = tokens[index + 1:]
594
+ if len(args) < 4 or args[0] != '--suite' or args[2] != '--':
595
+ refuse()
596
+ if not re.fullmatch(r'[A-Za-z0-9._-]{1,64}', args[1]):
597
+ refuse()
598
+ payload = args[3:]
599
+ if not payload[0] or payload[0].startswith('-') or any(re.search(r'[$`\x00\r\n]', value) for value in payload):
600
+ refuse()
601
+ source = raw.decode('utf8')
602
+ cleaned, count = re.subn(r'(?m)^(\s*)rm -rf "(\$_quarantine|\$_root|\$_entry|\$LISA_SCRATCH_ROOT)"', r'\1:', source)
603
+ if not re.fullmatch(r'[1-9][0-9]*', expected_count) or count != int(expected_count):
604
+ refuse()
605
+ print(json.dumps({'source': cleaned, 'original': source, 'payload': shlex.join(payload)}))
606
+ except (OSError, UnicodeError, ValueError):
607
+ refuse()
608
+ PY
609
+ )" || follow_refuse "$path" "supervisor source or public entry authority could not be authenticated"
610
+ rm_source="$(printf '%s' "$profile" | jq -er '.source')" || follow_refuse "$path" "invalid supervisor profile"
611
+ original="$(printf '%s' "$profile" | jq -er '.original')" || follow_refuse "$path" "invalid supervisor source"
612
+ payload="$(printf '%s' "$profile" | jq -er '.payload')" || follow_refuse "$path" "invalid supervisor payload"
613
+ follow_take "$path" "$depth" "$rm_source" "$original"
614
+ [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ] || follow_refuse "$path" "supervisor payload exceeds inspection depth"
615
+ follow_scan "$payload" "$((depth + 1))"
616
+ return 0
617
+ }
530
618
 
531
619
  FOLLOW_GUIDANCE="This command EXECUTES a file. These guards classify shell text, so the script an
532
620
  invocation runs is read and scanned as part of the command — a destructive line
@@ -813,7 +901,7 @@ follow_locate() {
813
901
  # executes, to a bounded depth. Full-line comments are dropped: a commented line
814
902
  # is never executed, so scanning it can only manufacture a false refusal.
815
903
  follow_take() {
816
- local path="$1" depth="$2" size="" content=""
904
+ local path="$1" depth="$2" size="" content="" rm_content="${3-}"
817
905
  case "$follow_paths" in
818
906
  *$'\n'"$path"$'\n'*) return 0 ;;
819
907
  esac
@@ -833,10 +921,22 @@ follow_take() {
833
921
  if [ "$follow_bytes" -gt "$FOLLOW_MAX_TOTAL_BYTES" ]; then
834
922
  follow_refuse "$path" "the files this command executes total more than the ${FOLLOW_MAX_TOTAL_BYTES}-byte inspection budget"
835
923
  fi
836
- content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
837
- || follow_refuse "$path" "it could not be read"
924
+ if [ "$#" -eq 4 ]; then
925
+ # Scan the same complete bytes whose digest was authenticated, not a
926
+ # second file read that could pair changed source with the cleanup view.
927
+ content="$(printf '%s' "$4" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "authenticated source could not be read"
928
+ else
929
+ content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
930
+ || follow_refuse "$path" "it could not be read"
931
+ fi
838
932
  follow_names="$follow_names $path"
839
933
  followed_text="$followed_text"$'\n'"$content"
934
+ if [ "$#" -lt 3 ]; then
935
+ rm_content="$content"
936
+ else
937
+ rm_content="$(printf '%s' "$rm_content" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "supervisor cleanup view could not be read"
938
+ fi
939
+ followed_rm_text="$followed_rm_text"$'\n'"$rm_content"
840
940
  if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
841
941
  # A `cd` inside the followed script governs what THAT script goes on to
842
942
  # execute, and nothing after it in the command that ran the script — the
@@ -855,6 +955,11 @@ follow_take() {
855
955
  follow_target() {
856
956
  local token="$1" depth="$2" path=""
857
957
  [ -n "$token" ] || return 0
958
+ # Entry authority belongs to each invocation, not the file-read cache. Only
959
+ # follow_supervisor may admit the exact public interpreter/argv profile.
960
+ case "$token" in
961
+ *lisa-scratch-run.sh) follow_refuse "$token" "supervisor requires its authenticated public interpreter entry" ;;
962
+ esac
858
963
  if ! follow_locate "$token"; then
859
964
  if [ "$depth" -eq 0 ]; then
860
965
  follow_refuse "$token" "no readable file could be resolved for it"
@@ -888,6 +993,9 @@ follow_direct() {
888
993
  return 0
889
994
  fi
890
995
  path="$follow_located"
996
+ case "$path" in
997
+ *lisa-scratch-run.sh) follow_refuse "$path" "supervisor requires its authenticated public interpreter entry" ;;
998
+ esac
891
999
  IFS= read -r -n 256 raw <"$path" 2>/dev/null || true
892
1000
  while [ "$k" -lt "${#raw}" ]; do
893
1001
  char="${raw:k:1}"
@@ -1219,7 +1327,9 @@ follow_scan() {
1219
1327
  fi
1220
1328
  follow_unwrap "${toks[j]}"
1221
1329
  tj="$follow_unwrapped"
1222
- follow_target "$tj" "$depth"
1330
+ if ! follow_supervisor "$tj" "$depth" "$stmt"; then
1331
+ follow_target "$tj" "$depth"
1332
+ fi
1223
1333
  cmd_pos=0
1224
1334
  i=$((j + 1))
1225
1335
  continue
@@ -1248,6 +1358,7 @@ follow_scan() {
1248
1358
  }
1249
1359
 
1250
1360
  follow_scan "$command_for_guards" 0
1361
+ rm_command_for_guards="$command_for_guards"$'\n'"$followed_rm_text"
1251
1362
  if [ -n "$follow_names" ]; then
1252
1363
  command_for_guards="$command_for_guards"$'\n'"$followed_text"
1253
1364
  # A refusal must say WHERE the match is. Without this the operator reads
@@ -1266,6 +1377,8 @@ fi
1266
1377
  # `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
1267
1378
  normalized_command_str="$(printf '%s' "$command_for_guards" \
1268
1379
  | awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
1380
+ rm_normalized_command_str="$(printf '%s' "$rm_command_for_guards" \
1381
+ | awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
1269
1382
 
1270
1383
  # matches / matches_cs run an ERE against the guarded command text. matches is
1271
1384
  # case-insensitive (the default for these guards); matches_cs is case-SENSITIVE
@@ -1570,7 +1683,7 @@ rm_assignment_value() {
1570
1683
  # small lexer below preserves quoted right-hand sides such as V="/tmp/x"
1571
1684
  # while excluding an argument such as echo "V=/tmp/x". Any ambiguity fails
1572
1685
  # closed.
1573
- RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$normalized_command_str" \
1686
+ RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$rm_normalized_command_str" \
1574
1687
  RM_ASSIGNMENT_STATEMENT="$rm_assignment_stmt" \
1575
1688
  python3 - <<'PY'
1576
1689
  import os
@@ -1769,7 +1882,7 @@ classify_rm_target() {
1769
1882
  # process substitution cannot deny anything either — it ends that subshell, not
1770
1883
  # this hook.
1771
1884
  rm_segments_status=0
1772
- rm_segments="$(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
1885
+ rm_segments="$(printf '%s' "$rm_normalized_command_str" | tr '&|;' '\n' \
1773
1886
  | grep -Ei -- "$RM_CMD"'([[:space:]]|$)')" || rm_segments_status=$?
1774
1887
  case "$rm_segments_status" in
1775
1888
  0 | 1) ;;
@@ -2281,9 +2394,10 @@ git_segments="$(printf '%s' "$normalized_command_str" \
2281
2394
  -e 's/[0-9]*>&[0-9-]*/>/g' \
2282
2395
  -e 's/[0-9]*<&[0-9-]*/</g' \
2283
2396
  -e 's/||/;/g' \
2284
- | tr '&;' '\n')" || git_segments_status=$?
2397
+ | tr '&;' '\n' \
2398
+ | grep -E -- "$RM_RF_CLUSTER|$RM_RF_SPLIT")" || git_segments_status=$?
2285
2399
  case "$git_segments_status" in
2286
- 0) ;;
2400
+ 0 | 1) ;;
2287
2401
  *) scan_failed "$git_segments_status" ;;
2288
2402
  esac
2289
2403
  while IFS= read -r git_stmt; do
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -527,6 +527,94 @@ follow_names=""
527
527
  follow_count=0
528
528
  follow_bytes=0
529
529
  followed_text=""
530
+ followed_rm_text=""
531
+ # Generated from the canonical reviewed supervisor, never from a host manifest.
532
+ # Regenerate through scripts/generate-scratch-supervisor-profile.mjs.
533
+ readonly SCRATCH_SUPERVISOR_SHA256='6fdec1ad0441d7636dc6749c44a23167baf7b75004f156b8a7d4ce1c18c790b7'
534
+ readonly SCRATCH_SUPERVISOR_CLEANUP_COUNT='7'
535
+
536
+ # An authenticated public supervisor owns its fixed cleanup sites. Its original
537
+ # source remains visible to every other policy; its actual payload is followed.
538
+ follow_supervisor() {
539
+ local token="$1" depth="$2" statement="$3" path="" profile="" payload="" rm_source="" original=""
540
+ case "$token" in *lisa-scratch-run.sh) ;; *) return 1 ;; esac
541
+ follow_locate "$token" || follow_refuse "$token" "supervisor source could not be resolved"
542
+ path="$follow_located"
543
+ profile="$(python3 - "$path" "$SCRATCH_SUPERVISOR_SHA256" "$statement" "$token" "$SCRATCH_SUPERVISOR_CLEANUP_COUNT" <<'PY'
544
+ import hashlib
545
+ import json
546
+ import os
547
+ import re
548
+ import shlex
549
+ import stat
550
+ import sys
551
+
552
+ path, digest, statement, operand, expected_count = sys.argv[1:]
553
+ def refuse():
554
+ sys.exit(2)
555
+
556
+ try:
557
+ info = os.lstat(path)
558
+ if not stat.S_ISREG(info.st_mode) or info.st_size > 262144:
559
+ refuse()
560
+ raw = open(path, 'rb').read(262145)
561
+ if len(raw) != info.st_size or not re.fullmatch(r'[0-9a-f]{64}', digest):
562
+ refuse()
563
+ if hashlib.sha256(raw).hexdigest() != digest:
564
+ refuse()
565
+ tokens = shlex.split(statement)
566
+ indices = [index for index, token in enumerate(tokens) if token == operand]
567
+ if len(indices) != 1 or '$' in operand or '`' in operand:
568
+ refuse()
569
+ index = indices[0]
570
+ if index < 1 or tokens[index - 1] not in ('sh', 'bash', '/bin/sh', '/bin/bash'):
571
+ refuse()
572
+ prefix = tokens[:index - 1]
573
+ if prefix[:1] == ['env']:
574
+ prefix = prefix[1:]
575
+ assignments = {}
576
+ for token in prefix:
577
+ if '=' not in token or re.search(r'[$`\r\n]', token):
578
+ refuse()
579
+ key, value = token.split('=', 1)
580
+ if key not in ('LISA_SCRATCH_BASE', 'LISA_SCRATCH_TRACE', 'TMPDIR') or key in assignments:
581
+ refuse()
582
+ assignments[key] = value
583
+ # Match actual shell :- precedence, including inherited and
584
+ # explicitly empty values. TMPDIR cannot override a nonempty inherited base.
585
+ effective = dict(os.environ, **assignments)
586
+ base = effective.get('LISA_SCRATCH_BASE') or effective.get('TMPDIR') or '/tmp'
587
+ if not base or not os.path.isabs(base) or re.search(r'[$`\r\n]', base):
588
+ refuse()
589
+ real_base = os.path.realpath(base)
590
+ roots = ['/tmp', '/var/tmp', os.environ.get('TMPDIR', '/tmp')]
591
+ if not any(os.path.commonpath([real_base, os.path.realpath(root)]) == os.path.realpath(root) for root in roots if root and os.path.isabs(root)):
592
+ refuse()
593
+ args = tokens[index + 1:]
594
+ if len(args) < 4 or args[0] != '--suite' or args[2] != '--':
595
+ refuse()
596
+ if not re.fullmatch(r'[A-Za-z0-9._-]{1,64}', args[1]):
597
+ refuse()
598
+ payload = args[3:]
599
+ if not payload[0] or payload[0].startswith('-') or any(re.search(r'[$`\x00\r\n]', value) for value in payload):
600
+ refuse()
601
+ source = raw.decode('utf8')
602
+ cleaned, count = re.subn(r'(?m)^(\s*)rm -rf "(\$_quarantine|\$_root|\$_entry|\$LISA_SCRATCH_ROOT)"', r'\1:', source)
603
+ if not re.fullmatch(r'[1-9][0-9]*', expected_count) or count != int(expected_count):
604
+ refuse()
605
+ print(json.dumps({'source': cleaned, 'original': source, 'payload': shlex.join(payload)}))
606
+ except (OSError, UnicodeError, ValueError):
607
+ refuse()
608
+ PY
609
+ )" || follow_refuse "$path" "supervisor source or public entry authority could not be authenticated"
610
+ rm_source="$(printf '%s' "$profile" | jq -er '.source')" || follow_refuse "$path" "invalid supervisor profile"
611
+ original="$(printf '%s' "$profile" | jq -er '.original')" || follow_refuse "$path" "invalid supervisor source"
612
+ payload="$(printf '%s' "$profile" | jq -er '.payload')" || follow_refuse "$path" "invalid supervisor payload"
613
+ follow_take "$path" "$depth" "$rm_source" "$original"
614
+ [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ] || follow_refuse "$path" "supervisor payload exceeds inspection depth"
615
+ follow_scan "$payload" "$((depth + 1))"
616
+ return 0
617
+ }
530
618
 
531
619
  FOLLOW_GUIDANCE="This command EXECUTES a file. These guards classify shell text, so the script an
532
620
  invocation runs is read and scanned as part of the command — a destructive line
@@ -813,7 +901,7 @@ follow_locate() {
813
901
  # executes, to a bounded depth. Full-line comments are dropped: a commented line
814
902
  # is never executed, so scanning it can only manufacture a false refusal.
815
903
  follow_take() {
816
- local path="$1" depth="$2" size="" content=""
904
+ local path="$1" depth="$2" size="" content="" rm_content="${3-}"
817
905
  case "$follow_paths" in
818
906
  *$'\n'"$path"$'\n'*) return 0 ;;
819
907
  esac
@@ -833,10 +921,22 @@ follow_take() {
833
921
  if [ "$follow_bytes" -gt "$FOLLOW_MAX_TOTAL_BYTES" ]; then
834
922
  follow_refuse "$path" "the files this command executes total more than the ${FOLLOW_MAX_TOTAL_BYTES}-byte inspection budget"
835
923
  fi
836
- content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
837
- || follow_refuse "$path" "it could not be read"
924
+ if [ "$#" -eq 4 ]; then
925
+ # Scan the same complete bytes whose digest was authenticated, not a
926
+ # second file read that could pair changed source with the cleanup view.
927
+ content="$(printf '%s' "$4" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "authenticated source could not be read"
928
+ else
929
+ content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
930
+ || follow_refuse "$path" "it could not be read"
931
+ fi
838
932
  follow_names="$follow_names $path"
839
933
  followed_text="$followed_text"$'\n'"$content"
934
+ if [ "$#" -lt 3 ]; then
935
+ rm_content="$content"
936
+ else
937
+ rm_content="$(printf '%s' "$rm_content" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "supervisor cleanup view could not be read"
938
+ fi
939
+ followed_rm_text="$followed_rm_text"$'\n'"$rm_content"
840
940
  if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
841
941
  # A `cd` inside the followed script governs what THAT script goes on to
842
942
  # execute, and nothing after it in the command that ran the script — the
@@ -855,6 +955,11 @@ follow_take() {
855
955
  follow_target() {
856
956
  local token="$1" depth="$2" path=""
857
957
  [ -n "$token" ] || return 0
958
+ # Entry authority belongs to each invocation, not the file-read cache. Only
959
+ # follow_supervisor may admit the exact public interpreter/argv profile.
960
+ case "$token" in
961
+ *lisa-scratch-run.sh) follow_refuse "$token" "supervisor requires its authenticated public interpreter entry" ;;
962
+ esac
858
963
  if ! follow_locate "$token"; then
859
964
  if [ "$depth" -eq 0 ]; then
860
965
  follow_refuse "$token" "no readable file could be resolved for it"
@@ -888,6 +993,9 @@ follow_direct() {
888
993
  return 0
889
994
  fi
890
995
  path="$follow_located"
996
+ case "$path" in
997
+ *lisa-scratch-run.sh) follow_refuse "$path" "supervisor requires its authenticated public interpreter entry" ;;
998
+ esac
891
999
  IFS= read -r -n 256 raw <"$path" 2>/dev/null || true
892
1000
  while [ "$k" -lt "${#raw}" ]; do
893
1001
  char="${raw:k:1}"
@@ -1219,7 +1327,9 @@ follow_scan() {
1219
1327
  fi
1220
1328
  follow_unwrap "${toks[j]}"
1221
1329
  tj="$follow_unwrapped"
1222
- follow_target "$tj" "$depth"
1330
+ if ! follow_supervisor "$tj" "$depth" "$stmt"; then
1331
+ follow_target "$tj" "$depth"
1332
+ fi
1223
1333
  cmd_pos=0
1224
1334
  i=$((j + 1))
1225
1335
  continue
@@ -1248,6 +1358,7 @@ follow_scan() {
1248
1358
  }
1249
1359
 
1250
1360
  follow_scan "$command_for_guards" 0
1361
+ rm_command_for_guards="$command_for_guards"$'\n'"$followed_rm_text"
1251
1362
  if [ -n "$follow_names" ]; then
1252
1363
  command_for_guards="$command_for_guards"$'\n'"$followed_text"
1253
1364
  # A refusal must say WHERE the match is. Without this the operator reads
@@ -1266,6 +1377,8 @@ fi
1266
1377
  # `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
1267
1378
  normalized_command_str="$(printf '%s' "$command_for_guards" \
1268
1379
  | awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
1380
+ rm_normalized_command_str="$(printf '%s' "$rm_command_for_guards" \
1381
+ | awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
1269
1382
 
1270
1383
  # matches / matches_cs run an ERE against the guarded command text. matches is
1271
1384
  # case-insensitive (the default for these guards); matches_cs is case-SENSITIVE
@@ -1570,7 +1683,7 @@ rm_assignment_value() {
1570
1683
  # small lexer below preserves quoted right-hand sides such as V="/tmp/x"
1571
1684
  # while excluding an argument such as echo "V=/tmp/x". Any ambiguity fails
1572
1685
  # closed.
1573
- RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$normalized_command_str" \
1686
+ RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$rm_normalized_command_str" \
1574
1687
  RM_ASSIGNMENT_STATEMENT="$rm_assignment_stmt" \
1575
1688
  python3 - <<'PY'
1576
1689
  import os
@@ -1769,7 +1882,7 @@ classify_rm_target() {
1769
1882
  # process substitution cannot deny anything either — it ends that subshell, not
1770
1883
  # this hook.
1771
1884
  rm_segments_status=0
1772
- rm_segments="$(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
1885
+ rm_segments="$(printf '%s' "$rm_normalized_command_str" | tr '&|;' '\n' \
1773
1886
  | grep -Ei -- "$RM_CMD"'([[:space:]]|$)')" || rm_segments_status=$?
1774
1887
  case "$rm_segments_status" in
1775
1888
  0 | 1) ;;
@@ -2281,9 +2394,10 @@ git_segments="$(printf '%s' "$normalized_command_str" \
2281
2394
  -e 's/[0-9]*>&[0-9-]*/>/g' \
2282
2395
  -e 's/[0-9]*<&[0-9-]*/</g' \
2283
2396
  -e 's/||/;/g' \
2284
- | tr '&;' '\n')" || git_segments_status=$?
2397
+ | tr '&;' '\n' \
2398
+ | grep -E -- "$RM_RF_CLUSTER|$RM_RF_SPLIT")" || git_segments_status=$?
2285
2399
  case "$git_segments_status" in
2286
- 0) ;;
2400
+ 0 | 1) ;;
2287
2401
  *) scan_failed "$git_segments_status" ;;
2288
2402
  esac
2289
2403
  while IFS= read -r git_stmt; do
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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.71.3",
3
+ "version": "4.71.5",
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"