@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
@@ -808,6 +808,62 @@ follow_split() {
808
808
  }
809
809
  return 0;
810
810
  }
811
+ # U records tell the walker the stream below may not match the shell, so
812
+ # a compound closer can no longer be trusted (follow_compound_close). They
813
+ # only ever ADD distrust; every other record is emitted exactly as before.
814
+ #
815
+ # A statement spanning lines (a quoted newline, a backslash continuation)
816
+ # reaches the line-oriented reader as several records, and a later line
817
+ # can spell `done` or `fi` on its own.
818
+ function record(kind, text) {
819
+ if (text ~ /\n/) { printf "U\t\n" }
820
+ printf "%s\t%s\n", kind, text;
821
+ }
822
+ # The first word of one separator-delimited piece of a comment, as the
823
+ # walker would read it if the split hands it over as a statement.
824
+ function comment_reserved(piece) {
825
+ sub(/^[[:space:]]+/, "", piece); sub(/[[:space:]].*$/, "", piece);
826
+ gsub(/["\047]/, "", piece);
827
+ return piece ~ /^(fi|done|esac|case|if|then|else|elif|do|while|until|for|select|function|!|\{|\})$/;
828
+ }
829
+ # This split does not recognise comments, so a comment is split as code. It
830
+ # is harmless only when that split cannot change any structure the walker
831
+ # tracks: no piece after a separator starts with a reserved word, quotes
832
+ # close, parentheses and braces balance, and nothing escapes or substitutes.
833
+ # Inside a case pattern a `)` ends the pattern, so it is never harmless
834
+ # there; a `(` is ignored there, which keeps the Gradle `#(` safe.
835
+ # Anything else is reported, and the walker stops restoring flow.
836
+ function comment_unsafe(from, pattern, j, c, cq, parens, braces, pieces, piece) {
837
+ cq = ""; parens = 0; braces = 0; pieces = 0; piece = "";
838
+ for (j = from; j <= n; j++) {
839
+ c = substr(s, j, 1);
840
+ if (c == "\n") { break }
841
+ if (c == "\\" || c == "`") { return 1 }
842
+ if (cq != "") {
843
+ if (c == cq) { cq = "" }
844
+ else if (cq == "\"" && c == "(" && substr(s, j - 1, 1) == "$") { return 1 }
845
+ piece = piece c; continue;
846
+ }
847
+ if (c == "\"" || c == "\047") { cq = c; piece = piece c; continue }
848
+ if (c ~ /[;&|(){}]/) {
849
+ if (pieces > 0 && comment_reserved(piece)) { return 1 }
850
+ pieces++; piece = "";
851
+ if (c == "(" && !pattern) { parens++ }
852
+ if (c == ")" && (pattern || --parens < 0)) { return 1 }
853
+ if (c == "{") { braces++ }
854
+ if (c == "}" && --braces < 0) { return 1 }
855
+ continue;
856
+ }
857
+ piece = piece c;
858
+ }
859
+ if (pieces > 0 && comment_reserved(piece)) { return 1 }
860
+ return cq != "" || parens != 0 || braces != 0;
861
+ }
862
+ function comment_check(at, pattern) {
863
+ if ((at == 1 || substr(s, at - 1, 1) ~ /[[:space:];&|()]/) && comment_unsafe(at + 1, pattern)) {
864
+ printf "U\t\n";
865
+ }
866
+ }
811
867
  BEGIN {
812
868
  s = ENVIRON["SN_FOLLOW_TEXT"]; n = length(s);
813
869
  q = ""; esc = 0; start = 1; sep = "o"; parens = 0;
@@ -820,6 +876,7 @@ follow_split() {
820
876
  if (functions > 0) {
821
877
  if (q != "") { if (c == q) { q = "" } continue }
822
878
  if (c == "\"" || c == "\047") { q = c; continue }
879
+ if (c == "#") { comment_check(i, 0) }
823
880
  if (c == "{") { functions++ }
824
881
  if (c == "}" && --functions == 0) {
825
882
  printf "N\t%s\t%s\n", sep, function_name;
@@ -833,10 +890,11 @@ follow_split() {
833
890
  quoted_expression = q == "\"" && ((c == "(" && substr(s, i - 1, 1) == "$" && !literal_dollars[i - 1]) || c == "`");
834
891
  if (q != "" && !quoted_expression) { if (c == q) { q = "" } continue }
835
892
  if (c == "\"" || c == "\047") { q = c; continue }
893
+ if (c == "#") { comment_check(i, cases > 0 && case_scopes[cases] == parens && case_patterns[cases]) }
836
894
  if (c == "`") {
837
895
  if (backtick) {
838
896
  piece = substr(s, start, i - start);
839
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
897
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
840
898
  printf "]\t\n"; start = backtick_start; q = backtick_quote; backtick = 0;
841
899
  } else {
842
900
  backtick_start = start; backtick_quote = q; q = ""; backtick = 1;
@@ -876,14 +934,14 @@ follow_split() {
876
934
  brace_groups[++brace_depth] = group;
877
935
  if (group) {
878
936
  brace_sep = sep;
879
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
937
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
880
938
  # A pipeline owns the whole compound command, not just the final
881
939
  # statement before |. Snapshot before any brace-body cd/return.
882
940
  outgoing = brace_pipe(i + 1, "{", "}");
883
941
  brace_pipes[brace_depth] = brace_sep == "|" || outgoing;
884
942
  if (brace_pipes[brace_depth]) { printf "(\t%s%s\nL\t%d\n", (brace_sep == "|" ? "|" : ""), (outgoing ? "+" : ""), list_async(i + 1) }
885
943
  printf "G\t%s\nL\t%d\n", brace_sep, list_async(i + 1);
886
- if (brace_redirect ~ /[^[:space:]]/) { printf "I\t%s\n", brace_redirect }
944
+ if (brace_redirect ~ /[^[:space:]]/) { record("I", brace_redirect) }
887
945
  # P retains incoming stdin provenance without wrapping each body
888
946
  # statement in another child: all statements share the brace cwd.
889
947
  if (brace_sep == "|") { sep = "P" }
@@ -894,7 +952,7 @@ follow_split() {
894
952
  group = brace_groups[brace_depth--];
895
953
  if (group) {
896
954
  piece = substr(s, start, i - start);
897
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
955
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
898
956
  printf "H\t\n";
899
957
  if (brace_pipes[brace_depth + 1]) { printf ")\t\n" }
900
958
  start = i + 1; post_brace = 1; continue;
@@ -915,10 +973,10 @@ follow_split() {
915
973
  }
916
974
  if (group) {
917
975
  piece = substr(s, start, i - start);
918
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
976
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
919
977
  outgoing = brace_pipe(i + 1, "(", ")");
920
978
  printf "(\t%s%s\nL\t%d\n", sep, (outgoing ? "+" : ""), list_async(i + 1);
921
- if (brace_redirect ~ /[^[:space:]]/) { printf "I\t%s\n", brace_redirect }
979
+ if (brace_redirect ~ /[^[:space:]]/) { record("I", brace_redirect) }
922
980
  if (sep == "|") { sep = "P" }
923
981
  start = i + 1; continue;
924
982
  }
@@ -927,12 +985,12 @@ follow_split() {
927
985
  piece = substr(s, start, i - start);
928
986
  group = scopes[parens--];
929
987
  if (group == 2) {
930
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
988
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
931
989
  printf "]\t\n"; start = expression_starts[parens + 1]; q = expression_quotes[parens + 1]; continue;
932
990
  }
933
991
  if (group) {
934
992
  piece = substr(s, start, i - start);
935
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
993
+ if (piece ~ /[^[:space:]]/) { record(sep, piece); sep = "o" }
936
994
  printf ")\t\n"; start = i + 1; post_brace = 1; continue;
937
995
  }
938
996
  }
@@ -942,7 +1000,7 @@ follow_split() {
942
1000
  piece = substr(s, start, i - start);
943
1001
  pipe = c == "|" && substr(s, i + 1, 1) != "|";
944
1002
  if (pipe || sep == "|") { printf "(\t%s%s\nL\t%d\n", (sep == "|" ? "|" : ""), (pipe ? "+" : ""), list_async(start) }
945
- if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", post_brace ? "R" : sep, piece }
1003
+ if (piece ~ /[^[:space:]]/) { record(post_brace ? "R" : sep, piece) }
946
1004
  post_brace = 0;
947
1005
  if (pipe || sep == "|") { printf ")\t\n" }
948
1006
  if (c == ";" && substr(s, i + 1, 1) == ";" && cases > 0) {
@@ -962,7 +1020,7 @@ follow_split() {
962
1020
  }
963
1021
  }
964
1022
  if (sep == "|") { printf "(\t|\nL\t%d\n", list_async(start) }
965
- printf "%s\t%s\n", sep, substr(s, start, n - start + 1);
1023
+ record(sep, substr(s, start, n - start + 1));
966
1024
  if (sep == "|") { printf ")\t\n" }
967
1025
  }
968
1026
  '
@@ -1432,6 +1490,77 @@ PY
1432
1490
  follow_redirect_output="$(printf '%s' "$result" | jq -er '.output')" || follow_refuse "compound redirection" "invalid output projection"
1433
1491
  }
1434
1492
 
1493
+ # Enter a compound command (`if`, `while`/`until`/`for`/`select`, `case`).
1494
+ #
1495
+ # Reaching anything inside it depends on runtime flow, so a definition there is
1496
+ # conditional. The enclosing flow state is saved so the matching closer can put
1497
+ # it back: a compound that has CLOSED no longer decides whether the next
1498
+ # top-level definition runs. Leaving the flag raised past the closer marked
1499
+ # every function defined after any loop or case as conditional, which refused
1500
+ # every Gradle-generated start script (CodySwannGT/lisa#4470). Brace groups
1501
+ # already restore this way through brace_flows.
1502
+ #
1503
+ # These read and write follow_scan's locals through bash's dynamic scoping.
1504
+ follow_compound_open() {
1505
+ local frame_scope=$scope_depth
1506
+ # A pipeline element is its own scope, but `if a | b; then` and
1507
+ # `x | while read l; do` open their compound at the ENCLOSING level.
1508
+ if [ "$scope_depth" -gt 0 ]; then
1509
+ case "${scope_kinds[scope_depth - 1]}" in '+' | '|' | '|+') frame_scope=$((scope_depth - 1)) ;; esac
1510
+ fi
1511
+ compound_flows[compound_depth]=$function_flow_unknown
1512
+ compound_kinds[compound_depth]="$1"
1513
+ compound_frame_scopes[compound_depth]=$frame_scope
1514
+ compound_opened_at="$compound_opened_at${i:-} "
1515
+ compound_depth=$((compound_depth + 1))
1516
+ function_flow_unknown=1
1517
+ }
1518
+
1519
+ # Close the innermost compound, restoring the flow state that encloses it.
1520
+ #
1521
+ # Only a closer of the SAME kind as the innermost open compound may lower the
1522
+ # flag. The walker splits text, it does not parse shell: a comment's `;` or a
1523
+ # heredoc payload line can reach it as a `fi`/`done` statement. A closer that
1524
+ # matches nothing, or arrives after a heredoc (see compound_trusted), proves the
1525
+ # walk has lost the structure, so every later closer keeps the flag where it is
1526
+ # — the verdict this walker gave before closers restored anything.
1527
+ #
1528
+ # A conditional `return` inside the compound can skip whatever follows it in a
1529
+ # function body or sourced file, so it re-raises the flag, as H does for a brace
1530
+ # group. `break`/`continue` resume at or inside the loop, so they never skip a
1531
+ # definition after `done`, and `exit` ends the shell before any later call runs:
1532
+ # neither needs to persist past the closer. A `break` inside a function body
1533
+ # can end the CALLER's loop and skip the rest of that body, but such a call only
1534
+ # has that effect when the call itself sits inside a loop body, which is
1535
+ # already conditional.
1536
+ #
1537
+ # The closer must also sit in the scope its compound opened in: a `done` inside
1538
+ # a command substitution (`$( … )` or backticks) is a different shell's syntax
1539
+ # error, not the end of this loop. The one cross-scope closer is `done | cmd`,
1540
+ # whose `done` is the first element of a pipeline. Callers pass only a closer
1541
+ # spelled as the bare first word of its statement; see the fi/done arm.
1542
+ follow_compound_close() {
1543
+ local frame_scope
1544
+ if [ "$compound_trusted" -eq 0 ] || [ "$compound_depth" -eq 0 ] \
1545
+ || [ "${compound_kinds[compound_depth - 1]}" != "$1" ]; then
1546
+ compound_trusted=0
1547
+ return 0
1548
+ fi
1549
+ frame_scope="${compound_frame_scopes[compound_depth - 1]}"
1550
+ if [ "$scope_depth" -ne "$frame_scope" ]; then
1551
+ case "$((scope_depth - frame_scope)):${scope_kinds[scope_depth - 1]}" in
1552
+ '1:+' | '1:|' | '1:|+') ;;
1553
+ *)
1554
+ compound_trusted=0
1555
+ return 0
1556
+ ;;
1557
+ esac
1558
+ fi
1559
+ compound_depth=$((compound_depth - 1))
1560
+ function_flow_unknown="${compound_flows[compound_depth]}"
1561
+ if [ "$return_flow_unknown" -ne 0 ]; then function_flow_unknown=1; fi
1562
+ }
1563
+
1435
1564
  # Walk one text for the files it executes. Runs in the PARENT shell, never in a
1436
1565
  # command substitution: follow_refuse() must be able to deny, and an exit inside
1437
1566
  # a subshell ends that subshell rather than this hook — the mistake documented
@@ -1467,6 +1596,18 @@ follow_scan() {
1467
1596
  local -a brace_descriptor_maps=() scope_descriptor_maps=()
1468
1597
  local -a brace_stdin_cats=() brace_stdin_cwds=() brace_stdin_unknowns=() scope_stdin_cats=() scope_stdin_cwds=() scope_stdin_unknowns=()
1469
1598
  local -a case_cwds=() case_unknowns=() case_changes=()
1599
+ # Open compound commands of THIS text, innermost last. See
1600
+ # follow_compound_open. The stack starts empty, so a closer can never lower
1601
+ # the flag below the value this scan was entered with.
1602
+ local compound_depth=0 compound_trusted=1 compound_opened_at=" " compound_prev
1603
+ local -a compound_flows=() compound_kinds=() compound_scopes=() compound_frame_scopes=() scope_kinds=()
1604
+ # The split treats every [[:space:]] byte as a blank, but bash separates words
1605
+ # only on space and tab (and ends a command on newline). A form feed,
1606
+ # vertical tab or carriage return is an ordinary word byte to bash, so
1607
+ # `<FF>done` is a command name there and a bare closer here. With none of the
1608
+ # three present, every [[:space:]] the split tests is a bash blank or a
1609
+ # newline, so this one check covers each such test in the split.
1610
+ case "$text" in *$'\f'* | *$'\v'* | *$'\r'*) compound_trusted=0 ;; esac
1470
1611
  if [ "$function_call_depth" -eq 0 ] && [ "$invocation_kind" != source ]; then
1471
1612
  # Ordinary child shells do not inherit unexported functions. Sourcing and
1472
1613
  # function calls share the caller's definitions instead.
@@ -1480,6 +1621,14 @@ follow_scan() {
1480
1621
  stmt="${line#*$'\t'}"
1481
1622
  statement_conditional=0
1482
1623
  case "$sep" in '&' | '?') statement_conditional=1 ;; esac
1624
+ # A heredoc payload reaches this walk as statements, so a payload line can
1625
+ # spell a closer. Once one starts, closers stop restoring flow for the rest
1626
+ # of this text. Checked before any skip so an unread payload still counts.
1627
+ # A here-string (`<<<`) carries no payload lines.
1628
+ # The same test also matches an arithmetic shift such as `$((1 << 2))`.
1629
+ # That only withholds a restore, which is the safe direction.
1630
+ case "${stmt//<<</}" in *'<<'*) compound_trusted=0 ;; esac
1631
+ if [ "$sep" = U ]; then compound_trusted=0; continue; fi
1483
1632
  # A reached && successor observes the preceding successful command's cwd.
1484
1633
  # Scope/list events invalidate this projection; it must never carry a
1485
1634
  # child's or a background list's directory into another shell/list.
@@ -1570,7 +1719,7 @@ follow_scan() {
1570
1719
  continue
1571
1720
  ;;
1572
1721
  C)
1573
- function_flow_unknown=1
1722
+ follow_compound_open case
1574
1723
  case_cwds[case_depth]="$follow_cwd"
1575
1724
  case_unknowns[case_depth]="$follow_cwd_unknown"
1576
1725
  case_changes[case_depth]=0
@@ -1589,6 +1738,9 @@ follow_scan() {
1589
1738
  follow_cwd=""; follow_cwd_unknown=1
1590
1739
  fi
1591
1740
  case_depth=$((case_depth - 1))
1741
+ # K closes the case; the literal `esac` statement the split emits
1742
+ # after it is not a closer and must not close another frame.
1743
+ follow_compound_close case
1592
1744
  fi
1593
1745
  continue
1594
1746
  ;;
@@ -1601,6 +1753,10 @@ follow_scan() {
1601
1753
  scope_descriptor_maps[scope_depth]="$follow_descriptor_map"
1602
1754
  function_scopes[scope_depth]=$function_count
1603
1755
  flow_scopes[scope_depth]=$function_flow_unknown
1756
+ compound_scopes[scope_depth]=$compound_depth
1757
+ # `[` is a substitution; for `(` the split's tail says whether this is
1758
+ # a pipeline element (`+`, `|`, `|+`) or a literal subshell.
1759
+ if [ "$sep" = '[' ]; then scope_kinds[scope_depth]='['; else scope_kinds[scope_depth]="$stmt"; fi
1604
1760
  return_flows[scope_depth]=$return_flow_unknown
1605
1761
  scope_stdin_cats[scope_depth]="$follow_stdin_cat"
1606
1762
  scope_stdin_cwds[scope_depth]="$follow_stdin_cwd"
@@ -1626,6 +1782,16 @@ follow_scan() {
1626
1782
  follow_cwd_unknown="${unknown_scopes[scope_depth]}"
1627
1783
  function_flow_unknown="${flow_scopes[scope_depth]}"
1628
1784
  return_flow_unknown="${return_flows[scope_depth]}"
1785
+ # A pipeline element is a scope of its own, so `done | cmd` closes, in
1786
+ # here, a loop opened outside. The scope must not undo that closer: the
1787
+ # flow after the pipeline is the one the loop's frame saved. The stack
1788
+ # itself is never rewound here. `if a | b; then` opens its `if` inside
1789
+ # the first element, and discarding that frame would let the `fi` close
1790
+ # an ENCLOSING compound instead.
1791
+ if [ "$compound_depth" -lt "${compound_scopes[scope_depth]}" ]; then
1792
+ function_flow_unknown="${compound_flows[compound_depth]}"
1793
+ if [ "$return_flow_unknown" -ne 0 ]; then function_flow_unknown=1; fi
1794
+ fi
1629
1795
  follow_stdin_cat="${scope_stdin_cats[scope_depth]}"
1630
1796
  follow_stdin_cwd="${scope_stdin_cwds[scope_depth]}"
1631
1797
  follow_stdin_unknown="${scope_stdin_unknowns[scope_depth]}"
@@ -1691,6 +1857,7 @@ follow_scan() {
1691
1857
  n=${#toks[@]}
1692
1858
  [ "$n" -gt 0 ] || continue
1693
1859
  i=0
1860
+ compound_opened_at=" "
1694
1861
  cmd_pos=1
1695
1862
  function_lookup_allowed=1
1696
1863
  eval_mode=0
@@ -1795,12 +1962,32 @@ follow_scan() {
1795
1962
  fi
1796
1963
  cmd_pos=0; i=$((i + 1)); continue
1797
1964
  ;;
1798
- if | while | until)
1799
- function_flow_unknown=1
1965
+ if)
1966
+ follow_compound_open if
1967
+ i=$((i + 1)); continue
1968
+ ;;
1969
+ while | until)
1970
+ follow_compound_open loop
1800
1971
  i=$((i + 1)); continue
1801
1972
  ;;
1802
1973
  for | select)
1803
- function_flow_unknown=1
1974
+ follow_compound_open loop
1975
+ cmd_pos=0; i=$((i + 1)); continue
1976
+ ;;
1977
+ # A closer counts only as the bare, unquoted FIRST word of its
1978
+ # statement. Anything else that unwraps to one — `"done"`, `\fi`,
1979
+ # `x=1 done`, `eval done`, `command done`, the tail of `x='a; fi'` — is
1980
+ # not a reserved word to the shell, so its appearance means the walk
1981
+ # cannot prove where compounds end. A literal `esac` statement follows
1982
+ # every K record and is harmless in that one spelling.
1983
+ 'fi' | 'done' | 'esac')
1984
+ if [ "$i" -ne 0 ] || [ "${toks[0]}" != "$t" ]; then
1985
+ compound_trusted=0
1986
+ elif [ "$t" = 'fi' ]; then
1987
+ follow_compound_close if
1988
+ elif [ "$t" = 'done' ]; then
1989
+ follow_compound_close loop
1990
+ fi
1804
1991
  cmd_pos=0; i=$((i + 1)); continue
1805
1992
  ;;
1806
1993
  # Prefixes that keep the command position open: assignments, wrappers,
@@ -1842,11 +2029,13 @@ follow_scan() {
1842
2029
  continue
1843
2030
  ;;
1844
2031
  sudo | env | command | exec | nohup | nice | time | builtin \
1845
- | do | then | else | '!' | '{' \
2032
+ | do | then | else | elif | '!' | '{' \
1846
2033
  | */sudo | */env | */nohup | */nice | */time)
1847
2034
  case "$t" in
1848
2035
  sudo | env | command | exec | nohup | nice | builtin | */sudo | */env | */nohup | */nice) function_lookup_allowed=0 ;;
1849
- do | then | else) function_flow_unknown=1 ;;
2036
+ # `elif` starts a condition list at a command position, so an
2037
+ # opener after it is a real compound (`elif if x; then …; fi`).
2038
+ do | then | else | elif) function_flow_unknown=1 ;;
1850
2039
  esac
1851
2040
  i=$((i + 1))
1852
2041
  continue
@@ -2081,6 +2270,37 @@ follow_scan() {
2081
2270
  # Keep successful-branch provenance separate from the continuation join.
2082
2271
  # OR can skip its RHS and still reach a following &&, so it cannot prove
2083
2272
  # that RHS's cwd. A semicolon observes every possible preceding outcome.
2273
+ # Every opener must have pushed a frame, or its own closer pops the
2274
+ # ENCLOSING one and restores flow inside a compound that is still open.
2275
+ # Bash reads a reserved word wherever a command may start, which this walk
2276
+ # does not fully model: after `coproc NAME`, after a function header whose
2277
+ # body is a bare compound (`f () while …`), or after `function NAME`. So an
2278
+ # opener word the walk did not open stops all later restores unless the
2279
+ # word before it is a plain argument (`echo if`, `# loops for x`), where
2280
+ # bash reads it as data too. `case` never opens here (the split's C record
2281
+ # does), so a statement-initial `case` means the split missed its `in`.
2282
+ if [ "$compound_trusted" -ne 0 ]; then
2283
+ for ((j = 0; j < n; j++)); do
2284
+ case "${toks[j]}" in
2285
+ coproc) compound_trusted=0 ;;
2286
+ function) [ "$j" -ne 0 ] || compound_trusted=0 ;;
2287
+ if | while | until | for | select | case)
2288
+ case "$compound_opened_at" in *" $j "*) continue ;; esac
2289
+ if [ "$j" -eq 0 ]; then
2290
+ compound_trusted=0
2291
+ else
2292
+ compound_prev="${toks[j - 1]}"
2293
+ case "$compound_prev" in
2294
+ '' | *[!A-Za-z0-9_./,:=+@%-]* | elif | coproc | function | time | then | do | else)
2295
+ compound_trusted=0
2296
+ ;;
2297
+ esac
2298
+ fi
2299
+ ;;
2300
+ esac
2301
+ [ "$compound_trusted" -ne 0 ] || break
2302
+ done
2303
+ fi
2084
2304
  and_cwd="$follow_cwd"; and_unknown="$follow_cwd_unknown"
2085
2305
  if [ "$sep" != '?' ] && [ "$function_flow_unknown" -eq 0 ]; then and_ready=1; fi
2086
2306
  if [ "$statement_conditional" -ne 0 ] && { [ "$join_unknown" -ne 0 ] || [ "$follow_cwd_unknown" -ne 0 ] || [ "$follow_cwd" != "$join_cwd" ]; }; then
package/package.json CHANGED
@@ -189,7 +189,7 @@
189
189
  "qs": "^6.16.0"
190
190
  },
191
191
  "name": "@codyswann/lisa",
192
- "version": "4.81.4",
192
+ "version": "4.81.6",
193
193
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
194
194
  "main": "dist/index.js",
195
195
  "exports": {
@@ -345,7 +345,7 @@
345
345
  "test": "tests"
346
346
  },
347
347
  "types": "./dist/index.d.ts",
348
- "lisaReleaseCommit": "e9f635e3e13ab6737422426cfe248257a081a4e5",
349
- "gitHead": "e9f635e3e13ab6737422426cfe248257a081a4e5",
350
- "lisaReleaseTag": "v4.81.4"
348
+ "lisaReleaseCommit": "08ee71200b5e384f43d4daa01f71d3cc75dc5944",
349
+ "gitHead": "08ee71200b5e384f43d4daa01f71d3cc75dc5944",
350
+ "lisaReleaseTag": "v4.81.6"
351
351
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.81.4",
3
+ "version": "4.81.6",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.81.4",
3
+ "version": "4.81.6",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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
  }
