@sjawhar/opencode-legion-envoy 6.10.1 → 6.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "6.10.1",
3
+ "version": "6.11.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
package/skills/AGENTS.md CHANGED
@@ -14,7 +14,7 @@ They are Markdown instructions loaded by Oh My Pi sessions; the daemon and the t
14
14
  | `legion-architect/` | root and sub-architects | tree ownership, decomposition, waves, gates, integration, sign-off |
15
15
  | `legion-controller/` | the controller root process | wake routing, keeping the admission slots full from `todo`, the daily report, escalation |
16
16
  | `legion-oracle/` | any role doing research | repository-grounded research |
17
- | `legion-retro/` | the implementer, at retro | the pre-merge retrospective, its Dispatch message, its last commit removing the issue's `.legion/<issue>/` from the head a human merges, and the PR body content the pull request's changed paths require at that head |
17
+ | `legion-retro/` | the implementer, at retro | the pre-merge retrospective, its Dispatch message, its last commit removing the issue's `.legion/<issue>/` from the head that merges, and the PR body content the pull request's changed paths require at that head |
18
18
  | `legion-worker/` | planner, implementer, tester, reviewer, merger | the phase contracts: handoffs, GitHub identity, PR body and READY discipline, the merge-gate order |
19
19
  | `thermonuclear-code-quality/` | the `thermonuclear-code-quality` agent | the maintainability rubric of the reviewer's pair (adapted from the MIT-licensed Thermos plugin in `cursor/plugins`; `LICENSE` beside it) |
20
20
  | `thermonuclear-deep-review/` | the `thermonuclear-deep-review` agent, and the reviewer (the Security Guidelines) | the correctness rubric of the reviewer's pair, with its tagged, diff-triggered Security Guidelines and the attack on the PR body's claims (adapted from the MIT-licensed Thermos plugin in `cursor/plugins`; `LICENSE` beside it) |
@@ -23,7 +23,7 @@ last day, and no pull request moving on GitHub (an owner working there leaves no
23
23
  The order keeper sweeps those. Never write the status of an issue that carries the `legion`
24
24
  label, or of any issue under one: the Legion daemon writes those statuses. A status your session
25
25
  writes on the root of a tree Legion is running is set back and the tree's architect told who wrote
26
- it; only a person in the dashboard, or `legion status`, stops that tree. On an issue under one, a
26
+ it; only a person in the dashboard, the controller's `set_status`, or the operator stops that tree. On an issue under one, a
27
27
  status that takes it out of the flow parks that issue and stops its workers.
28
28
 
29
29
  One agent keeps the backlog's order against those priorities. Setting an
@@ -16,8 +16,8 @@ separate coordinator to finish necessary work.
16
16
  (pattern `^[A-Z][A-Z0-9]*-[0-9]+$`, e.g. `LEGION-41`).
17
17
  - The daemon starts every Legion role itself and sequences each issue's phases from its fixed
18
18
  workflow table: planner, implementer, tester, reviewer, retro (the implementer again), merger,
19
- and, after a human merges, the implementer's production check. One role works an issue at a
20
- time, and the handoff or event that ends its phase is what starts the next; you start,
19
+ and, after the pull request merges, the implementer's production check. One role works an
20
+ issue at a time, and the handoff or event that ends its phase is what starts the next; you start,
21
21
  re-assign and order no worker. Message a known phase worker with `envoy_publish` to
22
22
  `notifications.role.` followed by its encoded role token. Phase workers escalate lifecycle,
23
23
  product, scope, design, and cross-phase decisions the same way: `envoy_publish` to your own
@@ -46,7 +46,11 @@ the merge credential. They override this skill's defaults where they conflict.
46
46
 
47
47
  ## 1. Decompose or adopt
48
48
 
49
- Inspect the root issue, acceptance criteria, existing children, and current handoffs.
49
+ Inspect the root issue, acceptance criteria, existing children, and current handoffs. Your issue
50
+ record is the `legion` tool's `read_record`; a child's committed handoff is a plain file on its
51
+ issue branch, read with `read` from that issue's workspace, `.legion/<child>/<phase>.json`, or
52
+ from GitHub:
53
+ `gh api repos/<owner>/<repo>/contents/.legion/<child>/<phase>.json?ref=legion/<child> --jq .content | base64 -d`.
50
54
  Decomposition is complete only when every child issue names the real surface its acceptance
51
55
  criteria are proven on and the repository skill that drives it; if the repository cannot
52
56
  exercise a criterion end to end, building that path is a child issue of this tree.
@@ -263,12 +267,18 @@ The daemon keeps this order from its fixed table; you start none of its steps:
263
267
  <tip-sha> --summary`, quoted in READY) and sends the READY packet with its completion, which
264
268
  refuses a head that still carries `.legion/<issue>/`; the daemon posts
265
269
  `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)` on the Dispatch
266
- issue and publishes it to the project's merge queue role when one is set. Legion never merges; a human merges under the repository's
267
- GitHub branch-protection and CODEOWNERS rules. If the merger reports a failed verification,
270
+ issue and publishes it to the project's merge queue role when one is set. The moment the daemon
271
+ accepts READY, the merger submits the pull request with
272
+ `gh pr merge <n> -R <owner>/<repo> --auto --squash --match-head-commit <head>` and tells you what
273
+ the command answered; the repository's required reviews and checks decide when it lands, and
274
+ Legion neither reads nor writes those rules. A READY the design gate refused is submitted later:
275
+ the merger submits nothing on a refusal, so once that spec version is approved and the daemon
276
+ posts the packet, tell the merger to submit it. If the merger reports a failed verification,
268
277
  treat it like `pr-blocked`: the merger holds the phase, so tell it to move the issue back with
269
278
  `request_backward_move`, naming what failed; never bypass. A READY refused because the head
270
279
  still carries `.legion/<issue>/` goes back to `retro`, so the implementer's retro removes it.
271
- 5. a human merges; the daemon then starts the **implementer** once more, on the production check.
280
+ 5. the merge lands under the repository's rules; the daemon then starts the **implementer** once
281
+ more, on the production check.
272
282
  It drives the changed path in production through the user's own access path and records
273
283
  what it saw on the pull request and on this issue. Sign off only after the implementer's production
274
284
  report exists. A defect it finds is a corrective child issue of this tree, not a note on a
@@ -286,12 +296,12 @@ with `request_backward_move`, and the daemon runs the phases from there: the imp
286
296
  the bookmark forward with the
287
297
  destination (the forward-merge procedure in `skill://legion-worker/references/conflicts-and-rewrites.md` — `jj new legion/<KEY> <destination>`,
