@codyswann/lisa 4.71.4 → 4.71.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 (91) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +20 -2
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +71 -10
  3. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.js +8 -0
  5. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  6. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +11 -8
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/dist/opencode/plugin-templates/block-managed-file-edits.sh +71 -10
  11. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +8 -2
  12. package/package.json +4 -4
  13. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +89 -5
  16. package/plugins/lisa/.codex-plugin/skills/lisa-queue-status/SKILL.md +43 -3
  17. package/plugins/lisa/hooks/block-direct-issue-create.sh +20 -2
  18. package/plugins/lisa/hooks/block-managed-file-edits.sh +71 -10
  19. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +89 -5
  20. package/plugins/lisa/skills/lisa-queue-status/SKILL.md +43 -3
  21. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +20 -2
  22. package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +71 -10
  23. package/plugins/lisa-agy/plugin.json +1 -1
  24. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +89 -5
  25. package/plugins/lisa-agy/skills/lisa-queue-status/SKILL.md +43 -3
  26. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  29. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +20 -2
  33. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +71 -10
  34. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +89 -5
  35. package/plugins/lisa-copilot/skills/lisa-queue-status/SKILL.md +43 -3
  36. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +20 -2
  38. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +71 -10
  39. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +89 -5
  40. package/plugins/lisa-cursor/skills/lisa-queue-status/SKILL.md +43 -3
  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/.codex-plugin/skills/expo-router-best-practices/references/static-export-verification.md +1 -1
  44. package/plugins/lisa-expo/skills/expo-router-best-practices/references/static-export-verification.md +1 -1
  45. package/plugins/lisa-expo-agy/plugin.json +1 -1
  46. package/plugins/lisa-expo-agy/skills/expo-router-best-practices/references/static-export-verification.md +1 -1
  47. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-expo-copilot/skills/expo-router-best-practices/references/static-export-verification.md +1 -1
  49. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-expo-cursor/skills/expo-router-best-practices/references/static-export-verification.md +1 -1
  51. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  69. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-rails-agy/plugin.json +1 -1
  74. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  79. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  84. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/src/base/hooks/block-direct-issue-create.sh +20 -2
  87. package/plugins/src/base/hooks/block-managed-file-edits.sh +71 -10
  88. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +89 -5
  89. package/plugins/src/base/skills/lisa-queue-status/SKILL.md +43 -3
  90. package/plugins/src/expo/skills/expo-router-best-practices/references/static-export-verification.md +1 -1
  91. package/typescript/copy-contents/.husky/commit-msg +28 -31
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "4.71.4",
3
+ "version": "4.71.6",
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.4",
3
+ "version": "4.71.6",
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.4",
3
+ "version": "4.71.6",
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.4",
3
+ "version": "4.71.6",
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.4",
3
+ "version": "4.71.6",
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.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.4",
3
+ "version": "4.71.6",
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.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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.71.4",
3
+ "version": "4.71.6",
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-wiki",
3
- "version": "4.71.4",
3
+ "version": "4.71.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.4",
3
+ "version": "4.71.6",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.4",
3
+ "version": "4.71.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.4",
3
+ "version": "4.71.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.71.4",
3
+ "version": "4.71.6",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -208,11 +208,29 @@ if [ "$tool_name" != "Bash" ]; then
208
208
  exit 0
209
209
  ;;
210
210
  esac
211
- case "$tool_name" in
211
+ # These terminal actions edit an existing item, not create the item.
212
+ # Strip only the measured action shape, then still inspect any preceding
213
+ # compound operation: create_issue_and_add_issue_comment remains a filing.
214
+ creation_name="$tool_name"
215
+ case "$creation_name" in
216
+ *[Aa]dd_[Ii]ssue_[Cc]omment)
217
+ creation_name="${creation_name%[Aa]dd_[Ii]ssue_[Cc]omment}" ;;
218
+ *[Aa]dd[Cc]omment[Tt]o[Jj]ira[Ii]ssue)
219
+ creation_name="${creation_name%[Aa]dd[Cc]omment[Tt]o[Jj]ira[Ii]ssue}" ;;
220
+ *[Aa]dd_[Cc]omment_[Tt]o_[Ii]ssue)
221
+ creation_name="${creation_name%[Aa]dd_[Cc]omment_[Tt]o_[Ii]ssue}" ;;
222
+ *[Aa]dd_[Ii]ssue_[Ll]abels)
223
+ creation_name="${creation_name%[Aa]dd_[Ii]ssue_[Ll]abels}" ;;
224
+ *[Aa]dd_[Ll]abels_[Tt]o_[Ii]ssue)
225
+ creation_name="${creation_name%[Aa]dd_[Ll]abels_[Tt]o_[Ii]ssue}" ;;
226
+ *[Aa]dd_[Rr]eaction_[Tt]o_[Ii]ssue_[Cc]omment)
227
+ creation_name="${creation_name%[Aa]dd_[Rr]eaction_[Tt]o_[Ii]ssue_[Cc]omment}" ;;
228
+ esac
229
+ case "$creation_name" in
212
230
  *[Cc]reate* | *[Nn]ew* | *[Aa]dd* | *[Ff]ile*) ;;
