@codyswann/lisa 4.81.5 → 4.81.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +236 -16
  2. package/dist/core/downstream-references.d.ts.map +1 -1
  3. package/dist/core/downstream-references.js +3 -0
  4. package/dist/core/downstream-references.js.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  6. package/dist/core/lisa-owned-hash-ledger.js +8 -0
  7. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  8. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +5 -2
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/dist/opencode/plugin-templates/parity-safety-net.sh +236 -16
  13. package/package.json +4 -4
  14. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa/hooks/parity-safety-net.sh +236 -16
  17. package/plugins/lisa-agy/hooks/parity-safety-net.sh +236 -16
  18. package/plugins/lisa-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +236 -16
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +236 -16
  30. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  32. package/plugins/lisa-devin/hooks/parity-safety-net.sh +236 -16
  33. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-agy/plugin.json +1 -1
  36. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  50. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-devin/.devin-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-phaser-devin/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-agy/plugin.json +1 -1
  71. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  78. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  85. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  89. package/plugins/src/base/hooks/parity-safety-net.sh +236 -16
@@ -808,6 +808,62 @@ follow_split() {
808
808
  }
809
809
  return 0;
810
810
  }
811
+ # U records tell the walker the stream below may not match the shell, so
812
+ # a compound closer can no longer be trusted (follow_compound_close). They
813
+ # only ever ADD distrust; every other record is emitted exactly as before.
814
+ #
815
+ # A statement spanning lines (a quoted newline, a backslash continuation)
816
+ # reaches the line-oriented reader as several records, and a later line
817
+ # can spell `done` or `fi` on its own.
818
+ function record(kind, text) {
819
+ if (text ~ /\n/) { printf "U\t\n" }
820
+ printf "%s\t%s\n", kind, text;
821
+ }
822
+ # The first word of one separator-delimited piece of a comment, as the
823
+ # walker would read it if the split hands it over as a statement.
824
+ function comment_reserved(piece) {
825
+ sub(/^[[:space:]]+/, "", piece); sub(/[[:space:]].*$/, "", piece);
826
+ gsub(/["\047]/, "", piece);
827
+ return piece ~ /^(fi|done|esac|case|if|then|else|elif|do|while|until|for|select|function|!|\{|\})$/;
828
+ }
829
+ # This split does not recognise comments, so a comment is split as code. It
830
+ # is harmless only when that split cannot change any structure the walker
831
+ # tracks: no piece after a separator starts with a reserved word, quotes
832
+ # close, parentheses and braces balance, and nothing escapes or substitutes.
833
+ # Inside a case pattern a `)` ends the pattern, so it is never harmless
834
+ # there; a `(` is ignored there, which keeps the Gradle `#(` safe.
835
+ # Anything else is reported, and the walker stops restoring flow.
836
+ function comment_unsafe(from, pattern, j, c, cq, parens, braces, pieces, piece) {
837
+ cq = ""; parens = 0; braces = 0; pieces = 0; piece = "";
838
+ for (j = from; j <= n; j++) {
839
+ c = substr(s, j, 1);
840
+ if (c == "\n") { break }
841
+ if (c == "\\" || c == "`") { return 1 }
842
+ if (cq != "") {
843
+ if (c == cq) { cq = "" }
844
+ else if (cq == "\"" && c == "(" && substr(s, j - 1, 1) == "$") { return 1 }
845
+ piece = piece c; continue;
846
+ }
847
+ if (c == "\"" || c == "\047") { cq = c; piece = piece c; continue }
848
+ if (c ~ /[;&|(){}]/) {
849
+ if (pieces > 0 && comment_reserved(piece)) { return 1 }
850
+ pieces++; piece = "";
851
+ if (c == "(" && !pattern) { parens++ }
852
+ if (c == ")" && (pattern || --parens < 0)) { return 1 }
853
+ if (c == "{") { braces++ }
854
+ if (c == "}" && --braces < 0) { return 1 }
855
+ continue;
856
+ }
857
+ piece = piece c;
858
+ }
859
+ if (pieces > 0 && comment_reserved(piece)) { return 1 }
860
+ return cq != "" || parens != 0 || braces != 0;
861
+ }
862
+ function comment_check(at, pattern) {
863
+ if ((at == 1 || substr(s, at - 1, 1) ~ /[[:space:];&|()]/) && comment_unsafe(at + 1, pattern)) {
864
+ printf "U\t\n";
865
+ }
866
+ }
811
867
  BEGIN {
812
868
  s = ENVIRON["SN_FOLLOW_TEXT"]; n = length(s);
813
869
  q = ""; esc = 0; start = 1; sep = "o"; parens = 0;
@@ -820,6 +876,7 @@ follow_split() {
820
876
  if (functions > 0) {
821
877
  if (q != "") { if (c == q) { q = "" } continue }
822
878
  if (c == "\"" || c == "\047") { q = c; continue }
879
+ if (c == "#") { comment_check(i, 0) }
823
880
  if (c == "{") { functions++ }
824
881
  if (c == "}" && --functions == 0) {
825
882
  printf "N\t%s\t%s\n", sep, function_name;
@@ -833,10 +890,11 @@ follow_split() {
833
890
  quoted_expression = q == "\"" && ((c == "(" && substr(s, i - 1, 1) == "$" && !literal_dollars[i - 1]) || c == "`");
834
891
  if (q != "" && !quoted_expression) { if (c == q) { q = "" } continue }
835
892
  if (c == "\"" || c == "\047") { q = c; continue }
893
+ if (c == "#") { comment_check(i, cases > 0 && case_scopes[cases] == parens && case_patterns[cases]) }
836
894
  if (c == "`") {
837
895
  if (backtick) {
838
896
  piece = substr(s, start, i - start);
839
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
897
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
840
898
  printf "]\t\n"; start = backtick_start; q = backtick_quote; backtick = 0;
841
899
  } else {
842
900
  backtick_start = start; backtick_quote = q; q = ""; backtick = 1;
@@ -876,14 +934,14 @@ follow_split() {
876
934
  brace_groups[++brace_depth] = group;
877
935
  if (group) {
878
936
  brace_sep = sep;
879
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
937
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
880
938
  # A pipeline owns the whole compound command, not just the final
881
939
  # statement before |. Snapshot before any brace-body cd/return.
882
940
  outgoing = brace_pipe(i + 1, "{", "}");
883
941
  brace_pipes[brace_depth] = brace_sep == "|" || outgoing;
884
942
  if (brace_pipes[brace_depth]) { printf "(\t%s%s\nL\t%d\n", (brace_sep == "|" ? "|" : ""), (outgoing ? "+" : ""), list_async(i + 1) }
885
943
  printf "G\t%s\nL\t%d\n", brace_sep, list_async(i + 1);
886
- if (brace_redirect ~ /[^[:space:]]/) { printf "I\t%s\n", brace_redirect }
944
+ if (brace_redirect ~ /[^[:space:]]/) { record("I", brace_redirect) }
887
945
  # P retains incoming stdin provenance without wrapping each body
888
946
  # statement in another child: all statements share the brace cwd.
889
947
  if (brace_sep == "|") { sep = "P" }
@@ -894,7 +952,7 @@ follow_split() {
894
952
  group = brace_groups[brace_depth--];
895
953
  if (group) {
896
954
  piece = substr(s, start, i - start);
897
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
955
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
898
956
  printf "H\t\n";
899
957
  if (brace_pipes[brace_depth + 1]) { printf ")\t\n" }
900
958
  start = i + 1; post_brace = 1; continue;
@@ -915,10 +973,10 @@ follow_split() {
915
973
  }
916
974
  if (group) {
917
975
  piece = substr(s, start, i - start);
918
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
976
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
919
977
  outgoing = brace_pipe(i + 1, "(", ")");
920
978
  printf "(\t%s%s\nL\t%d\n", sep, (outgoing ? "+" : ""), list_async(i + 1);
921
- if (brace_redirect ~ /[^[:space:]]/) { printf "I\t%s\n", brace_redirect }
979
+ if (brace_redirect ~ /[^[:space:]]/) { record("I", brace_redirect) }
922
980
  if (sep == "|") { sep = "P" }
923
981
  start = i + 1; continue;
924
982
  }
@@ -927,12 +985,12 @@ follow_split() {
927
985
  piece = substr(s, start, i - start);
928
986
  group = scopes[parens--];
929
987
  if (group == 2) {
930
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
988
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
931
989
  printf "]\t\n"; start = expression_starts[parens + 1]; q = expression_quotes[parens + 1]; continue;
932
990
  }
933
991
  if (group) {
934
992
  piece = substr(s, start, i - start);
935
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
993
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
936
994
  printf ")\t\n"; start = i + 1; post_brace = 1; continue;
937
995
  }
938
996
  }
@@ -942,7 +1000,7 @@ follow_split() {
942
1000
  piece = substr(s, start, i - start);
943
1001
  pipe = c == "|" && substr(s, i + 1, 1) != "|";
944
1002
  if (pipe || sep == "|") { printf "(\t%s%s\nL\t%d\n", (sep == "|" ? "|" : ""), (pipe ? "+" : ""), list_async(start) }
945
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", post_brace ? "R" : sep, piece }
1003
+ if (piece ~ /[^[:space:]]/) { record(post_brace ? "R" : sep, piece) }
946
1004
  post_brace = 0;
947
1005
  if (pipe || sep == "|") { printf ")\t\n" }
948
1006
  if (c == ";" && substr(s, i + 1, 1) == ";" && cases > 0) {
@@ -962,7 +1020,7 @@ follow_split() {
962
1020
  }
963
1021
  }
964
1022
  if (sep == "|") { printf "(\t|\nL\t%d\n", list_async(start) }
965
- printf "%s\t%s\n", sep, substr(s, start, n - start + 1);
1023
+ record(sep, substr(s, start, n - start + 1));
966
1024
  if (sep == "|") { printf ")\t\n" }
967
1025
  }
968
1026
  '
@@ -1432,6 +1490,77 @@ PY
1432
1490
  follow_redirect_output="$(printf '%s' "$result" | jq -er '.output')" || follow_refuse "compound redirection" "invalid output projection"
1433
1491
  }
1434
1492
 
1493
+ # Enter a compound command (`if`, `while`/`until`/`for`/`select`, `case`).
1494
+ #
1495
+ # Reaching anything inside it depends on runtime flow, so a definition there is
1496
+ # conditional. The enclosing flow state is saved so the matching closer can put
1497
+ # it back: a compound that has CLOSED no longer decides whether the next
1498
+ # top-level definition runs. Leaving the flag raised past the closer marked
1499
+ # every function defined after any loop or case as conditional, which refused
1500
+ # every Gradle-generated start script (CodySwannGT/lisa#4470). Brace groups
1501
+ # already restore this way through brace_flows.
1502
+ #
1503
+ # These read and write follow_scan's locals through bash's dynamic scoping.
1504
+ follow_compound_open() {
1505
+ local frame_scope=$scope_depth
1506
+ # A pipeline element is its own scope, but `if a | b; then` and
1507
+ # `x | while read l; do` open their compound at the ENCLOSING level.
1508
+ if [ "$scope_depth" -gt 0 ]; then
1509
+ case "${scope_kinds[scope_depth - 1]}" in '+' | '|' | '|+') frame_scope=$((scope_depth - 1)) ;; esac
1510
+ fi
1511
+ compound_flows[compound_depth]=$function_flow_unknown
1512
+ compound_kinds[compound_depth]="$1"
1513
+ compound_frame_scopes[compound_depth]=$frame_scope
1514
+ compound_opened_at="$compound_opened_at${i:-} "
1515
+ compound_depth=$((compound_depth + 1))
1516
+ function_flow_unknown=1
1517
+ }
1518
+
1519
+ # Close the innermost compound, restoring the flow state that encloses it.
1520
+ #
1521
+ # Only a closer of the SAME kind as the innermost open compound may lower the
1522
+ # flag. The walker splits text, it does not parse shell: a comment's `;` or a
1523
+ # heredoc payload line can reach it as a `fi`/`done` statement. A closer that
1524
+ # matches nothing, or arrives after a heredoc (see compound_trusted), proves the
1525
+ # walk has lost the structure, so every later closer keeps the flag where it is
1526
+ # — the verdict this walker gave before closers restored anything.
1527
+ #
1528
+ # A conditional `return` inside the compound can skip whatever follows it in a
1529
+ # function body or sourced file, so it re-raises the flag, as H does for a brace
1530
+ # group. `break`/`continue` resume at or inside the loop, so they never skip a
1531
+ # definition after `done`, and `exit` ends the shell before any later call runs:
1532
+ # neither needs to persist past the closer. A `break` inside a function body
1533
+ # can end the CALLER's loop and skip the rest of that body, but such a call only
1534
+ # has that effect when the call itself sits inside a loop body, which is
1535
+ # already conditional.
1536
+ #
1537
+ # The closer must also sit in the scope its compound opened in: a `done` inside
1538
+ # a command substitution (`$( … )` or backticks) is a different shell's syntax
1539
+ # error, not the end of this loop. The one cross-scope closer is `done | cmd`,
1540
+ # whose `done` is the first element of a pipeline. Callers pass only a closer
1541
+ # spelled as the bare first word of its statement; see the fi/done arm.
1542
+ follow_compound_close() {
1543
+ local frame_scope
1544
+ if [ "$compound_trusted" -eq 0 ] || [ "$compound_depth" -eq 0 ] \
1545
+ || [ "${compound_kinds[compound_depth - 1]}" != "$1" ]; then
1546
+ compound_trusted=0
1547
+ return 0
1548
+ fi
1549
+ frame_scope="${compound_frame_scopes[compound_depth - 1]}"
1550
+ if [ "$scope_depth" -ne "$frame_scope" ]; then
1551
+ case "$((scope_depth - frame_scope)):${scope_kinds[scope_depth - 1]}" in
1552
+ '1:+' | '1:|' | '1:|+') ;;
1553
+ *)
1554
+ compound_trusted=0
1555
+ return 0
1556
+ ;;
1557
+ esac
1558
+ fi
1559
+ compound_depth=$((compound_depth - 1))
1560
+ function_flow_unknown="${compound_flows[compound_depth]}"
1561
+ if [ "$return_flow_unknown" -ne 0 ]; then function_flow_unknown=1; fi
1562
+ }
1563
+
1435
1564
  # Walk one text for the files it executes. Runs in the PARENT shell, never in a
1436
1565
  # command substitution: follow_refuse() must be able to deny, and an exit inside
1437
1566
  # a subshell ends that subshell rather than this hook — the mistake documented
@@ -1467,6 +1596,18 @@ follow_scan() {
1467
1596
  local -a brace_descriptor_maps=() scope_descriptor_maps=()
1468
1597
  local -a brace_stdin_cats=() brace_stdin_cwds=() brace_stdin_unknowns=() scope_stdin_cats=() scope_stdin_cwds=() scope_stdin_unknowns=()
1469
1598
  local -a case_cwds=() case_unknowns=() case_changes=()
1599
+ # Open compound commands of THIS text, innermost last. See
1600
+ # follow_compound_open. The stack starts empty, so a closer can never lower
1601
+ # the flag below the value this scan was entered with.
1602
+ local compound_depth=0 compound_trusted=1 compound_opened_at=" " compound_prev
1603
+ local -a compound_flows=() compound_kinds=() compound_scopes=() compound_frame_scopes=() scope_kinds=()
1604
+ # The split treats every [[:space:]] byte as a blank, but bash separates words
1605
+ # only on space and tab (and ends a command on newline). A form feed,
1606
+ # vertical tab or carriage return is an ordinary word byte to bash, so
1607
+ # `<FF>done` is a command name there and a bare closer here. With none of the
1608
+ # three present, every [[:space:]] the split tests is a bash blank or a
1609
+ # newline, so this one check covers each such test in the split.
1610
+ case "$text" in *$'\f'* | *$'\v'* | *$'\r'*) compound_trusted=0 ;; esac
1470
1611
  if [ "$function_call_depth" -eq 0 ] && [ "$invocation_kind" != source ]; then
1471
1612
  # Ordinary child shells do not inherit unexported functions. Sourcing and
1472
1613
  # function calls share the caller's definitions instead.
@@ -1480,6 +1621,14 @@ follow_scan() {
1480
1621
  stmt="${line#*$'\t'}"
1481
1622
  statement_conditional=0
1482
1623
  case "$sep" in '&' | '?') statement_conditional=1 ;; esac
1624
+ # A heredoc payload reaches this walk as statements, so a payload line can
1625
+ # spell a closer. Once one starts, closers stop restoring flow for the rest
1626
+ # of this text. Checked before any skip so an unread payload still counts.
1627
+ # A here-string (`<<<`) carries no payload lines.
1628
+ # The same test also matches an arithmetic shift such as `$((1 << 2))`.
1629
+ # That only withholds a restore, which is the safe direction.
1630
+ case "${stmt//<<</}" in *'<<'*) compound_trusted=0 ;; esac
1631
+ if [ "$sep" = U ]; then compound_trusted=0; continue; fi
1483
1632
  # A reached && successor observes the preceding successful command's cwd.
1484
1633
  # Scope/list events invalidate this projection; it must never carry a
1485
1634
  # child's or a background list's directory into another shell/list.
@@ -1570,7 +1719,7 @@ follow_scan() {
1570
1719
  continue
1571
1720
  ;;
1572
1721
  C)
1573
- function_flow_unknown=1
1722
+ follow_compound_open case
1574
1723
  case_cwds[case_depth]="$follow_cwd"
1575
1724
  case_unknowns[case_depth]="$follow_cwd_unknown"
1576
1725
  case_changes[case_depth]=0
@@ -1589,6 +1738,9 @@ follow_scan() {
1589
1738
  follow_cwd=""; follow_cwd_unknown=1
1590
1739
  fi
1591
1740
  case_depth=$((case_depth - 1))
1741
+ # K closes the case; the literal `esac` statement the split emits
1742
+ # after it is not a closer and must not close another frame.
1743
+ follow_compound_close case
1592
1744
  fi
1593
1745
  continue
1594
1746
  ;;
@@ -1601,6 +1753,10 @@ follow_scan() {
1601
1753
  scope_descriptor_maps[scope_depth]="$follow_descriptor_map"
1602
1754
  function_scopes[scope_depth]=$function_count
1603
1755
  flow_scopes[scope_depth]=$function_flow_unknown
1756
+ compound_scopes[scope_depth]=$compound_depth
1757
+ # `[` is a substitution; for `(` the split's tail says whether this is
1758
+ # a pipeline element (`+`, `|`, `|+`) or a literal subshell.
1759
+ if [ "$sep" = '[' ]; then scope_kinds[scope_depth]='['; else scope_kinds[scope_depth]="$stmt"; fi
1604
1760
  return_flows[scope_depth]=$return_flow_unknown
1605
1761
  scope_stdin_cats[scope_depth]="$follow_stdin_cat"
1606
1762
  scope_stdin_cwds[scope_depth]="$follow_stdin_cwd"
@@ -1626,6 +1782,16 @@ follow_scan() {
1626
1782
  follow_cwd_unknown="${unknown_scopes[scope_depth]}"
1627
1783
  function_flow_unknown="${flow_scopes[scope_depth]}"
1628
1784
  return_flow_unknown="${return_flows[scope_depth]}"
1785
+ # A pipeline element is a scope of its own, so `done | cmd` closes, in
1786
+ # here, a loop opened outside. The scope must not undo that closer: the
1787
+ # flow after the pipeline is the one the loop's frame saved. The stack
1788
+ # itself is never rewound here. `if a | b; then` opens its `if` inside
1789
+ # the first element, and discarding that frame would let the `fi` close
1790
+ # an ENCLOSING compound instead.
1791
+ if [ "$compound_depth" -lt "${compound_scopes[scope_depth]}" ]; then
1792
+ function_flow_unknown="${compound_flows[compound_depth]}"
1793
+ if [ "$return_flow_unknown" -ne 0 ]; then function_flow_unknown=1; fi
1794
+ fi
1629
1795
  follow_stdin_cat="${scope_stdin_cats[scope_depth]}"
1630
1796
  follow_stdin_cwd="${scope_stdin_cwds[scope_depth]}"
1631
1797
  follow_stdin_unknown="${scope_stdin_unknowns[scope_depth]}"
@@ -1691,6 +1857,7 @@ follow_scan() {
1691
1857
  n=${#toks[@]}
1692
1858
  [ "$n" -gt 0 ] || continue
1693
1859
  i=0
1860
+ compound_opened_at=" "
1694
1861
  cmd_pos=1
1695
1862
  function_lookup_allowed=1
1696
1863
  eval_mode=0
@@ -1795,12 +1962,32 @@ follow_scan() {
1795
1962
  fi
1796
1963
  cmd_pos=0; i=$((i + 1)); continue
1797
1964
  ;;
1798
- if | while | until)
1799
- function_flow_unknown=1
1965
+ if)
1966
+ follow_compound_open if
1967
+ i=$((i + 1)); continue
1968
+ ;;
1969
+ while | until)
1970
+ follow_compound_open loop
1800
1971
  i=$((i + 1)); continue
1801
1972
  ;;
1802
1973
  for | select)
1803
- function_flow_unknown=1
1974
+ follow_compound_open loop
1975
+ cmd_pos=0; i=$((i + 1)); continue
1976
+ ;;
1977
+ # A closer counts only as the bare, unquoted FIRST word of its
1978
+ # statement. Anything else that unwraps to one — `"done"`, `\fi`,
1979
+ # `x=1 done`, `eval done`, `command done`, the tail of `x='a; fi'` — is
1980
+ # not a reserved word to the shell, so its appearance means the walk
1981
+ # cannot prove where compounds end. A literal `esac` statement follows
1982
+ # every K record and is harmless in that one spelling.
1983
+ 'fi' | 'done' | 'esac')
1984
+ if [ "$i" -ne 0 ] || [ "${toks[0]}" != "$t" ]; then
1985
+ compound_trusted=0
1986
+ elif [ "$t" = 'fi' ]; then
1987
+ follow_compound_close if
1988
+ elif [ "$t" = 'done' ]; then
1989
+ follow_compound_close loop
1990
+ fi
1804
1991
  cmd_pos=0; i=$((i + 1)); continue
1805
1992
  ;;
1806
1993
  # Prefixes that keep the command position open: assignments, wrappers,
@@ -1842,11 +2029,13 @@ follow_scan() {
1842
2029
  continue
1843
2030
  ;;
1844
2031
  sudo | env | command | exec | nohup | nice | time | builtin \
1845
- | do | then | else | '!' | '{' \
2032
+ | do | then | else | elif | '!' | '{' \
1846
2033
  | */sudo | */env | */nohup | */nice | */time)
1847
2034
  case "$t" in
1848
2035
  sudo | env | command | exec | nohup | nice | builtin | */sudo | */env | */nohup | */nice) function_lookup_allowed=0 ;;
1849
- do | then | else) function_flow_unknown=1 ;;
2036
+ # `elif` starts a condition list at a command position, so an
2037
+ # opener after it is a real compound (`elif if x; then …; fi`).
2038
+ do | then | else | elif) function_flow_unknown=1 ;;
1850
2039
  esac
1851
2040
  i=$((i + 1))
1852
2041
  continue
@@ -2081,6 +2270,37 @@ follow_scan() {
2081
2270
  # Keep successful-branch provenance separate from the continuation join.
2082
2271
  # OR can skip its RHS and still reach a following &&, so it cannot prove
2083
2272
  # that RHS's cwd. A semicolon observes every possible preceding outcome.
2273
+ # Every opener must have pushed a frame, or its own closer pops the
2274
+ # ENCLOSING one and restores flow inside a compound that is still open.
2275
+ # Bash reads a reserved word wherever a command may start, which this walk
2276
+ # does not fully model: after `coproc NAME`, after a function header whose
2277
+ # body is a bare compound (`f () while …`), or after `function NAME`. So an
2278
+ # opener word the walk did not open stops all later restores unless the
2279
+ # word before it is a plain argument (`echo if`, `# loops for x`), where
2280
+ # bash reads it as data too. `case` never opens here (the split's C record
2281
+ # does), so a statement-initial `case` means the split missed its `in`.
2282
+ if [ "$compound_trusted" -ne 0 ]; then
2283
+ for ((j = 0; j < n; j++)); do
2284
+ case "${toks[j]}" in
2285
+ coproc) compound_trusted=0 ;;
2286
+ function) [ "$j" -ne 0 ] || compound_trusted=0 ;;
2287
+ if | while | until | for | select | case)
2288
+ case "$compound_opened_at" in *" $j "*) continue ;; esac
2289
+ if [ "$j" -eq 0 ]; then
2290
+ compound_trusted=0
2291
+ else
2292
+ compound_prev="${toks[j - 1]}"
2293
+ case "$compound_prev" in
2294
+ '' | *[!A-Za-z0-9_./,:=+@%-]* | elif | coproc | function | time | then | do | else)
2295
+ compound_trusted=0
2296
+ ;;
2297
+ esac
2298
+ fi
2299
+ ;;
2300
+ esac
2301
+ [ "$compound_trusted" -ne 0 ] || break
2302
+ done
2303
+ fi
2084
2304
  and_cwd="$follow_cwd"; and_unknown="$follow_cwd_unknown"
2085
2305
  if [ "$sep" != '?' ] && [ "$function_flow_unknown" -eq 0 ]; then and_ready=1; fi
2086
2306
  if [ "$statement_conditional" -ne 0 ] && { [ "$join_unknown" -ne 0 ] || [ "$follow_cwd_unknown" -ne 0 ] || [ "$follow_cwd" != "$join_cwd" ]; }; then
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.81.5",
3
+ "version": "4.81.6",
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.81.5",
3
+ "version": "4.81.6",
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.81.5",
3
+ "version": "4.81.6",
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.81.5",
3
+ "version": "4.81.6",
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.81.5",
3
+ "version": "4.81.6",
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.81.5",
3
+ "version": "4.81.6",
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.81.5",
3
+ "version": "4.81.6",
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.81.5",
3
+ "version": "4.81.6",
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.81.5",
3
+ "version": "4.81.6",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"