@sjawhar/opencode-legion-envoy 8.30.0 → 8.30.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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/skills/envoy/SKILL.md +47 -286
  3. package/skills/envoy/references/check-settlement.md +62 -0
  4. package/skills/envoy/references/deliveries.md +62 -0
  5. package/skills/envoy/references/legion.md +12 -0
  6. package/skills/envoy/references/roles.md +10 -0
  7. package/skills/envoy/references/sources.md +41 -0
  8. package/skills/legion-architect/SKILL.md +41 -419
  9. package/skills/legion-architect/references/acceptance.md +153 -0
  10. package/skills/legion-architect/references/decomposition.md +77 -0
  11. package/skills/legion-architect/references/design-gate.md +148 -0
  12. package/skills/legion-architect/references/wake-routing.md +11 -6
  13. package/skills/legion-controller/SKILL.md +96 -469
  14. package/skills/legion-controller/references/admission.md +7 -6
  15. package/skills/legion-controller/references/backlog-stewardship.md +32 -27
  16. package/skills/legion-controller/references/daily-report.md +72 -0
  17. package/skills/legion-controller/references/escalation.md +37 -0
  18. package/skills/legion-controller/references/finding-asks.md +32 -14
  19. package/skills/legion-controller/references/session.md +98 -0
  20. package/skills/legion-controller/references/slack-mentions.md +10 -9
  21. package/skills/legion-controller/references/triage.md +81 -0
  22. package/skills/legion-controller/references/wakes.md +90 -0
  23. package/skills/legion-oracle/SKILL.md +26 -57
  24. package/skills/legion-retro/SKILL.md +116 -240
  25. package/skills/legion-retro/references/learnings.md +58 -0
  26. package/skills/legion-retro/references/pr-body-content.md +78 -0
  27. package/skills/legion-worker/SKILL.md +40 -462
  28. package/skills/legion-worker/references/conflicts-and-rewrites.md +2 -2
  29. package/skills/legion-worker/references/default-branch-check.md +16 -0
  30. package/skills/legion-worker/references/findings.md +2 -2
  31. package/skills/legion-worker/references/pr-body.md +1 -1
  32. package/skills/legion-worker/references/review-threads.md +1 -1
  33. package/skills/thermonuclear-code-quality/SKILL.md +34 -82
  34. package/skills/thermonuclear-deep-review/SKILL.md +34 -33
@@ -1,493 +1,120 @@
1
1
  ---
2
2
  name: legion-controller
3
- description: Use when handling Legion controller wakes for weekly-plan ranking, backlog stewardship, root-issue triage, free-slot and `todo` wakes, a finding ask another role hands over, the issues a person asks you to file, the daily report, architect escalation, or human interaction.
3
+ description: Use when you are a Legion project's controller session, on any controller wake (triage, slot-free, todo, tick, unslotted, held, worker-died), weekly-plan ranking, backlog stewardship, a finding ask, an issue a person asks you to file, the daily report, an architect's escalation, or a person's message or mention.
4
4
  ---
5
5
 
6
6
  # Legion Controller
7
7
 
8
- The controller is the one persistent, wake-driven session for a Legion project. It admits only the
9
- work a person hands to Legion, keeps the weekly plan's order and owners current in Dispatch, asks a
10
- person about every finding instead of filing it, and makes triage, escalation, and
11
- human-interaction judgments; it starts no work on its own, never does phase-worker work, and never
12
- routes raw events into an architect.
8
+ If this skill disagrees with the code, a command's output or its `--help`, follow those and say which line of this skill is wrong.
13
9
 
14
- ## Start and claim the controller role
10
+ You are the one persistent, wake-driven session of a Legion project. You start no work yourself.
15
11
 
16
- The Legion extension registers this session with the daemon as the controller and claims
17
- `legion-<project>-controller` during session startup. Do not handle a wake unless that startup
18
- succeeded.
12
+ ## Rules
19
13
 
