@codyswann/lisa 4.81.4 → 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 (156) 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 +17 -12
  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/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +13 -6
  18. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +4 -3
  19. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  20. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
  22. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +4 -0
  23. package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +13 -1
  24. package/plugins/lisa/hooks/parity-safety-net.sh +236 -16
  25. package/plugins/lisa/rules/reference/config-resolution.md +1 -0
  26. package/plugins/lisa/scripts/ready-queue-order.mjs +34 -14
  27. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  28. package/plugins/lisa/skills/lisa-session-manager/SKILL.md +13 -6
  29. package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +4 -3
  30. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  31. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  32. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
  33. package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +4 -0
  34. package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +13 -1
  35. package/plugins/lisa-agy/hooks/parity-safety-net.sh +236 -16
  36. package/plugins/lisa-agy/plugin.json +1 -1
  37. package/plugins/lisa-agy/scripts/ready-queue-order.mjs +34 -14
  38. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  39. package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +13 -6
  40. package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +4 -3
  41. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  42. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  43. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
  44. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +4 -0
  45. package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +13 -1
  46. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  49. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  53. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +236 -16
  55. package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -0
  56. package/plugins/lisa-copilot/scripts/ready-queue-order.mjs +34 -14
  57. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  58. package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +13 -6
  59. package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +4 -3
  60. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  61. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  62. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
  63. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +4 -0
  64. package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +13 -1
  65. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +236 -16
  67. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -0
  68. package/plugins/lisa-cursor/scripts/ready-queue-order.mjs +34 -14
  69. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  70. package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +13 -6
  71. package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +4 -3
  72. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  73. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  74. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
  75. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +4 -0
  76. package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +13 -1
  77. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  79. package/plugins/lisa-devin/hooks/parity-safety-net.sh +236 -16
  80. package/plugins/lisa-devin/rules/reference/config-resolution.md +1 -0
  81. package/plugins/lisa-devin/scripts/ready-queue-order.mjs +34 -14
  82. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  83. package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +13 -6
  84. package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +4 -3
  85. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  86. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  87. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
  88. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +4 -0
  89. package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +13 -1
  90. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-expo-agy/plugin.json +1 -1
  93. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  97. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  104. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  106. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  107. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  111. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  113. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  114. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  115. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  118. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  121. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  125. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  127. package/plugins/lisa-rails-agy/plugin.json +1 -1
  128. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  130. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  132. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  134. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  135. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  136. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  137. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  139. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  140. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  141. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  142. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  143. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  144. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  145. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  146. package/plugins/src/base/hooks/parity-safety-net.sh +236 -16
  147. package/plugins/src/base/rules/reference/config-resolution.md +1 -0
  148. package/plugins/src/base/scripts/ready-queue-order.mjs +34 -14
  149. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  150. package/plugins/src/base/skills/lisa-session-manager/SKILL.md +13 -6
  151. package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +4 -3
  152. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  153. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  154. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
  155. package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +4 -0
  156. package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +13 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.81.4",
3
+ "version": "4.81.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.81.4",
3
+ "version": "4.81.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.81.4",
3
+ "version": "4.81.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.81.4",
3
+ "version": "4.81.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.81.4",
3
+ "version": "4.81.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.81.4",
3
+ "version": "4.81.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"
@@ -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
@@ -733,6 +733,7 @@ bypassed). `/lisa:setup:session-manager` writes it, detecting a multi-repo layou
733
733
  | `sessionManager.notify.provider` | no | `telegram` | Only `telegram` is implemented. |
734
734
  | `sessionManager.notify.stateDir` | **yes** | — | This project's own Telegram state dir. Never the shared default `~/.claude/channels/telegram`, because one bot per state dir is how projects stay isolated. Setup writes `~/.claude/channels/telegram-<project>`. |