288
298
  never a rebase, since a rebase rewrites every descendant of the chain's fork point, including
289
- another tree's branch stacked on it), pushes it with the ordinary push procedure (a genuine
290
- fast-forward), and posts the before/after fingerprints; the tester re-runs the bare gates only;
299
+ another tree's branch stacked on it), pushes it as every role pushes (`jj git push`; a genuine
300
+ fast-forward, and a code push, so no `skip-checks: true` line), and posts the before/after fingerprints; the tester re-runs the bare gates only;
291
301
  the reviewer confirms and approves the new head by SHA (or continues its round if it had not
292
302
  approved); the daemon carries the issue on through retro to the merger, whose new READY packet the daemon posts.
293
303
  This merge happens only when GitHub reports `CONFLICTING`
294
- (`legion gh -- pr view <n> --json mergeable,mergeStateStatus`); read that on every end-game
304
+ (`gh pr view <n> --json mergeable,mergeStateStatus`); read that on every end-game
295
305
  wake — `phase-finished`, `catch-up`, `checks-red`, `review-stuck` — because a `CONFLICTING`
296
306
  PR gets no CI and no wake announces it. The moment you see it, tell the worker holding the issue's
297
307
  phase (`envoy_publish` to its role topic) to move the issue back to `implementing` with
@@ -299,19 +309,14 @@ phase (`envoy_publish` to its role topic) to move the issue back to `implementin
299
309
  naming the conflict for the human who merges. Do not let the merger send a READY packet for an
300
310
  obsolete approval.
301
311
 
302
- If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
303
- to resolve, open a `dispatch ask` that names the thread's URL and GitHub's message for a human to
304
- resolve it by hand, with options for resolved / could not; the merger does not complete while it
305
- is open. That is the one review-thread step a human takes: the review App cannot resolve a thread
306
- on a pull request the implementer opened, and the implementer's and merger's runs of the command
307
- close every accepted one.
308
-
309
- If a reviewer reports that `legion threads resolve` exited 1 counting threads that hold the
310
- implement App's pending draft (the daemon counts them and never names them), tell the worker
311
- holding the issue's phase (`envoy_publish` to its role topic) to move the issue back to
312
- `implementing` with `request_backward_move`, naming the count, so the implementer submits or
313
- discards its pending review; its own run of the command names those threads `left open … an
314
- unsubmitted draft in a pending review`. Open no ask for it: the implementer clears it.
312
+ If the implementer reports that GitHub refused to resolve a review thread — its own
313
+ `resolveReviewThread`, on a thread it answered or one the reviewer named as accepted — open a
314
+ `dispatch ask` that names the thread's id or URL and GitHub's message for a human to resolve it by
315
+ hand, with options for resolved / could not; the merger does not complete while it is open. That
316
+ is the one review-thread step a human takes: GitHub lets only the pull request author's App
317
+ resolve a thread, so the implementer resolves every thread with plain `gh`, the ones it answers
318
+ and the bot threads the reviewer accepted and named to it (an Envoy message to the implementer's
319
+ role topic, or its review body); the reviewer resolves none.
315
320
 
316
321
  ## 7. Close
317
322
 
@@ -16,36 +16,37 @@ The Legion extension registers this session with the daemon as the controller an
16
16
  `legion-<project>-controller` during session startup. Do not handle a wake unless that startup
17
17
  succeeded.
18
18
 
19
- The session carries no GitHub credential: its GitHub token variables are emptied, and both
20
- `legion gh -- <args>` and `legion threads resolve` are refused. The controller reads Dispatch and
21
- applies its controller capability with `legion status <KEY> <status>`; it never reads GitHub or
22
- merges a pull request.
19
+ The session carries no GitHub credential: its GitHub token variables are emptied and its
20
+ `GH_CONFIG_DIR` names a directory holding no login, so `gh` acts as nobody. The controller reads
21
+ Dispatch and the daemon's state, and applies its controller capability, through the `legion` tool
22
+ alone: `op: "read_state"` returns the daemon's whole state (`issues.<KEY>.phase`,
23
+ `issues.<KEY>.holdReason`, `issues.<KEY>.architect`, `admission.cap`, `admission.active`,
24
+ `admission.waiting`, `capabilities[]`, `daemon.project`), and `op: "set_status"` with `issue` and
25
+ `status` (`todo`, `backlog` or `icebox`) moves an issue. It never reads GitHub, and it runs no
26
+ `legion` from bash.
23
27
 
24
28
  For an interactive takeover from a hand-started OMP session, start OMP with
25
29
  `LEGION_CONTROLLER_SECRET` (or `LEGION_CONTROLLER_SECRET_FILE`, a path to a file holding it),
26
- `LEGION_DAEMON_URL`, `LEGION_PROJECT` (the daemon's project), `LEGION_STATE_DIR`, and `LEGION_GRANT_FILE`
27
- (an absolute path to a file only you can read, under a 0700 directory; the extension writes
28
- each command's grant there and every `bash` call is blocked without it) in its environment. Do
29
- not set `LEGION_CONTROLLER=1` — that marker is the launched controller's own (`legion controller
30
- start`'s session, or the daemon's pod), and a session carrying it claims at startup. Then run:
30
+ `LEGION_DAEMON_URL`, `LEGION_PROJECT` (the daemon's project) and `LEGION_STATE_DIR` in its
31
+ environment. Do not set `LEGION_CONTROLLER=1` — that marker is the launched controller's own
32
+ (`legion controller start`'s session, or the daemon's pod), and a session carrying it claims at
33
+ startup. Then run:
31
34
 
32
35
  ```text
