@sjawhar/pi-legion-envoy 1.23.0 → 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
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();
|
|
@@ -204,13 +204,13 @@ legion({
|
|
|
204
204
|
op: "spawn_worker",
|
|
205
205
|
issue: "LEGION-40",
|
|
206
206
|
role: "implementer",
|
|
207
|
-
task: "Run the legion-retro skill now. Capture durable learnings and post the issue
|
|
207
|
+
task: "Run the legion-retro skill now. Capture durable learnings and post the retro message on the Dispatch issue with dispatch_message; do not create a .legion handoff file."
|
|
208
208
|
})
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
Wait for the implementer to report its durable retro result. Retro output is
|
|
212
|
-
`docs/solutions/` plus
|
|
213
|
-
the reviewer-approved head after cleanup.
|
|
212
|
+
`docs/solutions/` plus one `dispatch_message` on the issue; it must not create a `.legion`
|
|
213
|
+
file or rewrite the reviewer-approved head after cleanup.
|
|
214
214
|
|
|
215
215
|
## 6. Architect sign-off and merge
|
|
216
216
|
|
|
@@ -17,7 +17,8 @@ retrospective's durable output.
|
|
|
17
17
|
1. Tester green and all code-review cycles finish.
|
|
18
18
|
2. The implementer pushes the `.legion/` deletion at the reviewer's direction, and the reviewer
|
|
19
19
|
approves that head.
|
|
20
|
-
3. Run this retro: commit durable learnings to `docs/solutions/` and post the
|
|
20
|
+
3. Run this retro: commit durable learnings to `docs/solutions/` and post the retro message on
|
|
21
|
+
the Dispatch issue.
|
|
21
22
|
Retro writes **no `.legion` file**, so it never re-dirties the cleaned handoff tree.
|
|
22
23
|
4. The merger verifies the tip is the approved head plus commits that change only
|
|
23
24
|
`docs/solutions/` — `jj diff --from <approved-sha> --to <tip-sha> --summary`, quoted in READY —
|
|
@@ -66,18 +67,22 @@ date: YYYY-MM-DD
|
|
|
66
67
|
status: active
|
|
67
68
|
module: affected-module
|
|
68
69
|
related_issues:
|
|
69
|
-
- "
|
|
70
|
+
- "LEGION-123" # the Dispatch issue
|
|
71
|
+
- "owner/repo#456" # the pull request
|
|
70
72
|
---
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
Commit the documentation on the existing issue branch, advance its existing bookmark, and push
|
|
74
|
-
that branch. Do not create a replacement branch or bookmark. Then post
|
|
75
|
-
the
|
|
76
|
-
|
|
76
|
+
that branch. Do not create a replacement branch or bookmark. Then post one Dispatch message on
|
|
77
|
+
the issue — `issue` is your `LEGION_ISSUE`; Legion issues live on Dispatch, never on a GitHub
|
|
78
|
+
issue, and the `gh` shim refuses every GitHub-issue write — naming the documents and the
|
|
79
|
+
one-to-three most useful takeaways. The message must carry this revived implementer's structured
|
|
80
|
+
attribution footer with `phase` set to `retro`; the body is capped at 2,000 characters:
|
|
77
81
|
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
```ts
|
|
83
|
+
dispatch_message({
|
|
84
|
+
issue: "<KEY>",
|
|
85
|
+
body: `## Retro Complete
|
|
81
86
|
|
|
82
87
|
**Learnings documented in:**
|
|
83
88
|
- docs/solutions/<path>.md
|
|
@@ -85,11 +90,11 @@ legion gh -- issue comment <issue-number> \
|
|
|
85
90
|
**Key takeaways:**
|
|
86
91
|
- <reusable lesson>
|
|
87
92
|
|
|
88
|
-
<!-- legion: {"session":"<session-id>","phase":"retro"}
|
|
89
|
-
|
|
93
|
+
<!-- legion: {"session":"<session-id>","phase":"retro"} -->`,
|
|
94
|
+
})
|
|
90
95
|
```
|
|
91
96
|
|
|
92
|
-
The
|
|
97
|
+
The Dispatch message and the `docs/solutions/` commit are the only retro outputs. Never write a
|
|
93
98
|
handoff, phase artifact, local feedback log, or completion label; `.legion/` was deleted before
|
|
94
99
|
retro and nothing recreates it. Report completion with `legion handoff complete` alone (its
|
|
95
100
|
summary: two sentences for the architect) — no `legion handoff write`.
|
|
@@ -100,7 +105,7 @@ Before returning, verify all of the following:
|
|
|
100
105
|
|
|
101
106
|
- The reviewer cleanup commit remains below the retro documentation commit, and the reviewer's
|
|
102
107
|
approval of that cleanup head stands: the merger accepts the approved head plus this commit.
|
|
103
|
-
- The learning documents and
|
|
108
|
+
- The learning documents and the Dispatch message both exist.
|
|
104
109
|
- No `.legion` file was created or modified by retro.
|
|
105
110
|
- The fresh-eyes analysis was considered alongside the implementer's context.
|
|
106
111
|
- The merger remains a subsequent step, not work performed by retro.
|
|
@@ -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
|
|
|
@@ -152,22 +163,29 @@ that execs `legion gh -- "$@"`, so `gh …` and `legion gh -- …` are the same
|
|
|
152
163
|
redeems a fresh token from your session's grant — identity is supplied per call, never stored.
|
|
153
164
|
Never run `gh auth login` or `gh auth setup-git`; there is no login state to create. The shim
|
|
154
165
|
refuses `pr merge` (and a raw `gh api …/merge`): no worker role merges a pull request — the merge
|
|
155
|
-
queue does, under its own authority.
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
queue does, under its own authority. It also refuses every GitHub-issue write — the `issue`
|
|
167
|
+
subcommand's `comment`, `create`, `edit`, `close`, `reopen`, `delete`, `pin`, `unpin`, `transfer`,
|
|
168
|
+
`lock`, `unlock`, and `develop`, and any raw `gh api` call to an `/issues` path whose method is not
|
|
169
|
+
GET (an explicit `-X`, or the POST that `-f`/`-F`/`--input` imply; pull-request conversation
|
|
170
|
+
comments live on that path too, so edit them with `gh pr comment`) — printing
|
|
171
|
+
`Legion issues live on Dispatch; use dispatch_message or dispatch_comment on <your LEGION_ISSUE>`:
|
|
172
|
+
Legion never reads or writes a GitHub issue (LEGION-78). `pr comment`, `pr review`,
|
|
173
|
+
`api …/pulls/…`, `api graphql`, and issue reads are unaffected. The credential reaches `legion`
|
|
174
|
+
through the file `$LEGION_GRANT_FILE` names, written before each of your bash commands by the
|
|
175
|
+
extension; never `cat`, `echo`, copy, or `export` it — `legion credential`, `legion gh`,
|
|
176
|
+
`jj git push`, and `legion handoff complete` read it themselves. The file is the pane's, not the
|
|
177
|
+
command's: a `task` subagent, an `eval` subprocess, or a background job in your pane reads the
|
|
178
|
+
grant your last bash command minted, so its `legion gh` or `jj git push` succeeds only within 60
|
|
179
|
+
seconds of that call and 403s afterwards — a timing artifact, not a broken credential; run
|
|
180
|
+
credentialed commands from your own bash calls.
|
|
163
181
|
|
|
164
182
|
## GitHub PR comment attribution
|
|
165
183
|
|
|
166
184
|
Append this exact structured footer to **every** pull-request comment and review that this
|
|
167
185
|
phase posts on GitHub. It preserves session provenance on the artifact itself so work stays
|
|
168
186
|
attributable to the session that produced it. Dispatch comments carry session provenance
|
|
169
|
-
natively through their own `actor`/`origin` fields; this footer is
|
|
170
|
-
|
|
187
|
+
natively through their own `actor`/`origin` fields; this footer is for GitHub PR artifacts and
|
|
188
|
+
for the retro's Dispatch message (`skills/legion-retro`):
|
|
171
189
|
|
|
172
190
|
```html
|
|
173
191
|
<!-- legion: {"session":"<session-id>","phase":"<phase>"} -->
|