@sjawhar/opencode-legion-envoy 3.11.0 → 3.11.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "3.11.0",
3
+ "version": "3.11.2",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -312,6 +312,12 @@ and is not re-staffed. A todo with a finished spec reads as queued work that nob
312
312
  (LEGION-173 sat in todo for two weeks with a complete spec; AGENTC-1010's v4 plan sat in backlog
313
313
  with nobody building it).
314
314
 
315
+ A close that says the defect cannot happen cites the code that makes it impossible. An issue
316
+ closed because a rewrite forecloses it names the file and line in the rewrite that does so; a
317
+ close that cannot name one is not foreclosed, it is unread. The cheapest way for a rewrite to reach
318
+ parity is to port the code, defect included: LEGION-211's bare `git worktree prune`, filed against
319
+ the TypeScript daemon, had been ported into the Go coordinator and was live in production.
320
+
315
321
  The audit finds four shapes:
316
322
 
317
323
  - **Unstaffed work.** A plan or measurement exists, and no one is building it.
@@ -28,7 +28,10 @@ NATS `>` matches **one or more** trailing tokens, so it does not match the lifec
28
28
  subject and its child events.
29
29
 
30
30
  For a typical push, this receives `pr.42` with `synchronize`, then any comments or reviews, then
31
- one `pr.42.checks` event when that head's checks settle. The family is `pr.42` (lifecycle),
31
+ one `pr.42.checks` event when that head's checks settle. A settlement is published for every
32
+ commit of the pull request whose checks settle, the current head or not (a head pushed with
33
+ GitHub's `skip-checks` trailer runs none, so the commit before it settles for it), and its `sha`
34
+ names the commit: compare it with the head you are waiting on. The family is `pr.42` (lifecycle),
32
35
  `pr.42.comment`, `pr.42.review`, `pr.42.mention`, and `pr.42.checks`. A closed lifecycle payload
33
36
  carries `merged`, `merge_commit_sha`, `merged_by`, and `head_sha`.
34
37
 
@@ -147,13 +150,14 @@ advertises.
147
150
  ## Waiting for CI or a merge
148
151
 
149
152
  Subscribe to `notifications.github.example-org.example-repo.pr.42.>` and end the turn. The single
150
- `pr.42.checks` event wakes you when the current head settles; a `pr.42` `closed` event with
153
+ `pr.42.checks` event whose `sha` is the current head wakes you when it settles (an earlier
154
+ commit's settlement can arrive first); a `pr.42` `closed` event with
151
155
  `merged: true` tells you the PR merged. Do not create `gh` pollers.
152
- Settlement waits for the head to be quiet for a few seconds, every reported check run to finish,
156
+ Settlement waits for the commit to be quiet for a few seconds, every reported check run to finish,
153
157
  and every recorded GitHub check suite to be `completed`. It covers those reported checks and suites
154
- for the head, not GitHub's required-checks set; until then, a silent subscription is normal.
158
+ for that commit, not GitHub's required-checks set; until then, a silent subscription is normal.
155
159
 
156
- Check settlement is at-least-once: a settlement can be followed by a `superseded_settlement: "true"` payload. Every settlement carries its attempt set `check_runs` — the latest GitHub check-run id per check name, sorted by name — plus the listener's `generation` (the record's state version) and `snapshot` (the record's hash). Consumers order same-head settlements by the attempt set, compared per shared name: no id lower and some id higher (or a new name — a new name counts as higher) is newer; every shared id equal and no new name is the same set; no id higher, no new name, and some id lower is older; anything else (a higher or new alongside a lower) is a mixed view and is dropped as a conflict (names only in the stored set are ignored — a check can vanish from GitHub's view, and a record recreated after the seven-day KV TTL starts sparse). Within one producer record per-name ids never decrease, and a consumer's fence is the per-name maximum over every view it has accepted — an accepted set merges into the fence, nothing is pruned — so the fence never decreases either: a newer attempt is newer whatever its completion time, no timestamps take part in ordering, and a name an incomplete view omitted cannot later reappear as new. At the same set the listener's `generation` orders its own settlements: lower is stale; equal is a duplicate when the `snapshot` matches and otherwise a conflict (an equal pair with a different snapshot cannot occur within one record's lifetime; a recreated record may reuse one and is dropped). A live settlement is a possibly incomplete view of the head (a missed webhook, a record recreated after the KV TTL): it decides the outcome of every name it reports — at any id the ordering accepted, including the same run observed in place — and says nothing about the rest: a known failure among them stands (the consumer keeps failure names, not a per-name status map), and the head is red while any failure remains. A consumer that reconciles a verdict from GitHub's rollup compares the rollup's attempt set the same way, but GitHub's read is complete: its failing check runs and failing commit statuses replace the stored ones wholesale. Statuses have no check run and the listener never sees them, so a consumer keeps them apart from check-run failures: a check run that shares a status's name cannot retire it — only GitHub does (likewise a deleted check's failure). A newer rollup set merges into the fence and takes the identity (no listener generation); the same set applies GitHub's verdict and keeps the listener identity for duplicate detection; an older, mixed, or empty-over-fenced set is ignored. A terminal read (green or red) then holds the tie at that set: a live settlement at the same set is accepted only if its effective outcome — the check-run failures it reports plus the stored ones it omits and the stored commit-status failures — agrees with the reconciled verdict, refreshing the listener identity without releasing GitHub's authority; a disagreeing one is stale whatever its generation until the set advances; a pending or cancelled-only read uncertifies a green head, leaves a red one untouched, and holds nothing — it releases any authority held at that set — so the terminal live settlement that follows applies at once, subject to the ordinary generation and duplicate rules (a replay or a lower generation still does not apply). Pending is therefore not a commutative join: a pending read after a live green uncertifies it until the next terminal view. Two remainders. An in-place conclusion change on an existing run id: GitHub's view stands and the listener's is recovered by the next successful, non-skipped read at that set — the dropped delivery is not replayed. A check whose highest run is deleted on GitHub: the fence keeps that id, so a rollup reporting a lower run under the same name is older until a newer run appears. A consumer that orders head changes by the PR's `updated_at` (GitHub's second resolution) accepts a read of a different head at an equal clock — a stale read returning the previous head within the same second as its replacement rewinds that consumer until its next accurate, non-skipped read. A head publishes only when at least one check has a positive run id; legacy checks without one remain in the status groups and failing names but not in `check_runs`. A legacy in-progress check whose completion is never observed holds the head unsettled until it reruns; rerun the affected check to release it.
160
+ Check settlement is at-least-once: a settlement can be followed by a `superseded_settlement: "true"` payload. Every settlement carries its attempt set `check_runs` — the latest GitHub check-run id per check name, sorted by name — plus the listener's `generation` (the record's state version) and `snapshot` (the record's hash). Consumers order settlements of one commit by the attempt set, compared per shared name: no id lower and some id higher (or a new name — a new name counts as higher) is newer; every shared id equal and no new name is the same set; no id higher, no new name, and some id lower is older; anything else (a higher or new alongside a lower) is a mixed view and is dropped as a conflict (names only in the stored set are ignored — a check can vanish from GitHub's view, and a record recreated after the seven-day KV TTL starts sparse). Within one producer record per-name ids never decrease, and a consumer's fence is the per-name maximum over every view it has accepted — an accepted set merges into the fence, nothing is pruned — so the fence never decreases either: a newer attempt is newer whatever its completion time, no timestamps take part in ordering, and a name an incomplete view omitted cannot later reappear as new. At the same set the listener's `generation` orders its own settlements: lower is stale; equal is a duplicate when the `snapshot` matches and otherwise a conflict (an equal pair with a different snapshot cannot occur within one record's lifetime; a recreated record may reuse one and is dropped). A live settlement is a possibly incomplete view of the head (a missed webhook, a record recreated after the KV TTL): it decides the outcome of every name it reports — at any id the ordering accepted, including the same run observed in place — and says nothing about the rest: a known failure among them stands (the consumer keeps failure names, not a per-name status map), and the head is red while any failure remains. A consumer that reconciles a verdict from GitHub's rollup compares the rollup's attempt set the same way, but GitHub's read is complete: its failing check runs and failing commit statuses replace the stored ones wholesale. Statuses have no check run and the listener never sees them, so a consumer keeps them apart from check-run failures: a check run that shares a status's name cannot retire it — only GitHub does (likewise a deleted check's failure). A newer rollup set merges into the fence and takes the identity (no listener generation); the same set applies GitHub's verdict and keeps the listener identity for duplicate detection; an older, mixed, or empty-over-fenced set is ignored. A terminal read (green or red) then holds the tie at that set: a live settlement at the same set is accepted only if its effective outcome — the check-run failures it reports plus the stored ones it omits and the stored commit-status failures — agrees with the reconciled verdict, refreshing the listener identity without releasing GitHub's authority; a disagreeing one is stale whatever its generation until the set advances; a pending or cancelled-only read uncertifies a green head, leaves a red one untouched, and holds nothing — it releases any authority held at that set — so the terminal live settlement that follows applies at once, subject to the ordinary generation and duplicate rules (a replay or a lower generation still does not apply). Pending is therefore not a commutative join: a pending read after a live green uncertifies it until the next terminal view. Two remainders. An in-place conclusion change on an existing run id: GitHub's view stands and the listener's is recovered by the next successful, non-skipped read at that set — the dropped delivery is not replayed. A check whose highest run is deleted on GitHub: the fence keeps that id, so a rollup reporting a lower run under the same name is older until a newer run appears. A consumer that orders head changes by the PR's `updated_at` (GitHub's second resolution) accepts a read of a different head at an equal clock — a stale read returning the previous head within the same second as its replacement rewinds that consumer until its next accurate, non-skipped read. A head publishes only when at least one check has a positive run id; legacy checks without one remain in the status groups and failing names but not in `check_runs`. A legacy in-progress check whose completion is never observed holds the head unsettled until it reruns; rerun the affected check to release it.
157
161
 