33
36
  /legion-claim-controller
34
37
  ```
35
38
 
36
39
  The command checks that `LEGION_PROJECT` is the daemon's project, registers this session with the
37
- daemon, and claims the Envoy role for it before controller commands can act. From then on this session's
38
- shell commands are wrapped with a controller grant, but the grant holds no GitHub credential;
39
- `legion status <KEY> <status>` works through the controller secret in this session's environment
40
- (`LEGION_CONTROLLER_SECRET` or its `_FILE`), not the grant — if it fails, that is the variable to check.
41
- The takeover moves the role and the daemon's recorded session id to this session. Never pass a
42
- secret as a command argument or copy it into a transcript. The Envoy registration heartbeat keeps
43
- the role afterwards, so `/legion-claim-controller` is the manual override, not a routine step after
44
- a listener restart.
40
+ daemon, and claims the Envoy role for it; the `legion` tool's controller operations then work in
41
+ this session, each `set_status` minting its grant in-process from the registration's secret, which
42
+ holds no GitHub credential. The takeover moves the role and the daemon's recorded session id to
43
+ this session. Never pass a secret as a command argument or copy it into a transcript. The Envoy
44
+ registration heartbeat keeps the role afterwards, so `/legion-claim-controller` is the manual
45
+ override, not a routine step after a listener restart.
45
46
 
46
47
  Two limits of a takeover session. It caches the controller secret it started with: the next
47
- `legion controller start` mints a new secret, every `bash` call in the takeover session then fails
48
- with a 403 from the grant mint, and the fix is to start a fresh OMP with the new secret, not to
48
+ `legion controller start` mints a new secret, every `set_status` in the takeover session then
49
+ fails with a 403 from the grant mint, and the fix is to start a fresh OMP with the new secret, not to
49
50
  retry. And the role does not follow `/new` or `/fork` in a takeover session — without
50
51
  `LEGION_CONTROLLER=1` the new session is not a Legion session to the extension — so after either
51
52
  command run `/legion-claim-controller` again.
@@ -65,7 +66,7 @@ message, so run the start procedure each time one arrives. A human reaches you t
65
66
  message to your session on the Agents page, a reply to an ask you opened, a mention) or Envoy. A
66
67
  plain user turn in your session other than the start message or a task an operator sent with
67
68
  `legion claims deliver` is a person writing from Dispatch's Agents page: answer it first, in the
68
- conversation. `legion state` and `legion status <KEY> <status>` work as below. There is no
69
+ conversation. The `legion` tool's `read_state` and `set_status` work as below. There is no
69
70
  `legion controller start` against this daemon, and nobody can replace you with one.
70
71
 
71
72
  ### Started by the operator
@@ -74,7 +75,7 @@ Under `controller: operator`, the default, the daemon launches no controller: th
74
75
  `legion controller start --config controller.yaml [--daemon-url <url>]` on
75
76
  their own machine, and you are that foreground OMP session. The command fetched a fresh controller
76
77
  secret from the daemon with the operator's token, wrote it to a 0600 file under `LEGION_STATE_DIR`
77
- (`~/.local/state/legion/<project>-controller` by default) beside the `gh` shim and the `legion`
78
+ (`~/.local/state/legion/<project>-controller` by default) beside the `legion`
78
79
  launcher, and started you with `LEGION_CONTROLLER=1` and the controller's environment. The
79
80
  extension registers on `/legion/v1/claims/register` with the
80
81
  secret, claims the role, then subscribes to `notifications.legion.<project>.controller`, where the
@@ -84,8 +85,8 @@ daemon's own (`kubernetes` or `tmux`). The daemon reads your liveness from the E
84
85
  (the holder of `legion-<project>-controller` and its `last_seen`): keep the session running.
85
86
  Exiting it leaves the project without a controller until the operator runs the command again —
86
87
  the daemon logs `controller not registered; run legion controller start` once per
87
- boot-timeout interval and launches nothing itself. `legion state` and `legion status <KEY>
88
- <status>` work here over `LEGION_DAEMON_URL`. A second `legion controller start` replaces you: it
88
+ boot-timeout interval and launches nothing itself. The `legion` tool's `read_state` and
89
+ `set_status` work here over `LEGION_DAEMON_URL`. A second `legion controller start` replaces you: it
89
90
  mints a new secret, so your grants stop working and the role moves to the new session.
90
91
 
91
92
  ### What happened before you started (Go daemon)
@@ -100,7 +101,7 @@ procedure with nothing typed.
100
101
  At every start, after the claim recheck ([Turn discipline](#turn-discipline)) and before anything
101
102
  else:
102
103
 
103
- 1. Read `legion state --json` and handle each issue whose `issues.<KEY>.phase` is `held` (its
104
+ 1. Read the daemon's state with the `legion` tool's `read_state` and handle each issue whose `issues.<KEY>.phase` is `held` (its
104
105
  `issues.<KEY>.holdReason` is `escalated` when its architect sent it to you, and absent while the
105
106
  architect is still deciding or while its tree lingers or is closed, where the hold waits for the
106
107
  tree's re-admission and needs nothing from you), and each tree root whose
@@ -114,7 +115,7 @@ else:
114
115
  and follow `Links:` up through each `child_of` parent (a `child_of` under `Referenced by:` is a
115
116
  child of this issue, not its parent). Leave a child that has an ancestor in `admission.active`
116
117
  or `admission.waiting`: that tree's architect owns it. Triage every other row as a root,
117
- children outside a live tree included, when `legion state --json` does not record it under
118
+ children outside a live tree included, when `read_state` does not record it under
118
119
  `issues`. A root recorded there and now in `triage` is work the daemon holds that a human
119
120
  pulled back: never re-admit it yourself; name it in your summary to the human ("<KEY> was
120
121
  pulled back to triage; what do you want?").
@@ -135,7 +136,7 @@ labels, so a human adds it from the issue header). Two parties hand work over: a
135
136
  the label from the issue header, and you, when you fill a free slot (below). You label an issue
136
137
  only when you take it or file it for Legion, so the label means Legion has the issue or had it.
137
138
  A person's label is their decision: never take it off. A child needs no label: it runs under its
138
- tree's architect once its root is admitted. `legion status <KEY> todo` admits a root, or a child
139
+ tree's architect once its root is admitted. `set_status` to `todo` admits a root, or a child
139
140
  outside a live tree (admitted as a root of its own), only while it carries the label, so an issue
140
141
  you hand over or file for Legion to run carries it first (`--label` on `dispatch issue-update` or
141
142
  `dispatch issue`). Taking the label off a waiting root drops it from the waiting line; taking it
@@ -150,7 +151,7 @@ included, which comes on its interval even with every slot taken: read each root
150
151
  `admission.active` whose `issues.<KEY>.phase` is still `admitted` with `dispatch read`. When its
151
152
  `Claimed by:` line is `nobody` or ends `· not running`, tell that tree's architect to claim again
152
153
  with `envoy_publish` to `notifications.role.` followed by its claim token,
153
- `issues.<KEY>.architect.locator.claim` in `legion state --json`. A claim that is its architect's
154
+ `issues.<KEY>.architect.locator.claim` in `read_state`. A claim that is its architect's
154
155
  own, or one that still holds, needs nothing.
155
156
 
156
157
  ## Keeping the slots full (Go daemon)
@@ -174,7 +175,7 @@ unsent:
174
175
  claim](#trees-waiting-on-a-root-claim-go-daemon) all get a turn from it. Its payload's
175
176
  `openCapabilities`, when present, names the deployment capabilities (the secrets broker, model
176
177
  fallback, every role's CPU and memory limits) the deployment's own configuration leaves open with
177
- no decision recorded. `legion state --json` lists every row under `capabilities`, each open one
178
+ no decision recorded. `read_state` lists every row under `capabilities`, each open one
178
179
  with its `detail` and the `configLine` to write into `legion.yaml`
179
180
  (`capabilities.decided.<name>: "<reason>"`); name them in the day's report. A gap is the
180
181
  operator's to close or to decide, never yours, and it never stops the walk.
@@ -186,7 +187,7 @@ are free. When they narrow the scope (a repository, a kind of change), a candida
186
187
  skipped (the last row of the table below). With no scope stated, every issue of the project is in
187
188
  scope.
188
189
 
189
- **How many.** Read `legion state --json`. The free slots are `admission.cap` minus the roots in
190
+ **How many.** Read the daemon's state with `read_state`. The free slots are `admission.cap` minus the roots in
190
191
  `admission.active` and in `admission.waiting` (a waiting root takes the next slot before anything
191
192
  you add). With none free, stop.
192
193
 
@@ -211,7 +212,7 @@ dispatch issues --project <PROJECT> --status todo --priority 0 --limit 250 --off
211
212
  When the first line ends `(showing 1-250 of N)`, the next page is `--offset 250`, then `500`. Read
212
213
  pages only as far as you need: stop listing once the free slots are filled. `<PROJECT>` is the
213
214
  Dispatch project key, the prefix of this deployment's issue keys (`PROJ-12` → `PROJ`), which is
214
- also `daemon.project` in `legion state --json`: the project key exactly as `legion.yaml` writes it.
215
+ also `daemon.project` in `read_state`: the project key exactly as `legion.yaml` writes it.
215
216
  A row that shows `claimed by …` and does not end its claim with `· not running` (the route, when
216
217
  the row shows one, comes after the claim) is claimed, as the table below says: skip it without
217
218
  reading it.
@@ -225,9 +226,9 @@ leans on `External links:`, and the label row is the one that never depends on h
225
226
  |---|---|
226
227
  | It carries the `legion` label | `Labels:` lists `legion`, in any case. Legion has the issue or had it, as **Candidates** above says. |
227
228
  | It has any child | `dispatch read --ref dispatch://<KEY>/children` lists any child, open or `done`. It is an umbrella, and a finished umbrella is still no leaf. That also skips an issue whose only child is done, which is accepted. Its open children are candidates themselves, each in its own place in the order. |
