@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
@@ -805,6 +805,62 @@ follow_split() {
805
805
  }
806
806
  return 0;
807
807
  }
808
+ # U records tell the walker the stream below may not match the shell, so
809
+ # a compound closer can no longer be trusted (follow_compound_close). They
810
+ # only ever ADD distrust; every other record is emitted exactly as before.
811
+ #
812
+ # A statement spanning lines (a quoted newline, a backslash continuation)
813
+ # reaches the line-oriented reader as several records, and a later line
814
+ # can spell `done` or `fi` on its own.
815
+ function record(kind, text) {
816
+ if (text ~ /\n/) { printf "U\t\n" }
817
+ printf "%s\t%s\n", kind, text;
818
+ }
819
+ # The first word of one separator-delimited piece of a comment, as the
820
+ # walker would read it if the split hands it over as a statement.
821
+ function comment_reserved(piece) {
822
+ sub(/^[[:space:]]+/, "", piece); sub(/[[:space:]].*$/, "", piece);
823
+ gsub(/["\047]/, "", piece);
824
+ return piece ~ /^(fi|done|esac|case|if|then|else|elif|do|while|until|for|select|function|!|\{|\})$/;
825
+ }
826
+ # This split does not recognise comments, so a comment is split as code. It
827
+ # is harmless only when that split cannot change any structure the walker
828
+ # tracks: no piece after a separator starts with a reserved word, quotes
829
+ # close, parentheses and braces balance, and nothing escapes or substitutes.
830
+ # Inside a case pattern a `)` ends the pattern, so it is never harmless
831
+ # there; a `(` is ignored there, which keeps the Gradle `#(` safe.
832
+ # Anything else is reported, and the walker stops restoring flow.
833
+ function comment_unsafe(from, pattern, j, c, cq, parens, braces, pieces, piece) {
834
+ cq = ""; parens = 0; braces = 0; pieces = 0; piece = "";
835
+ for (j = from; j <= n; j++) {
836
+ c = substr(s, j, 1);
837
+ if (c == "\n") { break }
838
+ if (c == "\\" || c == "`") { return 1 }
839
+ if (cq != "") {
840
+ if (c == cq) { cq = "" }
841
+ else if (cq == "\"" && c == "(" && substr(s, j - 1, 1) == "$") { return 1 }
842
+ piece = piece c; continue;
843
+ }
844
+ if (c == "\"" || c == "\047") { cq = c; piece = piece c; continue }
845
+ if (c ~ /[;&|(){}]/) {
846
+ if (pieces > 0 && comment_reserved(piece)) { return 1 }
847
+ pieces++; piece = "";
848
+ if (c == "(" && !pattern) { parens++ }
849
+ if (c == ")" && (pattern || --parens < 0)) { return 1 }
850
+ if (c == "{") { braces++ }
851
+ if (c == "}" && --braces < 0) { return 1 }
852
+ continue;
853
+ }
854
+ piece = piece c;
855
+ }
856
+ if (pieces > 0 && comment_reserved(piece)) { return 1 }
857
+ return cq != "" || parens != 0 || braces != 0;
858
+ }
859
+ function comment_check(at, pattern) {
860
+ if ((at == 1 || substr(s, at - 1, 1) ~ /[[:space:];&|()]/) && comment_unsafe(at + 1, pattern)) {
861
+ printf "U\t\n";
862
+ }
863
+ }
808
864
  BEGIN {
809
865
  s = ENVIRON["SN_FOLLOW_TEXT"]; n = length(s);
810
866
  q = ""; esc = 0; start = 1; sep = "o"; parens = 0;
@@ -817,6 +873,7 @@ follow_split() {
817
873
  if (functions > 0) {
818
874
  if (q != "") { if (c == q) { q = "" } continue }
819
875
  if (c == "\"" || c == "\047") { q = c; continue }
876
+ if (c == "#") { comment_check(i, 0) }
820
877
  if (c == "{") { functions++ }
821
878
  if (c == "}" && --functions == 0) {
822
879
  printf "N\t%s\t%s\n", sep, function_name;
@@ -830,10 +887,11 @@ follow_split() {
830
887
  quoted_expression = q == "\"" && ((c == "(" && substr(s, i - 1, 1) == "$" && !literal_dollars[i - 1]) || c == "`");
831
888
  if (q != "" && !quoted_expression) { if (c == q) { q = "" } continue }
832
889
  if (c == "\"" || c == "\047") { q = c; continue }
890
+ if (c == "#") { comment_check(i, cases > 0 && case_scopes[cases] == parens && case_patterns[cases]) }
833
891
  if (c == "`") {
834
892
  if (backtick) {
835
893
  piece = substr(s, start, i - start);
836
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
894
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
837
895
  printf "]\t\n"; start = backtick_start; q = backtick_quote; backtick = 0;
838
896
  } else {
839
897
  backtick_start = start; backtick_quote = q; q = ""; backtick = 1;
@@ -873,14 +931,14 @@ follow_split() {
873
931
  brace_groups[++brace_depth] = group;
874
932
  if (group) {
875
933
  brace_sep = sep;
876
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
934
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
877
935
  # A pipeline owns the whole compound command, not just the final
878
936
  # statement before |. Snapshot before any brace-body cd/return.
879
937
  outgoing = brace_pipe(i + 1, "{", "}");
880
938
  brace_pipes[brace_depth] = brace_sep == "|" || outgoing;
881
939
  if (brace_pipes[brace_depth]) { printf "(\t%s%s\nL\t%d\n", (brace_sep == "|" ? "|" : ""), (outgoing ? "+" : ""), list_async(i + 1) }
882
940
  printf "G\t%s\nL\t%d\n", brace_sep, list_async(i + 1);
883
- if (brace_redirect ~ /[^[:space:]]/) { printf "I\t%s\n", brace_redirect }
941
+ if (brace_redirect ~ /[^[:space:]]/) { record("I", brace_redirect) }
884
942
  # P retains incoming stdin provenance without wrapping each body
885
943
  # statement in another child: all statements share the brace cwd.
886
944
  if (brace_sep == "|") { sep = "P" }
@@ -891,7 +949,7 @@ follow_split() {
891
949
  group = brace_groups[brace_depth--];
892
950
  if (group) {
893
951
  piece = substr(s, start, i - start);
894
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
952
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
895
953
  printf "H\t\n";
896
954
  if (brace_pipes[brace_depth + 1]) { printf ")\t\n" }
897
955
  start = i + 1; post_brace = 1; continue;
@@ -912,10 +970,10 @@ follow_split() {
912
970
  }
913
971
  if (group) {
914
972
  piece = substr(s, start, i - start);
915
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
973
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
916
974
  outgoing = brace_pipe(i + 1, "(", ")");
917
975
  printf "(\t%s%s\nL\t%d\n", sep, (outgoing ? "+" : ""), list_async(i + 1);
918
- if (brace_redirect ~ /[^[:space:]]/) { printf "I\t%s\n", brace_redirect }
976
+ if (brace_redirect ~ /[^[:space:]]/) { record("I", brace_redirect) }
919
977
  if (sep == "|") { sep = "P" }
920
978
  start = i + 1; continue;
921
979
  }
@@ -924,12 +982,12 @@ follow_split() {
924
982
  piece = substr(s, start, i - start);
925
983
  group = scopes[parens--];
926
984
  if (group == 2) {
927
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
985
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
928
986
  printf "]\t\n"; start = expression_starts[parens + 1]; q = expression_quotes[parens + 1]; continue;
929
987
  }
930
988
  if (group) {
931
989
  piece = substr(s, start, i - start);
932
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
990
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
933
991
  printf ")\t\n"; start = i + 1; post_brace = 1; continue;
934
992
  }
935
993
  }
@@ -939,7 +997,7 @@ follow_split() {
939
997
  piece = substr(s, start, i - start);
940
998
  pipe = c == "|" && substr(s, i + 1, 1) != "|";
941
999
  if (pipe || sep == "|") { printf "(\t%s%s\nL\t%d\n", (sep == "|" ? "|" : ""), (pipe ? "+" : ""), list_async(start) }
942
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", post_brace ? "R" : sep, piece }
1000
+ if (piece ~ /[^[:space:]]/) { record(post_brace ? "R" : sep, piece) }
943
1001
  post_brace = 0;
944
1002
  if (pipe || sep == "|") { printf ")\t\n" }
945
1003
  if (c == ";" && substr(s, i + 1, 1) == ";" && cases > 0) {
@@ -959,7 +1017,7 @@ follow_split() {
959
1017
  }
960
1018
  }
961
1019
  if (sep == "|") { printf "(\t|\nL\t%d\n", list_async(start) }
962
- printf "%s\t%s\n", sep, substr(s, start, n - start + 1);
1020
+ record(sep, substr(s, start, n - start + 1));
963
1021
  if (sep == "|") { printf ")\t\n" }
964
1022
  }
965
1023
  '
@@ -1429,6 +1487,77 @@ PY
1429
1487
  follow_redirect_output="$(printf '%s' "$result" | jq -er '.output')" || follow_refuse "compound redirection" "invalid output projection"
1430
1488
  }
1431
1489
 
1490
+ # Enter a compound command (`if`, `while`/`until`/`for`/`select`, `case`).
1491
+ #
1492
+ # Reaching anything inside it depends on runtime flow, so a definition there is
1493
+ # conditional. The enclosing flow state is saved so the matching closer can put
1494
+ # it back: a compound that has CLOSED no longer decides whether the next
1495
+ # top-level definition runs. Leaving the flag raised past the closer marked
1496
+ # every function defined after any loop or case as conditional, which refused
1497
+ # every Gradle-generated start script (CodySwannGT/lisa#4470). Brace groups
1498
+ # already restore this way through brace_flows.
1499
+ #
1500
+ # These read and write follow_scan's locals through bash's dynamic scoping.
1501
+ follow_compound_open() {
1502
+ local frame_scope=$scope_depth
1503
+ # A pipeline element is its own scope, but `if a | b; then` and
1504
+ # `x | while read l; do` open their compound at the ENCLOSING level.
1505
+ if [ "$scope_depth" -gt 0 ]; then
1506
+ case "${scope_kinds[scope_depth - 1]}" in '+' | '|' | '|+') frame_scope=$((scope_depth - 1)) ;; esac
1507
+ fi
1508
+ compound_flows[compound_depth]=$function_flow_unknown
1509
+ compound_kinds[compound_depth]="$1"
1510
+ compound_frame_scopes[compound_depth]=$frame_scope
1511
+ compound_opened_at="$compound_opened_at${i:-} "
1512
+ compound_depth=$((compound_depth + 1))
1513
+ function_flow_unknown=1
1514
+ }
1515
+
1516
+ # Close the innermost compound, restoring the flow state that encloses it.
1517
+ #
1518
+ # Only a closer of the SAME kind as the innermost open compound may lower the
1519
+ # flag. The walker splits text, it does not parse shell: a comment's `;` or a
1520
+ # heredoc payload line can reach it as a `fi`/`done` statement. A closer that
1521
+ # matches nothing, or arrives after a heredoc (see compound_trusted), proves the
1522
+ # walk has lost the structure, so every later closer keeps the flag where it is
1523
+ # — the verdict this walker gave before closers restored anything.
1524
+ #
1525
+ # A conditional `return` inside the compound can skip whatever follows it in a
1526
+ # function body or sourced file, so it re-raises the flag, as H does for a brace
1527
+ # group. `break`/`continue` resume at or inside the loop, so they never skip a
1528
+ # definition after `done`, and `exit` ends the shell before any later call runs:
1529
+ # neither needs to persist past the closer. A `break` inside a function body
1530
+ # can end the CALLER's loop and skip the rest of that body, but such a call only
1531
+ # has that effect when the call itself sits inside a loop body, which is
1532
+ # already conditional.
1533
+ #
1534
+ # The closer must also sit in the scope its compound opened in: a `done` inside
1535
+ # a command substitution (`$( … )` or backticks) is a different shell's syntax
1536
+ # error, not the end of this loop. The one cross-scope closer is `done | cmd`,
1537
+ # whose `done` is the first element of a pipeline. Callers pass only a closer
1538
+ # spelled as the bare first word of its statement; see the fi/done arm.
1539
+ follow_compound_close() {
1540
+ local frame_scope
1541
+ if [ "$compound_trusted" -eq 0 ] || [ "$compound_depth" -eq 0 ] \
1542
+ || [ "${compound_kinds[compound_depth - 1]}" != "$1" ]; then
1543
+ compound_trusted=0
1544
+ return 0
1545
+ fi
1546
+ frame_scope="${compound_frame_scopes[compound_depth - 1]}"
1547
+ if [ "$scope_depth" -ne "$frame_scope" ]; then
1548
+ case "$((scope_depth - frame_scope)):${scope_kinds[scope_depth - 1]}" in
1549
+ '1:+' | '1:|' | '1:|+') ;;
1550
+ *)
1551
+ compound_trusted=0
1552
+ return 0
1553
+ ;;
1554
+ esac
1555
+ fi
1556
+ compound_depth=$((compound_depth - 1))
1557
+ function_flow_unknown="${compound_flows[compound_depth]}"
1558
+ if [ "$return_flow_unknown" -ne 0 ]; then function_flow_unknown=1; fi
1559
+ }
1560
+
1432
1561
  # Walk one text for the files it executes. Runs in the PARENT shell, never in a
1433
1562
  # command substitution: follow_refuse() must be able to deny, and an exit inside
1434
1563
  # a subshell ends that subshell rather than this hook — the mistake documented
@@ -1464,6 +1593,18 @@ follow_scan() {
1464
1593
  local -a brace_descriptor_maps=() scope_descriptor_maps=()
1465
1594
  local -a brace_stdin_cats=() brace_stdin_cwds=() brace_stdin_unknowns=() scope_stdin_cats=() scope_stdin_cwds=() scope_stdin_unknowns=()
1466
1595
  local -a case_cwds=() case_unknowns=() case_changes=()
1596
+ # Open compound commands of THIS text, innermost last. See
1597
+ # follow_compound_open. The stack starts empty, so a closer can never lower
1598
+ # the flag below the value this scan was entered with.
1599
+ local compound_depth=0 compound_trusted=1 compound_opened_at=" " compound_prev
1600
+ local -a compound_flows=() compound_kinds=() compound_scopes=() compound_frame_scopes=() scope_kinds=()
1601
+ # The split treats every [[:space:]] byte as a blank, but bash separates words
1602
+ # only on space and tab (and ends a command on newline). A form feed,
1603
+ # vertical tab or carriage return is an ordinary word byte to bash, so
1604
+ # `<FF>done` is a command name there and a bare closer here. With none of the
1605
+ # three present, every [[:space:]] the split tests is a bash blank or a
1606
+ # newline, so this one check covers each such test in the split.
1607
+ case "$text" in *$'\f'* | *$'\v'* | *$'\r'*) compound_trusted=0 ;; esac
1467
1608
  if [ "$function_call_depth" -eq 0 ] && [ "$invocation_kind" != source ]; then
1468
1609
  # Ordinary child shells do not inherit unexported functions. Sourcing and
1469
1610
  # function calls share the caller's definitions instead.
@@ -1477,6 +1618,14 @@ follow_scan() {
1477
1618
  stmt="${line#*$'\t'}"
1478
1619
  statement_conditional=0
1479
1620
  case "$sep" in '&' | '?') statement_conditional=1 ;; esac
1621
+ # A heredoc payload reaches this walk as statements, so a payload line can
1622
+ # spell a closer. Once one starts, closers stop restoring flow for the rest
1623
+ # of this text. Checked before any skip so an unread payload still counts.
1624
+ # A here-string (`<<<`) carries no payload lines.
1625
+ # The same test also matches an arithmetic shift such as `$((1 << 2))`.
1626
+ # That only withholds a restore, which is the safe direction.
1627
+ case "${stmt//<<</}" in *'<<'*) compound_trusted=0 ;; esac
1628
+ if [ "$sep" = U ]; then compound_trusted=0; continue; fi
1480
1629
  # A reached && successor observes the preceding successful command's cwd.
1481
1630
  # Scope/list events invalidate this projection; it must never carry a
1482
1631
  # child's or a background list's directory into another shell/list.
@@ -1567,7 +1716,7 @@ follow_scan() {
1567
1716
  continue
1568
1717
  ;;
1569
1718
  C)
1570
- function_flow_unknown=1
1719
+ follow_compound_open case
1571
1720
  case_cwds[case_depth]="$follow_cwd"
1572
1721
  case_unknowns[case_depth]="$follow_cwd_unknown"
1573
1722
  case_changes[case_depth]=0
@@ -1586,6 +1735,9 @@ follow_scan() {
1586
1735
  follow_cwd=""; follow_cwd_unknown=1
1587
1736
  fi
1588
1737
  case_depth=$((case_depth - 1))
1738
+ # K closes the case; the literal `esac` statement the split emits
1739
+ # after it is not a closer and must not close another frame.
1740
+ follow_compound_close case
1589
1741
  fi
1590
1742
  continue
1591
1743
  ;;
@@ -1598,6 +1750,10 @@ follow_scan() {
1598
1750
  scope_descriptor_maps[scope_depth]="$follow_descriptor_map"
1599
1751
  function_scopes[scope_depth]=$function_count
1600
1752
  flow_scopes[scope_depth]=$function_flow_unknown
1753
+ compound_scopes[scope_depth]=$compound_depth
1754
+ # `[` is a substitution; for `(` the split's tail says whether this is
1755
+ # a pipeline element (`+`, `|`, `|+`) or a literal subshell.
1756
+ if [ "$sep" = '[' ]; then scope_kinds[scope_depth]='['; else scope_kinds[scope_depth]="$stmt"; fi
1601
1757
  return_flows[scope_depth]=$return_flow_unknown
1602
1758
  scope_stdin_cats[scope_depth]="$follow_stdin_cat"
1603
1759
  scope_stdin_cwds[scope_depth]="$follow_stdin_cwd"
@@ -1623,6 +1779,16 @@ follow_scan() {
1623
1779
  follow_cwd_unknown="${unknown_scopes[scope_depth]}"
1624
1780
  function_flow_unknown="${flow_scopes[scope_depth]}"
1625
1781
  return_flow_unknown="${return_flows[scope_depth]}"
1782
+ # A pipeline element is a scope of its own, so `done | cmd` closes, in
1783
+ # here, a loop opened outside. The scope must not undo that closer: the
1784
+ # flow after the pipeline is the one the loop's frame saved. The stack
1785
+ # itself is never rewound here. `if a | b; then` opens its `if` inside
1786
+ # the first element, and discarding that frame would let the `fi` close
1787
+ # an ENCLOSING compound instead.
1788
+ if [ "$compound_depth" -lt "${compound_scopes[scope_depth]}" ]; then
1789
+ function_flow_unknown="${compound_flows[compound_depth]}"
1790
+ if [ "$return_flow_unknown" -ne 0 ]; then function_flow_unknown=1; fi
1791
+ fi
1626
1792
  follow_stdin_cat="${scope_stdin_cats[scope_depth]}"
1627
1793
  follow_stdin_cwd="${scope_stdin_cwds[scope_depth]}"
1628
1794
  follow_stdin_unknown="${scope_stdin_unknowns[scope_depth]}"
@@ -1688,6 +1854,7 @@ follow_scan() {
1688
1854
  n=${#toks[@]}
1689
1855
  [ "$n" -gt 0 ] || continue
1690
1856
  i=0
1857
+ compound_opened_at=" "
1691
1858
  cmd_pos=1
1692
1859
  function_lookup_allowed=1
1693
1860
  eval_mode=0
@@ -1792,12 +1959,32 @@ follow_scan() {
1792
1959
  fi
1793
1960
  cmd_pos=0; i=$((i + 1)); continue
1794
1961
  ;;
1795
- if | while | until)
1796
- function_flow_unknown=1
1962
+ if)
1963
+ follow_compound_open if
1964
+ i=$((i + 1)); continue
1965
+ ;;
1966
+ while | until)
1967
+ follow_compound_open loop
1797
1968
  i=$((i + 1)); continue
1798
1969
  ;;
1799
1970
  for | select)
1800
- function_flow_unknown=1
1971
+ follow_compound_open loop
1972
+ cmd_pos=0; i=$((i + 1)); continue
1973
+ ;;
1974
+ # A closer counts only as the bare, unquoted FIRST word of its
1975
+ # statement. Anything else that unwraps to one — `"done"`, `\fi`,
1976
+ # `x=1 done`, `eval done`, `command done`, the tail of `x='a; fi'` — is
1977
+ # not a reserved word to the shell, so its appearance means the walk
1978
+ # cannot prove where compounds end. A literal `esac` statement follows
1979
+ # every K record and is harmless in that one spelling.
1980
+ 'fi' | 'done' | 'esac')
1981
+ if [ "$i" -ne 0 ] || [ "${toks[0]}" != "$t" ]; then
1982
+ compound_trusted=0
1983
+ elif [ "$t" = 'fi' ]; then
1984
+ follow_compound_close if
1985
+ elif [ "$t" = 'done' ]; then
1986
+ follow_compound_close loop
1987
+ fi
1801
1988
  cmd_pos=0; i=$((i + 1)); continue
1802
1989
  ;;
1803
1990
  # Prefixes that keep the command position open: assignments, wrappers,
@@ -1839,11 +2026,13 @@ follow_scan() {
1839
2026
  continue
1840
2027
  ;;
1841
2028
  sudo | env | command | exec | nohup | nice | time | builtin \
1842
- | do | then | else | '!' | '{' \
2029
+ | do | then | else | elif | '!' | '{' \
1843
2030
  | */sudo | */env | */nohup | */nice | */time)
1844
2031
  case "$t" in
1845
2032
  sudo | env | command | exec | nohup | nice | builtin | */sudo | */env | */nohup | */nice) function_lookup_allowed=0 ;;
1846
- do | then | else) function_flow_unknown=1 ;;
2033
+ # `elif` starts a condition list at a command position, so an
2034
+ # opener after it is a real compound (`elif if x; then …; fi`).
2035
+ do | then | else | elif) function_flow_unknown=1 ;;
1847
2036
  esac
1848
2037
  i=$((i + 1))
1849
2038
  continue
@@ -2078,6 +2267,37 @@ follow_scan() {
2078
2267
  # Keep successful-branch provenance separate from the continuation join.
2079
2268
  # OR can skip its RHS and still reach a following &&, so it cannot prove
2080
2269
  # that RHS's cwd. A semicolon observes every possible preceding outcome.
2270
+ # Every opener must have pushed a frame, or its own closer pops the
2271
+ # ENCLOSING one and restores flow inside a compound that is still open.
2272
+ # Bash reads a reserved word wherever a command may start, which this walk
2273
+ # does not fully model: after `coproc NAME`, after a function header whose
2274
+ # body is a bare compound (`f () while …`), or after `function NAME`. So an
2275
+ # opener word the walk did not open stops all later restores unless the
2276
+ # word before it is a plain argument (`echo if`, `# loops for x`), where
2277
+ # bash reads it as data too. `case` never opens here (the split's C record
2278
+ # does), so a statement-initial `case` means the split missed its `in`.
2279
+ if [ "$compound_trusted" -ne 0 ]; then
2280
+ for ((j = 0; j < n; j++)); do
2281
+ case "${toks[j]}" in
2282
+ coproc) compound_trusted=0 ;;
2283
+ function) [ "$j" -ne 0 ] || compound_trusted=0 ;;
2284
+ if | while | until | for | select | case)
2285
+ case "$compound_opened_at" in *" $j "*) continue ;; esac
2286
+ if [ "$j" -eq 0 ]; then
2287
+ compound_trusted=0
2288
+ else
2289
+ compound_prev="${toks[j - 1]}"
2290
+ case "$compound_prev" in
2291
+ '' | *[!A-Za-z0-9_./,:=+@%-]* | elif | coproc | function | time | then | do | else)
2292
+ compound_trusted=0
2293
+ ;;
2294
+ esac
2295
+ fi
2296
+ ;;
2297
+ esac
2298
+ [ "$compound_trusted" -ne 0 ] || break
2299
+ done
2300
+ fi
2081
2301
  and_cwd="$follow_cwd"; and_unknown="$follow_cwd_unknown"
2082
2302
  if [ "$sep" != '?' ] && [ "$function_flow_unknown" -eq 0 ]; then and_ready=1; fi
2083
2303
  if [ "$statement_conditional" -ne 0 ] && { [ "$join_unknown" -ne 0 ] || [ "$follow_cwd_unknown" -ne 0 ] || [ "$follow_cwd" != "$join_cwd" ]; }; then