213
231
  *) exit 0 ;;
214
232
  esac
215
- case "$tool_name" in
233
+ case "$creation_name" in
216
234
  *[Ii]ssue* | *[Tt]icket* | *[Tt]ask* | *[Ss]tory* | *[Bb]ug* | *[Ee]pic* | *[Ww]ork*) ;;
217
235
  *) exit 0 ;;
218
236
  esac
@@ -595,9 +595,25 @@ BACKSLASH = chr(92)
595
595
  NEWLINE = chr(10)
596
596
 
597
597
  STATEMENT_SEPARATORS = {";", "|", "||", "&", "&&", "(", ")", "&|"}
598
- # `>`, `>>`, and the noclobber override `>|`.
599
- REDIRECT_OUT = {">", ">>", ">|"}
598
+ # Ordinary output, noclobber override, and combined stdout/stderr output.
599
+ # shlex separates fd prefixes (e.g. 10>>) from the operator automatically.
600
+ REDIRECT_OUT = {">", ">>", ">|", "&>", "&>>", ">&"}
600
601
  REDIRECT_IN = "<"
602
+ SYNTAX_OPERATORS = STATEMENT_SEPARATORS | REDIRECT_OUT | {REDIRECT_IN}
603
+ # shlex groups adjacent punctuation even across distinct shell operators.
604
+ # Retain other shell operators intact so a here-string is not three `<` reads.
605
+ PUNCTUATION = re.compile(r"^[();<>|&]+$")
606
+ SHELL_OPERATOR = re.compile("|".join(
607
+ re.escape(operator) for operator in sorted(
608
+ SYNTAX_OPERATORS | {"<<<", "<<", "<&", "<>", ";;&", ";;", ";&", "|&"},
609
+ key=lambda operator: (-len(operator), operator),
610
+ )
611
+ ))
612
+
613
+
614
+ class LiteralOperator(str):
615
+ """Quoted or escaped operator text is an argument, never shell syntax."""
616
+
601
617
 
602
618
  SHELL_PROGRAMS = {"bash", "dash", "ksh", "sh", "zsh"}
603
619
  SOURCE_BUILTINS = {"source", "."}
@@ -661,8 +677,30 @@ def normalise_lines(text):
661
677
  Returns:
662
678
  The same text with line breaks spelled as separators.
663
679
  """
664
- joined = text.replace(BACKSLASH + NEWLINE, " ")
665
- return joined.replace(NEWLINE, " ; ")
680
+ output = []
681
+ quote = ""
682
+ position = 0
683
+ while position < len(text):
684
+ character = text[position]
685
+ if character == BACKSLASH and quote != "'" and position + 1 < len(text):
686
+ following = text[position + 1]
687
+ # The shell deletes continuations; inserting whitespace can split
688
+ # an operator or pathname and hide the actual output target.
689
+ if following != NEWLINE:
690
+ output.extend((character, following))
691
+ position += 2
692
+ continue
693
+ if quote:
694
+ if character == quote:
695
+ quote = ""
696
+ elif character in "'\"":
697
+ quote = character
698
+ if character == NEWLINE and not quote:
699
+ output.append(" ; ")
700
+ else:
701
+ output.append(character)
702
+ position += 1
703
+ return "".join(output)
666
704
 
667
705
 
668
706
  def tokenize(text):
@@ -674,13 +712,30 @@ def tokenize(text):
674
712
  Returns:
675
713
  The token list, or None when the text does not lex.
676
714
  """
677
- lexer = shlex.shlex(normalise_lines(text), posix=True, punctuation_chars=True)
715
+ normalized = normalise_lines(text)
716
+ lexer = shlex.shlex(normalized, posix=True, punctuation_chars=True)
678
717
  lexer.whitespace_split = True