228
- | An ancestor is Legion's | Follow `Links:` up through each `child_of` parent, reading each one, and skip when any ancestor carries the `legion` label or is recorded under `issues` in `legion state --json`, whatever its status: a Legion tree, running or parked, owns its children. An ancestor's claim or route does not skip the issue: a coordinator holding an umbrella files `todo` leaves for others to pick up, and the claim on the issue itself is what keeps two sessions off the same work. A `child_of` under `Referenced by:` is a child of this issue, not its parent. |
229
+ | An ancestor is Legion's | Follow `Links:` up through each `child_of` parent, reading each one, and skip when any ancestor carries the `legion` label or is recorded under `issues` in `read_state`, whatever its status: a Legion tree, running or parked, owns its children. An ancestor's claim or route does not skip the issue: a coordinator holding an umbrella files `todo` leaves for others to pick up, and the claim on the issue itself is what keeps two sessions off the same work. A `child_of` under `Referenced by:` is a child of this issue, not its parent. |
229
230
  | Someone is designing it | `Open asks:` lists any ask, a `Spec approval: awaiting …` line shows the spec waits on a human, or `Events:` show an `artifact.version` or an `ask.opened` from the last seven days: a session or a person is shaping it even when nobody claims or routes it. |
230
- | Legion ran it without the label now on it | `legion state --json` records it under `issues`, whatever its status, or `Events:` show a status write by `session legion-daemon:<PROJECT>`, the daemon's actor on every `legion status` (yours included) and on its own `in_progress` at admission. That covers a root a person took the label off, and one that ran before the daemon required the label and never had it. Name each one you skip for this in your summary. The walk never sends a root Legion already ran back into Legion: a person does that with the label and `todo`, and you do it only when a wake below says to (`worker-died`). |
231
+ | Legion ran it without the label now on it | `read_state` records it under `issues`, whatever its status, or `Events:` show a status write by `session legion-daemon:<PROJECT>`, the daemon's actor on every status it writes (your `set_status` included) and on its own `in_progress` at admission. That covers a root a person took the label off, and one that ran before the daemon required the label and never had it. Name each one you skip for this in your summary. The walk never sends a root Legion already ran back into Legion: a person does that with the label and `todo`, and you do it only when a wake below says to (`worker-died`). |
231
232
  | A running session or a person claims it | `Claimed by:` names anyone and does not end `· not running`. `· liveness unknown` counts as claimed: the agent registry could not be read, so nothing says the holder stopped. A claim ending `· not running` has lapsed, and the issue is free. |