158
162
  ## When a subscription is silent
159
163
 
@@ -9,6 +9,9 @@ Retro is mandatory for every issue that passed review. The architect revives the
9
9
  implementer so the person with implementation context performs the retrospective, and the
10
10
  skill obtains a separate fresh-eyes perspective. Retro runs before merge.
11
11
 
12
+ Every path this skill cites (`packages/...`, `docs/...`) is in sjawhar/legion, the Legion
13
+ repository, which need not be the repository you are working in.
14
+
12
15
  ## Merge-gate ordering
13
16
 
14
17
  Follow this ordering exactly. It keeps the reviewed branch clean while preserving the
@@ -11,6 +11,9 @@ phase gets its own long-lived process against the same jj workspace, run in turn
11
11
  the phase assigned to you, report its completion to the architect, and leave the durable
12
12
  copy the next phase can trust.
13
13
 
14
+ Every path this skill cites (`packages/...`, `docs/...`, `AGENTS.md`) is in sjawhar/legion, the
15
+ Legion repository, which need not be the repository you are working in.
16
+
14
17
  ## Identity, scope, and role
15
18
 
16
19
  The daemon spawns you as a separate `omp --mode rpc` process (behind `legion worker-shim`,
@@ -102,8 +105,9 @@ committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legio
102
105
  5. `review.json`
103
106
 
104
107
  Read only files that precede the assigned phase. Every handoff is validated when it is read:
105
- `validatePhaseHandoff` (`packages/contracts/src/handoff-schema.ts`) checks the file, and the
106
- ledger (`packages/daemon/src/handoff/ledger.ts`) treats a file that fails validation as missing.
108
+ `validatePhaseHandoff` (`packages/contracts/src/handoff-schema.ts`) checks the
109
+ file, and the ledger (`packages/daemon/src/handoff/ledger.ts`) treats a file that
110
+ fails validation as missing.
107
111
  Undeclared fields pass validation untouched and reach the next worker; a declared field of the
108
112
  wrong type fails the whole file, so the `legion` tool's `handoff_read` returns null for that phase.
109
113
  Write the phase-specific fields the next phase and the architect need, consistent with what
@@ -141,6 +145,22 @@ Anything else, stop and send the owning architect the `jj -R "$LEGION_WORKSPACE"
141
145
  evidence; the architect decides, and an operator performs any operation-log restore with every
142
146
  other tree paused.
143
147
 
148
+ **Filesystem and process safety:** Your pane runs as the operator's own user, so one mistaken
149
+ path can destroy the machine every agent shares (on 2026-09-13 a probe script's leftover
150
+ `rm -rf "$work" "$HOME"` deleted the operator's SSH and signing keys and stopped every worker).
151
+ The extension refuses, before it runs, a `bash` command, `eval` code, or `hub` process start
152
+ (yours or a `task` subagent's) that would delete, move, truncate, overwrite an existing file by
153
+ redirection or `tee`, or `chmod -R`/`chown -R` anything outside `$LEGION_WORKSPACE` and any
154
+ directory below `/tmp` except `/tmp` itself, a glob over it, and its tmux and ssh socket
155
+ directories. It cannot tell which allowed `/tmp` directory belongs to your pane. It follows
156
+ `$HOME`, `~`, variables, `cd`, and the scripts a command runs. A target with no proven path prefix
157
+ is refused; an unknown trailing component under a prefix already proven inside your workspace or
158
+ permitted `/tmp` remains allowed. `pkill` and `killall` are refused, and `kill` only reaches a
159
+ process you started (a descendant of your Oh My Pi process): stop your own long-running processes
160
+ through the hub tool. The refusal names the target and the rule; do not rewrite a script just to
161
+ silence it. This is a mistake-guard rather than a sandbox. What it cannot read, a compiled program
162
+ or code whose paths are only known at run time, is still yours to keep inside the workspace.
163
+
144
164
  ## Phase work
145
165
 
146
166
  Specifications written into Dispatch follow `skill://dispatch`'s [Writing a spec](../dispatch/SKILL.md#writing-a-spec).
@@ -161,14 +181,19 @@ assignment, since `jj split`/`jj describe` keep its author). Never set or overri
161
181
  `user.name`/`user.email` in any jj or Git scope — not `jj config set`, not `--config`, not
162
182
  `git config`: `--config` outranks the pane environment and would put the wrong App back on your
163
183
  commits, and the repository-scoped jj config is one file shared by every issue workspace of the
164
- clone. Before a push, check
184
+ clone. Legion has two GitHub Apps, not one per role: your role's App is the **implement** App
185
+ if you are the implementer or the merger, and the **review** App if you are the planner, tester,
186
+ reviewer, or an architect (in Legion's own deployment, `legion-implementer[bot]` and
187
+ `legion-reviewer[bot]`). A planner's commits authored by the review App are right. Before a push,
188
+ check
165
189
  `jj -R "$LEGION_WORKSPACE" log -r 'main@origin..@' -T 'author.email() ++ " | " ++ committer.email() ++ " " ++ description.first_line() ++ "\n"'`
166
190
  shows your role's App in both columns **on every commit you made** — not on the whole list:
167
191
  earlier phases' commits are legitimately authored by their own role's App, and a conflict-forced
168
192
  rebase legitimately sets the committer of every rebased commit, other roles' included, to the
169
- rebaser. A wrong identity on your own commit is a pane-environment problem to report to the
170
- architect, not something to pin (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`,
171
- Hazard 1). Your session receives the credential capability it needs; invoke GitHub through the
193
+ rebaser. A wrong identity on your own commit, the other App or none, is a pane-environment
194
+ problem to report to the architect, not something to pin
195
+ (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`, Hazard 1).
196
+ Your session receives the credential capability it needs; invoke GitHub through the
172
197
  credential helper:
173
198
 
174
199
  ```bash
@@ -225,8 +250,8 @@ legion gh -- pr comment <pr-number> \
225
250
 
226
251
  The plan lives in `.legion/plan.json` and the Dispatch issue document; never commit a plan or spec file to the repository.
227
252
  No `docs/plans/*`, `docs/superpowers/plans/*`, or spec markdown goes into the pull request: plan
228
- and spec content goes into the issue, never into a PR (the root `AGENTS.md`'s `docs/plans/` row
229
- is human-authored design history, not a Legion artifact). A skill step that says "save the plan
253
+ and spec content goes into the issue, never into a PR (the root `AGENTS.md`
254
+ calls its own `docs/plans/` human-authored design history, not a Legion artifact). A skill step that says "save the plan
230
255
  to a file" is satisfied by the handoff write in the completion gate below; the planner's only
231
256
  commit is `plan: record handoff`.
232
257
 
@@ -652,9 +677,16 @@ This publishes your phase's completion to the architect's role and clears the da
652
677
  record of this issue's active phase. Do not add pipeline labels, run a controller loop, or
653
678
  invent a different completion protocol — this is the whole contract.
654
679
 
655
- A reviewer's phase ends with its completion, not with its review: submit the review on GitHub
656
- first, then commit the handoff and complete. The daemon moves the issue once both are in — the
657
- decision GitHub reports and your completion, in either order — so a review posted without a
680
+ A reviewer's phase ends with its completion, not with its review. A round that writes a handoff
681
+ takes this order: write, commit and push the handoff; submit the review of the head that push
682
+ made, by its SHA; then complete. An approval waits for the CI verdict to settle green at that head
683
+ before you submit it, since an approval stands only on green checks and GitHub can dismiss one
684
+ once the head moves, and a verdict that settles red there makes the round's decision a request for
685
+ changes naming the failing checks; a request for changes does not wait, since it stands whatever CI says and the
686
+ issue leaves reviewing with it. A review of a head the handoff push then replaces names a head
687
+ the pull request no longer has. A round that writes none (the final approval of the `.legion/`
688
+ deletion head) reviews the head as it is. The daemon moves the issue once both are in —
689
+ the decision GitHub reports and your completion, in either order — so a review posted without a
658
690
  completion leaves the issue in reviewing until you finish.
659
691
 
660
692
  **A refused completion is information, not a retry loop.** The daemon attributes your report to