@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.
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +236 -16
- package/dist/core/downstream-references.d.ts.map +1 -1
- package/dist/core/downstream-references.js +3 -0
- package/dist/core/downstream-references.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +8 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +17 -12
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +236 -16
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +13 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +13 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +236 -16
- package/plugins/lisa/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa/skills/lisa-session-manager/SKILL.md +13 -6
- package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
- package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +13 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +236 -16
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +13 -6
- package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +13 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +236 -16
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa-copilot/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +13 -6
- package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +13 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +236 -16
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -0
- package/plugins/lisa-cursor/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +13 -6
- package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +13 -1
- package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/hooks/parity-safety-net.sh +236 -16
- package/plugins/lisa-devin/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa-devin/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +13 -6
- package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +13 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +236 -16
- package/plugins/src/base/rules/reference/config-resolution.md +1 -0
- package/plugins/src/base/scripts/ready-queue-order.mjs +34 -14
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/src/base/skills/lisa-session-manager/SKILL.md +13 -6
- package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +4 -1
- package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +4 -0
- 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:]]/) {
|
|
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:]]/) {
|
|
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:]]/) {
|
|
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:]]/) {
|
|
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:]]/) {
|
|
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:]]/) {
|
|
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:]]/) {
|
|
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:]]/) {
|
|
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:]]/) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -738,6 +738,7 @@ bypassed). `/lisa:setup:session-manager` writes it, detecting a multi-repo layou
|
|
|
738
738
|
| `sessionManager.notify.provider` | no | `telegram` | Only `telegram` is implemented. |
|
|
739
739
|
| `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>`. |
|
|
740
740
|
| `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. |
|
|
741
|
+
| `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. |
|
|
741
742
|
|
|
742
743
|
- **Non-secret only.** The bot token lives in `<stateDir>/.env` (directory 0700, file 0600),
|
|
743
744
|
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
|
|
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?":
|
|
11
|
-
* then
|
|
12
|
-
*
|
|
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 (
|
|
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
|
|
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
|
|
299
|
-
* rank ascending, then creation time ascending, then
|
|
300
|
-
* tiebreak. Excluded items keep their input order. With no
|
|
301
|
-
* assigned item is someone else's — the safe
|
|
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 =>
|
|
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
|
|
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"
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
@@ -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
|
|
22
|
-
then
|
|
23
|
-
|
|
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,
|
|
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 }>}
|
|
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 {
|
|
151
|
+
return {
|
|
152
|
+
json: { tracker, me, operators, total: items.length, pages, ...order },
|
|
153
|
+
};
|
|
148
154
|
}
|