232
233
  | Its route reaches a running session | `Route:` names a route with nothing after it, or with `(held by …)`. `(nobody holds it right now)` and `(that session is not running right now)` reach nobody; `(the Envoy listener did not answer, …)` counts as reaching someone. `Route: none` is free. |
233
234
  | A pull request is linked or named | `External links:` lists a pull request (kind `github_pr`, or a URL ending `/pull/<n>`), or a comment or message among `Events:` names one. You cannot read GitHub, so an open, merged, or closed pull request all count. A person who wants Legion on it anyway hands it over themselves: the label, then `todo`. |
@@ -255,7 +256,7 @@ leans on `External links:`, and the label row is the one that never depends on h
255
256
  EOF
256
257
  ```
257
258
 
258
- The daemon admits each root when Dispatch's event reaches it; the next `legion state --json`
259
+ The daemon admits each root when Dispatch's event reaches it; the next `read_state`
259
260
  shows it in `admission.active`. When the candidates run out with slots still free, leave those
260
261
  slots free and say so in your summary.
261
262
 
@@ -276,7 +277,7 @@ dispatch issue --project <PROJECT> --title 'Legion daily report' --spec-file - <
276
277
 
277
278
  Legion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. This issue is not work, so it carries no `legion` label and stays in icebox.
278
279
  EOF
279
- legion status <report KEY> icebox
280
+ legion({ op: "set_status", issue: "<report KEY>", status: "icebox" })
280
281
  ```
281
282
 
282
283
  **When.** On your first turn of each UTC day, whatever it is: your start, or a wake of any kind.
@@ -290,7 +291,7 @@ every pull request by its URL.
290
291
 
291
292
  - **Finished.** `dispatch issues --project <PROJECT> --status done --updated-since <the
292
293
  previous report's time, or 24 hours ago> --limit 250`, read every page, and keep the issues
293
- `legion state --json` records under `issues` whose `issue.closed` event is after the previous
294
+ `read_state` records under `issues` whose `issue.closed` event is after the previous
294
295
  report. For each, `dispatch read` it: the pull request under `External links:` is the one that
295
296
  merged.
296
297
  - **Closed without a change.** Those with no pull request, each with the reason its closing
@@ -298,12 +299,12 @@ every pull request by its URL.
298
299
  - **Running.** Each root in `admission.active` with its `issues.<KEY>.phase`, and the roots in
299
300
  `admission.waiting`. A root whose architect told you its claim was refused is named as waiting
300
301
  on that holder: its architect started nothing and asked them to release it or take the issue
301
- back. Nothing in `legion state` records that, so read the tree's issue (its `Claimed by:` line
302
+ back. Nothing in `read_state` records that, so read the tree's issue (its `Claimed by:` line
302
303
  and the ask or message the architect opened) before you name it.
303
304
  - **The slots and the walk.** The free slots, as **How many** counts them, then this turn's walk
304
305
  when it made one: what it took, and how many candidates each row of the table skipped, so a
305
306
  reader can see why a free slot stays empty.
306
- - **Open capabilities.** Each row of `capabilities` in `legion state --json` whose `status` is
307
+ - **Open capabilities.** Each row of `capabilities` in `read_state` whose `status` is
307
308
  `open` (the `tick` payload's `openCapabilities`): its name, its `detail`, and its `configLine`,
308
309
  the `legion.yaml` line that records a decision. The operator closes or decides it; you only
309
310
  report it, and a report with none says nothing of them.
@@ -341,19 +342,19 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
341
342
 
342
343
  | Wake | Content | Controller action |
343
344
  |---|---|---|
344
- | New issue created in the Dispatch project, status `triage`: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above); the start procedure above catches one sent while no controller ran | issue key + triage context (incl. pre-existing children) | Triage: `legion status <KEY> todo` to admit, or set `backlog`/`icebox` to park |
345
- | `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `legion state --json`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
345
+ | New issue created in the Dispatch project, status `triage`: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above); the start procedure above catches one sent while no controller ran | issue key + triage context (incl. pre-existing children) | Triage: `set_status` to `todo` to admit, or to `backlog`/`icebox` to park |
346
+ | `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `read_state`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
346
347
  | `todo on <KEY>` from the Go daemon (payload `{kind: "todo"}`) | an issue not handed to Legion that changed while in `todo` and a slot stood free, sent half a minute later | Verify a free slot, then walk the whole `todo` list ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
347
- | `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `legion state --json` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides it) |
348
+ | `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `read_state` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides it) |
348
349
  | Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `dispatch ask` only for a human to-do |
