@sjawhar/pi-legion-envoy 1.23.1 → 1.24.1
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 +146 -7
- package/dist/skills/legion-worker/SKILL.md +15 -4
- package/package.json +1 -1
package/dist/legion.js
CHANGED
|
@@ -30665,6 +30665,30 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery) {
|
|
|
30665
30665
|
throw new LegionDaemonApiError("POST", path, response.status, responseBody);
|
|
30666
30666
|
return schema.parse(JSON.parse(responseBody));
|
|
30667
30667
|
};
|
|
30668
|
+
const recoveries = new Map;
|
|
30669
|
+
const secretAfterRefusal = async (active, sessionId, refusedSecret) => {
|
|
30670
|
+
let record = recoveries.get(sessionId);
|
|
30671
|
+
if (record === undefined) {
|
|
30672
|
+
record = {};
|
|
30673
|
+
recoveries.set(sessionId, record);
|
|
30674
|
+
}
|
|
30675
|
+
if (record.latestSecret !== undefined && record.latestSecret !== refusedSecret) {
|
|
30676
|
+
return record.latestSecret;
|
|
30677
|
+
}
|
|
30678
|
+
if (record.inFlight === undefined) {
|
|
30679
|
+
const current = record;
|
|
30680
|
+
const inFlight = postOnce(WORKER_SESSION_PATH, { sessionId, recoveryToken: active.recoveryToken(sessionId) }, LegionDaemonApi.WorkerSession.response).then((recovered) => {
|
|
30681
|
+
active.onRecovered?.(sessionId, recovered);
|
|
30682
|
+
current.latestSecret = recovered.secret;
|
|
30683
|
+
return recovered;
|
|
30684
|
+
}).finally(() => {
|
|
30685
|
+
if (current.inFlight === inFlight)
|
|
30686
|
+
current.inFlight = undefined;
|
|
30687
|
+
});
|
|
30688
|
+
record.inFlight = inFlight;
|
|
30689
|
+
}
|
|
30690
|
+
return (await record.inFlight).secret;
|
|
30691
|
+
};
|
|
30668
30692
|
const post = async (path, body, schema) => {
|
|
30669
30693
|
try {
|
|
30670
30694
|
return await postOnce(path, body, schema);
|
|
@@ -30673,12 +30697,8 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery) {
|
|
|
30673
30697
|
if (path === WORKER_SESSION_PATH || recovery === undefined || capability === undefined || !isInvalidSessionSecret(error)) {
|
|
30674
30698
|
throw error;
|
|
30675
30699
|
}
|
|
30676
|
-
const
|
|
30677
|
-
|
|
30678
|
-
recoveryToken: recovery.recoveryToken(capability.sessionId)
|
|
30679
|
-
}, LegionDaemonApi.WorkerSession.response);
|
|
30680
|
-
recovery.onRecovered?.(capability.sessionId, recovered);
|
|
30681
|
-
return await postOnce(path, { ...body, secret: recovered.secret }, schema);
|
|
30700
|
+
const secret = await secretAfterRefusal(recovery, capability.sessionId, capability.secret);
|
|
30701
|
+
return await postOnce(path, { ...body, secret }, schema);
|
|
30682
30702
|
}
|
|
30683
30703
|
};
|
|
30684
30704
|
const get = async (path, schema) => {
|
|
@@ -34715,6 +34735,112 @@ function isSingleLegionCommand(command) {
|
|
|
34715
34735
|
return false;
|
|
34716
34736
|
return trimmed.split(/\s+/, 1)[0] === "legion";
|
|
34717
34737
|
}
|
|
34738
|
+
var JJ_LOG_REWRITE_WORDS = ["undo", "abandon"];
|
|
34739
|
+
var JJ_OP_WORDS = ["op", "operation"];
|
|
34740
|
+
var JJ_OP_LOG_REWRITE_WORDS = ["restore", "revert"];
|
|
34741
|
+
var JJ_MENTION = /\bjj\b/;
|
|
34742
|
+
var JJ_LOG_REWRITE_MENTION = /\b(?:undo|abandon)\b|\b(?:op|operation)\b\W+(?:restore|revert)\b/;
|
|
34743
|
+
function jjLogRewriteMention(text) {
|
|
34744
|
+
if (!JJ_MENTION.test(text))
|
|
34745
|
+
return;
|
|
34746
|
+
const match = JJ_LOG_REWRITE_MENTION.exec(text);
|
|
34747
|
+
return match === null ? undefined : match[0].replace(/\W+/g, " ");
|
|
34748
|
+
}
|
|
34749
|
+
function splitShellCommands(command) {
|
|
34750
|
+
const commands = [];
|
|
34751
|
+
let words = [];
|
|
34752
|
+
let text = "";
|
|
34753
|
+
let inWord = false;
|
|
34754
|
+
let quote;
|
|
34755
|
+
const endWord = () => {
|
|
34756
|
+
if (inWord)
|
|
34757
|
+
words.push(text);
|
|
34758
|
+
text = "";
|
|
34759
|
+
inWord = false;
|
|
34760
|
+
};
|
|
34761
|
+
const endCommand = () => {
|
|
34762
|
+
endWord();
|
|
34763
|
+
if (words.length > 0)
|
|
34764
|
+
commands.push(words);
|
|
34765
|
+
words = [];
|
|
34766
|
+
};
|
|
34767
|
+
for (let i = 0;i < command.length; i += 1) {
|
|
34768
|
+
const char = command.charAt(i);
|
|
34769
|
+
if (quote !== undefined) {
|
|
34770
|
+
if (char === quote)
|
|
34771
|
+
quote = undefined;
|
|
34772
|
+
else if (quote === '"' && char === "\\" && command.charAt(i + 1) === `
|
|
34773
|
+
`)
|
|
34774
|
+
i += 1;
|
|
34775
|
+
else if (quote === '"' && char === "\\" && i + 1 < command.length) {
|
|
34776
|
+
i += 1;
|
|
34777
|
+
text += command.charAt(i);
|
|
34778
|
+
} else
|
|
34779
|
+
text += char;
|
|
34780
|
+
continue;
|
|
34781
|
+
}
|
|
34782
|
+
if (char === '"' || char === "'") {
|
|
34783
|
+
quote = char;
|
|
34784
|
+
inWord = true;
|
|
34785
|
+
} else if (char === "\\") {
|
|
34786
|
+
if (command.charAt(i + 1) === `
|
|
34787
|
+
`)
|
|
34788
|
+
i += 1;
|
|
34789
|
+
else if (i + 1 < command.length) {
|
|
34790
|
+
i += 1;
|
|
34791
|
+
text += command.charAt(i);
|
|
34792
|
+
inWord = true;
|
|
34793
|
+
}
|
|
34794
|
+
} else if (char === " " || char === "\t" || char === "<" || char === ">") {
|
|
34795
|
+
endWord();
|
|
34796
|
+
} else if (char === "&" && (command.charAt(i - 1) === ">" || command.charAt(i - 1) === "<" || command.charAt(i + 1) === ">")) {
|
|
34797
|
+
endWord();
|
|
34798
|
+
} else if (";&|\n()`".includes(char)) {
|
|
34799
|
+
endCommand();
|
|
34800
|
+
} else {
|
|
34801
|
+
text += char;
|
|
34802
|
+
inWord = true;
|
|
34803
|
+
}
|
|
34804
|
+
}
|
|
34805
|
+
if (quote !== undefined)
|
|
34806
|
+
return;
|
|
34807
|
+
endCommand();
|
|
34808
|
+
return commands;
|
|
34809
|
+
}
|
|
34810
|
+
function jjLogRewriteInvocation(command) {
|
|
34811
|
+
const commands = splitShellCommands(command);
|
|
34812
|
+
if (commands === undefined) {
|
|
34813
|
+
return jjLogRewriteMention(command) === undefined ? undefined : command.trim();
|
|
34814
|
+
}
|
|
34815
|
+
for (const words of commands) {
|
|
34816
|
+
const mentioned = words.find((word) => jjLogRewriteMention(word) !== undefined);
|
|
34817
|
+
if (mentioned !== undefined)
|
|
34818
|
+
return mentioned;
|
|
34819
|
+
const jj = words.findIndex((word) => word === "jj" || word.endsWith("/jj"));
|
|
34820
|
+
if (jj === -1)
|
|
34821
|
+
continue;
|
|
34822
|
+
const args = words.slice(jj + 1);
|
|
34823
|
+
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));
|
|
34824
|
+
if (rewritesLog)
|
|
34825
|
+
return words.slice(jj).join(" ");
|
|
34826
|
+
}
|
|
34827
|
+
return;
|
|
34828
|
+
}
|
|
34829
|
+
function jjLogRewriteAttempt(toolCall) {
|
|
34830
|
+
const { toolName, input } = toolCall;
|
|
34831
|
+
if (toolName === "bash") {
|
|
34832
|
+
return typeof input.command === "string" ? jjLogRewriteInvocation(input.command) : undefined;
|
|
34833
|
+
}
|
|
34834
|
+
let text;
|
|
34835
|
+
if (toolName === "eval" && typeof input.code === "string")
|
|
34836
|
+
text = input.code;
|
|
34837
|
+
else if (toolName === "hub") {
|
|
34838
|
+
text = [input.application, ...Array.isArray(input.args) ? input.args : [], input.text].filter((part) => typeof part === "string").join(" ");
|
|
34839
|
+
} else
|
|
34840
|
+
return;
|
|
34841
|
+
const mention = jjLogRewriteMention(text);
|
|
34842
|
+
return mention === undefined ? undefined : `${toolName}: jj ${mention}`;
|
|
34843
|
+
}
|
|
34718
34844
|
var LEGION_LOADED_MARKER = Symbol.for("legion.pi-envoy.legion-loaded");
|
|
34719
34845
|
var CODE_MUTATION_TOOLS = ["edit", "write", "apply_patch"];
|
|
34720
34846
|
var MERGER_BLOCKED_TOOLS = [...CODE_MUTATION_TOOLS, "task"];
|
|
@@ -34738,8 +34864,10 @@ function legionExtension(pi) {
|
|
|
34738
34864
|
subagentSession ??= isSubagentSession(context);
|
|
34739
34865
|
return subagentSession;
|
|
34740
34866
|
};
|
|
34867
|
+
let phaseWorkerPane;
|
|
34868
|
+
let daemonClient;
|
|
34741
34869
|
const roleDaemon = () => {
|
|
34742
|
-
|
|
34870
|
+
daemonClient ??= createLegionDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"), fetch, {
|
|
34743
34871
|
recoveryToken: (sessionId) => {
|
|
34744
34872
|
if (capability !== undefined && sessionId === capability.sessionID) {
|
|
34745
34873
|
return requiredSecret(process.env, "LEGION_BOOT_TOKEN");
|
|
@@ -34756,6 +34884,7 @@ function legionExtension(pi) {
|
|
|
34756
34884
|
capability = { ...capability, secret: recovered.secret };
|
|
34757
34885
|
}
|
|
34758
34886
|
});
|
|
34887
|
+
return daemonClient;
|
|
34759
34888
|
};
|
|
34760
34889
|
const rerunReadyAfterRegain = async (endpoint, role, reason, call) => {
|
|
34761
34890
|
try {
|
|
@@ -35009,6 +35138,16 @@ function legionExtension(pi) {
|
|
|
35009
35138
|
toolCallId: toolCall.toolCallId,
|
|
35010
35139
|
toolName: toolCall.toolName
|
|
35011
35140
|
});
|
|
35141
|
+
phaseWorkerPane ??= classifySession(process.env).kind === "phase-worker";
|
|
35142
|
+
if (phaseWorkerPane) {
|
|
35143
|
+
const jjAttempt = jjLogRewriteAttempt(toolCall);
|
|
35144
|
+
if (jjAttempt !== undefined) {
|
|
35145
|
+
return {
|
|
35146
|
+
block: true,
|
|
35147
|
+
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.'
|
|
35148
|
+
};
|
|
35149
|
+
}
|
|
35150
|
+
}
|
|
35012
35151
|
if (await checkSubagentSession(context))
|
|
35013
35152
|
return;
|
|
35014
35153
|
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
|
|