735
735
  | `sessionManager.trustedAuthorIds` | no | — | Optional list of **stable tracker user ids**, as strings: the GitHub numeric user id (`gh api users/<login> --jq .id`), the Jira `accountId`, or the Linear user id. Not logins or display names: matching compares ids, so a login silently matches no one and excludes every item. When set, the manager takes only items authored by these people. |
736
+ | `sessionManager.operatorIds` | no | — | Optional, non-empty list of the people the manager works for, as tracker user ids matched against the **assignee** the same way "me" is: the GitHub **login**, the Jira `accountId`, or the Linear user id, spelled exactly as the tracker API returns it (matching is case-sensitive, so look the OPERATOR up rather than typing from memory: `gh api users/<operator-login> --jq .login` returns the canonical GitHub spelling. Do not use `gh api user`, which returns whoever the credential belongs to — the agent account, when that is the case this key exists for). Dispatch order is assigned to an operator, then to "me" (the tracker credential's identity), then unassigned; items assigned to anyone else are skipped. Set it whenever the tracker credential is not the operator's own (an agent account), or the operator's Ready items are never dispatched. |
736
737
 
737
738
  - **Non-secret only.** The bot token lives in `<stateDir>/.env` (directory 0700, file 0600),
738
739
  written by setup. A credential-named field or a token-shaped value anywhere in this block is a
@@ -1,15 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Vendor-neutral Ready-queue normalization and mine-first dispatch order.
3
+ * Vendor-neutral Ready-queue normalization and operator-first dispatch order.
4
4
  *
5
5
  * ## Why this exists
6
6
  *
7
7
  * Intake has always taken Ready work in whatever order the tracker returned
8
8
  * it, and `intake.assignee` only NARROWS a selection — it never ranks one. A
9
9
  * dispatcher that runs several workers at once needs a stable answer to "what
10
- * next?": the work already assigned to me first (somebody chose me for it),
11
- * then unassigned work, most urgent and oldest first. Work assigned to someone
12
- * else is theirs, so it is excluded, never re-ordered.
10
+ * next?": work assigned to the operator first (the person the dispatcher works
11
+ * for), then work assigned to me (the identity the tracker credential belongs
12
+ * to, often an agent account rather than the operator), then unassigned work,
13
+ * most urgent and oldest first. Work assigned to anyone else is theirs, so it
14
+ * is excluded, never re-ordered. When the credential IS the operator's, the
15
+ * first two groups coincide. With "me" as the only identity, an operator whose
16
+ * credential is an agent account had every one of their own items excluded as
17
+ * someone else's (CodySwannGT/lisa#4467).
13
18
  *
14
19
  * The three trackers disagree on every field this needs — GitHub has priority
15
20
  * LABELS, Linear a native 0..4 number where 0 means "none", Jira a priority
