@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 +1 -1
- package/skills/AGENTS.md +1 -1
- package/skills/dispatch/references/issues.md +1 -1
- package/skills/legion-architect/SKILL.md +27 -22
- package/skills/legion-controller/SKILL.md +48 -47
- package/skills/legion-retro/SKILL.md +15 -9
- package/skills/legion-worker/SKILL.md +151 -154
- package/skills/legion-worker/references/conflicts-and-rewrites.md +33 -31
- package/skills/legion-worker/references/merge-gate.md +60 -24
- package/skills/legion-worker/references/pr-body.md +21 -18
- package/skills/legion-worker/references/review-threads.md +68 -92
package/package.json
CHANGED
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
|
|
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,
|
|
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
|
|
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.
|
|
267
|
-
|
|
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.
|
|
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
|
|
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
|
-
(`
|
|
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
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
20
|
-
`
|
|
21
|
-
|
|
22
|
-
|
|
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)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 `
|
|
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
|
|
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 `
|
|
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
|
|
88
|
-
|
|
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
|
|
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 `
|
|
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. `
|
|
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 `
|
|
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. `
|
|
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
|
|
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 `
|
|
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 `
|
|
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 | `
|
|
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 `
|
|
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
|
|
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
|
-
`
|
|
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 `
|
|
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 `
|
|
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: `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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 (`
|
|
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.
|
|
34
|
-
|
|
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 &&
|
|
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" &&
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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,
|
|
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)
|
|
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
|
|