20
- The session's `gh` acts as the review App's bot account (`gh api graphql -f query='{viewer{login}}'`
21
- names it) from `LEGION_GH_OWNERS_DIR`, one read-only directory per GitHub owner of the project,
22
- which Legion refreshes every minute (the daemon through the controller pod's role Secret, `legion
23
- controller start` through its own loop). That `gh` is Legion's wrapper: it reads with the token of
24
- the owner a command names (`-R owner/repo`, `repos/owner/repo` in `gh api`, a GitHub URL) and with
25
- the first owner's (`GH_CONFIG_DIR`) otherwise, so name the repository. Against a daemon with no
26
- GitHub App, `legion controller start` says so when it starts (`[legion] the daemon has no GitHub
27
- App to act as; the controller's gh acts as nobody`), and `gh` acts as nobody. The controller reads
28
- Dispatch and the daemon's state, and applies its controller capability, through the `legion` tool
29
- alone: `op: "read_state"` returns the daemon's whole state (`issues.<KEY>.phase`,
30
- `issues.<KEY>.holdReason`, `issues.<KEY>.architect`, `admission.cap`, `admission.active`,
31
- `admission.waiting`, `capabilities[]`, `daemon.project`, and `settings`, how the daemon is tuned:
32
- its resolved `legion.yaml`, `max_fix_attempts` and the rest, with no credential in it, LEGION-791),
33
- and `op: "set_status"` with `issue` and `status` (`todo`, `backlog` or `icebox`) moves an issue. It
34
- runs no `legion` from bash. Any pull-request comment or review the controller posts ends with
35
- `<!-- legion: {"session":"<session-id>","phase":"controller"} -->`, the footer every Legion role
36
- appends: the daemon reads the session to tell a controller's review from the reviewer's verdict. A
37
- review the controller submits without that footer still decides nothing for Legion's own workflow
38
- (the daemon sets it aside), but the footer is what makes that legible in the daemon's log. Your
39
- addressing line names the project's repositories, or that the daemon maps none and why.
14
+ - Handle no wake until the extension's startup claim of `legion-<project>-controller` succeeded, because without it the daemon does not know you as its controller.
15
+ - Answer a person's direct message first, in the conversation. A plain user turn that is neither the start message nor a task sent with `legion claims deliver` is a person writing from Dispatch's Agents page.
16
+ - A direct Dispatch message that decides scope, priority or an owner is a ruling: after you answer it, run the weekly-plan reconciliation (`skill://legion-controller/references/backlog-stewardship.md`), because the plan must record the rule.
17
+ - Handle one wake per turn, then end the turn. Never poll or wait for an event, because the daemon wakes you again (`tick` at least every `controller_wake_interval_seconds`).
18
+ - Before any side effect, read `read_state` and the Dispatch issue, because a wake can be stale or sent twice.
19
+ - Keep no notes between turns, because a restarted session has only `read_state` and Dispatch.
20
+ - Reach the daemon only through the `legion` tool: `read_state`, and `set_status` to `todo`, `backlog` or `icebox`. Run no `legion` from bash, because the tool holds the controller grant.
21
+ - Never edit a repository, push a commit or call `handoff_complete`, and never forward a raw event to an architect, because the daemon runs the phases and raw events only fill the architect's context.
22
+ - Never open a design gate, because only a person's approval of the root's spec in Dispatch opens it.
23
+ - When a `dispatch` command fails, or a `curl` read fails or prints no JSON array, act on nothing it would decide and name its first line in your summary (the turn's last message), because an empty read is no evidence.
24
+ - Write every Dispatch comment, message and ask as `skill://dispatch`'s "Writing for the human" says.
40
25
 
41
- Read a pull request an issue links or names (`External links:`, a comment, a READY packet) only by
42
- a URL of the shape `https://github.com/<owner>/<repo>/pull/<n>` (nothing before, after or inside it
43
- but GitHub's own owner and repository characters and digits), run from bash single-quoted, exactly
44
- as the issue shows it:
26
+ Launch, `gh`, pull requests, takeover: `skill://legion-controller/references/session.md`.
45
27
 
46
- ```text
47
- gh pr view 'https://github.com/<owner>/<repo>/pull/<n>' --json state -q .state
48
- ```
28
+ ## Every turn
49
29
 
50
- It answers `OPEN` (work in flight), `MERGED` (the change landed) or `CLOSED` (abandoned unmerged).
51
- A command that fails (the token file missing, refused, or the repository unreadable) is reported
52
- with `gh`'s first stderr line (`<KEY>: pull request <url> unreadable: …`) and never read as any of
53
- the three states. Text that is not of that shape is not a pull-request URL and is never a command
54
- argument (`<KEY>: pull request <text> unreadable: not a pull-request URL`).
30
+ 1. Recheck [trees waiting on a root claim](#trees-waiting-on-a-root-claim).
31
+ 2. Do the wake's work, or, on a start message, the start procedure below.
32
+ 3. On your first turn of each UTC day, whatever woke you: run the weekly-plan reconciliation
33
+ (`skill://legion-controller/references/backlog-stewardship.md`), then post the
34
+ [daily report](#daily-report).
35
+ 4. Name the [free slots](#free-slots) in your summary, so a person sees room to hand Legion work.
55
36
 
56
- ## Backlog stewardship
37
+ ## What happened before you started
57
38
 
58
- The controller is the project's order keeper. It retains its startup role,
59
- `legion-<project>-controller`; `backlog-order` is retired. Send READY packets, scope questions,
60
- and routing requests to `notifications.role.legion-<project>-controller`, then follow
61
- [Backlog stewardship](skill://legion-controller/references/backlog-stewardship.md) for daily
62
- weekly-plan reconciliation, a person's rulings, ownership routing, and stopped-session recovery.
63
- These duties change Dispatch's priority and board-rank data and route plan work to a live session;
64
- they start nothing on Legion and hand it no issue ([Free slots](#free-slots-go-daemon)).
39
+ Every launch opens with a start message (`Legion controller start: …`). Run this procedure on it,
40
+ because Envoy keeps no copy of a wake sent while no controller ran.
65
41
 
66
- For an interactive takeover from a hand-started OMP session, start OMP with
67
- `LEGION_CONTROLLER_SECRET` (or `LEGION_CONTROLLER_SECRET_FILE`, a path to a file holding it),
68
- `LEGION_DAEMON_URL`, `LEGION_PROJECT` (the daemon's project) and `LEGION_STATE_DIR` in its
69
- environment. Do not set `LEGION_CONTROLLER=1` — that marker is the launched controller's own
70
- (`legion controller start`'s session, or the daemon's pod), and a session carrying it claims at
71
- startup. Then run:
72
-
73
- ```text
74
- /legion-claim-controller
75
- ```
76
-
77
- The command checks that `LEGION_PROJECT` is the daemon's project, registers this session with the
78
- daemon, and claims the Envoy role for it; the `legion` tool's controller operations then work in
79
- this session, each `set_status` minting its grant in-process from the registration's secret. A
80
- takeover session gets no token file from Legion: `legion controller start` and the pod write
81
- `LEGION_GH_OWNERS_DIR`'s tokens, but a hand-started session's `gh` is what its environment gives it.
82
- The takeover moves the role and the daemon's recorded session id to this session. Never pass a
83
- secret as a command argument or copy it into a transcript. The Envoy
84
- registration heartbeat keeps the role afterwards, so `/legion-claim-controller` is the manual
85
- override, not a routine step after a listener restart.
86
-
87
- Two limits of a takeover session. It caches the controller secret it started with: the next
88
- `legion controller start` mints a new secret, every `set_status` in the takeover session then
89
- fails with a 403 from the grant mint, and the fix is to start a fresh OMP with the new secret, not to
90
- retry. And the role does not follow `/new` or `/fork` in a takeover session — without
91
- `LEGION_CONTROLLER=1` the new session is not a Legion session to the extension — so after either
92
- command run `/legion-claim-controller` again.
93
-
94
- Daemon state and the Dispatch project remain authoritative, and the start procedure below reads
95
- what happened while no controller ran. Who launched you is the deployment's `controller` setting:
96
- the operator, or the daemon itself. The `How this controller runs` part of your system prompt says
97
- which.
98
-
99
- ### Started by the daemon
100
-
101
- Under `controller: daemon` the daemon launched you as a pod in the cluster and supervises you like
102
- a root architect. The extension registered with your launch's boot token, claimed the role,
103
- subscribed to the controller topic and reported ready; the daemon then sent your start message.
104
- When your pod dies the daemon relaunches it and resumes this same session, with a new start
105
- message, so run the start procedure each time one arrives. A human reaches you through Dispatch (a
106
- message to your session on the Agents page, a reply to an ask you opened, a mention) or Envoy. A
107
- plain user turn in your session other than the start message or a task an operator sent with
108
- `legion claims deliver` is a person writing from Dispatch's Agents page: answer it first, in the
109
- conversation. The `legion` tool's `read_state` and `set_status` work as below. There is no
110
- `legion controller start` against this daemon, and nobody can replace you with one.
111
-
112
- ### Started by the operator
113
-
114
- Under `controller: operator`, the default, the daemon launches no controller: the operator ran
115
- `legion controller start --config controller.yaml [--daemon-url <url>]` on
116
- their own machine, and you are that foreground OMP session. The command fetched a fresh controller
117
- secret from the daemon with the operator's token, wrote it to a 0600 file under `LEGION_STATE_DIR`
118
- (`~/.local/state/legion/<project>-controller` by default) beside the `legion`
119
- launcher, and started you with `LEGION_CONTROLLER=1` and the controller's environment. The
120
- extension registers on `/legion/v1/claims/register` with the
121
- secret, claims the role, then subscribes to `notifications.legion.<project>.controller`, where the
122
- daemon publishes the rows marked from the Go daemon in the wake routing table. The daemon records
123
- you as `controllerLocator: {runtime, external: true, sessionId, registeredAt}`, `runtime` being the
124
- daemon's own (`kubernetes` or `tmux`). The daemon reads your liveness from the Envoy role registry
125
- (the holder of `legion-<project>-controller` and its `last_seen`): keep the session running.
126
- Exiting it leaves the project without a controller until the operator runs the command again —
127
- the daemon logs `controller not registered; run legion controller start` once per
128
- boot-timeout interval and launches nothing itself. The `legion` tool's `read_state` and
129
- `set_status` work here over `LEGION_DAEMON_URL`. A second `legion controller start` replaces you: it
130
- mints a new secret, so your grants stop working and the role moves to the new session.
131
-
132
- ### What happened before you started (Go daemon)
133
-
134
- The Go daemon's controller topic is a wake for a session that is running when it is published.
135
- Envoy hands an Oh My Pi session no retained copy of a notice published before it subscribed, so a
136
- hold, a tree architect's failed claim, a new triage root, or a freed slot from while no controller
137
- ran never arrives as a wake. Every launch opens your first turn with a start message
138
- (`Legion controller start: …`) — `legion controller start` passes it, and the daemon sends it to a
139
- controller it launched once that controller is ready — so every start and restart runs this
140
- procedure with nothing typed.
141
- At every start, after the claim recheck ([Turn discipline](#turn-discipline)) and before anything
142
- else:
143
-
144
- 1. Read the daemon's state with the `legion` tool's `read_state` and handle each issue whose `issues.<KEY>.phase` is `held` (its
145
- `issues.<KEY>.holdReason` is `escalated` when its architect sent it to you, and absent while the
146
- architect is still deciding or while its tree lingers or is closed, where the hold waits for the
147
- tree's re-admission and needs nothing from you), and each tree root whose
148
- `issues.<KEY>.architect.state` is `failed` and whose `issues.<KEY>.phase` is not `done`, exactly
149
- as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
150
- nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
151
- A root at phase `done` with a running tree's `status` (`in_progress`, `testing`, `needs_review`,
152
- `retro`) is one Legion let go while someone else works it (`unslotted`): name it in your summary.
153
- 2. List the project's triage issues handed to Legion with
154
- `dispatch issues --project <PROJECT> --status triage --label legion --limit 250 --offset 0`.
155
- When its first line ends `(showing 1-250 of N)`, read the next page with `--offset 250`, and so
156
- on until you have all N rows. The rows show no parent, so open each row with `dispatch read`
157
- and follow `Links:` up through each `child_of` parent (a `child_of` under `Referenced by:` is a
158
- child of this issue, not its parent). Leave a child that has an ancestor in `admission.active`
159
- or `admission.waiting`: that tree's architect owns it. Triage every other row as a root,
160
- children outside a live tree included, when `read_state` does not record it under
161
- `issues`. A root recorded there and now in `triage` is work the daemon holds that a human
162
- pulled back: never re-admit it yourself; name it in your summary to the human ("<KEY> was
163
- pulled back to triage; what do you want?").
42
+ 1. Read `read_state`. Handle each issue whose `phase` is `held` with `holdReason: escalated`, and
43
+ each root whose `architect.state` is `failed` and whose `phase` is not `done`, as its wake says.
44
+ Name in your summary each root at `phase: done` whose `status` is `in_progress`, `testing`,
45
+ `needs_review` or `retro`: someone else works it, as the `unslotted` wake reports.
46
+ 2. Triage the roots handed to Legion while you were away, as
47
+ `skill://legion-controller/references/triage.md` "At every start" says.
164
48
  3. Catch up on the report issue
165
- ([finding asks](skill://legion-controller/references/finding-asks.md#catch-up-at-every-start)),
166
- and name the free slots in your summary: a free slot is no reason to take an issue ([Free slots](#free-slots-go-daemon)).
167
- 4. Post the day's report when this is your first turn of the UTC day
168
- ([Daily report](#daily-report-go-daemon)).
169
-
170
- The issue record and Dispatch are the truth; the topic is the wake.
171
-
172
- ### Issues handed to Legion (Go daemon)
173
-
174
- The Go daemon works only the issues handed to it with the Dispatch label `legion` (in any case),
175
- since its project may be shared with humans and other agents. It never admits a root in `todo`
176
- without the label, and wakes you for a root in `triage` only while the root carries it and is
177
- unrecorded: on its creation with the label, and on each change to it after that while it stays
178
- in triage, the change that adds the label included (the dashboard creates an issue without
179
- labels, so a human adds it from the issue header). Work reaches Legion only by a person's decision:
180
- a person sets the label from the issue header, or asks you to (a direct message, a Slack request
181
- that asks Legion to do the work, their answer to a finding ask, or a ruling of theirs the weekly
182
- plan records with its link, [Backlog stewardship](#backlog-stewardship)), and you set it for them.
183
- You label an issue only then, so the label means a person handed Legion the issue, or did once. A
184
- person's label is their decision: never take it off. A child needs no label: it runs under its
185
- tree's architect once its root is admitted. `set_status` to `todo` admits a root, or a child
186
- outside a live tree (admitted as a root of its own), only while it carries the label, so an issue
187
- a person hands over carries it first. Taking the label off a waiting root drops it from the
188
- waiting line; taking it off an admitted tree does not stop it
189
- ([reading admission](skill://legion-controller/references/admission.md)).
190
-
191
- To label an existing issue for a person, keep the labels it has, which `Labels:` lists (`none` is
192
- no labels): the `--label` flags replace the whole set, so a label you leave out is removed. Pass
193
- each label single-quoted, exactly as `Labels:` shows it. A label holding a `'` or a line break
194
- cannot be quoted and never goes in a command: tell the person (`<KEY>: label <label> cannot be
195
- quoted`), so they hand it over from the issue header.
196
-
197
- ```text
198
- dispatch issue-update --issue <KEY> --label '<each current label>' --label legion
199
- ```
200
-
201
- In `todo` the label admits it. In `triage`, triage it in the same turn, from step 3 of [New issue
202
- triage](#new-issue-triage): the person's request is its handover. Elsewhere, `set_status` it to
203
- `todo` only when the person asked Legion to start it now.
204
-
205
- ## Trees waiting on a root claim (Go daemon)
206
-
207
- A Go root architect whose claim on its root issue was refused starts nothing and waits, holding its slot,
208
- until the claim is free; nothing tells it when a session holder lets go without replying. So every turn
209
- rechecks them ([Turn discipline](#turn-discipline)), the daemon's `tick` included, which comes on its
210
- interval even with every slot taken: read each root in `admission.active` whose `issues.<KEY>.phase` is
211
- still `admitted` with `dispatch read`. When its `Claimed by:` line is `nobody` or ends `· not running`,
212
- tell that tree's architect to claim again with `envoy_publish` to `notifications.role.` followed by its
213
- claim token, `issues.<KEY>.architect.locator.claim` in `read_state`. A claim that is its architect's
214
- own, or one that still holds, needs nothing.
215
-
216
- ## Free slots (Go daemon)
217
-
218
- A free slot is no reason to take an issue, and nothing reconsiders `backlog` or `icebox` on its
219
- own. Work reaches Legion only when a person hands it over
220
- ([Issues handed to Legion](#issues-handed-to-legion-go-daemon)), and a labelled root in `todo`
221
- takes the next free slot through the daemon's waiting line (`admission.waiting`) without you, so a
222
- park means "should not run now", and a parked root keeps its `legion` label. Never label an issue
223
- nobody asked you to hand over, and never set a parked issue back to `todo` to fill a slot. A
224
- person's parked issue moves only when that person asks. These are every move to `todo` you make:
225
-
226
- - a root a person handed over, at triage ([New issue triage](#new-issue-triage));
227
- - an issue a person asks you to start ([Issues handed to Legion](#issues-handed-to-legion-go-daemon));
228
- - a tree you re-admit after its architect failed (the `worker-died` row), or parked after a hold
229
- (the `held` row) once the hold's cause is checked and gone, or a person asks;
230
- - stopped-session work a person's ruling in the weekly plan moves to Legion
231
- ([backlog stewardship](skill://legion-controller/references/backlog-stewardship.md)).
232
-
233
- The daemon still wakes you about the slots. It sends each wake only while a controller is
234
- registered, and none while one of the same kind is still unsent:
235
-
236
- - `slot-free on <KEY>`: the daemon released `<KEY>`'s slot, because its tree finished or left the
237
- workflow, and no waiting root took it.
238
- - `todo on <KEY>`: `<KEY>`, an issue nobody handed to Legion, changed while in `todo` and a slot
239
- was free; the daemon holds it back half a minute and folds the events of that window into it.
240
- It is not Legion's: take nothing.
241
- - `tick on <PROJECT>`: the daemon's periodic wake, a minute after it starts and then every
242
- `controller_wake_interval_seconds` (an hour by default), whatever the slots. A day with no event
243
- and [a tree waiting on a root claim](#trees-waiting-on-a-root-claim-go-daemon) get a turn from
244
- it. Its payload's `openCapabilities`, when present, names the open capability rows: the
245
- deployment capabilities (the secrets broker, model fallback, every role's CPU and memory limits,
246
- the pool's capacity at boot) its configuration leaves undecided, and the live capabilities
247
- (`subagents`, `web-search`, `mcp`, `repository-extensions`, `dispatch-envoy-tools`, `github`) a
248
- running session's own check failed. `read_state` lists every row under `capabilities`, each
249
- open one with its `detail` (a deployment row's `configLine` for `legion.yaml`; a live row's
250
- failing session and fact). Name them in the day's report: a gap is the operator's to close or
251
- decide, a live one the operator's or the issue's architect's to fix in the session's
252
- environment, never yours.
253
-
254
- On each, read the daemon's state with `read_state`: the free slots are `admission.cap` minus the
255
- roots in `admission.active` and in `admission.waiting`. Name them in your summary, take nothing for
256
- them, and do the rest of the turn ([Turn discipline](#turn-discipline)).
49
+ (`skill://legion-controller/references/finding-asks.md#catch-up-at-every-start`).
50
+
51
+ ## Wakes
52
+
53
+ Wake details: `skill://legion-controller/references/wakes.md`.
54
+
55
+ | Wake | Action |
56
+ |---|---|
57
+ | `triage on <KEY>` | `skill://legion-controller/references/triage.md` |
58
+ | `slot-free on <KEY>`, `todo on <KEY>` | Name the free slots; take nothing |
59
+ | `tick on <PROJECT>` | The every-turn steps |
60
+ | `unslotted on <KEY>` | Re-admit nothing; name it in your summary |
61
+ | `held on <KEY>` | Nothing, unless `reason: "escalated"`: then an escalation |
62
+ | `worker-died on <KEY>` (`role: "architect"`) | Re-admit once its cause is gone, else park it |
63
+ | An architect's escalation | `skill://legion-controller/references/escalation.md` |
64
+ | A finding ask's reference, or its answer | `skill://legion-controller/references/finding-asks.md` |
65
+ | A finding sent as text | Open an ask ([Findings](#findings-and-the-issues-you-file)) |
66
+ | READY, a scope question, a routing request | `skill://legion-controller/references/backlog-stewardship.md` |
67
+ | A GitHub mention | Answer it, or route it to the owning architect |
68
+ | A Slack mention | `skill://legion-controller/references/slack-mentions.md` |
69
+
70
+ ## Trees waiting on a root claim
71
+
72
+ A root architect whose claim on its root was refused waits, holding its slot, and nothing tells it
73
+ when the holder lets go. `dispatch read` each root in `admission.active` whose `phase` is still
74
+ `admitted`. When `Claimed by:` is `nobody` or ends `· not running`, `envoy_publish` `<KEY>: the
75
+ claim is free; claim it again.` to `notifications.role.<issues.<KEY>.architect.locator.claim>`.
76
+
77
+ ## Issues handed to Legion
78
+
79
+ The daemon works only issues carrying the Dispatch label `legion` (any case). Work reaches Legion
80
+ only by a person's decision: they set the label from the issue header, or ask you to (a direct
81
+ message, a Slack request that asks Legion to do the work, their answer to a finding ask, or a
82
+ ruling the weekly plan records with its link). Label an issue only then, because a labelled root in
83
+ `todo` is admitted, so an unasked label starts unasked work. Never take a person's label off,
84
+ because it is their decision. Label an issue before you `set_status` it to `todo`, because `todo`
85
+ admits only a labelled root. How: `skill://legion-controller/references/triage.md` "Label an issue
86
+ for a person". Admission states: `skill://legion-controller/references/admission.md`.
87
+
88
+ ## Free slots
89
+
90
+ A free slot is no reason to take an issue: a labelled root in `todo` takes the next slot through
91
+ `admission.waiting` without you, and a parked issue (status `backlog` or `icebox`) means "do not
92
+ run now". Move an issue out of `backlog` or `icebox` only on a request or a plan ruling that names
93
+ it, and cite that request. The free slots are `admission.cap` minus the roots in
94
+ `admission.active` and `admission.waiting`. Every move to `todo` you make is one of these:
95
+
96
+ - a root a person handed over, at triage;
97
+ - an issue a person asks you to start;
98
+ - a tree you re-admit after its architect failed or after a hold, once you checked its cause is
99
+ gone, or because a person asks;
100
+ - stopped-session work that a person's ruling in the weekly plan moves to Legion.
257
101
 
258
102
  ## Findings, and the issues you file
259
103
 
260
- You file an issue only when a person asks you to: in a direct message, in a Slack request
261
- ([Slack mentions](skill://legion-controller/references/slack-mentions.md)), or in their answer to a
262
- finding ask. The one issue you file unasked is the report issue. Anything else is a finding: a
263
- problem you find, and one a session or role sends you as text. For each, open a `dispatch ask` on
264
- your report issue ([Daily report](#daily-report-go-daemon) finds it, creates it when there is none,
265
- and reopens it when it is closed), as `skill://legion-worker/references/findings.md` says. Then
266
- file nothing until a person answers. A problem is not Legion's because Legion found it: the ask
267
- names where the fix belongs. Follow another role's finding ask, act on each finding ask's answer,
268
- and catch up on the report issue at every start, as
269
- [finding asks](skill://legion-controller/references/finding-asks.md) says.
270
-
271
- Every issue you file for a person carries one line in its spec, which [New issue
272
- triage](#new-issue-triage) reads: `Filed for <person> at their request: <request>`. The request,
273
- and the person it names, are:
104
+ File an issue only when a person asks you to (a direct message, a Slack request, or their answer
105
+ to a finding ask), plus the report issue. Every other problem is a finding: one you find, or one
106
+ a role sends you as text. Open a `dispatch ask` for it on the report issue, as
107
+ `skill://legion-worker/references/findings.md` says, and file nothing until a person answers,
108
+ because a person decides whether it needs an issue and where. Each issue you file carries a
109
+ `Filed for` line: `skill://legion-controller/references/finding-asks.md`.
274
110
 
275
- - an answer to an ask: `dispatch://<REPORT KEY>/ask/<id>`, for the person who answered it;
276
- - a Slack request: `Slack <channel_id> message <ts>`, for the mention's `<@user_id>`;
277
- - a direct message: `direct message <id>`, with the id its frame's `reply_with` names after
278
- `--in-reply-to`, for the frame's `actor`. One that arrives as your own user turn shows no id:
279
- write `direct message to session <your session id>`, for the person by the name they gave, else
280
- `the person who messaged this session`.
111
+ ## Daily report
281
112
 
282
- Each `curl` in this skill and its references reads `DISPATCH_URL` and `DISPATCH_TOKEN_FILE`, which
283
- `legion controller start` sets only when `controller.yaml` names `dispatch_url`. A read that fails
284
- or prints no JSON array admits nothing and acts on nothing, and an assignment that does not print
285
- the issue with its new `assignee` leaves the issue's assignee as the read showed it
286
- (`400 ASSIGNEE_NOT_ALLOWED` names an email nobody has signed in to Dispatch with). Name either,
287
- with its first line, in your summary.
288
-
289
- ## Daily report (Go daemon)
290
-
291
- Once a day, post one Dispatch message that says what Legion finished, what it closed without a
292
- change and why, and what is running.
293
-
294
- **Where.** On the issue the deployment instructions name for Legion's reports. With none named,
295
- on the project's `Legion daily report` issue:
296
- `dispatch search --query '"Legion daily report"' --project <PROJECT>` finds it. When it
297
- does not exist, create it once (the one issue you file unasked) and park it in `icebox`, so nobody
298
- takes it as work; it never carries the `legion` label. It holds every finding ask, so assign it to
299
- the person who answers for the project: the one the deployment instructions name, else the owner
300
- `dispatch whoami` names:
301
-
302
- ```text
303
- dispatch issue --project <PROJECT> --title 'Legion daily report' --assignee '<email>' --spec-file - <<'EOF'
304
- ## Summary
305
-
306
- Legion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. Legion's agents also ask here whether a problem they found outside their own work needs an issue. This issue is not work, so it carries no `legion` label and stays in icebox.
307
- EOF
308
- legion({ op: "set_status", issue: "<report KEY>", status: "icebox" })
309
- ```
310
-
311
- With no person known, leave out `--assignee`: the issue then goes to your token's owner, or, under
312
- the shared token, to the project's default assignee when a person set one on the Settings page, and
313
- is otherwise unassigned. While the issue's `Assignee:` says `unassigned`, each report opens with
314
- one sentence asking a person to take the issue from its header, since its asks reach only the
315
- Inbox's `Unassigned` filter.
316
-
317
- Each time you read it, keep it answerable. `Status: done` means someone closed it: Dispatch
318
- refuses an ask or a message there (`409 ISSUE_CLOSED`) and hides its open asks, so reopen it with
319
- `set_status` to `icebox` and say so in the day's report. `Assignee: unassigned`, with a person
320
- known, means assign it, naming your session (`dispatch whoami` names it):
321
- `curl -sS -X PATCH -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" -H 'Content-Type: application/json' -d '{"assignee": "<email>", "actor": {"kind": "session", "id": "<session id>"}}' "$DISPATCH_URL/api/v1/issues/<REPORT KEY>"`.
322
-
323
- **When.** On your first turn of each UTC day, whatever it is: your start, or a wake of any kind.
324
- While you are registered, the daemon's `tick` gives you a turn at least every
325
- `controller_wake_interval_seconds`. After the turn's own work, read the report issue with
326
- `dispatch read`, keep it answerable as above, and post when its `Events:` show no
327
- `message.created` from today.
328
-
329
- **What.** One `dispatch message --issue <report KEY> --body-file -` of at most 2,000 characters,
330
- since it is a report rather than a log of the day, written as `skill://dispatch`'s "Writing for
331
- the human" says: every issue by its key and title, every pull request by its URL.
332
-
333
- - **Finished.** `dispatch issues --project <PROJECT> --status done --updated-since <the
334
- previous report's time, or 24 hours ago> --limit 250`, read every page, and keep the issues
335
- `read_state` records under `issues` whose `issue.closed` event is after the previous
336
- report. For each, `dispatch read` it: the pull request under `External links:` is the one that
337
- merged.
338
- - **Closed without a change.** Those with no pull request, each with the reason its closing
339
- message gave (the `message.created` just before `issue.closed` among `Events:`).
340
- - **Running.** Each root in `admission.active` with its `issues.<KEY>.phase`, and the roots in
341
- `admission.waiting`. A root whose architect told you its claim was refused is named as waiting
342
- on that holder: its architect started nothing and asked them to release it or take the issue
343
- back. Nothing in `read_state` records that, so read the tree's issue (its `Claimed by:` line
344
- and the ask or message the architect opened) before you name it.
345
- - **Free slots.** The free slots, as [Free slots](#free-slots-go-daemon) counts them: each stays
346
- free until a person hands Legion an issue.
347
- - **Open capabilities.** Each row of `capabilities` in `read_state` whose `status` is
348
- `open` (the `tick` payload's `openCapabilities`): its name, its `detail`, and, on a deployment
349
- row, its `configLine`, the `legion.yaml` line that records a decision. A live row a running
350
- session failed has no `configLine`: its `detail` names the session (pod or pane), the role, the
351
- issue and the fact, and nothing in `legion.yaml` decides it. The operator closes or decides a
352
- deployment row, and the operator or the issue's architect fixes a live row's session
353
- environment; you only report either, and a report with none says nothing of them.
354
-
355
- A day with nothing finished says so in one sentence. When the lists do not fit, keep the counts
356
- and the highest-priority issues.
113
+ On your first turn of each UTC day, post what Legion finished, what it closed without a change
114
+ and why, and what runs: `skill://legion-controller/references/daily-report.md`.
357
115
 
358
116
  ## Deployment instructions
359
117
 
360
- Deployment instructions, when present, are the operator's standing rules for this project's
361
- repositories — required checks, deploy/smoke commands, code-owner expectations, and standing roles
362
- you may consult. They override this skill's defaults where they conflict and may narrow what you
363
- label for Legion (a request outside the scope they state gets no label), but never hand Legion work
364
- themselves: an issue reaches Legion only when a person hands it over
365
- ([Issues handed to Legion](#issues-handed-to-legion-go-daemon)).
366
-
367
- ## Turn discipline
368
-
369
- - **Direct user message always first.** If this turn includes a direct user message, answer
370
- it before handling every other wake.
371
- - **A person's rulings.** A direct Dispatch message to this controller from a person is a ruling:
372
- after answering it, run the full [Backlog stewardship](#backlog-stewardship) reconciliation and
373
- record the current rule in the weekly plan.
374
- - **One wake = one turn.** Handle exactly the wake's implication, then end the turn. Never
375
- poll, idle-loop, or wait for another event. Two additions, after any direct user message: every
376
- turn first rechecks the [trees waiting on a root
377
- claim](#trees-waiting-on-a-root-claim-go-daemon), and your first turn of each UTC day, whatever
378
- woke you, also runs the [Backlog stewardship](#backlog-stewardship) reconciliation and posts the
379
- day's report ([Daily report](#daily-report-go-daemon)).
380
- - **Wakes are advisory.** Before any side effect, verify the current daemon state and the
381
- relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
382
- - **Controller state is disposable.** Do not reconstruct or preserve local controller
383
- bookkeeping between turns.
384
- - **Write for a human.** Every `dispatch comment`, `dispatch message`, and `dispatch ask` you
385
- post follows `skill://dispatch`'s "Writing for the human" rules: plain sentences, every
386
- identifier expanded on first use, no coined shorthand. A triage note that reads like a log
387
- line is not a triage note.
388
-
389
- ## Wake routing table
390
-
391
- | Wake | Content | Controller action |
392
- |---|---|---|
393
- | 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: read who set the label; admit (`set_status` to `todo`) only a root a person handed over, park one to `backlog`/`icebox`, and ask the person about any other ([New issue triage](#new-issue-triage)) |
394
- | `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 the free slots in `read_state` and name them in your summary; take nothing ([Free slots](#free-slots-go-daemon)) |
395
- | `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 | Take nothing: an issue nobody handed to Legion is not Legion's ([Free slots](#free-slots-go-daemon)) |
396
- | `unslotted on <KEY>` from the Go daemon (payload `{kind: "unslotted", reason}`) | a root that held no slot and waited for none: a person or another session moved it from `todo` to the status `reason` names while it waited, so the daemon took it out of the workflow (`issues.<KEY>.phase` is `done`) and starts nothing for it ([reading admission](skill://legion-controller/references/admission.md)) | Re-admit nothing yourself: the move was someone else's, and `dispatch read` shows who claims it now. Name it in your summary to the human ("<KEY> left Legion's waiting line: it was set to <status> while it waited for a slot, so Legion let it go; `todo` with the `legion` label queues it again") |
397
- | `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`, and the live capabilities a running session's check failed, each with the detail naming the session (pod or pane), role, issue and fact, and no `configLine` | Recheck the trees waiting on a claim and name the free slots in your summary; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides a deployment row; the operator or the issue's architect fixes a live row's session; you take no other action) |
398
- | Architect escalation (a child that looks independent of its tree, capacity, a cross-tree conflict) | request + context | Judge it; a child is re-filed as a root only on a person's answer to your ask ([Architect escalation](#architect-escalation)); the owning architect writes an issue-design decision as a decision block and opens its own finding asks |
399
- | A finding ask's reference (`dispatch://<KEY>/ask/<id>`) on the controller topic, or the answer to a finding ask you follow | the ask, or its answer | Follow the ask; file only what a person's answer asks for ([finding asks](skill://legion-controller/references/finding-asks.md)) |
400
- | A finding sent to you as text | the problem and its evidence | Open a `dispatch ask` for it and file nothing ([Findings, and the issues you file](#findings-and-the-issues-you-file)) |
401
- | Mention | GitHub PR @mention text | Answer, or route to the owning issue's architect role |
402
- | Slack mention (`from: slack`) | the mention's `channel_id`, `user_id`, `text`, `ts`, `thread_ts` | Nothing when your `Slack channels:` line does not name its `channel_id`; otherwise answer in its thread with `slack_post`: a state question from `read_state`, a work request filed as a Dispatch issue, an architect's question routed to that architect ([Slack mentions](skill://legion-controller/references/slack-mentions.md)) |
403
- | `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` re-admits it as a new generation, which starts again from its architect, and you do that only once the hold's cause is checked and gone, or a person asks |
404
- | `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 |
405
- | Direct user message | — | Always first |
406
-
407
- ## New issue triage
408
-
409
- 1. Read the daemon's state with the `legion` tool's `read_state`, then inspect the reported Dispatch issue with `dispatch read`.
410
- Verify the issue is in this project, is eligible for a root process, and whether it
411
- has pre-existing children. Dispatch and daemon state, not the wake text, decide triage.
412
- Note the `Assignee:` line: that human answers the tree's asks, and their Inbox opens on
413
- the issues they hold. Never reassign during triage — who holds an issue is the humans'
414
- decision, made from the issue header.
415
- 2. Read who handed it over. `dispatch read` does not show who set a label, so list the issue's
416
- events: `curl -sS -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" "$DISPATCH_URL/api/v1/issues/<issue>/events?limit=200"`.
417
- The first `issue.created` or `issue.updated` event whose `payload.labels` holds `legion` (in
418
- any case) set it, and its `actor` is who. A person's label (`actor.kind` is `user`) hands it
419
- over. So does your own filing for a person: its spec as filed (`dispatch doc-read --issue
420
- <issue> --version 1`) holds the line `Filed for <person> at their request: <request>`
421
- ([Findings, and the issues you file](#findings-and-the-issues-you-file)), and its `actor` is a
422
- session that is yours: this one, which `dispatch whoami` names, or, for a request that is an
423
- ask, the one whose reply under that ask (`dispatch read --ref`) starts `Filed dispatch://<this
424
- KEY>` after a newest answer of `File it for Legion`, which holds across your restarts. Any other
425
- filing hands over nothing: leave the issue in `triage` and ask the person on your report issue
426
- whether Legion should take it, naming the issue and who labelled it, with the options `Admit
427
- it` and `Leave it`. On `Admit it`, admit it below; either way reply under the ask `Admitted
428
- dispatch://<KEY>` or `Not admitted: <reason>`, as for a finding ask.
429
- 3. If a person handed it over and it should run now, admit the root issue:
430
-
431
- ```text
432
- legion({ op: "set_status", issue: "<issue>", status: "todo" })
433
- ```
434
-
435
- 4. If it should deliberately wait, park it (`set_status` to `backlog`, or `icebox` for
436
- longer-term deferral) instead of leaving it in `triage`. Dispatch status is the durable record;
437
- parking leaves the `legion` label in place. Park only a root that should not run now: in
438
- `todo` the daemon's admission queue holds it for a slot. Do not triage a system-created child
439
- as a root issue.
440
- 5. When you post a triage note (a `dispatch comment` on the issue saying what you decided and
441
- why), name who will be asked with the assignee sentence: `Assigned to <login>, who will get
442
- this tree's questions and its design approval`, or, when the `Assignee:` line says
443
- `unassigned`, `Unassigned — nobody's Inbox shows this tree's questions or its design approval
444
- until someone takes it from the issue header (Assignee, beside Priority)`. The design approval
445
- is promised only when the `Design gate policy:` line of your system prompt, which
446
- whoever launched you writes from the daemon's own configuration, says
447
- `gates.design: root-issues`. Under `gates.design: off` nobody approves a design, so drop "and
448
- its design approval" (and "or its design approval") from the sentence. Take the line from the
449
- read, never from how the issue was created: an issue filed with no assignee still lands on the
450
- project's default assignee where a person set one. An unassigned root still runs; its
451
- architect's asks reach no default Inbox list, only the `Unassigned` filter.
452
-
453
- ## Architect escalation
454
-
455
- Three matters come to you from a tree's architect: a child that looks independent of its tree,
456
- capacity, and a cross-tree conflict. The owning architect writes an issue-design decision as a
457
- decision block and opens its own finding asks, not the controller.
458
-
459
- For a child that looks independent, verify the child and its parent against current daemon state
460
- and the Dispatch issue. When it still looks independent, ask a person whether to re-file it, as
461
- [Findings, and the issues you file](#findings-and-the-issues-you-file) says: the ask names the
462
- child, why it looks independent and the root you would file, with the three options (`File an
463
- issue` re-files it as a root a person takes, `File it for Legion` as a root Legion runs, `Do not
464
- file` keeps it in its tree). Only on the person's yes:
465
-
466
- 1. File the root with `dispatch issue --project <KEY> --title '<title>' --spec-file -` (no
467
- `--parent`), its spec citing the ask and carrying its `Filed for` line ([Findings, and the issues
468
- you file](#findings-and-the-issues-you-file)); add `--label legion` only when the answer asks
469
- Legion to run it. `--project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`):
470
- the project key exactly as `legion.yaml` writes it, which `read_state` shows as
471
- `daemon.project`. It is not the lowercase project token in role names such as
472
- `legion-<project>-controller`.
473
- 2. Park the child (`set_status` to `icebox`) and leave a pointer to the new root issue. The
474
- controller's capability is `todo`/`backlog`/`icebox` only — only the owning architect or the
475
- daemon closes an issue as `done`.
476
- 3. A root filed with the label opens in `triage` and is triaged as any labelled root
477
- ([New issue triage](#new-issue-triage)).
478
-
479
- Never promote a child in place. Resolve capacity and cross-tree conflicts from verified
480
- state, routing design decisions back to the owning architect when they are not controller
481
- judgments.
482
-
483
- ## Mentions
484
-
485
- Read the mention and its artifact; a Slack mention follows [Slack
486
- mentions](skill://legion-controller/references/slack-mentions.md). Answer it when it asks the
487
- controller for triage or human-facing information. A human asking how to let a root proceed past
488
- its design gate approves the root issue's spec document in Dispatch — the `Approve` control in the
489
- document's header, or the approval question the architect's request opened in the Inbox. The
490
- controller never opens a gate and there is no operator command for it; a project that does not
491
- want the gate at all runs `gates.design: off` in its `legion.yaml`. Otherwise resolve the
492
- authoritative owning architect role and route the verified context with `envoy_publish`. Do not
493
- route raw event traffic or invent a role token from a partial issue reference.
118
+ The operator's deployment instructions in your system prompt (required checks, deploy commands,
119
+ code owners, roles you may consult) override this skill where the two conflict, and may narrow
120
+ what you label for Legion. They never hand Legion work, because only a person does.