@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 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 recovered = await postOnce(WORKER_SESSION_PATH, {
30677
- sessionId: capability.sessionId,
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
- return createLegionDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"), fetch, {
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:** Never run `jj op restore` in a Legion workspace. It rewrites
124
- the shared operation log. If a mistake reaches that point, stop and send the owning
125
- architect the `jj -R "$LEGION_WORKSPACE" log` evidence; recover only through the approved,
126
- path-scoped workflow.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.23.1",
3
+ "version": "1.24.1",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [