@sjawhar/pi-legion-envoy 0.13.0 → 0.14.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/README.md
CHANGED
|
@@ -92,8 +92,9 @@ stateless request to the dispatch service, which writes the issue or comment. Th
|
|
|
92
92
|
`dispatch` skill (shipped in `skills/`) says when and how to ask. Replies route back to the
|
|
93
93
|
asking session, which is auto-subscribed to the thread's GitHub topic on every successful
|
|
94
94
|
call; a Legion role's session survives kill/resume because Legion resurrection resumes the
|
|
95
|
-
same OMP session file. Lifecycle and scope decisions
|
|
96
|
-
architect — Dispatch is for durable questions to the human, not for coordination
|
|
95
|
+
same OMP session file. Lifecycle and scope decisions go through `envoy_publish` to the owning
|
|
96
|
+
architect's role topic — Dispatch is for durable questions to the human, not for coordination
|
|
97
|
+
between roles.
|
|
97
98
|
|
|
98
99
|
The tool's model-facing schema is the shared contract's zod shape
|
|
99
100
|
(`@legion/envoy-client/dispatch-contract`) serialised to JSON Schema, so OMP shows the model
|
package/dist/legion.js
CHANGED
|
@@ -32487,7 +32487,34 @@ async function persistedTranscript(context) {
|
|
|
32487
32487
|
throw new Error("Legion session transcript has no agent id");
|
|
32488
32488
|
return { sessionFile, agentId };
|
|
32489
32489
|
}
|
|
32490
|
+
function isSingleLegionCommand(command) {
|
|
32491
|
+
if (typeof command !== "string")
|
|
32492
|
+
return false;
|
|
32493
|
+
const trimmed = command.trim();
|
|
32494
|
+
if (trimmed.length === 0)
|
|
32495
|
+
return false;
|
|
32496
|
+
let quote;
|
|
32497
|
+
for (const char of trimmed) {
|
|
32498
|
+
if (quote !== undefined) {
|
|
32499
|
+
if (char === quote)
|
|
32500
|
+
quote = undefined;
|
|
32501
|
+
continue;
|
|
32502
|
+
}
|
|
32503
|
+
if (char === '"' || char === "'") {
|
|
32504
|
+
quote = char;
|
|
32505
|
+
continue;
|
|
32506
|
+
}
|
|
32507
|
+
if (char === `
|
|
32508
|
+
` || char === ";" || char === "&" || char === "|")
|
|
32509
|
+
return false;
|
|
32510
|
+
}
|
|
32511
|
+
if (quote !== undefined)
|
|
32512
|
+
return false;
|
|
32513
|
+
return trimmed.split(/\s+/, 1)[0] === "legion";
|
|
32514
|
+
}
|
|
32490
32515
|
var LEGION_LOADED_MARKER = Symbol.for("legion.pi-envoy.legion-loaded");
|
|
32516
|
+
var CODE_MUTATION_TOOLS = ["edit", "write", "apply_patch"];
|
|
32517
|
+
var MERGER_BLOCKED_TOOLS = [...CODE_MUTATION_TOOLS, "task"];
|
|
32491
32518
|
function legionExtension(pi) {
|
|
32492
32519
|
logger2.debug("extension instance loaded", { extension: import.meta.url });
|
|
32493
32520
|
globalThis[LEGION_LOADED_MARKER] = import.meta.url;
|
|
@@ -32731,20 +32758,24 @@ function legionExtension(pi) {
|
|
|
32731
32758
|
});
|
|
32732
32759
|
pi.on("tool_call", async (toolCall, context) => {
|
|
32733
32760
|
const sessionID = context.sessionManager.getSessionId();
|
|
32734
|
-
|
|
32761
|
+
const active = capability?.sessionID === sessionID ? capability : undefined;
|
|
32762
|
+
if (active?.role === "architect" && (CODE_MUTATION_TOOLS.includes(toolCall.toolName) || toolCall.toolName === "bash" && !isSingleLegionCommand(toolCall.input.command))) {
|
|
32735
32763
|
return { block: true, reason: "the architect delegates all code work to phase workers" };
|
|
32736
32764
|
}
|
|
32737
|
-
if (
|
|
32738
|
-
if (
|
|
32739
|
-
return {
|
|
32765
|
+
if (active?.kind === "phase-worker") {
|
|
32766
|
+
if (active.role === "reviewer" && CODE_MUTATION_TOOLS.includes(toolCall.toolName)) {
|
|
32767
|
+
return {
|
|
32768
|
+
block: true,
|
|
32769
|
+
reason: "the reviewer edits nothing except the final .legion/ cleanup commit via bash"
|
|
32770
|
+
};
|
|
32740
32771
|
}
|
|
32741
|
-
if (
|
|
32772
|
+
if (active.role === "merger" && MERGER_BLOCKED_TOOLS.includes(toolCall.toolName)) {
|
|
32742
32773
|
return { block: true, reason: "the merger only verifies and reports" };
|
|
32743
32774
|
}
|
|
32744
32775
|
}
|
|
32745
32776
|
if (toolCall.toolName !== "bash" || typeof toolCall.input.command !== "string")
|
|
32746
32777
|
return;
|
|
32747
|
-
if (
|
|
32778
|
+
if (active === undefined) {
|
|
32748
32779
|
if (process.env.LEGION_ROLE !== undefined && process.env.LEGION_CONTROLLER !== "1") {
|
|
32749
32780
|
return {
|
|
32750
32781
|
block: true,
|
|
@@ -32755,10 +32786,10 @@ function legionExtension(pi) {
|
|
|
32755
32786
|
}
|
|
32756
32787
|
try {
|
|
32757
32788
|
const grant = await roleDaemon().grant({
|
|
32758
|
-
tree:
|
|
32759
|
-
issue:
|
|
32789
|
+
tree: active.tree,
|
|
32790
|
+
issue: active.issue,
|
|
32760
32791
|
sessionId: sessionID,
|
|
32761
|
-
secret:
|
|
32792
|
+
secret: active.secret
|
|
32762
32793
|
});
|
|
32763
32794
|
const stateDir = requiredEnvironment(process.env, "LEGION_STATE_DIR");
|
|
32764
32795
|
const workerBin = await installWorkerGhShim(stateDir);
|
|
@@ -14,12 +14,22 @@ separate coordinator to finish necessary work.
|
|
|
14
14
|
|
|
15
15
|
- Use the `legion` tool for lifecycle writes. Its issue key format is
|
|
16
16
|
`owner/repo#number`.
|
|
17
|
-
- Use `
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
- Use `legion({ op: "spawn_worker", issue, role, task })` for every Legion role spawn.
|
|
18
|
+
Message a known phase worker with `envoy_publish` to `notifications.role.` followed by
|
|
19
|
+
its encoded role token (the token `spawn_worker` returned for it); re-assign it by
|
|
20
|
+
calling `spawn_worker` again on the same existing role, which resumes the same process
|
|
21
|
+
instead of starting a fresh one. Phase workers escalate lifecycle, scope, and
|
|
22
|
+
cross-phase matters the same way: `envoy_publish` to your own encoded token. Any role
|
|
23
|
+
may use `dispatch` directly for a standalone human question; replies return to the
|
|
24
|
+
asking session.
|
|
25
|
+
- The daemon spawns each role as its own process with the issue's context already in its
|
|
26
|
+
environment. Never hand-format a role token: the daemon encodes one as
|
|
27
|
+
`legion-<project>-<encoded-owner>__<encoded-repo>-<number>-<role>` (escaping `_`, `.`,
|
|
28
|
+
and `-` within the owner/repo names); for example, project `acme`, issue
|
|
29
|
+
`sjawhar/legion#41`, role `architect` encodes to `legion-acme-sjawhar__legion-41-architect`.
|
|
30
|
+
Reuse a token you already hold (your own, or one `spawn_worker` returned) or compute
|
|
31
|
+
another with the `roleToken` helper from `@legion/contracts` exactly the way the daemon
|
|
32
|
+
does.
|
|
23
33
|
- Use only the live label vocabulary: `needs-approval`, `human-approved`,
|
|
24
34
|
`legion-child`, and `legion-backlog`. Do not attempt to apply a label whose ownership
|
|
25
35
|
belongs to the controller or Sami.
|
|
@@ -30,12 +40,16 @@ separate coordinator to finish necessary work.
|
|
|
30
40
|
## 1. Decompose or adopt
|
|
31
41
|
|
|
32
42
|
Inspect the root issue, acceptance criteria, existing children, and current handoffs.
|
|
43
|
+
Decomposition is complete only when every child issue names the real surface its acceptance
|
|
44
|
+
criteria are proven on and the repository skill that drives it; if the repository cannot
|
|
45
|
+
exercise a criterion end to end, building that path is a child issue of this tree.
|
|
33
46
|
|
|
34
47
|
- **Existing children:** adopt them. Do not replace or re-decompose human-created work.
|
|
35
48
|
Put every adopted child into the initial wave. **You MUST call**
|
|
36
49
|
`legion({ op: "wave_release", children: ["owner/repo#41", "owner/repo#42"] })`
|
|
37
|
-
**before any `
|
|
38
|
-
child's role activity. Then spawn each child's
|
|
50
|
+
**before any `spawn_worker` call for an adopted child.** Until release, the daemon
|
|
51
|
+
holds that child's role activity. Then spawn each child's daemon-managed sub-architect
|
|
52
|
+
owner.
|
|
39
53
|
- **No children:** choose a single-issue tree only when its acceptance criteria can be
|
|
40
54
|
completed and integrated as one unit. Otherwise create complete child issues with:
|
|
41
55
|
|
|
@@ -81,32 +95,37 @@ explicit lifecycle write:
|
|
|
81
95
|
legion({ op: "wave_release", children: ["owner/repo#41", "owner/repo#42"] })
|
|
82
96
|
```
|
|
83
97
|
|
|
84
|
-
After release, spawn each relevant owner
|
|
98
|
+
After release, spawn each relevant owner; for example:
|
|
85
99
|
|
|
86
100
|
```text
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
legion({
|
|
102
|
+
op: "spawn_worker",
|
|
103
|
+
issue: "owner/repo#41",
|
|
104
|
+
role: "architect",
|
|
105
|
+
task: "Own this child through its lifecycle and report its evidence."
|
|
106
|
+
})
|
|
91
107
|
```
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
The daemon spawns that sub-architect as its own process with the child's context already
|
|
110
|
+
in its environment; a resume of an existing role continues the same process instead of
|
|
111
|
+
starting a fresh one. Keep the returned session identifiers, because retro and adjustment
|
|
112
|
+
use those live sessions. Park while children are in flight. On each child closure,
|
|
113
|
+
re-scope open work, close obsolete work with a reason, and release the next wave only
|
|
114
|
+
when it now makes sense. There is no inter-child dependency mechanism to encode.
|
|
98
115
|
|
|
99
116
|
## 3. Children complete
|
|
100
117
|
|
|
101
118
|
Treat `children-complete` as the edge into the end-game, not as a reason to close the
|
|
102
|
-
parent.
|
|
103
|
-
|
|
119
|
+
parent. Spawn the parent's `tester` role, scoped to the parent's own acceptance criteria
|
|
120
|
+
and current `main` integration surface:
|
|
104
121
|
|
|
105
122
|
```text
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
legion({
|
|
124
|
+
op: "spawn_worker",
|
|
125
|
+
issue: "owner/repo#40",
|
|
126
|
+
role: "tester",
|
|
127
|
+
task: "Verify this parent issue against its acceptance criteria on current main; return reproducible integration evidence."
|
|
128
|
+
})
|
|
110
129
|
```
|
|
111
130
|
|
|
112
131
|
If that tester finds a failure, create and release a new corrective child wave, then
|
|
@@ -115,25 +134,25 @@ failure forward.
|
|
|
115
134
|
|
|
116
135
|
## 4. Integration verification
|
|
117
136
|
|
|
118
|
-
Read the
|
|
137
|
+
Read the tester's evidence, not merely a child PR's check status. The parent test
|
|
119
138
|
is successful only when every parent acceptance criterion has evidence against current
|
|
120
139
|
main. Route a failed criterion into a corrective child wave; route a passing result to
|
|
121
140
|
review and the merge-gate sequence.
|
|
122
141
|
|
|
123
142
|
## 5. Retro
|
|
124
143
|
|
|
125
|
-
Retro is mandatory for every issue that passed review, before merge.
|
|
126
|
-
implementer
|
|
144
|
+
Retro is mandatory for every issue that passed review, before merge. Message the
|
|
145
|
+
implementer's live session (idle since it completed its phase; the daemon never tears
|
|
146
|
+
it down) with `envoy_publish` to its role token, naming the skill:
|
|
127
147
|
|
|
128
148
|
```text
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
to: "<implementer agent identifier>",
|
|
149
|
+
envoy_publish({
|
|
150
|
+
topic: "notifications.role.<implementer's encoded token>",
|
|
132
151
|
message: "Run the legion-retro skill now. Capture durable learnings and post the issue comment; do not create a .legion handoff file."
|
|
133
152
|
})
|
|
134
153
|
```
|
|
135
154
|
|
|
136
|
-
Wait for the
|
|
155
|
+
Wait for the messaged implementer to report its durable retro result. Retro output is
|
|
137
156
|
`docs/solutions/` plus an issue comment; it must not create a `.legion` file or change
|
|
138
157
|
the reviewer-approved head after cleanup.
|
|
139
158
|
|
|
@@ -154,10 +173,11 @@ When the config-armed final merge gate applies, preserve this order exactly:
|
|
|
154
173
|
approval already satisfies the gate and you immediately continue to the merger; do not
|
|
155
174
|
wait for a new wake. If it returns `approved: false`, request or retain Sami approval
|
|
156
175
|
and park for a later `pr-ready` wake. Do not poll or retry this check;
|
|
157
|
-
5.
|
|
176
|
+
5. The merger verifies the approved head and publishes `READY #<n> at <sha>` to
|
|
177
|
+
`notifications.role.pr-queue`; it never merges. The merge queue approves and merges under its own authority.
|
|
158
178
|
|
|
159
|
-
If anything changes the approved head, return to review; do not
|
|
160
|
-
an obsolete approval.
|
|
179
|
+
If anything changes the approved head, return to review; do not let the merger publish
|
|
180
|
+
`READY` for an obsolete approval.
|
|
161
181
|
|
|
162
182
|
## 7. Close
|
|
163
183
|
|
|
@@ -183,14 +203,17 @@ corresponding lifecycle procedure.
|
|
|
183
203
|
| Wake | Procedure |
|
|
184
204
|
| --- | --- |
|
|
185
205
|
| `child-closed` | Read the child completion and remaining open children. Re-scope or close obsolete open work; release an appropriate next wave, or await `children-complete`. |
|
|
186
|
-
| `children-complete` | Execute steps 3–4:
|
|
206
|
+
| `children-complete` | Execute steps 3–4: parent integration verification; failures become a new child wave, success advances to review and retro. |
|
|
187
207
|
| `child-reopened` | Treat the completion edge as reset. Reassess the reopened child and return the tree to children-in-flight; do not continue an already-started end-game. |
|
|
208
|
+
| `phase-complete` | Payload `{type:"phase-complete", issue, role, summary}`. May arrive live or via `catchup-overseer`'s `phaseCompletions`. Read the committed handoff for that phase, then spawn the next phase's owner, or `spawn_worker` on the same role again to resume it with corrections if the handoff shows unresolved gaps. |
|
|
209
|
+
| `worker-queued` | Payload `{type:"worker-queued", issue, role}`. The deployment's worker cap is full; this role's spawn is queued. Do not respawn or retry — wait for `worker-started`. |
|
|
210
|
+
| `worker-started` | Payload `{type:"worker-started", issue, role}`. A previously queued role has been promoted and is now running. Treat it exactly as a normal spawn: resume tracking that role's live session. |
|
|
188
211
|
| `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/Sami/merger order only for that current head. |
|
|
189
212
|
| `pr-blocked` | Read the failed CI evidence and recovery attempts. Assign a focused implementer or corrective child, then return it through testing and review; do not treat the blocked PR as final. |
|
|
190
213
|
| `pr-closed-unmerged` | Decide from current scope whether to reopen the work, send a fresh implementer, or cancel it with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
|
|
191
|
-
| `issue-comment` | Interpret the comment in the issue's design context. Answer it, adjust the plan, or relay it
|
|
192
|
-
| `catchup-overseer` | Verify its gates, child counts, and PR verdicts against current artifacts, then resume the applicable numbered lifecycle step. It is a current-state snapshot, not a raw-event replay. |
|
|
193
|
-
| `revive-worker` | The extension has revived the backed worker. Do not create a duplicate;
|
|
214
|
+
| `issue-comment` | Interpret the comment in the issue's design context. Answer it, adjust the plan, or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
|
|
215
|
+
| `catchup-overseer` | Verify its gates, child counts, and PR verdicts against current artifacts, then resume the applicable numbered lifecycle step. It is a current-state snapshot, not a raw-event replay. For each entry in its `phaseCompletions` (`{issue, role, summary, at}`, phases that completed while you were not live), handle it exactly as a `phase-complete` wake. |
|
|
216
|
+
| `revive-worker` | The extension has revived the backed worker. Do not create a duplicate; message the restored worker via `envoy_publish` to its role token if action is needed and rely on its committed handoff over recollection. |
|
|
194
217
|
| `reopened` | Reopen the root lifecycle: inspect the reason and current artifacts, reassess scope and children, and resume at the first applicable numbered step. |
|
|
195
218
|
|
|
196
219
|
## Escalation judgment
|
|
@@ -19,7 +19,7 @@ retrospective's durable output.
|
|
|
19
19
|
3. Run this retro: commit durable learnings to `docs/solutions/` and post the issue comment.
|
|
20
20
|
Retro writes **no `.legion` file**, so it never re-dirties the cleaned handoff tree.
|
|
21
21
|
4. Sami approves the final reviewed head.
|
|
22
|
-
5. The merger
|
|
22
|
+
5. The merger verifies the approved head, publishes `READY`, and pushes nothing; the merge queue merges.
|
|
23
23
|
|
|
24
24
|
Do not start retro before step 2, skip it because the change seems mechanical, or merge before
|
|
25
25
|
steps 3 and 4. The design gate is not a substitute for this final merge gate.
|
|
@@ -24,10 +24,12 @@ Your role token is not the issue key spelled out literally. The daemon encodes i
|
|
|
24
24
|
`legion-<project>-<encoded-owner>__<encoded-repo>-<number>-<role>`, escaping `_`, `.`, and
|
|
25
25
|
`-` within the owner and repo names (`_u`, `_d`, `_h`) so `__` is always the one safe
|
|
26
26
|
separator. For example, project `acme`, issue `sjawhar/legion#41`, role `architect` encodes
|
|
27
|
-
to `legion-acme-sjawhar__legion-41-architect`. Never hand-format one for another role:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
to `legion-acme-sjawhar__legion-41-architect`. Never hand-format one for another role: your
|
|
28
|
+
own role topic and your tree's architect's topic are stated at the end of your system
|
|
29
|
+
prompt (a "Legion addressing" line the daemon appends), a sibling role's topic is yours
|
|
30
|
+
with the trailing `-<role>` replaced, and the `roleToken` helper in `@legion/contracts`
|
|
31
|
+
computes any other one exactly the way the daemon does — prefer a topic you've already
|
|
32
|
+
been given before recomputing one.
|
|
31
33
|
|
|
32
34
|
If the handshake fails (a rejected boot token, or a bootstrap failure after your role
|
|
33
35
|
registered), the extension logs it and exits the process outright — it does not retry, and
|
|
@@ -165,10 +167,9 @@ The provisioned issue workspace configures `credential.helper` with the daemon's
|
|
|
165
167
|
credential command, so `jj -R "$LEGION_WORKSPACE" git push` authenticates transparently
|
|
166
168
|
through the same session capability. Never handle a token.
|
|
167
169
|
|
|
168
|
-
Then
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
bookmark or PR.
|
|
170
|
+
Then open the pull request with `legion gh -- pr create`. The credential helper and
|
|
171
|
+
`legion gh` provide the GitHub identity; never export, fetch, or replace a token. Other
|
|
172
|
+
phases advance the existing branch rather than creating a replacement bookmark or PR.
|
|
172
173
|
|
|
173
174
|
## PR body and merge-queue discipline
|
|
174
175
|
|