@sjawhar/pi-legion-envoy 1.23.1 → 1.24.0
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/dist/legion.js +117 -0
- package/dist/skills/legion-worker/SKILL.md +15 -4
- package/package.json +1 -1
package/dist/legion.js
CHANGED
|
@@ -34715,6 +34715,112 @@ function isSingleLegionCommand(command) {
|
|
|
34715
34715
|
return false;
|
|
34716
34716
|
return trimmed.split(/\s+/, 1)[0] === "legion";
|
|
34717
34717
|
}
|
|
34718
|
+
var JJ_LOG_REWRITE_WORDS = ["undo", "abandon"];
|
|
34719
|
+
var JJ_OP_WORDS = ["op", "operation"];
|
|
34720
|
+
var JJ_OP_LOG_REWRITE_WORDS = ["restore", "revert"];
|
|
34721
|
+
var JJ_MENTION = /\bjj\b/;
|
|
34722
|
+
var JJ_LOG_REWRITE_MENTION = /\b(?:undo|abandon)\b|\b(?:op|operation)\b\W+(?:restore|revert)\b/;
|
|
34723
|
+
function jjLogRewriteMention(text) {
|
|
34724
|
+
if (!JJ_MENTION.test(text))
|
|
34725
|
+
return;
|
|
34726
|
+
const match = JJ_LOG_REWRITE_MENTION.exec(text);
|
|
34727
|
+
return match === null ? undefined : match[0].replace(/\W+/g, " ");
|
|
34728
|
+
}
|
|
34729
|
+
function splitShellCommands(command) {
|
|
34730
|
+
const commands = [];
|
|
34731
|
+
let words = [];
|
|
34732
|
+
let text = "";
|
|
34733
|
+
let inWord = false;
|
|
34734
|
+
let quote;
|
|
34735
|
+
const endWord = () => {
|
|
34736
|
+
if (inWord)
|
|
34737
|
+
words.push(text);
|
|
34738
|
+
text = "";
|
|
34739
|
+
inWord = false;
|
|
34740
|
+
};
|
|
34741
|
+
const endCommand = () => {
|
|
34742
|
+
endWord();
|
|
34743
|
+
if (words.length > 0)
|
|
34744
|
+
commands.push(words);
|
|
34745
|
+
words = [];
|
|
34746
|
+
};
|
|
34747
|
+
for (let i = 0;i < command.length; i += 1) {
|
|
34748
|
+
const char = command.charAt(i);
|
|
34749
|
+
if (quote !== undefined) {
|
|
34750
|
+
if (char === quote)
|
|
34751
|
+
quote = undefined;
|
|
34752
|
+
else if (quote === '"' && char === "\\" && command.charAt(i + 1) === `
|
|
34753
|
+
`)
|
|
34754
|
+
i += 1;
|
|
34755
|
+
else if (quote === '"' && char === "\\" && i + 1 < command.length) {
|
|
34756
|
+
i += 1;
|
|
34757
|
+
text += command.charAt(i);
|
|
34758
|
+
} else
|
|
34759
|
+
text += char;
|
|
34760
|
+
continue;
|
|
34761
|
+
}
|
|
34762
|
+
if (char === '"' || char === "'") {
|
|
34763
|
+
quote = char;
|
|
34764
|
+
inWord = true;
|
|
34765
|
+
} else if (char === "\\") {
|
|
34766
|
+
if (command.charAt(i + 1) === `
|
|
34767
|
+
`)
|
|
34768
|
+
i += 1;
|
|
34769
|
+
else if (i + 1 < command.length) {
|
|
34770
|
+
i += 1;
|
|
34771
|
+
text += command.charAt(i);
|
|
34772
|
+
inWord = true;
|
|
34773
|
+
}
|
|
34774
|
+
} else if (char === " " || char === "\t" || char === "<" || char === ">") {
|
|
34775
|
+
endWord();
|
|
34776
|
+
} else if (char === "&" && (command.charAt(i - 1) === ">" || command.charAt(i - 1) === "<" || command.charAt(i + 1) === ">")) {
|
|
34777
|
+
endWord();
|
|
34778
|
+
} else if (";&|\n()`".includes(char)) {
|
|
34779
|
+
endCommand();
|
|
34780
|
+
} else {
|
|
34781
|
+
text += char;
|
|
34782
|
+
inWord = true;
|
|
34783
|
+
}
|
|
34784
|
+
}
|
|
34785
|
+
if (quote !== undefined)
|
|
34786
|
+
return;
|
|
34787
|
+
endCommand();
|
|
34788
|
+
return commands;
|
|
34789
|
+
}
|
|
34790
|
+
function jjLogRewriteInvocation(command) {
|
|
34791
|
+
const commands = splitShellCommands(command);
|
|
34792
|
+
if (commands === undefined) {
|
|
34793
|
+
return jjLogRewriteMention(command) === undefined ? undefined : command.trim();
|
|
34794
|
+
}
|
|
34795
|
+
for (const words of commands) {
|
|
34796
|
+
const mentioned = words.find((word) => jjLogRewriteMention(word) !== undefined);
|
|
34797
|
+
if (mentioned !== undefined)
|
|
34798
|
+
return mentioned;
|
|
34799
|
+
const jj = words.findIndex((word) => word === "jj" || word.endsWith("/jj"));
|
|
34800
|
+
if (jj === -1)
|
|
34801
|
+
continue;
|
|
34802
|
+
const args = words.slice(jj + 1);
|
|
34803
|
+
const rewritesLog = args.some((arg) => JJ_LOG_REWRITE_WORDS.includes(arg)) || args.some((arg) => JJ_OP_WORDS.includes(arg)) && args.some((arg) => JJ_OP_LOG_REWRITE_WORDS.includes(arg));
|
|
34804
|
+
if (rewritesLog)
|
|
34805
|
+
return words.slice(jj).join(" ");
|
|
34806
|
+
}
|
|
34807
|
+
return;
|
|
34808
|
+
}
|
|
34809
|
+
function jjLogRewriteAttempt(toolCall) {
|
|
34810
|
+
const { toolName, input } = toolCall;
|
|
34811
|
+
if (toolName === "bash") {
|
|
34812
|
+
return typeof input.command === "string" ? jjLogRewriteInvocation(input.command) : undefined;
|
|
34813
|
+
}
|
|
34814
|
+
let text;
|
|
34815
|
+
if (toolName === "eval" && typeof input.code === "string")
|
|
34816
|
+
text = input.code;
|
|
34817
|
+
else if (toolName === "hub") {
|
|
34818
|
+
text = [input.application, ...Array.isArray(input.args) ? input.args : [], input.text].filter((part) => typeof part === "string").join(" ");
|
|
34819
|
+
} else
|
|
34820
|
+
return;
|
|
34821
|
+
const mention = jjLogRewriteMention(text);
|
|
34822
|
+
return mention === undefined ? undefined : `${toolName}: jj ${mention}`;
|
|
34823
|
+
}
|
|
34718
34824
|
var LEGION_LOADED_MARKER = Symbol.for("legion.pi-envoy.legion-loaded");
|
|
34719
34825
|
var CODE_MUTATION_TOOLS = ["edit", "write", "apply_patch"];
|
|
34720
34826
|
var MERGER_BLOCKED_TOOLS = [...CODE_MUTATION_TOOLS, "task"];
|
|
@@ -34738,6 +34844,7 @@ function legionExtension(pi) {
|
|
|
34738
34844
|
subagentSession ??= isSubagentSession(context);
|
|
34739
34845
|
return subagentSession;
|
|
34740
34846
|
};
|
|
34847
|
+
let phaseWorkerPane;
|
|
34741
34848
|
const roleDaemon = () => {
|
|
34742
34849
|
return createLegionDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"), fetch, {
|
|
34743
34850
|
recoveryToken: (sessionId) => {
|
|
@@ -35009,6 +35116,16 @@ function legionExtension(pi) {
|
|
|
35009
35116
|
toolCallId: toolCall.toolCallId,
|
|
35010
35117
|
toolName: toolCall.toolName
|
|
35011
35118
|
});
|
|
35119
|
+
phaseWorkerPane ??= classifySession(process.env).kind === "phase-worker";
|
|
35120
|
+
if (phaseWorkerPane) {
|
|
35121
|
+
const jjAttempt = jjLogRewriteAttempt(toolCall);
|
|
35122
|
+
if (jjAttempt !== undefined) {
|
|
35123
|
+
return {
|
|
35124
|
+
block: true,
|
|
35125
|
+
reason: `refused \`${jjAttempt}\`: jj undo, jj abandon, and jj op restore/revert/abandon/undo ` + "rewrite the jj operation log, which every Legion issue workspace shares (each is a jj " + "workspace of one clone), so they rewrite other trees' commits too. Recover forward with " + "a new commit or `jj restore <paths>` of files; anything else, stop and send the owning " + 'architect the `jj -R "$LEGION_WORKSPACE" log` evidence.'
|
|
35126
|
+
};
|
|
35127
|
+
}
|
|
35128
|
+
}
|
|
35012
35129
|
if (await checkSubagentSession(context))
|
|
35013
35130
|
return;
|
|
35014
35131
|
const sessionID = context.sessionManager.getSessionId();
|
|
@@ -120,10 +120,21 @@ transcript, the committed file wins: it is the copy that survived.
|
|
|
120
120
|
- **Before pushing, check ancestry:** `jj -R "$LEGION_WORKSPACE" log -r 'ancestors(@, 5)'`
|
|
121
121
|
— verify only your issue's commits are in the chain, not unrelated work.
|
|
122
122
|
|
|
123
|
-
**Shared operation safety:**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
**Shared operation safety:** Every Legion issue workspace is a `jj workspace` of one shared
|
|
124
|
+
clone, so they all share one operation log: `jj undo`, `jj abandon`, and
|
|
125
|
+
`jj op restore|revert|abandon|undo` rewrite it for every tree at once (on 2026-09-12 one
|
|
126
|
+
worker's `jj undo` rewrote nine of another tree's commits). The extension refuses them in every
|
|
127
|
+
phase-worker pane before they run — a `bash` command in any position of a pipeline or `&&`
|
|
128
|
+
chain, with or without `-R`, judged on the whole argument list; `eval` code; and a `hub`
|
|
129
|
+
process start — from your own tool calls and from any `task` subagent you spawn (it runs in
|
|
130
|
+
your pane, against the same log), and a `bash` command whose quoted text merely mentions `jj`
|
|
131
|
+
with one of those words (a heredoc, an echo, a commit message) is refused too: write such text
|
|
132
|
+
with the `write` tool or say "operation-log rollback" instead. `jj restore <paths>`,
|
|
133
|
+
`jj op log`, and `jj op show` stay allowed. Recover forward only: a new commit
|
|
134
|
+
(`jj -R "$LEGION_WORKSPACE" new`) or `jj -R "$LEGION_WORKSPACE" restore <paths>` of files.
|
|
135
|
+
Anything else, stop and send the owning architect the `jj -R "$LEGION_WORKSPACE" log`
|
|
136
|
+
evidence; the architect decides, and an operator performs any operation-log restore with every
|
|
137
|
+
other tree paused.
|
|
127
138
|
|
|
128
139
|
## Phase work
|
|
129
140
|
|