@@ -252,12 +257,18 @@ function shape(input) {
252
257
  * Order matters only for the reason reported: an item can fail several rules,
253
258
  * and "someone else owns it" is the most useful thing to say about it.
254
259
  * @param {Record<string, any>} item
255
- * @param {{ me: string | null, trusted: ReadonlySet<string>, humanNeededLabel: string }} context
260
+ * @param {{ me: string | null, operators: ReadonlySet<string>, trusted: ReadonlySet<string>, humanNeededLabel: string }} context
256
261
  * @returns {string | null}
257
262
  */
258
263
  function exclusionReason(item, context) {
259
264
  const assignee = idOrNull(item.assigneeId);
260
- if (assignee !== null && assignee !== context.me) return "assigned-to-other";
265
+ if (
266
+ assignee !== null &&
267
+ assignee !== context.me &&
268
+ !context.operators.has(assignee)
269
+ ) {
270
+ return "assigned-to-other";
271
+ }
261
272
  if (item.isContainer === true) return "container";
262
273
  if (
263
274
  isHumanGated({
@@ -293,20 +304,25 @@ function compareWithinGroup(left, right) {
293
304
  }
294
305
 
295
306
  /**
296
- * Order normalized Ready items for dispatch, mine first.
307
+ * Order normalized Ready items for dispatch: the operator's, then mine, then
308
+ * unassigned.
297
309
  *
298
- * Group 0 is assigned to `me`, group 1 unassigned; within a group, priority
299
- * rank ascending, then creation time ascending, then ref as a stable
300
- * tiebreak. Excluded items keep their input order. With no `me`, every
301
- * assigned item is someone else's — the safe direction, since claiming
302
- * assigned work steals it.
310
+ * Group 0 is assigned to an operator, group 1 to `me`, group 2 unassigned;
311
+ * within a group, priority rank ascending, then creation time ascending, then
312
+ * ref as a stable tiebreak. Excluded items keep their input order. With no
313
+ * `me` and no operators, every assigned item is someone else's — the safe
314
+ * direction, since claiming assigned work steals it.
315
+ *
316
+ * `operators` match the way `me` does, against the normalized assignee id:
317
+ * GitHub login, Jira accountId, Linear user id.
303
318
  * @param {readonly Record<string, any>[]} items - Normalized items
304
- * @param {{ me?: string | null, trustedAuthorIds?: readonly unknown[], humanNeededLabel?: string }} [options]
319
+ * @param {{ me?: string | null, operators?: readonly unknown[], trustedAuthorIds?: readonly unknown[], humanNeededLabel?: string }} [options]
305
320
  * @returns {{ ordered: Record<string, any>[], excluded: { ref: string, reason: string }[] }}
306
321
  */
307
322
  export function orderReadyQueue(items, options = {}) {
308
323
  const context = {
309
324
  me: idOrNull(options.me),
325
+ operators: new Set((options.operators ?? []).map(idOrNull).filter(Boolean)),
310
326
  trusted: new Set(
311
327
  (options.trustedAuthorIds ?? []).map(idOrNull).filter(Boolean)
312
328
  ),
@@ -319,7 +335,11 @@ export function orderReadyQueue(items, options = {}) {
319
335
  const eligible = judged
320
336
  .filter(one => one.reason === null)
321
337
  .map(one => one.item);
322
- const group = item => (idOrNull(item.assigneeId) === null ? 1 : 0);
338
+ const group = item => {
339
+ const assignee = idOrNull(item.assigneeId);
340
+ if (assignee === null) return 2;
341
+ return context.operators.has(assignee) ? 0 : 1;
342
+ };
323
343
 
324
344
  return {
325
345
  ordered: [...eligible].sort(
@@ -727,6 +727,35 @@ function findCredentials(value, path) {
727
727
  );
728
728
  }
729
729
 
730
+ /**
731
+ * Validate `sessionManager.operatorIds`: the people the manager works for,
732
+ * whose assigned items it dispatches first. Matched against the assignee the
733
+ * way "me" is, so the GitHub entry is a login, not the numeric id
734
+ * `trustedAuthorIds` takes. An empty list reads as configured but names no
735
+ * one, so it is refused like an empty `trustedAuthorIds`.
736
+ * @param {unknown} ids The value, if present.
737
+ * @returns {string[]} Problems found.
738
+ */
739
+ function validateOperatorIds(ids) {
740
+ if (ids === undefined) return [];
741
+ if (
742
+ !Array.isArray(ids) ||
743
+ !ids.every(id => typeof id === "string" && id.trim() !== "")
744
+ ) {
745
+ return [
746
+ `sessionManager.operatorIds must be an array of tracker user ids as ` +
747
+ `strings, matched against the assignee: the GitHub login, the Jira ` +
748
+ `accountId or the Linear user id.`,
749
+ ];
750
+ }
751
+ return ids.length === 0
752
+ ? [
753
+ `sessionManager.operatorIds must name at least one person. Remove ` +
754
+ `the key instead if the manager works only for its own identity.`,
755
+ ]
756
+ : [];
757
+ }
758
+
730
759
  /**
731
760
  * Validate the opt-in `sessionManager` block.
732
761
  *
@@ -786,6 +815,7 @@ export function validateSessionManager(block) {
786
815
  `if every author's item may be dispatched.`,
787
816
  ]
788
817
  : []),
818
+ ...validateOperatorIds(block.operatorIds),
789
819
  ...findCredentials(block, "sessionManager"),
790
820
  ];
791
821
  }
@@ -66,7 +66,12 @@ the manager.
66
66
  claims go through `lisa-tracker-claim`. Never call a vendor API around them.
67
67
  - **"Me".** Resolve once per manager run and store it with `ledger control --me <id>`: GitHub
68
68
  `gh api user --jq .login`; Jira the accountId from the `lisa-atlassian-access` identity probe;
69
- Linear `viewer.id` through `lisa-linear-access`.
69
+ Linear `viewer.id` through `lisa-linear-access`. "Me" is whoever the tracker credential belongs
70
+ to, which is often an agent account.
71
+ - **Operators.** `sessionManager.operatorIds` names the people the manager works for (the same
72
+ kind of id as "me": GitHub login, Jira accountId, Linear user id). Their assigned items come
73
+ first, then items assigned to "me", then unassigned. Items assigned to anyone else are never
74
+ dispatched. Never treat an operator's item as someone else's.
70
75
  - **Workers** start only through `spawn`. The worker prompt is exactly
71
76
  `/lisa:implement <REF> managedBy=<manager-name>`; `lisa-implement` owns what `managedBy=` means.
72
77
  Never start a worker another way, and never reuse a worker for another item or a new attempt.
@@ -207,8 +212,8 @@ Skip while `capacity` reports `paused`. Otherwise:
207
212
  Raw Jira issues carry `key` and `fields` (summary, priority, assignee, created, labels,
208
213
  description, reporter, issuetype, subtasks); raw Linear nodes carry identifier, title, priority,
209
214
  createdAt, assignee, labels, description, creator and children with their state type.
210
- - `ordered` is the dispatch order (mine, then unassigned, then priority, then age). `excluded`
211
- needs no action.
215
+ - `ordered` is the dispatch order: assigned to an operator, then to "me", then unassigned; each
216
+ group by priority, then age. `excluded` needs no action.
212
217
  2. **While `capacity` says `can_spawn`** and items remain, take the next item that has no pending
213
218
  question in the ledger:
214
219
  1. Read it fully (`lisa-tracker-read`): title, description, labels, project, parent, comments.
@@ -223,9 +228,11 @@ Skip while `capacity` reports `paused`. Otherwise:
223
228
  `cd "$(node "$SM/session-manager.mjs" repo-dir --repo <key> --raw --project-root "$ROOT")"`
224
229
  (omit `--repo` on a single-repo project), invoke `lisa-tracker-claim <ref>`, then
225
230
  `cd "$ROOT" && pwd` and confirm the path. Continue only on `claimed` or `reused` with the
226
- assignee equal to "me"; anything else means skip. Then re-read the item with
227
- `lisa-tracker-read`: if the re-read shows a different assignee, or shows the state did not
228
- change to the `claimed` role, skip it (someone else holds it, or the claim did not land).
231
+ assignee equal to "me" or to one of `sessionManager.operatorIds` (the claim assigns only an
232
+ unassigned item, so an operator's item keeps the operator); anything else means skip. Then
233
+ re-read the item with `lisa-tracker-read`: if the re-read shows an assignee that is neither
234
+ "me" nor an operator, or shows the state did not change to the `claimed` role, skip it
235
+ (someone else holds it, or the claim did not land).
229
236
  Never spawn on the claim's word alone.
230
237
  4. **Spawn.** `spawn <ref> [--repo <key>]`.
231
238
  - exit 0: post `[<REF>] started in <repo> (<name>)` to Telegram.