679
718
  # `#` is not a comment introducer on a typed command line, and letting shlex
680
719
  # treat it as one silently truncates the rest of the line.
681
720
  lexer.commenters = ""
682
721
  try:
683
- return list(lexer)
722
+ tokens = []
723
+ start = 0
724
+ for token in lexer:
725
+ # shlex strips quotes/escapes and reads one character ahead when
726
+ # emitting punctuation. Exclude its character pushback from the
727
+ # consumed source span so a quoted operator retains its provenance.
728
+ end = lexer.instream.tell() - len(lexer._pushback_chars)
729
+ raw = normalized[start:end].strip()
730
+ start = end
731
+ if PUNCTUATION.fullmatch(token):
732
+ if raw != token:
733
+ tokens.append(LiteralOperator(token))
734
+ else:
735
+ tokens.extend(SHELL_OPERATOR.findall(token))
736
+ else:
737
+ tokens.append(token)
738
+ return tokens
684
739
  except ValueError:
685
740
  return None
686
741
 
@@ -742,7 +797,7 @@ def statements(tokens):
742
797
  grouped = []
743
798
  current = []
744
799
  for token in tokens:
745
- if token in STATEMENT_SEPARATORS:
800
+ if not isinstance(token, LiteralOperator) and token in STATEMENT_SEPARATORS:
746
801
  grouped.append(current)
747
802
  current = []
748
803
  continue
@@ -926,8 +981,14 @@ def write_targets(statement):
926
981
  found = []
927
982
  _token, program, args = command_word(statement)
928
983
  for index, token in enumerate(statement):
929
- if token in REDIRECT_OUT and index + 1 < len(statement):
930
- found.append(statement[index + 1])
984
+ if not isinstance(token, LiteralOperator) and token in REDIRECT_OUT and index + 1 < len(statement):
985
+ target = statement[index + 1]
986
+ # >&digit duplicates an fd, >&digit- moves it, and >&- closes it.
987
+ # Only the nonnumeric >&word form opens a pathname; &>1 still
988
+ # writes a file named 1 and must remain a write candidate.
989
+ if token == ">&" and re.fullmatch(r"(?:[0-9]+-?|-)", target):
990
+ continue
991
+ found.append(target)
931
992
  if program == "tee":
932
993
  found.extend(token for token in args if not token.startswith("-"))
933
994
  elif program == "sed" and any(IN_PLACE.match(token) for token in args):
@@ -953,7 +1014,7 @@ def executed_script(statement):
953
1014
  if program in SHELL_PROGRAMS:
954
1015
  for index, token in enumerate(statement):
955
1016
  # `bash < script.sh` runs that file as surely as `bash script.sh`.
956
- if token == REDIRECT_IN and index + 1 < len(statement):
1017
+ if not isinstance(token, LiteralOperator) and token == REDIRECT_IN and index + 1 < len(statement):
957
1018
  return statement[index + 1]
958
1019
  return shell_script_operand(args)
959
1020
  if program in SOURCE_BUILTINS:
@@ -582,10 +582,55 @@ sails through the arm gate and the latch goes on over the top of it.
582
582
  **Read `reviewDecision` explicitly. Never infer it from a check count.**
583
583
 
584
584
  ```bash
585
- gh pr view <pr> --json reviewDecision,reviewThreads \
586
- --jq '{decision: .reviewDecision, unresolved: [.reviewThreads[]? | select(.isResolved == false)] | length}'
585
+ review_pages=$(gh api graphql --paginate --slurp \
586
+ -F owner=<owner> -F repo=<repo> -F pr=<pr> \
587
+ -f query='query($owner:String!,$repo:String!,$pr:Int!,$endCursor:String){
588
+ repository(owner:$owner,name:$repo){pullRequest(number:$pr){
589
+ state reviewDecision autoMergeRequest{enabledAt}
590
+ reviewThreads(first:100,after:$endCursor){
591
+ nodes{isResolved isOutdated}
592
+ pageInfo{hasNextPage endCursor}
593
+ }
594
+ }}
595
+ }') || {
596
+ printf '%s\n' 'Could not read PR review threads; merge readiness is unknown.' >&2
597
+ exit 1
598
+ }
599
+ printf '%s\n' "$review_pages" | jq -e '
600
+ def complete_page:
601
+ try (
602
+ ((.errors // []) | length) == 0 and
603
+ (.data.repository.pullRequest | . as $pr |
604
+ has("state") and has("reviewDecision") and has("autoMergeRequest") and
605
+ (["OPEN", "CLOSED", "MERGED"] | index($pr.state)) != null and
606
+ ([null, "APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED"] |
607
+ index($pr.reviewDecision)) != null and
608
+ (.autoMergeRequest == null or
609
+ (.autoMergeRequest.enabledAt | type) == "string") and
610
+ (.reviewThreads.nodes | type) == "array" and
611
+ all(.reviewThreads.nodes[];
612
+ (.isResolved | type) == "boolean" and (.isOutdated | type) == "boolean") and
613
+ (.reviewThreads.pageInfo.hasNextPage | type) == "boolean")
614
+ ) catch false;
615
+ if type != "array" then
616
+ error("Incomplete PR review response; merge readiness is unknown")
617
+ elif length == 0 or any(.[]; complete_page | not) or
618
+ .[-1].data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage != false
619
+ then error("Incomplete PR review response; merge readiness is unknown")
620
+ else {
621
+ state: .[-1].data.repository.pullRequest.state,
622
+ armed: (.[-1].data.repository.pullRequest.autoMergeRequest != null),
623
+ decision: .[-1].data.repository.pullRequest.reviewDecision,
624
+ unresolved: ([.[].data.repository.pullRequest.reviewThreads.nodes[] |
625
+ select(.isResolved == false)] | length)
626
+ } end'
587
627
  ```
588
628
 
629
+ `reviewThreads` is a GraphQL connection, not a supported `gh pr view --json`
630
+ field. Read every page, including outdated threads that remain unresolved.
631
+ If either the API request or response validation fails, stop with the diagnostic
632
+ above; a failed read must never become a count of zero unresolved threads.
633
+
589
634
  `reviewDecision` is **not part of `statusCheckRollup`**. That is the whole reason
590
635
  this stayed invisible: a failing-check count reads **zero** on a PR that can
591
636
  never merge, and the checks tab is entirely green. Any rule keyed on "no red
@@ -1440,9 +1485,48 @@ states above, but any point where this run concludes it is finished, hands back,
1440
1485
  or gives up. Re-read, from the live PR, immediately before reporting:
1441
1486
 
1442
1487
  ```bash
1443
- gh pr view <pr> --json state,autoMergeRequest,reviewDecision,reviewThreads \
1444
- --jq '{state, armed: (.autoMergeRequest != null), decision: .reviewDecision,
1445
- unresolved: [.reviewThreads[]? | select(.isResolved == false)] | length}'
1488
+ review_pages=$(gh api graphql --paginate --slurp \
1489
+ -F owner=<owner> -F repo=<repo> -F pr=<pr> \
1490
+ -f query='query($owner:String!,$repo:String!,$pr:Int!,$endCursor:String){
1491
+ repository(owner:$owner,name:$repo){pullRequest(number:$pr){
1492
+ state reviewDecision autoMergeRequest{enabledAt}
1493
+ reviewThreads(first:100,after:$endCursor){
1494
+ nodes{isResolved isOutdated}
1495
+ pageInfo{hasNextPage endCursor}
1496
+ }
1497
+ }}
1498
+ }') || {
1499
+ printf '%s\n' 'Could not read PR review threads; merge readiness is unknown.' >&2
1500
+ exit 1
1501
+ }
1502
+ printf '%s\n' "$review_pages" | jq -e '
1503
+ def complete_page:
1504
+ try (
1505
+ ((.errors // []) | length) == 0 and
1506
+ (.data.repository.pullRequest | . as $pr |
1507
+ has("state") and has("reviewDecision") and has("autoMergeRequest") and
1508
+ (["OPEN", "CLOSED", "MERGED"] | index($pr.state)) != null and
1509
+ ([null, "APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED"] |
1510
+ index($pr.reviewDecision)) != null and
1511
+ (.autoMergeRequest == null or
1512
+ (.autoMergeRequest.enabledAt | type) == "string") and
1513
+ (.reviewThreads.nodes | type) == "array" and
1514
+ all(.reviewThreads.nodes[];
1515
+ (.isResolved | type) == "boolean" and (.isOutdated | type) == "boolean") and
1516
+ (.reviewThreads.pageInfo.hasNextPage | type) == "boolean")
1517
+ ) catch false;
1518
+ if type != "array" then
1519
+ error("Incomplete PR review response; merge readiness is unknown")
1520
+ elif length == 0 or any(.[]; complete_page | not) or
1521
+ .[-1].data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage != false
1522
+ then error("Incomplete PR review response; merge readiness is unknown")
1523
+ else {
1524
+ state: .[-1].data.repository.pullRequest.state,
1525
+ armed: (.[-1].data.repository.pullRequest.autoMergeRequest != null),
1526
+ decision: .[-1].data.repository.pullRequest.reviewDecision,
1527
+ unresolved: ([.[].data.repository.pullRequest.reviewThreads.nodes[] |
1528
+ select(.isResolved == false)] | length)
1529
+ } end'
1446
1530
  ```
