@sjawhar/opencode-legion-envoy 1.20.2 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13649,7 +13649,7 @@ var MessageEventPayloadSchema = object({
13649
13649
  });
13650
13650
  var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
13651
13651
  id: string2(),
13652
- issue_key: string2(),
13652
+ issue_key: string2().nullable(),
13653
13653
  author: object({ kind: string2(), id: string2() }),
13654
13654
  body: string2(),
13655
13655
  target: string2(),
@@ -13662,6 +13662,7 @@ var MessageDeliveryEventPayloadSchema = object({
13662
13662
  attempt: number2().int().positive().optional(),
13663
13663
  delivery: _enum2(["btw", "aside", "steer"]).optional(),
13664
13664
  session_id: string2().optional(),
13665
+ target: string2().optional(),
13665
13666
  title: string2().optional(),
13666
13667
  state: _enum2(["sent", "failed"]).optional(),
13667
13668
  error: string2().optional()
@@ -15634,17 +15635,18 @@ async function executeDispatchTool(input) {
15634
15635
  }
15635
15636
  case "dispatch_message": {
15636
15637
  const inReplyTo = messageInReplyTo(args);
15637
- const message = await client.message(issue(), {
15638
+ const issueKey = issue();
15639
+ const message = await client.message(issueKey, {
15638
15640
  body: stringArg(args, "body"),
15639
15641
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
15640
15642
  actor
15641
15643
  });
15642
- const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
15644
+ const messageRef = `dispatch://${issueKey}/message/${message.id}`;
15643
15645
  return {
15644
15646
  text: `Posted message ${message.id} (${messageRef})`,
15645
15647
  details: {
15646
- issue: message.issue_key,
15647
- topic: dispatchIssueSubject(message.issue_key, ">"),
15648
+ issue: issueKey,
15649
+ topic: dispatchIssueSubject(issueKey, ">"),
15648
15650
  message: message.id
15649
15651
  }
15650
15652
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.20.2",
3
+ "version": "1.21.1",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -148,11 +148,16 @@ not construct a token from a partial issue reference.
148
148
 
149
149
  ## Legion exception lane
150
150
 
151
- `no_holder` and `delivery_failed` for a Legion role are daemon liveness signals, not reasons to
152
- add a second subscriber or manually retry. For example, treat a `delivery_failed` wake as the
153
- daemon's responsibility to revive or recreate the backing worker and re-deliver; otherwise it
154
- resurrects the root process with derived catch-up and the workspace handoffs. Raw delivery failures
155
- stay out of architect context.
151
+ `no_holder`, `delivery_failed`, and `receipt_timeout` for a Legion role are daemon liveness
152
+ signals, not reasons to add a second subscriber or manually retry. `no_holder`: no live session
153
+ holds the role. `delivery_failed`: the message was not forwarded at all — the holder lookup
154
+ failed, the holder was stale, or the publish errored. `receipt_timeout`: the listener forwarded
155
+ the message to a registered, live holder and no receipt arrived inside the window; its payload
156
+ keeps `original_topic`, `event_id`, `payload`, and the original `dedupe_key`, so the daemon can
157
+ re-send the same message and the receiver's dedupe drops a copy that did arrive. Treat any of
158
+ these wakes as the daemon's responsibility to revive or recreate the backing worker and
159
+ re-deliver; otherwise it resurrects the root process with derived catch-up and the workspace
160
+ handoffs. Raw delivery failures stay out of architect context.
156
161
 
157
162
  ## Slack
158
163
 
@@ -285,7 +285,11 @@ entire end-game sequence has completed.
285
285
  ## Wake routing
286
286
 
287
287
  Handle one delivered wake by verifying the relevant live artifact and then performing the
288
- corresponding lifecycle procedure.
288
+ corresponding lifecycle procedure. Architect-addressed wakes always reach the architect that owns
289
+ the payload issue: a claimed child sub-architect, otherwise the nearest claimed ancestor, then the
290
+ root. A wake about an architect role's own launch reaches the architect above it. `phase-complete`,
291
+ worker lifecycle, child lifecycle, and design-gate wakes are architect-only and never go to an
292
+ active phase worker.
289
293
 
290
294
  | Wake | Procedure |
291
295
  | --- | --- |
@@ -305,8 +309,8 @@ corresponding lifecycle procedure.
305
309
  | `pr-merged` | Payload `{type:"pr-merged", pr, mergeCommitSha}`. The merge queue landed 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. |
306
310
  | `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. |
307
311
  | `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. |
308
- | `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. `gates[LEGION_TREE].open` is the design gate's current state: `true` means the root spec is approved at its current version and you may spawn; `false` (or no `open` key, meaning no gate is registered) means the sequence in section 1 still applies. 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. Then compare `childCounts[LEGION_ISSUE].open` (the children not `done`) with `legion state` and Dispatch: any **open** released child — `todo` through `retro` — with no architect role claim gets `spawn_worker` for its architect, a `child-adopted` or `child-status` wake you missed while not live; a `done` child gets nothing, whether or not a lingering legacy tree of its own still shows in `legion state`. |
309
- | `worker-died` | Payload `{type:"worker-died", issue, role}`. The daemon probed and retried this role's worker through `MAX_LAUNCH_FAILURES` attempts and could not confirm a boot — never a raw-event replay or a silent revive. Reassess the work and `spawn_worker` again for the role (it resumes the same agent via `--resume` if a session file survived) or reassign it if the failure looks environmental, not agent-specific. |
312
+ | `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. |
313
+ | `worker-died` | Payload `{type:"worker-died", issue, role}`. Two causes, one verdict: the daemon retried this role's boot through `MAX_LAUNCH_FAILURES` attempts and could not confirm it, or the worker booted and acknowledged every prompt without ever starting a turn through `MAX_PROMPT_RETIRES` retire-and-relaunch cycles (LEGION-93) — never a raw-event replay or a silent revive. Your next `spawn_worker` for the role is the retry (one cold launch, three prompts, and `worker-died` again if the agent is still broken). Reassess the work and `spawn_worker` again for the role (it resumes the same agent via `--resume` if a session file survived) or reassign it if the failure looks environmental, not agent-specific. |
310
314
  | `reopened` | Reopen the root lifecycle: inspect the reason and current artifacts, reassess scope and children, and resume at the first applicable numbered step. |
311
315
 
312
316
  ## Escalation judgment
@@ -45,7 +45,7 @@ before step 3. The design gate is not a substitute for review and retro.
45
45
 
46
46
  1. Re-read the issue, its acceptance criteria, the PR, test evidence, and review evidence.
47
47
  Confirm the PR carries both proofs: the implementer's own `E2E (implementer)` line and the tester's `E2E (tester)` line,
48
- each naming a production-like surface (a scratch daemon, a smoke rig, a sandbox repository, a devN
48
+ each naming a production-like surface (the daemon's test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the operator's next daemon restart, recorded on the PR; a sandbox repository, a devN
49
49
  stack, staging, or a local stack with real migrations), a command or run id, an observation, a head
50
50
  SHA, and a negative control. If either is missing, or links only a unit suite, the retro's first
51
51
  durable learning is that gap and the issue goes back — to the implementer for its own proof, to the
@@ -22,13 +22,14 @@ completes the boot handshake for you at session start — it registers with the
22
22
  your role, and signals readiness. You never call `envoy_role_set` yourself.
23
23
 
24
24
  Your role token is not the issue key spelled out literally. The daemon encodes it as
25
- `legion-<project>-<KEY>-<role>`. For example, project `acme`, issue `LEGION-41`, role
26
- `architect` encodes to `legion-acme-LEGION-41-architect`. Never hand-format one for another
27
- role: your own role topic and your tree's architect's topic are stated at the end of your
28
- system prompt (a "Legion addressing" line the daemon appends), a sibling role's topic is
29
- yours with the trailing `-<role>` replaced, and the `roleToken` helper in `@legion/contracts`
30
- computes any other one exactly the way the daemon does — prefer a topic you've already
31
- been given before recomputing one.
25
+ `legion-<project>-<KEY>-<role>`. For example, project `acme`, issue `LEGION-41`, role `architect`
26
+ encodes to `legion-acme-LEGION-41-architect`. Never hand-format one for another role: your own role
27
+ topic and the topic of the architect that owns your issue are stated at the end of your system
28
+ prompt (a "Legion addressing" line the daemon appends: on a child issue that is the child's
29
+ sub-architect when one is claimed, else the root's), a sibling role's topic is yours with the
30
+ trailing `-<role>` replaced, and the `roleToken` helper in `@legion/contracts` computes any other
31
+ one exactly the way the daemon does — prefer a topic you've already been given before recomputing
32
+ one.
32
33
 
33
34
  If the handshake fails (a rejected boot token, or a bootstrap failure after your role
34
35
  registered), the extension logs it and exits the process outright — it does not retry, and
@@ -251,7 +252,7 @@ PR opens, and every later phase keeps it current rather than replacing it:
251
252
  ```
252
253
  ## Verification
253
254
 
254
- **CI:** `Tests` run <run-id> — jobs lint, pr-title, typecheck, test all success at <head-sha>.
255
+ **CI:** `Tests` run <run-id> — jobs lint, typecheck, test all success at <head-sha>; `PR Title` run <run-id> — job pr-title success at <head-sha>.
255
256
 
256
257
  **Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
257
258
  - Thread <id>: fixed in <commit-sha> — <one line>.
@@ -276,6 +277,7 @@ Verified the implementer's proof by <re-running its command | driving the same s
276
277
  **Fast-follow:** <one named cleanup item and where it will land>, or "none".
277
278
 
278
279
  **Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
280
+ **Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, rebase onto the new base and push — the new head runs Tests against the new merge result — and cite that run in the PR body.
279
281
  ```
280
282
 
281
283
  - **Threads are dispositioned individually, never resolved in bulk.** Every open review
@@ -301,11 +303,11 @@ Verified the implementer's proof by <re-running its command | driving the same s
301
303
  changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
302
304
  Findings about naming, duplication, or wording are batched into the single `Fast-follow`
303
305
  line instead of iterating per push.
304
- - **Rebase only on a real conflict.** Sami, 2026-09-11, verbatim:
306
+ - **Rebase only on a real conflict, except after a base retarget.** Sami, 2026-09-11, verbatim:
305
307
  "Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
306
- The implementer rebases the issue branch only when GitHub reports it `CONFLICTING` or the
307
- controller asks because of a conflict — never to pick up `main` or to refresh CI. A single
308
- failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
308
+ The implementer rebases the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
309
+ because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never rebase to
310
+ pick up `main` or refresh CI. A single failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
309
311
  pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
310
312
  confirmation, not a new round (see *The unchanged-diff check* below). Before rebasing, record
311
313
  the fingerprint at the current tip; after pushing the rebased branch, record it at the new
@@ -317,9 +319,10 @@ Verified the implementer's proof by <re-running its command | driving the same s
317
319
  field names naming, duplication, or wording cleanup only; anything that changes behaviour,
318
320
  hides an error, or breaks a gate lands in this PR.
319
321
  - **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
320
- The proof is the changed behaviour exercised on the surface a user reaches it through — a
321
- scratch daemon, a smoke rig, a sandbox repository, a real browser, a devN stack, a local stack
322
- with real migrations — with the exact command or run id, what was observed, the head SHA, and
322
+ The proof is the changed behaviour exercised on the surface a user reaches it through — the daemon's
323
+ test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the
324
+ operator's next daemon restart, recorded on the PR; a sandbox repository, a real browser, a devN stack,
325
+ or a local stack with real migrations — with the exact command or run id, what was observed, the head SHA,
323
326
  one negative control. The same proof goes into `.legion/implement.json` as its required `proof`
324
327
  array (`legion handoff write --phase implement` refuses a payload without one and names the
325
328
  field), and into the PR body, because the reviewer and the merger verify facts on GitHub and