349
350
  | Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
350
- | `held on <KEY>` from the Go daemon (payload `{kind: "held", phase, role?, reason?}`) | the held issue, the phase it left, and the role whose claim failed, or `reason: "escalated"` | Verify the hold in `legion state` (the issue's phase is `held`). Without `reason`, a phase worker's launches or prompts ran out and the tree's architect decides retry or escalate: no action. With `reason: "escalated"` (on the record, `issues.<KEY>.holdReason` is `escalated`), the architect sent it to you: handle it as an architect escalation below. Parking the tree is `legion status <root> backlog`; setting the root back to `todo` later re-admits it as a new generation, which starts again from its architect |
351
- | `worker-died on <KEY>` from the Go daemon with `role: "architect"` | the tree root whose architect's claim failed, and the phase the root was in | The tree's architect ran out of launches or prompts and the daemon relaunches nothing; every other notice of the tree goes to that architect, so nobody inside the tree can act. Verify in `legion state` (`issues.<KEY>.architect.state` is `failed`); if the root's phase is `done`, the tree is already parked: no action. Otherwise re-admit the tree (`legion status <root> backlog`, then `todo`: a new generation, whose architect starts again with fresh budgets) or leave it parked and say why on the issue |
351
+ | `held on <KEY>` from the Go daemon (payload `{kind: "held", phase, role?, reason?}`) | the held issue, the phase it left, and the role whose claim failed, or `reason: "escalated"` | Verify the hold in `read_state` (the issue's phase is `held`). Without `reason`, a phase worker's launches or prompts ran out and the tree's architect decides retry or escalate: no action. With `reason: "escalated"` (on the record, `issues.<KEY>.holdReason` is `escalated`), the architect sent it to you: handle it as an architect escalation below. Parking the tree is `set_status` of the root to `backlog`; setting the root back to `todo` later re-admits it as a new generation, which starts again from its architect |
352
+ | `worker-died on <KEY>` from the Go daemon with `role: "architect"` | the tree root whose architect's claim failed, and the phase the root was in | The tree's architect ran out of launches or prompts and the daemon relaunches nothing; every other notice of the tree goes to that architect, so nobody inside the tree can act. Verify in `read_state` (`issues.<KEY>.architect.state` is `failed`); if the root's phase is `done`, the tree is already parked: no action. Otherwise re-admit the tree (`set_status` of the root to `backlog`, then to `todo`: a new generation, whose architect starts again with fresh budgets) or leave it parked and say why on the issue |
352
353
  | Direct user message | — | Always first |
353
354
 
354
355
  ## New issue triage
355
356
 
356
- 1. Read `legion state --json`, then inspect the reported Dispatch issue with `dispatch read`.
357
+ 1. Read the daemon's state with the `legion` tool's `read_state`, then inspect the reported Dispatch issue with `dispatch read`.
357
358
  Verify the issue is in this project, is eligible for a root process, and whether it
358
359
  has pre-existing children. Dispatch and daemon state, not the wake text, decide triage.
359
360
  Note the `Assignee:` line: that human answers the tree's asks, and their Inbox opens on
@@ -362,14 +363,14 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
362
363
  2. If it should run now, admit the root issue:
363
364
 
364
365
  ```text
365
- legion status <issue> todo
366
+ legion({ op: "set_status", issue: "<issue>", status: "todo" })
366
367
  ```
367
368
 
368
369
  3. If it should deliberately wait, move it to a parked status instead of leaving it in
369
370
  `triage`:
370
371
 
371
372
  ```text
372
- legion status <issue> backlog
373
+ legion({ op: "set_status", issue: "<issue>", status: "backlog" })
373
374
  ```
374
375
 
375
376
  (or `icebox` for longer-term deferral). Dispatch status is the durable record; there is no
@@ -410,10 +411,10 @@ and the Dispatch issue. If the work belongs in an independent root:
410
411
  1. File a **fresh root issue** with `dispatch issue --project <KEY> --title '<title>' --spec-file -`
411
412
  (no `--parent`; add `--label legion`, without which it is never admitted).
412
413
  `--project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`): the
413
- project key exactly as `legion.yaml` writes it, which `legion state --json` shows as
414
+ project key exactly as `legion.yaml` writes it, which `read_state` shows as
414
415
  `daemon.project`. It is not the lowercase project token in role names such as
415
416
  `legion-<project>-controller`.
416
- 2. Park the child (`legion status <child> icebox`) and leave
417
+ 2. Park the child (`set_status` to `icebox`) and leave
417
418
  a pointer to the new root issue. The controller's capability is `todo`/`backlog`/`icebox`
418
419
  only — only the owning architect or the daemon closes an issue as `done`.
419
420
  3. Admit or deliberately backlog the new root through the normal triage procedure.
@@ -30,8 +30,8 @@ retrospective's durable output.
30
30
  `jj diff --from <approved-sha> --to <tip-sha> --summary` in READY, and sends the READY packet
31
31
  with its completion, which refuses a head that still carries `.legion/<issue>/`; the daemon
32
32
  posts it on the Dispatch issue and publishes it to the project's merge-queue role when one is
33
- configured. A human merges under the repository's GitHub branch-protection and CODEOWNERS
34
- requirements; GitHub's merge queue participates only when the repository enables it.
33
+ configured. On READY's acceptance the merger submits the merge, and it lands under the
34
+ repository's rules: its required reviews and checks, and its merge queue when it enables one.
35
35
  5. After that merge, the implementer — not the reviewer or merger — verifies the change in production
36
36
  and records it on the PR and the issue: the agent that developed it is responsible for testing
37
37
  in production. The architect's sign-off waits for that record.
@@ -139,11 +139,11 @@ merger.
139
139
  empty. `pipefail` makes a failed read fail the line (without it the line exits with `tee`'s
140
140
  status and leaves two empty files), and the parentheses keep it to that line, since your
141
141
  session's shell persists and a later `cmd | head` would exit 141 under it:
142
- `cd -- "$LEGION_WORKSPACE" && ( set -o pipefail && legion gh -- api repos/{owner}/{repo}/pulls/{number} --jq .body | tee <dir>/before.md <dir>/body.md ) && test -s <dir>/before.md`.
142
+ `cd -- "$LEGION_WORKSPACE" && ( set -o pipefail && gh api repos/{owner}/{repo}/pulls/{number} --jq .body | tee <dir>/before.md <dir>/body.md ) && test -s <dir>/before.md`.
143
143
  In `<dir>/body.md`, change the lines it carries for that content and add any it lacks where
144
144
  the instructions place them; other phases' lines stay as they wrote them.
145
145
  4. Write it back before the push:
146
- `cd -- "$LEGION_WORKSPACE" && legion gh -- api --method PATCH repos/{owner}/{repo}/pulls/{number} -F body=@<dir>/body.md`.
146
+ `cd -- "$LEGION_WORKSPACE" && gh api --method PATCH repos/{owner}/{repo}/pulls/{number} -F body=@<dir>/body.md`.
147
147
  The checks the push starts read the body as it stands then; an edit after the push re-runs
148
148
  only a check that also starts on an edited body. Such a check re-runs now at the approved
149
149
  head, on a diff without your commits, and may fail there; READY reads the head your push makes.
@@ -153,12 +153,17 @@ When you cannot compute that content, cannot do the work a line affirms within
153
153
  nothing and do not complete: tell the architect with `envoy_publish` to its role topic, naming
154
154
  what failed.
155
155
 
156
- Then push both commits with one `legion push`, so one CI run covers them. When the push is
157
- refused after step 4 wrote the body, write `<dir>/before.md` back the same way before you report
158
- the refusal to the architect, so the body matches the head GitHub has. After the push, post one
156
+ Then push both commits with one push, so one CI run covers them (`<KEY>` is your `LEGION_ISSUE`):
157
+ `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> -r @- && jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>`.
158
+ It carries `docs/solutions/` and the removal of `.legion/<KEY>/`, so it is not a handoff-only
159
+ push: its head's message never ends with `skip-checks: true` and carries none of GitHub's
160
+ `[skip ci]`-family keywords (`skill://legion-worker`, *Every role pushes its own commits*). When
161
+ the push is refused after step 4 wrote the body (`jj git push` refuses a branch another role
162
+ moved; never force), write `<dir>/before.md` back the same way before you report the refusal to
163
+ the architect with its output, so the body matches the head GitHub has. After the push, post one
159
164
  Dispatch message on
160
165
  the issue — `--issue` is your `LEGION_ISSUE`; Legion issues live on Dispatch, never on a GitHub
161
- issue, and the `gh` shim refuses every GitHub-issue write — naming the documents, the
166
+ issue, so no `gh issue` write and no GitHub-issue comment — naming the documents, the
162
167
  one-to-three most useful takeaways, the two proofs you read, and the production check that
163
168
  follows the merge. The message must carry this revived implementer's structured
164
169
  attribution footer with `phase` set to `retro`; the body is capped at 2,000 characters:
@@ -185,7 +190,8 @@ The `docs/solutions/` commit, the removal of `.legion/<issue>/`, the PR body lin
185
190
  make stale, and the Dispatch message are the only retro outputs. Never write a handoff, phase
186
191
  artifact, local feedback log, or completion label, and change nothing under `.legion/` but that
187
192
  removal. Report completion with the `legion` tool's `handoff_complete` alone (its summary: two
188
- sentences for the architect) — no `handoff_write`.
193
+ sentences for the architect); retro writes no handoff, so the commit it reports is the one your
194
+ workspace stands on, the removal commit your push made, so push before you complete.
189
195
 
190
196
  ## Completion check
191
197