@@ -68,7 +68,12 @@ the manager.
68
68
  claims go through `lisa-tracker-claim`. Never call a vendor API around them.
69
69
  - **"Me".** Resolve once per manager run and store it with `ledger control --me <id>`: GitHub
70
70
  `gh api user --jq .login`; Jira the accountId from the `lisa-atlassian-access` identity probe;
71
- Linear `viewer.id` through `lisa-linear-access`.
71
+ Linear `viewer.id` through `lisa-linear-access`. "Me" is whoever the tracker credential belongs
72
+ to, which is often an agent account.
73
+ - **Operators.** `sessionManager.operatorIds` names the people the manager works for (the same
74
+ kind of id as "me": GitHub login, Jira accountId, Linear user id). Their assigned items come
75
+ first, then items assigned to "me", then unassigned. Items assigned to anyone else are never
76
+ dispatched. Never treat an operator's item as someone else's.
72
77
  - **Workers** start only through `spawn`. The worker prompt is exactly
73
78
  `/lisa:implement <REF> managedBy=<manager-name>`; `lisa-implement` owns what `managedBy=` means.
74
79
  Never start a worker another way, and never reuse a worker for another item or a new attempt.
@@ -209,8 +214,8 @@ Skip while `capacity` reports `paused`. Otherwise:
209
214
  Raw Jira issues carry `key` and `fields` (summary, priority, assignee, created, labels,
210
215
  description, reporter, issuetype, subtasks); raw Linear nodes carry identifier, title, priority,
211
216
  createdAt, assignee, labels, description, creator and children with their state type.
212
- - `ordered` is the dispatch order (mine, then unassigned, then priority, then age). `excluded`
213
- needs no action.
217
+ - `ordered` is the dispatch order: assigned to an operator, then to "me", then unassigned; each
218
+ group by priority, then age. `excluded` needs no action.
214
219
  2. **While `capacity` says `can_spawn`** and items remain, take the next item that has no pending
215
220
  question in the ledger:
216
221
  1. Read it fully (`lisa-tracker-read`): title, description, labels, project, parent, comments.
@@ -225,9 +230,11 @@ Skip while `capacity` reports `paused`. Otherwise:
225
230
  `cd "$(node "$SM/session-manager.mjs" repo-dir --repo <key> --raw --project-root "$ROOT")"`
226
231
  (omit `--repo` on a single-repo project), invoke `lisa-tracker-claim <ref>`, then
227
232
  `cd "$ROOT" && pwd` and confirm the path. Continue only on `claimed` or `reused` with the
228
- assignee equal to "me"; anything else means skip. Then re-read the item with
229
- `lisa-tracker-read`: if the re-read shows a different assignee, or shows the state did not
230
- change to the `claimed` role, skip it (someone else holds it, or the claim did not land).
233
+ assignee equal to "me" or to one of `sessionManager.operatorIds` (the claim assigns only an
234
+ unassigned item, so an operator's item keeps the operator); anything else means skip. Then
235
+ re-read the item with `lisa-tracker-read`: if the re-read shows an assignee that is neither
236
+ "me" nor an operator, or shows the state did not change to the `claimed` role, skip it
237
+ (someone else holds it, or the claim did not land).
231
238
  Never spawn on the claim's word alone.
232
239
  4. **Spawn.** `spawn <ref> [--repo <key>]`.
233
240
  - exit 0: post `[<REF>] started in <repo> (<name>)` to Telegram.
@@ -18,9 +18,10 @@ session list from `claude agents`; and the project's own Telegram chat.
18
18
 
19
19
  ## Candidate selection
20
20
 
21
- Leaf items in the `ready` role, ordered: assigned to the operator, then unassigned, then by priority,
22
- then oldest. Items assigned to someone else, containers, human-held items and (when configured)
23
- items by untrusted authors are skipped. As many as the machine has capacity for.
21
+ Leaf items in the `ready` role, ordered: assigned to an operator (`sessionManager.operatorIds`),
22
+ then to the manager's own tracker identity, then unassigned; within each, by priority, then oldest.
23
+ Items assigned to someone else, containers, human-held items and (when configured) items by
24
+ untrusted authors are skipped. As many as the machine has capacity for.
24
25
 
25
26
  ## Scope/bounds
26
27
 
@@ -123,9 +123,11 @@ function readEnvelopeQueue(ctx, tracker) {
123
123
  }
124
124
 
125
125
  /**
126
- * `queue --tracker <github|jira|linear>`: the Ready queue, mine first.
126
+ * `queue --tracker <github|jira|linear>`: the Ready queue, the operator's
127
+ * items first (`sessionManager.operatorIds`), then mine, then unassigned.
127
128
  * @param {object} ctx - CLI context
128
- * @returns {Promise<{ json: object }>} `{tracker,me,total,pages,ordered,excluded}`
129
+ * @returns {Promise<{ json: object }>}
130
+ * `{tracker,me,operators,total,pages,ordered,excluded}`
129
131
  */
130
132
  export async function queue(ctx) {
131
133
  const tracker = ctx.values.tracker;
@@ -139,10 +141,14 @@ export async function queue(ctx) {
139
141
  ? fetchGithubQueue(ctx, project)
140
142
  : readEnvelopeQueue(ctx, tracker);
141
143
  const me = ctx.values.me ?? (await readLedger(ctx.root)).control.me ?? null;
144
+ const operators = config?.operatorIds ?? [];
142
145
  const order = orderReadyQueue(items, {
143
146
  me,
147
+ operators,
144
148
  trustedAuthorIds: config?.trustedAuthorIds ?? undefined,
145
149
  humanNeededLabel: humanNeededLabel(project, tracker),
146
150
  });
147
- return { json: { tracker, me, total: items.length, pages, ...order } };
151
+ return {
152
+ json: { tracker, me, operators, total: items.length, pages, ...order },
153
+ };
148
154
  }
@@ -126,6 +126,7 @@ function validationView(config) {
126
126
  ...config,
127
127
  repos: config.repos ?? undefined,
128
128
  trustedAuthorIds: config.trustedAuthorIds ?? undefined,
129
+ operatorIds: config.operatorIds,
129
130
  };
130
131
  }
131
132
 
@@ -111,7 +111,10 @@ Per cycle
111
111
  queue --tracker jira|linear --input <file> [--me <id>]
112
112
  <file> = {complete:true,pages,items:[raw vendor issues]}; exit 2 unless
113
113
  complete is exactly true.
114
- Both print {tracker,me,total,pages,ordered,excluded:[{ref,reason}]}.
114
+ Both print {tracker,me,operators,total,pages,ordered,excluded:[{ref,
115
+ reason}]}. ordered: items assigned to sessionManager.operatorIds
116
+ first, then to --me, then unassigned; each group by priority, then age.
117
+ Items assigned to anyone else are excluded (assigned-to-other).
115
118
  --me defaults to the ledger's control.me.
116
119
  ledger get [<ref>]
117
120
  ledger record-spawn <ref> --worker <name> [--session-id <id>] [--repo <key>]
@@ -66,6 +66,7 @@ export const SESSION_MANAGER_DEFAULTS = Object.freeze({
66
66
  * stall: StallBlock,
67
67
  * notify: NotifyBlock,
68
68
  * trustedAuthorIds: string[] | null,
69
+ * operatorIds?: string[] | null,
69
70
  * }} SessionManagerConfig
70
71
  * @typedef {{
71
72
  * name: string,
@@ -120,6 +121,9 @@ export function resolveSessionManagerConfig(globalConfig, localConfig) {
120
121
  ...merged,
121
122
  repos,
122
123
  trustedAuthorIds: merged.trustedAuthorIds ?? null,
124
+ // Left as declared: absent stays undefined and an explicit null stays null,
125
+ // so validation can refuse the null instead of reading it as "no operators".
126
+ operatorIds: merged.operatorIds,
123
127
  };
124
128
  }
125
129
 
@@ -56,7 +56,19 @@ writes `$SM`, and pass `--project-root "$ROOT"` (the absolute project root) on e
56
56
  bypass-permissions worker; say so plainly. The list holds stable tracker user ids as strings,
57
57
  never logins: the GitHub numeric user id (`gh api users/<login> --jq .id`), the Jira
58
58
  `accountId`, or the Linear user id. A login matches no one and silently excludes every item.
59
- 4. `node "$SM/session-manager.mjs" validate --project-root "$ROOT"` must print `valid: true`.
59
+ 4. Set `sessionManager.operatorIds` to a non-empty **array** holding the operator's own tracker
60
+ user id, e.g. `"operatorIds": ["<operator-id>"]` (a bare string fails `validate`). Look up the
61
+ OPERATOR, not the credential: `gh api user` / `viewer.id` return whoever the tracker credential
62
+ belongs to, which is the agent account in exactly the case this key exists for. The manager dispatches
63
+ the operator's assigned items first, then items assigned to the tracker credential's identity
64
+ ("me"), then unassigned ones, and skips items assigned to anyone else. When the tracker
65
+ credential is not the operator's (an agent or bot account), leaving this unset means the
66
+ operator's own Ready items are never dispatched; say so plainly. Unlike `trustedAuthorIds`, this
67
+ is matched against the assignee, exactly and case-sensitively, so use the same kind of id as
68
+ "me", spelled as the tracker API returns it: the GitHub **login** (`gh api users/<login> --jq
69
+ .login`), the Jira `accountId`, or the Linear user id (the operator's entry in the team's member
70
+ list, or `viewer.id` only when the operator runs the lookup with their own credential).
71
+ 5. `node "$SM/session-manager.mjs" validate --project-root "$ROOT"` must print `valid: true`.
60
72
 
61
73
  ## Step 3: Store the bot token
62
74