1447
1531
 
1448
1532
  If `state == OPEN` and `armed` and either `decision == "CHANGES_REQUESTED"` or
@@ -86,11 +86,51 @@ Alongside the two work queues, report the **arming state of the repo's open pull
86
86
  Run the sweep rather than eyeballing the list — an LLM reading PR pages one at a time is exactly the shell-loop-by-prose failure #3512 records:
87
87
 
88
88
  ```bash
89
- gh pr list --state open --limit 200 \
90
- --json number,title,url,isDraft,labels,body,autoMergeRequest \
91
- | node "${CLAUDE_PLUGIN_ROOT}/scripts/pr-arming-sweep.mjs"
89
+ repository=$(gh repo view --json nameWithOwner --jq .nameWithOwner) || {
90
+ printf '%s\n' 'NOT_MEASURED: could not resolve the current repository.' >&2
91
+ exit 2
92
+ }
93
+ pages=$(gh api --paginate --slurp \
94
+ "repos/$repository/pulls?state=open&per_page=100") || {
95
+ printf '%s\n' 'NOT_MEASURED: could not read every open pull request page.' >&2
96
+ exit 2
97
+ }
98
+ prs=$(printf '%s\n' "$pages" | jq -ce '
99
+ def complete_pr:
100
+ try (
101
+ has("auto_merge") and
102
+ (.auto_merge == null or
103
+ (.auto_merge.merge_method as $method | ["merge", "squash", "rebase"] |
104
+ index($method)) != null) and
105
+ (.number | type) == "number" and .number > 0 and
106
+ (.title | type) == "string" and (.html_url | type) == "string" and
107
+ (.draft | type) == "boolean" and has("body") and
108
+ (.body == null or (.body | type) == "string") and
109
+ (.labels | type) == "array" and all(.labels[]; (.name | type) == "string")
110
+ ) catch false;
111
+ if type != "array" then error("Invalid PR page response")
112
+ elif length == 0 or any(.[]; type != "array") then error("Missing PR pages")
113
+ elif any(.[][]; complete_pr | not) then error("Incomplete PR metadata")
114
+ else [.[][] | {
115
+ number, title, url: .html_url, isDraft: .draft, labels, body,
116
+ autoMergeRequest: .auto_merge
117
+ }] end') || {
118
+ printf '%s\n' 'NOT_MEASURED: incomplete pull request response.' >&2
119
+ exit 2
120
+ }
121
+ printf '%s\n' "$prs" | node \
122
+ "${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa}}/scripts/pr-arming-sweep.mjs"
92
123
  ```
93
124
 
125
+ `gh api --paginate --slurp` follows every REST page before the sweep starts;
126
+ there is no repository-wide row cap. Keep `auto_merge` present during conversion:
127
+ an absent field is an unread measurement, while explicit `null` means unarmed.
128
+ On any API or response validation failure, report `NOT_MEASURED` and stop this
129
+ read. Never report a partial queue as clean. The script path honors an exported
130
+ plugin root and otherwise resolves from the current project's installed Lisa
131
+ package; bare `plugins/lisa` paths are relative to Lisa's own checkout and do not
132
+ work from a consuming project.
133
+
94
134
  Report the verdict verbatim, and never translate it into an absence:
95
135
 
96
136
  - `MEASURED_CLEAN` — a **positive assertion**: N open PRs were read and every one is armed or deliberately unarmed. Say the count. "Nothing to report" is not the same sentence and must not be substituted for it.
@@ -1,6 +1,6 @@
1
1
  # Static Export Verification
2
2
 
3
- When `expo export` runs with static output (`output: "static"` in app.json),
3
+ When `expo export` runs with static output (`expo.web.output: "static"` in app.json),
4
4
  every route must produce an HTML file with the right title and the expected
5
5
  initial markup. `expo export` succeeding only proves the bundle built — a
6
6
  missing route, a wrong title, or an empty shell all ship green unless the