@sjawhar/opencode-legion-envoy 1.38.1 → 1.40.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.
@@ -14277,12 +14277,19 @@ var stateIssue = strictObject({
14277
14277
  parent: nonEmptyString.optional(),
14278
14278
  lastAppliedSeq: number2().int().nonnegative().optional()
14279
14279
  });
14280
+ var stateWorkspaceLost = strictObject({
14281
+ at: nonEmptyString,
14282
+ generation: number2().int().nonnegative(),
14283
+ fromRef: nonEmptyString,
14284
+ previousSessionId: nonEmptyString.optional()
14285
+ });
14280
14286
  var stateTree = strictObject({
14281
14287
  status: _enum2(TREE_STATUSES),
14282
14288
  generation: number2().int().nonnegative(),
14283
14289
  launchFailures: number2().int().nonnegative(),
14284
14290
  readyConfirmedAt: number2().optional(),
14285
- locator: stateTreeLocator.optional()
14291
+ locator: stateTreeLocator.optional(),
14292
+ workspaceLost: stateWorkspaceLost.optional()
14286
14293
  });
14287
14294
  var stateGate = strictObject({
14288
14295
  artifactId: nonEmptyString,
@@ -14296,7 +14303,8 @@ var stateRole = strictObject({
14296
14303
  sessionId: nonEmptyString.optional(),
14297
14304
  readyConfirmedAt: number2().optional(),
14298
14305
  launchFailures: number2().int().nonnegative().optional(),
14299
- locator: stateLocator.optional()
14306
+ locator: stateLocator.optional(),
14307
+ workspaceLost: stateWorkspaceLost.optional()
14300
14308
  });
14301
14309
  var stateQueuedWorkerIdentity = {
14302
14310
  roleToken: nonEmptyString,
@@ -14335,7 +14343,8 @@ var LegionDaemonApi = {
14335
14343
  request: strictObject({
14336
14344
  secret: nonEmptyString,
14337
14345
  sessionId: nonEmptyString,
14338
- ompSessionFile: nonEmptyString.optional()
14346
+ ompSessionFile: nonEmptyString.optional(),
14347
+ pluginVersion: nonEmptyString
14339
14348
  }),
14340
14349
  response: object({})
14341
14350
  },
@@ -14350,7 +14359,8 @@ var LegionDaemonApi = {
14350
14359
  rootSessionId: nonEmptyString,
14351
14360
  agentId: nonEmptyString,
14352
14361
  bootToken: nonEmptyString,
14353
- ompSessionFile: nonEmptyString
14362
+ ompSessionFile: nonEmptyString,
14363
+ pluginVersion: nonEmptyString
14354
14364
  }),
14355
14365
  response: object({
14356
14366
  roleTokens: record(string2(), string2()),
@@ -14392,7 +14402,8 @@ var LegionDaemonApi = {
14392
14402
  bootToken: nonEmptyString,
14393
14403
  sessionId: nonEmptyString,
14394
14404
  agentId: nonEmptyString,
14395
- ompSessionFile: nonEmptyString
14405
+ ompSessionFile: nonEmptyString,
14406
+ pluginVersion: nonEmptyString
14396
14407
  }),
14397
14408
  response: object({
14398
14409
  roleToken: nonEmptyString,
@@ -14472,7 +14483,7 @@ var LegionDaemonApi = {
14472
14483
  response: object({ grantId: nonEmptyString, expiresAt: nonEmptyString })
14473
14484
  },
14474
14485
  GitHubToken: {
14475
- request: strictObject({ grantId: nonEmptyString, merge: literal(true).optional() }),
14486
+ request: strictObject({ grantId: nonEmptyString }),
14476
14487
  response: object({ token: nonEmptyString, appLogin: string2().endsWith("[bot]") })
14477
14488
  },
14478
14489
  GitCredential: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.38.1",
3
+ "version": "1.40.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -15,6 +15,21 @@ the 800-character limit (850/800)`); it never truncates it. A tool call with sev
15
15
  (`<tool> was not called: N problems`), so one corrected call lands. GitHub threads and markers no
16
16
  longer exist.
17
17
 
18
+ ## Design changes are brainstormed here
19
+
20
+ Sami, 2026-09-17, verbatim: "Make sure your agents know that they should be doing brainstorming with me
21
+ through dispatch for major design changes." For a major design change the design conversation itself
22
+ happens in Dispatch: write the spec document early, while it is still a draft with real alternatives, and
23
+ put each open question in it as an `ask` block beside the options and trade-offs it depends on
24
+ ([Writing a spec](#writing-a-spec), [Typed blocks](#typed-blocks)). He answers in place and the document
25
+ grows into the record. A finished spec dropped after a chat-only design, or a set of one-line issue asks
26
+ pointing at a document, is not brainstorming with him.
27
+ Sami, 2026-09-17, verbatim: "Can you please stop doing this thing where you have these one-off,
28
+ shorthand, compressed decision asks that are completely disconnected from any discussion of the
29
+ design or the trade-offs? This is just very obviously not the most effective way to have a design
30
+ communication." A question lives beside the options and trade-offs it depends on, in the spec or
31
+ discussion it came from — never as a compressed standalone ask.
32
+
18
33
  ## Writing for the human
19
34
 
20
35
  Sami, 2026-09-12, on what Legion had been producing: "It's completely incomprehensible. It's just
@@ -26,8 +41,10 @@ vocabulary, and is often on a phone. Write for that person.
26
41
  not "fix 8c", "READY-target", "PR B", "spec@v3", "the pair", "the packet" — say what the thing is.
27
42
  - Expand every identifier the first time it appears: an issue key gets its title, a PR number its
28
43
  title, a file what it is for, a session id who it is. Link a URL rather than pasting a bare id.
29
- - Frame a request as current state → desired state → proposed change, with at least two options,
30
- what each costs, and your recommendation with its reason.
44
+ - A question lives beside the options and trade-offs it depends on, in the spec or discussion it
45
+ came from — never a compressed standalone ask (his words are quoted under [Design changes are
46
+ brainstormed here](#design-changes-are-brainstormed-here)). Give the reader the options, what
47
+ each costs, and your recommendation with its reason; do not prescribe yourself a form.
31
48
  - Before posting, test it: could Sami, reading only this text on his phone, know what he is being
32
49
  told or asked? If not, rewrite it. Length is not the problem; density is.
33
50
  - When an ask or message communicates a judgment, lead with that judgment in one sentence and put the mechanism underneath it. Do not make the reader ask a second time whether the result is a win. This shapes communication only when a judgment exists; it does not pre-decide an open question or remove its genuine options. Inferred from the AGENTC-186 12-hour-cap incident (platform PO, 2026-09-17).
@@ -64,7 +81,7 @@ rest. Use these headings in this order.
64
81
  | Section | Required content | Form |
65
82
  | --- | --- | --- |
66
83
  | **Summary** | The problem, what changes for whom, and how we will know it worked — in plain words. | Three sentences at most. |
67
- | **Decisions needed** | Only decisions that need human authority, taste, or risk appetite. Each is one plain question, two or three options with what each costs, and your recommendation with its reason — understandable without opening anything else. Each is a `dispatch_ask`; anchor it only when it concerns a document passage. An answered item moves into Requirements with its provenance. If there is nothing to decide, write `None: this records what was agreed.` and do not ask for a review. | One decision per line. |
84
+ | **Decisions needed** | Only decisions that need human authority, taste, or risk appetite: each one plain question, two or three options with what each costs, and your recommendation with its reason — written as an `ask` block directly under those options, so it is answered in context (see [Design changes are brainstormed here](#design-changes-are-brainstormed-here)). An answered item moves into Requirements with its provenance. | One `ask` block per decision; empty is fine. |
68
85
  | **New since we talked** | Every design point the human did not settle in conversation, marked `inferred:` with the reasoning. Empty is fine. | One plain sentence per point. |
69
86
  | **Acceptance** | Each outcome names what a user will observe and the check that proves it (browser scenario, API call, or command). An outcome without a check is not acceptance. | Numbered lines. |
70
87
  | **Requirements** | What must hold, and where each came from: a quoted human sentence, or `inferred:` plus the reasoning. Readers treat inferred requirements as hypotheses. | `requirement \| where it comes from` table, or prose if the reader follows it more easily. |
@@ -180,7 +197,7 @@ production import). Every `dispatch_ask` passes three gates first:
180
197
  2. **Is there genuine uncertainty?** If not, it is a plan you execute. The one legitimate ask
181
198
  without uncertainty is permission for an action only a human can authorise — a production
182
199
  write, an external send, a console action — and then the question is that action in one
183
- sentence with `Done` / `Can't` options (below).
200
+ sentence, with options that name its outcomes (below).
184
201
  3. **Can someone who has not read the code answer it on a phone?** What he can see today, what
185
202
  changes for a reader, two options with what each costs, your recommendation. No slice or
186
203
  decision numbers, no coined nouns, no internal identifiers he has never used, no jargon you
@@ -242,9 +259,10 @@ that follow from it:
242
259
  - Expand every term the reader has not used first. A product name, an internal setting, an
243
260
  acronym, a value you coined this session — write what it is in the ask, in his words.
244
261
  - A runbook the human must execute is one ask per step, each self-contained: what to do, where,
245
- what result proves it, `Done` / `Can't` options. Each later step opens only after the previous is
246
- answered and states that step's verified result in one line ("Step 1 done: the licence shows
247
- Cloud Identity Free on the admin console.") — never a pointer to the earlier ask.
262
+ what result proves it, and options that name the step's outcomes. Each later step opens only
263
+ after the previous is answered and states that step's verified result in one line ("Step 1
264
+ done: the licence shows Cloud Identity Free on the admin console.") — never a pointer to the
265
+ earlier ask.
248
266
 
249
267
  **A decision about an uploaded artifact links it.** If the human must read an artifact to answer,
250
268
  the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
@@ -282,13 +300,13 @@ click. One ask per item, `urgency: "high"` when work is stopped on it; while it
282
300
  working on everything that is not.
283
301
 
284
302
  A to-do handed to a human is an ordinary question: phrase the to-do as the question and give it
285
- the options you want, typically `Done` / `Can't`. Nothing about the options is special to the
286
- server; if you need a reason with `Can't`, say so in the option's description, and the human's
287
- free-text answer carries it:
303
+ the options that name its outcomes, in the human's words - there is no fixed vocabulary and the
304
+ server treats no label specially. If an outcome needs a reason, say so in that option's
305
+ description, and the human's free-text answer carries it:
288
306
  ```ts
289
307
  dispatch_ask({ issue: "DSP-42",
290
- question: "Confirm the deployment is complete.",
291
- options: [{ label: "Done" }, { label: "Can't", description: "Say what is missing." }] })
308
+ question: "Run the production deploy for #19125?",
309
+ options: [{ label: "Deployed" }, { label: "Blocked", description: "Say what is missing." }] })
292
310
  ```
293
311
 
294
312
  Correct or refine an open ask in place instead of opening a second question:
@@ -242,24 +242,18 @@ Preserve this order exactly:
242
242
  3. retro commits its learnings under `docs/solutions/` on top of the approved head; that
243
243
  commit does not void the approval and never returns the tree to the tester or reviewer;
244
244
  4. the merger verifies the current head is the reviewer-approved head plus only commits that
245
- change `docs/solutions/` (`jj diff --from <approved-sha> --to <tip-sha> --summary`, quoted in READY)
246
- and publishes `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)`
247
- to the project's controller topic (the merge queue; named in its `Legion addressing` line);
248
- it never merges. The controller verifies the gates against live GitHub — the current head,
249
- required checks, review threads, mergeability, that only `.legion/` deletions lie between the
250
- head the `## Verification` block names and the approved sha, that only `docs/solutions/`
251
- changed between the approved and current shas, and that the block is complete at the head it
252
- names — and merges the current sha, pinned, under the implement App's identity and the
253
- repository's own rules (branch protection, CODEOWNERS); it does not check the approval itself,
254
- and whether a human must approve first is that repository's setting, not Legion's, so you never
255
- ask for or wait on such an approval. If the controller reports a failed gate to you, treat it
256
- like `pr-blocked`: fix through the phases, never bypass.
257
- 5. the controller merges; you then `spawn_worker` the **implementer** once more with the
258
- production-check task. It drives the changed path in production through the user's own access
259
- path and records what it saw on the pull request and on this issue. Close only after the implementer's production report exists.
260
- A defect it finds is a corrective child issue of this tree, not a note on a closed one; a deploy
261
- the implementer cannot perform is its `dispatch_ask` with `Done` / `Can't` options, and the
262
- issue waits for it.
245
+ change `docs/solutions/` (`jj diff --from <approved-sha> --to <tip-sha> --summary`, quoted in
246
+ READY) and posts `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)`
247
+ on the Dispatch issue. When its `Legion addressing` line names the project's merge queue, it
248
+ publishes the same packet there too. Legion never merges; a human merges under the repository's
249
+ GitHub branch-protection and CODEOWNERS rules. If the merger reports a failed verification,
250
+ treat it like `pr-blocked`: fix through the phases, never bypass.
251
+ 5. a human merges; you then `spawn_worker` the **implementer** once more with the production-check
252
+ task. It drives the changed path in production through the user's own access path and records
253
+ what it saw on the pull request and on this issue. Close only after the implementer's production
254
+ report exists. A defect it finds is a corrective child issue of this tree, not a note on a
255
+ closed one; a deploy the implementer cannot perform is its `dispatch_ask` naming that deploy,
256
+ with options for its outcomes, and the issue waits for it.
263
257
 
264
258
  What returns the tree to review: a changed diff — a commit above the approved head that
265
259
  touches anything outside `docs/solutions/`, or a rebase whose fingerprint (the `legion-worker`
@@ -275,9 +269,9 @@ PR gets no CI and no wake announces it, and send the implementer to rebase the m
275
269
  it. Do not let the merger publish `READY` for an obsolete approval.
276
270
 
277
271
  If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
278
- to resolve, open a `dispatch_ask` with `Done` / `Can't` options that names the thread's URL
279
- and GitHub's message for a human to resolve it by hand; the merger does not publish while it is
280
- open. That is the one review-thread step a human takes: the review App cannot resolve threads,
272
+ to resolve, open a `dispatch_ask` that names the thread's URL and GitHub's message for a human to
273
+ resolve it by hand, with options for resolved / could not; the merger does not publish while it
274
+ is open. That is the one review-thread step a human takes: the review App cannot resolve threads,
281
275
  and the implementer's and merger's runs of the command close every accepted one.
282
276
 
283
277
  ## 7. Close
@@ -314,10 +308,11 @@ active phase worker.
314
308
  | `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. A `reviewer` completion whose GitHub review is `CHANGES_REQUESTED` (the daemon returns the issue's Dispatch status to `in_progress` for this, on the reviewer's completion and again when you spawn the corrective implementer unless the daemon already knows the issue is `in_progress`) means `spawn_worker` the **implementer** again with the review findings — thread URLs and blocking items — as its task, then route back through tester and reviewer in order; never `spawn_worker` the reviewer directly off this wake and never proceed to retro on this verdict. A reviewer completion with an `APPROVED` review proceeds to retro (step 5). A `reviewer` completion after a conflict-forced rebase whose review body names an unchanged fingerprint is a confirmation, not a round: if retro already completed, `spawn_worker` the merger; otherwise resume the step you were on. An `implementer` completion that follows the merge is its production report: read the record on the pull request and the issue, then run step 7 — the issue is already at `retro`, the daemon writes no status for this completion, and you set `done` yourself. A `tester` completion whose handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof, goes back to the **implementer** with that finding — never forward to the reviewer, and never by supplying the proof from another role. A worker that reports no surface reaches the changed path gets a child issue in this tree (infrastructure, tooling, or a skill) and a resume once it lands; that report is never a reason to advance the phase. |
315
309
  | `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. `legion state` shows the queue (`workerAdmission.queue`: role token, issue, role, kind, and the time the task was first queued — never the task text); read it before re-sending. A `spawn_worker` identical to the queued task changes nothing and is not announced again. Different text replaces the queued task silently in the same FIFO slot and retains its original queue time. A `spawn_worker` that fails with "got no response in 3 attempts" was already retried by the plugin under one request id and may still have reached the daemon: read the queue and the role's claim in `legion state` before sending it again. |
316
310
  | `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. |
311
+ | `worker-recovered` | Payload `{type:"worker-recovered", issue, role, fromRef, delivery?}`. The worker's tree volume was lost and the daemon replaced it from the committed handoff on `fromRef`. `delivery: "spawned"` means the current assignment was preserved on the new worker; do not resend it. `delivery: "queued"` means that preserved assignment awaits capacity; wait for `worker-started`. Without `delivery`, inspect `.legion/` and the active phase before deciding whether work needs a new assignment. |
317
312
  | `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
318
313
  | `pr-review` | Payload `{type:"pr-review", state, author, body}`. Delivered to whichever role is currently active for the issue, falling back to you when no worker phase is active. Follows the same verdict rule as a reviewer's `phase-complete`: `state: "changes_requested"` sends the implementer back in with the review findings, then tester, then reviewer — never the reviewer again and never retro; that `spawn_worker` returns the issue to `in_progress` on its own (the daemon writes it for a corrective implementer whenever the PR's latest recorded review is changes requested, a human's after approval included), so you set nothing by hand; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. `state: "approved"` on a rebased head whose body names an unchanged fingerprint is that confirmation: proceed to retro if it has not run, otherwise to the merger — never to a second retro or test round. |
319
314
  | `pr-blocked` | Payload `{type:"pr-blocked", pr, attempts}`. `attempts` counts heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count; a push the daemon cannot classify (a listener without `changed_paths`, a list capped at 100, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. 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. |
320
- | `pr-merged` | Payload `{type:"pr-merged", pr, mergeCommitSha}`. The controller merged the PR. `spawn_worker` the **implementer** with the production-check task naming that merge commit (it resumes the same agent; a retired role has no live holder, so never `envoy_publish` for this). Its `phase-complete` is what brings you to step 7: verify the record on the pull request and this issue first, then sign off naming it and set the issue `done`. A merge is not the close. |
315
+ | `pr-merged` | Payload `{type:"pr-merged", pr, mergeCommitSha}`. The PR merged because a human merged it under the repository's rules. `spawn_worker` the **implementer** with the production-check task naming that merge commit (it resumes the same agent; a retired role has no live holder, so never `envoy_publish` for this). Its `phase-complete` is what brings you to step 7: verify the record on the pull request and this issue first, then sign off naming it and set the issue `done`. A merge is not the close. |
321
316
  | `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. |
322
317
  | `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. |
323
318
  | `catchup-overseer` | Verify its child counts and PR verdicts against current artifacts, then resume the applicable lifecycle step. This is a current-state snapshot, not a raw-event replay. A root architect uses `gates[LEGION_TREE].open`: `true` means the root spec is approved and section 2 may continue; `false`, or no `open` key, means section 1 still applies. A resumed sub-architect receives `overseerCatchup(state, LEGION_ISSUE)` for its own subtree: its `gates` intentionally omits the root gate because a child spec is never gated. Do not request or register a gate; resume at section 2. Handle each `phaseCompletions` entry exactly as a `phase-complete` wake, then compare `childCounts[LEGION_ISSUE].open` with `legion state` and Dispatch before deciding the next action. |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: legion-controller
3
- description: Use when handling Legion controller wakes for root-issue triage, backlog admission, architect escalation, resync healing, merge-queue READY handling, or human interaction.
3
+ description: Use when handling Legion controller wakes for root-issue triage, backlog admission, architect escalation, resync healing, or human interaction.
4
4
  ---
5
5
 
6
6
  # Legion Controller
@@ -19,15 +19,10 @@ server (the pane runs plain `omp`, not `--mode rpc`, and no `legion worker-shim`
19
19
  it with `tmux -L legion-<project> select-window -t <window id> \; attach -t legion-<project>`,
20
20
  the window id being `controllerLocator.tmuxWindowId` in `legion state --json` — every window
21
21
  opens detached, so a bare `attach` lands on whichever window is current). Sami may attach and
22
- type into this session at any time. The pane carries the same credential environment as a
23
- worker's — `LEGION_GRANT_FILE`, `GH_CONFIG_DIR`, the GitHub token variables emptied; the
24
- `<state_dir>/worker-bin`-first `PATH` the daemon renders reaches no pane today (tmux drops the
25
- `-e PATH=` pair at pane creation, LEGION-91), so a bare `gh` is whatever the box has — so
26
- `legion gh -- <args>` works here exactly as it does for a phase worker (`legion` resolves through
27
- `<state_dir>/bin`, which the pane inherits from the daemon's own `PATH`):
28
- before every `bash` call the extension mints a short-lived controller grant and writes it to
29
- the file `LEGION_GRANT_FILE` names (never into the command text or the tool's `env`), `legion`
30
- reads it from there, and that grant is the only one the daemon lets merge a pull request.
22
+ type into this session at any time. The pane carries no GitHub credential: its GitHub token
23
+ variables are emptied, and both `legion gh -- <args>` and `legion threads resolve` are refused.
24
+ The controller reads Dispatch and applies its controller capability with `legion status <KEY>
25
+ <status>`; it never reads GitHub or merges a pull request.
31
26
 
32
27
  For an interactive takeover from a hand-started OMP session, start OMP with
33
28
  `LEGION_CONTROLLER_SECRET` (or `LEGION_CONTROLLER_SECRET_FILE`, a path to a file holding it),
@@ -43,10 +38,10 @@ claims at startup and reports its transcript as the pane's. Then run:
43
38
 
44
39
  The command resolves the project from daemon state, claims the Envoy role for the current
45
40
  session, and posts readiness before controller commands can act. From then on this session's
46
- shell commands are wrapped with a controller grant exactly like the daemon pane's, so
47
- `legion gh -- <args>` works here; `legion status <KEY> <status>` works too, but through the
48
- controller secret in this session's environment (`LEGION_CONTROLLER_SECRET` or its `_FILE`),
49
- not the grant — if it fails, that is the variable to check. The takeover moves the role
41
+ shell commands are wrapped with a controller grant, but the grant holds no GitHub credential;
42
+ `legion status <KEY> <status>` works through the controller secret in this session's environment
43
+ (`LEGION_CONTROLLER_SECRET` or its `_FILE`), not the grant — if it fails, that is the variable to check.
44
+ The takeover moves the role
50
45
  and the daemon's recorded session id to this session; it never replaces the transcript the
51
46
  daemon recorded for its own pane, so a later respawn of that pane resumes the pane's own
52
47
  conversation, not yours. Never pass a secret as a command argument or copy it into a transcript.
@@ -79,17 +74,16 @@ registeredAt}` and reads your liveness from the Envoy role registry (the holder
79
74
  `legion-<project>-controller` and its `last_seen`), not from a pane: keep the session running.
80
75
  Exiting it leaves the project without a controller until the operator runs the command again —
81
76
  the daemon logs `controller not registered; run legion controller start` once per boot-timeout
82
- interval and launches nothing itself. `legion state`, `legion gh -- <args>`, and
83
- `legion status <KEY> <status>` work here over `LEGION_DAEMON_URL` (the port-forward). A second
84
- `legion controller start` replaces you: it mints a new secret, so your grants stop working and
85
- the role moves to the new session.
77
+ interval and launches nothing itself. `legion state` and `legion status <KEY> <status>` work here
78
+ over `LEGION_DAEMON_URL` (the port-forward). A second `legion controller start` replaces you: it
79
+ mints a new secret, so your grants stop working and the role moves to the new session.
86
80
 
87
81
  ## Deployment instructions
88
82
 
89
83
  Deployment instructions, when present, are the operator's standing rules for this repository —
90
- required checks, deploy/smoke commands, code-owner expectations, standing roles you may consult,
91
- the merge credential. They override this skill's defaults where they conflict; they never
92
- override a Sami ruling quoted here.
84
+ required checks, deploy/smoke commands, code-owner expectations, and standing roles you may
85
+ consult. They override this skill's defaults where they conflict; they never override a Sami ruling
86
+ quoted here.
93
87
 
94
88
  ## Turn discipline
95
89
 
@@ -117,8 +111,8 @@ override a Sami ruling quoted here.
117
111
  | Resync report: `admission-drift` entry | issue key + whether the daemon added it to, or removed it from, its admission list (the detail says which) | No action: the daemon already repaired it in the same run. An issue that reappears in consecutive reports is a live leak — file a LEGION issue on Dispatch with both reports pasted as evidence (never a GitHub issue) |
118
112
  | `child-status` | child key + status transition | Not controller-actionable by default; if the daemon could not route it to the parent's architect role, verify the transition and forward it with `envoy_publish` |
119
113
  | Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
120
- | READY from a merger (`notifications.role.<controller token>`) | `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)` + gate facts | Run the Merge queue gates against live GitHub; merge, or report the failed gate to the tree's architect |
121
- | `pr.<n>.checks` settled on a PR with a pending READY | check rollup for the head | Re-run the Merge queue gates for that READY; merge, report, or keep waiting only if still pending |
114
+ | READY packet seen on a Dispatch issue (via issue subscription) | READY line + gate facts | No action: a human merges; the merger has already notified the queue role if the project has one |
115
+ | `worker-recovered` (role `architect`) from the daemon | issue, fromRef | A root architect's tree volume was lost; it restarted as a new session. Verify the tree is active in `legion state` and that the architect posts its next step on the issue within one resync interval; otherwise treat it as an anomaly. |
122
116
  | Closed-tree activity (comment, review, CI on a closed tree) | issue, root, event summary | Read the artifact; if work should resume, `legion status <root> todo`; otherwise no action — the event is not held or redelivered |
123
117
  | Direct user message | — | Always first |
124
118
 
@@ -202,188 +196,3 @@ gate at all runs `gates.design: off` in its `legion.yaml`. Otherwise resolve the
202
196
  owning architect role and route the verified context with `envoy_publish`. Do not route raw
203
197
  event traffic or invent a role token from a partial issue reference.
204
198
 
205
- ## Merge queue
206
-
207
- The controller is the project's merge queue. A merger reports a pull request ready by
208
- publishing to the controller topic; the controller re-reads every gate from live GitHub and
209
- merges, or tells the tree's architect exactly which gate failed. The merger's report is a
210
- claim, never evidence.
211
-
212
- **READY message shape.** Defined once in `packages/pi-envoy/roles/merger.md` and mirrored here
213
- verbatim. The first line is
214
- `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)`: the pull
215
- request number, the sha of the pull request's current head, the sha the reviewer's head-pinned
216
- approval names, the issue key, and the pull request URL. The rest of the message is the PR
217
- body's gate facts (the `## Verification` block). You need every field: the URL addresses the
218
- pull request from this pane's working directory (which is not a checkout), the key finds the
219
- tree's architect (below), the current sha is the only head you may merge, and the approved sha
220
- anchors the two path-only compares in gates 5 and 6. The controller never verifies the
221
- approval itself: whether a review must exist before merge is the repository's own
222
- branch-protection or CODEOWNERS rule, which GitHub enforces at `pr merge` time and Legion
223
- neither reads nor writes.
224
-
225
- **Three shas.** This repository's flow leaves three commits that matter, and they are normally
226
- all different. The *verified* sha is the head the tester and the reviewer worked at: the
227
- `## Verification` block's own `CI`, `Thermo`, and `E2E` lines name it, and they must agree. After
228
- that head is found clean the implementer pushes the `.legion/` handoff deletion and the reviewer
229
- approves *that* head by name — the *approved* sha, one commit later. Retro then commits its
230
- `docs/solutions/` learning on top — the *current* sha. READY carries the current and approved
231
- shas; the verified sha you read from the block. The gates check the block at the verified sha
232
- and prove, with two compares, that nothing but the `.legion/` deletion lies between verified and
233
- approved, and nothing but `docs/solutions/` between approved and current.
234
-
235
- **Gates.** Read them from live GitHub, never from the message or the PR body alone. Every `gh`
236
- command takes the pull request URL, or `--repo <owner>/<repo>` taken from it, because this
237
- session's working directory has no git remote to resolve a bare number against:
238
-
239
- ```text
240
- legion gh -- pr view <pr url> --json headRefOid,baseRefName,mergeable,body,files
241
- legion gh -- pr checks <pr url> --required --json name,state,bucket,link
242
- legion gh -- api repos/<owner>/<repo>/rules/branches/<baseRefName from pr view> --jq '[.[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context]'
243
- legion gh -- api repos/<owner>/<repo>/branches/<baseRefName from pr view> --jq '.protection.required_status_checks.contexts'
244
- legion gh -- api graphql -f query='query($owner:String!,$repo:String!,$n:Int!,$after:String){repository(owner:$owner,name:$repo){pullRequest(number:$n){reviewThreads(first:100,after:$after){pageInfo{hasNextPage endCursor}nodes{isResolved}}}}}' -F owner=<owner> -F repo=<repo> -F n=<n> -F after=<null for the first page>
245
- legion gh -- api repos/<owner>/<repo>/compare/<verified sha>...<approved sha> --jq '{status, files: [.files[].filename]}'
246
- legion gh -- api repos/<owner>/<repo>/compare/<approved sha>...<current sha> --jq '{status, files: [.files[].filename]}'
247
- ```
248
-
249
- 1. **head**: `headRefOid` equals the `<current sha>` in the READY. Any other head is a different
250
- pull request as far as this READY is concerned.
251
- 2. **checks**: `pr checks --required --json …` exits 0 with at least one row, and every row's
252
- `bucket` is `pass` or `skipping`. That is the only green. The five buckets the CLI emits
253
- (`gh pr checks --help`): `pass` and `skipping` are green — a job skipped by its `if:` (a
254
- path-filtered workflow skips the jobs whose paths a pull request does not touch, and GitHub
255
- treats a skipped job as satisfying a required check);
256
- `pending` is pending; `fail` and `cancel` never merge (the Flake rule applies to both). With
257
- `--json` the command exits 0 whenever rows exist, whatever their buckets, so the buckets
258
- decide, never the exit code. Exit 1 comes only with no rows: `no checks reported on the
259
- '<branch>' branch` when nothing has reported at the head yet (a freshly pushed head has no
260
- check runs for a few seconds; a head that conflicts with the base never gets any, but that
261
- head fails gate 4 — report it, do not subscribe), or `no required checks reported on the
262
- '<branch>' branch` when checks exist but none is required. Either message is **pending**,
263
- never green: subscribe to the pull request's `pr.<n>` and `pr.<n>.checks` topics exactly as
264
- the Pending READY paragraph below says, and re-run the gates on that wake. (Without `--json`
265
- the CLI exits 8 for pending rows and 1 for a failing row or no rows; you never run it that
266
- way — the rows are what you read.) Two exceptions, both read from the repository, never from
267
- the absence of rows:
268
- - A repository that genuinely requires no checks. Required checks live in two places, and
269
- both must be empty: the `rules/branches/<baseRefName>` query above (rulesets) returns `[]`
270
- **and** the `branches/<baseRefName>` query above (the classic branch-protection summary,
271
- which the implement App can read; the admin endpoint
272
- `branches/<baseRefName>/protection/required_status_checks` is not readable under your credentials
273
- and is not used) returns `[]`. Only then does the `no required checks reported` exit let
274
- this gate hold with no check rows. A repository whose required checks are classic
275
- protection answers `[]` for rulesets and the check names in the classic summary; `null`
276
- from the classic query (no `protection` object in the answer) is not `[]` and leaves this
277
- gate pending.
278
- - A private repository on GitHub's free plan cannot define required checks at all: the
279
- rulesets query answers HTTP 403 with a JSON body whose `message` **contains** the phrase
280
- `make this repository public to enable this feature` (`gh` prints the whole message with
281
- `(HTTP 403)` appended). Match that phrase as a substring — it is the stable tail; the head
282
- names the plan (`Upgrade to GitHub Pro` for a user-owned repository, `Upgrade to GitHub
283
- Team` for an organization-owned one) and the sentence ends with a period inside a JSON
284
- wrapper, so literal equality never matches. Any other 403 — `Resource not accessible by
285
- integration` included — is a permission error and stays an error, never "no required
286
- checks". Under this exception gate 2 requires every check reported on the head to be green
287
- instead: `legion gh -- pr checks <pr url> --json name,state,bucket,link` (without
288
- `--required`) exits 0 with at least one row and every row's `bucket` is `pass` or
289
- `skipping`. A `pending` row is pending, a `fail` or `cancel` row never merges, and no rows
290
- (the exit-1 `no checks reported`) stays pending exactly as above. This is stricter than
291
- "no required checks, merge", and GitHub still enforces whatever protection the repository
292
- does have at `pr merge` time, so a wrong read costs a refused merge reported to the
293
- architect, never an unprotected one.
294
- Where each of these reads was observed — the CLI version, the two repositories, the exact
295
- answers — is recorded in
296
- `docs/solutions/legion/controller-gate-2-required-checks-live-reads.md`. The rule above is
297
- what you execute; the live answers are what you read.
298
- 3. **threads**: zero unresolved review threads across every page. Start with `after: null`, then
299
- repeat the query with the prior page's `pageInfo.endCursor` until `hasNextPage` is false; the
300
- count of `isResolved: false` across all pages must be 0. A missing `pageInfo`, a missing cursor
301
- while `hasNextPage` is true, or any failed page is a failed gate: do not merge. This follows the
302
- pagination `legion threads resolve` uses, but the controller reads only and never resolves a
303
- review thread.
304
- 4. **mergeable**: `mergeable` is not `CONFLICTING` and not `UNKNOWN`.
305
- 5. **cleanup only**: `compare/<verified sha>...<approved sha>` reports `status` `identical` or
306
- `ahead`, and every path in `files` starts with `.legion/` — the handoff deletion the reviewer
307
- directed, and nothing else. Anything else between the two is the failed gate
308
- `cleanup changed more than .legion`.
309
- 6. **retro only**: `compare/<approved sha>...<current sha>` reports `status` `identical` or
310
- `ahead`, and every path in `files` starts with `docs/solutions/`. Anything else between the
311
- two is the failed gate `head moved beyond retro`: the approval no longer covers the head.
312
- 7. **verification block**: the PR body's `## Verification` block (the template in
313
- `skills/legion-worker/SKILL.md`) is complete and current at the verified sha. The tester
314
- fills the `E2E` line before review; the reviewer writes the `Thermo` line at the head it
315
- audited; approval lands one commit later on the cleanup head; so the block names the verified
316
- sha, never the approved or the current one. Line by line: the `CI` line names a run and
317
- reports success at one sha; the `Thermo` line names the same sha and a verdict, unless the
318
- pull request is docs-only, in which case the template omits that line entirely — docs-only
319
- is a fact you read, never one you take from the omission itself: every `path` in the `files`
320
- list of the `pr view` command above starts with `docs/` or ends with `.md`
321
- (`--jq '[.files[].path | select((startswith("docs/") or endswith(".md")) | not)]'` is `[]`);
322
- a missing `Thermo` line on any other pull request fails this gate; the `E2E`
323
- line names the same sha and has a `Negative control` line — those lines agreeing on one sha
324
- is what defines the verified sha; the `Threads` line reports `0 unresolved` (its per-thread
325
- lines name fixing commits, never the head — do not look for a sha there); the `Fast-follow`
326
- and `Chain` lines are filled in. No `<placeholder>` text remains anywhere in the block.
327
-
328
- When all seven hold, merge:
329
- `legion gh -- pr merge <pr url> --squash --match-head-commit <current sha>`. The head pin makes
330
- GitHub refuse the merge if a push landed after gate 1 read the head; that refusal is a failed
331
- `head` gate, reported like any other. The grant your `bash` call carries is the controller's
332
- own, the only grant the daemon honours for a merge; the merge runs under the implement App's
333
- identity and the repository's own rules (branch protection, CODEOWNERS). Whether a human must
334
- approve first is that repository's setting — you neither read nor bypass it, and you never
335
- admin-merge without an explicit deployment grant from Sami for that specific merge.
336
-
337
- **Failed gate.** Reply to the tree's architect naming the gate (`head`, `checks`, `threads`,
338
- `mergeable`, `cleanup changed more than .legion`, `head moved beyond retro`, or
339
- `verification block`) and the evidence you read (the shas, the check name and run link, the
340
- thread count, the `mergeable` value, the offending paths from the compare). Do not merge, do not
341
- retry on a timer. The architect fixes through the phases.
342
-
343
- **Flake.** A required check that failed or was cancelled (`bucket` `fail` or `cancel`) for a
344
- reason unrelated to the change (a runner outage, a rate limit, a known-flaky job) may be rerun
345
- once: `legion gh -- run rerun <run-id> --failed --repo <owner>/<repo>`, the run id taken from
346
- the failing row's `link` (`https://github.com/<owner>/<repo>/actions/runs/<run-id>/job/<job-id>`).
347
- Then stop. The rerun's result reaches you as a `pr.<n>.checks` wake; re-run the gates then.
348
- A second failure is a failed gate, reported as above.
349
-
350
- **Conflicts and unknown mergeability.** `mergeable == CONFLICTING` is the only reason to ask
351
- for a rebase: reply to the tree's architect asking for one. Never request a rebase for any other
352
- reason — the CI queue is long and slow, and an unnecessary rebase clogs it for every other pull
353
- request. `mergeable == UNKNOWN` means GitHub has not finished computing it: do not merge, do
354
- not poll; re-read on the next `pr.<n>.checks` wake.
355
-
356
- **Pending READY.** A READY that cannot merge yet only because checks are still running (a
357
- `pending` row), none has reported at the head yet (gate 2's `no checks reported` exit), a flake
358
- rerun was issued, or `mergeable` is `UNKNOWN` is pending. Subscribe to that pull request's
359
- events so its settlement wakes you:
360
-
361
- ```text
362
- envoy_subscribe({ topics: ["notifications.github.<owner>.<repo>.pr.<n>", "notifications.github.<owner>.<repo>.pr.<n>.checks"] })
363
- ```
364
-
365
- On that wake, re-run the gates against the shas from the READY in your conversation, then
366
- `envoy_unsubscribe` those topics once you have merged or reported a failed gate. The controller
367
- never polls; READY and `pr.<n>.checks` are the only wakes. If you were resumed and no longer
368
- have the READY in your conversation, ask that issue's merger (its token is the `roles` key in
369
- `legion state --json` whose `issue` is `<KEY>` and whose `role` is `merger`; publish to
370
- `notifications.role.` followed by that key) to republish it; never guess a sha.
371
-
372
- **Finding the tree's architect.** Never hand-format a role token: the daemon lower-cases the
373
- issue key inside it (`LEGION-16` becomes `legion-16`) and rejects any other shape, so a token
374
- you assemble from `<KEY>` never matches a live role. Read it instead: `legion state --json`
375
- gives `issues[<KEY>].parent`; follow `parent` until it is absent — that key is the root (the
376
- `trees` map lists the same roots). Then take the `roles` key whose `issue` equals that root and
377
- whose `role` is `architect`, and publish to `notifications.role.` followed by that exact key.
378
- Every registered root architect and phase worker appears in `roles`, so the lookup is
379
- unambiguous. (Phase workers get an addressing line in their system prompt; the controller does
380
- not, so state is your only source.)
381
-
382
- **After a successful merge, publish nothing to the architect.** The daemon derives
383
- `{type:"pr-merged", pr, mergeCommitSha}` from GitHub's own merged webhook and routes it to the
384
- tree's architect itself. A second copy from you would make the architect run its sign-off twice.
385
-
386
- **Policy questions go to Sami.** Whether a pull request should merge at all, whether an admin
387
- merge is warranted, or a gate that looks wrong for this repository is not a controller judgment:
388
- ask with `dispatch_ask` on the issue, in plain sentences, and leave the READY pending until the
389
- answer arrives.
@@ -22,10 +22,11 @@ retrospective's durable output.
22
22
  Retro writes **no `.legion` file**, so it never re-dirties the cleaned handoff tree.
23
23
  4. The merger verifies the tip is the approved head plus commits that change only
24
24
  `docs/solutions/` — `jj diff --from <approved-sha> --to <tip-sha> --summary`, quoted in READY —
25
- publishes `READY`, and pushes nothing; the merge queue merges under the repository's own
26
- rules.
27
- 5. After the merge lands, the implementer — not the reviewer, the merger, or the queue — verifies
28
- the change in production and records it on the PR and the issue (Sami, 2026-09-13, verbatim:
25
+ posts `READY` on the Dispatch issue, and publishes the same packet to the project's merge-queue
26
+ role when configured. A human merges under the repository's GitHub branch-protection and
27
+ CODEOWNERS requirements; GitHub's merge queue participates only when the repository enables it.
28
+ 5. After that merge, the implementer — not the reviewer or merger — verifies the change in production
29
+ and records it on the PR and the issue (Sami, 2026-09-13, verbatim:
29
30
  "the agent that developed it should be responsible for testing in production"). The
30
31
  architect's sign-off waits for that record.
31
32
  The record is the pull request's `Production:` line, one pull-request comment, and a
@@ -111,6 +111,10 @@ predecessor phases already wrote. The durable copy lives in
111
111
  `$LEGION_WORKSPACE/.legion/<phase>.json`. If a committed handoff conflicts with memory or a prior
112
112
  transcript, the committed file wins: it is the copy that survived.
113
113
 
114
+ If your system prompt begins with `Your workspace was recreated…`, read
115
+ `.legion/workspace-recovered.json`, then your phase's committed handoff, and reconcile before any
116
+ new work.
117
+
114
118
  ## jj Safety Rules
115
119
 
116
120
  - **Always `jj -R "$LEGION_WORKSPACE" new` to create isolated commits.** Never
@@ -176,8 +180,8 @@ Four facts about `gh` in a worker pane. The `gh` on your `PATH` is a shim
176
180
  that execs `legion gh -- "$@"`, so `gh …` and `legion gh -- …` are the same call, and each call
177
181
  redeems a fresh token from your session's grant — identity is supplied per call, never stored.
178
182
  Never run `gh auth login` or `gh auth setup-git`; there is no login state to create. The shim
179
- refuses `pr merge` (and a raw `gh api …/merge`): no worker role merges a pull request — the merge
180
- queue does, under its own authority. It also refuses every GitHub-issue write — the `issue`
183
+ refuses `pr merge` (and a raw `gh api …/merge` or a GraphQL mutation) for every role: Legion never
184
+ merges. It also refuses every GitHub-issue write — the `issue`
181
185
  subcommand's `comment`, `create`, `edit`, `close`, `reopen`, `delete`, `pin`, `unpin`, `transfer`,
182
186
  `lock`, `unlock`, and `develop`, and any raw `gh api` call to an `/issues` path whose method is not
183
187
  GET (an explicit `-X`, or the POST that `-f`/`-F`/`--input` imply; pull-request conversation
@@ -244,10 +248,10 @@ branch name alone is ambiguous. The credential helper and `legion gh` provide th
244
248
  identity; never export, fetch, or replace a token. Other phases advance the existing branch
245
249
  rather than creating a replacement bookmark or PR.
246
250
 
247
- ## PR body and merge-queue discipline
251
+ ## PR body and READY discipline
248
252
 
249
- The implementer writes the PR body in the merge queue's READY format from the moment the
250
- PR opens, and every later phase keeps it current rather than replacing it:
253
+ The implementer writes the PR body in the READY format from the moment the PR opens, and every
254
+ later phase keeps it current rather than replacing it:
251
255
 
252
256
  ```
253
257
  ## Verification
@@ -282,13 +286,13 @@ Verified the implementer's proof by <re-running its command | driving the same s
282
286
 
283
287
  **A proof** is the changed behaviour exercised on the surface a user reaches it through, recorded
284
288
  as the exact command or run id, what was observed, the head SHA, and one negative control —
285
- a deliberately broken input and the refusal or failure it produced. The surface is
289
+ a deliberately broken input and the refusal or failure observed. The surface is
286
290
  **production-like** — the repository's real-process test harness and fixtures, a sandbox
287
291
  repository, a real browser, a devN stack, staging, or a local stack with real migrations, one that
288
292
  has the resource the change touches — and each `E2E` line carries a **link** to that run,
289
- screenshot, or e2e; the merge queue does not approve a user-facing change without it, and a
290
- green unit suite is not it. A unit or integration test is a regression lock, never proof of a
291
- criterion. Sami, 2026-09-13, verbatim: "They need to test everything in a production-like
293
+ screenshot, or e2e; human review does not replace user-facing verification, and a green unit suite
294
+ is not it. A unit or integration test is a regression lock, never proof of a criterion. Sami,
295
+ 2026-09-13, verbatim: "They need to test everything in a production-like
292
296
  environment before merging, and it is the agent that develops the feature that is responsible
293
297
  for doing that. If there's anything blocking that, we need to fix it: if it's infrastructure, we
294
298
  need to fix it; if it's tooling, we need to develop it; if it's skills, we need to fix the skills
@@ -315,7 +319,7 @@ this proof.
315
319
  whose newest comment is the opener's own `Accepted:` reply, one `resolveReviewThread` per
316
320
  thread, prints `resolved <url>` or `left open <url> — newest reply by <login> is not an acceptance`,
317
321
  and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one; report that
318
- exit to the architect, which opens a `Done` / `Can't` ask for a human to resolve the thread by hand —
322
+ exit to the architect, which opens an ask for a human to resolve the thread by hand —
319
323
  never skip it silently. The merger runs the same command once more before publishing READY
320
324
  and does not publish while any `left open` line remains.
321
325
  - **Correctness fixes land in this PR; cleanup is one named fast-follow.** A finding that
@@ -401,21 +405,19 @@ this proof.
401
405
  - The merger runs `legion threads resolve --pr <n> --repo <owner>/<repo>` (it acts as the same
402
406
  code-writing App as the implementer; resolving a thread changes no commit, so this run never
403
407
  invalidates the approval), does not publish while any `left open` line remains or the command
404
- exits 1 (report the thread to the architect instead), then
405
- proves that rule with two commands and publishes. First
406
- `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R "$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`,
407
- whose output is quoted in READY (an empty output is quoted as
408
- `no file changes above the approved head`); then the same with `'~docs/solutions'` appended,
409
- which must print nothing. Then it publishes
408
+ exits 1 (report the thread to the architect instead), then proves that rule with two commands.
409
+ First `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R
410
+ "$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`, whose output is quoted
411
+ in READY (an empty output is quoted as `no file changes above the approved head`); then the same
412
+ with `'~docs/solutions'` appended, which must print nothing. The merger always posts
410
413
  `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)` (the shape
411
- `packages/pi-envoy/roles/merger.md` defines) with that summary and the PR body's gate facts to
412
- the project's controller topic (the merge queue, named in the `Legion addressing` line at the
413
- end of the system prompt) with `envoy_publish`; on a 404 no-holder it publishes the same `READY`
414
- to the architect's topic and stays idle. The READY packet names both the implementer's and the
415
- tester's `E2E` lines; a missing one is reported to the architect instead of published. The
416
- merger never merges; the controller verifies the
417
- gates against live GitHub and merges under its own authority.
418
- - **After the queue merges, the implementer verifies in production.** Sami, 2026-09-13,
414
+ `packages/pi-envoy/roles/merger.md` defines), its summary, and the PR body's gate facts as a
415
+ `dispatch_message` on the issue. When the `Legion addressing` line names a merge queue, it also
416
+ publishes the same packet there with `envoy_publish`; a 404 means the Dispatch message remains
417
+ the durable notice and the merger stays idle. The READY packet names both the implementer's and
418
+ tester's `E2E` lines; a missing one is reported to the architect instead of published. Legion
419
+ never merges.
420
+ - **After a human merges, the implementer verifies in production.** Sami, 2026-09-13,
419
421
  verbatim: "the agent that developed it should be responsible for testing in production."
420
422
  The architect sends the implementer back once the merge lands; the implementer watches the
421
423
  deploy slot that carries the merge to `production-apply` (or the equivalent publish step),
@@ -428,11 +430,10 @@ this proof.
428
430
  carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
429
431
  read GitHub, the architect reads the issue. When the deploy that carries the merge has not
430
432
  happened (a shared profile still holding the previous plugin release, a daemon still running
431
- the previous commit, a slot nobody has run), open a `dispatch_ask` with `Done` / `Can't`
432
- options naming the exact install or restart step, keep the `Production:` line at
433
- `pending <what is missing>`, and complete the check once the human answers Done. Never record
434
- a staging pass as the production check, and never let the architect sign off on a `pending`
435
- line.
433
+ the previous commit, a slot nobody has run), open a `dispatch_ask` naming the exact install or
434
+ restart step, with options for its outcomes, keep the `Production:` line at `pending <what is
435
+ missing>`, and complete the check once the human answers that it is done. Never record a
436
+ staging pass as the production check, and never let the architect sign off on a `pending` line.
436
437
 
437
438
  ## When no surface reaches the changed path
438
439