@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 still go through `hub` to the owning
96
- architect — Dispatch is for durable questions to the human, not for coordination between roles.
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
- if (capability !== undefined && capability.kind === "root-architect" && capability.sessionID === sessionID && ["edit", "write", "bash", "apply_patch"].includes(toolCall.toolName)) {
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 (capability !== undefined && capability.kind === "phase-worker" && capability.sessionID === sessionID) {
32738
- if (capability.role === "reviewer" && ["edit", "write", "apply_patch"].includes(toolCall.toolName)) {
32739
- return { block: true, reason: "the reviewer does not modify the branch" };
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 (capability.role === "merger" && ["edit", "write", "apply_patch", "task"].includes(toolCall.toolName)) {
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 (capability === undefined || capability.sessionID !== sessionID) {
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: capability.tree,
32759
- issue: capability.issue,
32789
+ tree: active.tree,
32790
+ issue: active.issue,
32760
32791
  sessionId: sessionID,
32761
- secret: capability.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 `task` for every Legion role spawn and `hub` to direct or revive a known phase
18
- worker. Phase workers escalate lifecycle, scope, and cross-phase matters inward to you
19
- through hub. Any role may use `dispatch` directly for a standalone human question;
20
- replies return to the asking session.
21
- - The runtime, not you, appends a machine `<legion-spawn>` block. Each Legion `task`
22
- text must start with `Legion-Issue: <owner/repo#n>` on its first line.
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 `task` spawn for an adopted child.** Until release, the daemon holds that
38
- child's role activity. Then spawn each child's in-process `legion-architect` owner.
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 with an issue-prefixed task; for example:
98
+ After release, spawn each relevant owner; for example:
85
99
 
86
100
  ```text
87
- task({
88
- agent: "legion-architect",
89
- task: "Legion-Issue: owner/repo#41\nOwn this child through its lifecycle and report its evidence."
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
- Do not add a `<legion-spawn>` block. Keep the child agent IDs and session identifiers
94
- returned by `task`, because retro and adjustment use those live sessions. Park while
95
- children are in flight. On each child closure, re-scope open work, close obsolete work
96
- with a reason, and release the next wave only when it now makes sense. There is no
97
- inter-child dependency mechanism to encode.
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. Launch one **fresh** `legion-tester` for the parent, scoped to the parent's own
103
- acceptance criteria and current `main` integration surface:
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
- task({
107
- agent: "legion-tester",
108
- task: "Legion-Issue: owner/repo#40\nFreshly verify this parent issue against its acceptance criteria on current main; return reproducible integration evidence."
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 fresh tester's evidence, not merely a child PR's check status. The parent test
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. Revive the parked
126
- implementer that owns the reviewed work through `hub`, naming the skill in the message:
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
- hub({
130
- op: "send",
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 revived implementer to report its durable retro result. Retro output is
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. `legion-merger` verifies the approved head and squash-merges without pushing.
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 ask the merger to merge
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: fresh parent integration verification; failures become a new child wave, success advances to review and retro. |
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 through `hub` to the responsible worker; scope and product decisions remain with you. |
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; direct the restored worker through `hub` if action is needed and rely on its committed handoff over recollection. |
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 squash-merges and pushes nothing.
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: the
28
- daemon's boot response already gives you your own token, and the `roleToken` helper in
29
- `@legion/contracts` computes any other one exactly the way the daemon does — reuse a token
30
- you've already been given before recomputing it.
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 create the pull request with the `github` tool's `pr_create` operation. The credential
169
- helper and `legion gh` provide the GitHub identity; never export, fetch, or replace a
170
- token. Other phases advance the existing branch rather than creating a replacement
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [