@sjawhar/pi-legion 0.0.0 → 8.0.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.
Files changed (30) hide show
  1. package/README.md +74 -0
  2. package/agents/deep-worker.md +64 -0
  3. package/agents/oracle.md +38 -0
  4. package/agents/plan-gap-analyst.md +59 -0
  5. package/agents/plan-reviewer.md +61 -0
  6. package/agents/thermonuclear-code-quality.md +28 -0
  7. package/agents/thermonuclear-deep-review.md +28 -0
  8. package/dist/THIRD_PARTY_NOTICES +30 -0
  9. package/dist/legion.js +16807 -0
  10. package/dist/skills/ce-simplify-code/LICENSE +21 -0
  11. package/dist/skills/ce-simplify-code/SKILL.md +64 -0
  12. package/dist/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
  13. package/dist/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
  14. package/dist/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
  15. package/dist/skills/legion-architect/SKILL.md +370 -0
  16. package/dist/skills/legion-controller/SKILL.md +419 -0
  17. package/dist/skills/legion-oracle/SKILL.md +74 -0
  18. package/dist/skills/legion-retro/SKILL.md +196 -0
  19. package/dist/skills/legion-worker/SKILL.md +482 -0
  20. package/dist/skills/legion-worker/references/cleanup-deletion.md +22 -0
  21. package/dist/skills/legion-worker/references/conflicts-and-rewrites.md +126 -0
  22. package/dist/skills/legion-worker/references/merge-gate.md +117 -0
  23. package/dist/skills/legion-worker/references/pr-body.md +146 -0
  24. package/dist/skills/legion-worker/references/review-threads.md +101 -0
  25. package/dist/skills/legion-worker/references/systematic-rename.md +19 -0
  26. package/dist/skills/thermonuclear-code-quality/LICENSE +21 -0
  27. package/dist/skills/thermonuclear-code-quality/SKILL.md +192 -0
  28. package/dist/skills/thermonuclear-deep-review/LICENSE +21 -0
  29. package/dist/skills/thermonuclear-deep-review/SKILL.md +98 -0
  30. package/package.json +43 -1
@@ -0,0 +1,419 @@
1
+ ---
2
+ name: legion-controller
3
+ description: Use when handling Legion controller wakes for root-issue triage, keeping the admission slots full from `todo` issues, the daily report, architect escalation, or human interaction.
4
+ ---
5
+
6
+ # Legion Controller
7
+
8
+ The controller is the one persistent, wake-driven session for a Legion project. It keeps the
9
+ project's admission slots full with the highest-priority work, and makes triage, escalation, and
10
+ human-interaction judgments; it never does phase-worker work or routes raw events into an
11
+ architect.
12
+
13
+ ## Start and claim the controller role
14
+
15
+ The Legion extension registers this session with the daemon as the controller and claims
16
+ `legion-<project>-controller` during session startup. Do not handle a wake unless that startup
17
+ succeeded.
18
+
19
+ The session carries no GitHub credential: its GitHub token variables are emptied, and both
20
+ `legion gh -- <args>` and `legion threads resolve` are refused. The controller reads Dispatch and
21
+ applies its controller capability with `legion status <KEY> <status>`; it never reads GitHub or
22
+ merges a pull request.
23
+
24
+ For an interactive takeover from a hand-started OMP session, start OMP with
25
+ `LEGION_CONTROLLER_SECRET` (or `LEGION_CONTROLLER_SECRET_FILE`, a path to a file holding it),
26
+ `LEGION_DAEMON_URL`, `LEGION_PROJECT` (the daemon's project), `LEGION_STATE_DIR`, and `LEGION_GRANT_FILE`
27
+ (an absolute path to a file only you can read, under a 0700 directory; the extension writes
28
+ each command's grant there and every `bash` call is blocked without it) in its environment. Do
29
+ not set `LEGION_CONTROLLER=1` — that marker is the launched controller's own (`legion controller
30
+ start`'s session, or the daemon's pod), and a session carrying it claims at startup. Then run:
31
+
32
+ ```text
33
+ /legion-claim-controller
34
+ ```
35
+
36
+ The command checks that `LEGION_PROJECT` is the daemon's project, registers this session with the
37
+ daemon, and claims the Envoy role for it before controller commands can act. From then on this session's
38
+ shell commands are wrapped with a controller grant, but the grant holds no GitHub credential;
39
+ `legion status <KEY> <status>` works through the controller secret in this session's environment
40
+ (`LEGION_CONTROLLER_SECRET` or its `_FILE`), not the grant — if it fails, that is the variable to check.
41
+ The takeover moves the role and the daemon's recorded session id to this session. Never pass a
42
+ secret as a command argument or copy it into a transcript. The Envoy registration heartbeat keeps
43
+ the role afterwards, so `/legion-claim-controller` is the manual override, not a routine step after
44
+ a listener restart.
45
+
46
+ Two limits of a takeover session. It caches the controller secret it started with: the next
47
+ `legion controller start` mints a new secret, every `bash` call in the takeover session then fails
48
+ with a 403 from the grant mint, and the fix is to start a fresh OMP with the new secret, not to
49
+ retry. And the role does not follow `/new` or `/fork` in a takeover session — without
50
+ `LEGION_CONTROLLER=1` the new session is not a Legion session to the extension — so after either
51
+ command run `/legion-claim-controller` again.
52
+
53
+ Daemon state and the Dispatch project remain authoritative, and the start procedure below reads
54
+ what happened while no controller ran. Who launched you is the deployment's `controller` setting:
55
+ the operator, or the daemon itself. The `How this controller runs` part of your system prompt says
56
+ which.
57
+
58
+ ### Started by the daemon
59
+
60
+ Under `controller: daemon` the daemon launched you as a pod in the cluster and supervises you like
61
+ a root architect: you run headless (`omp --mode rpc`), nobody types into your session, and nobody
62
+ reads your replies as they appear. The extension registered with your launch's boot token, claimed
63
+ the role, subscribed to the controller topic and reported ready; the daemon then sent your start
64
+ message. When your pod dies the daemon relaunches it and resumes this same session, with a new
65
+ start message, so run the start procedure each time one arrives. A human reaches you through
66
+ Dispatch (a message to your session on the Agents page, a reply to an ask you opened, a mention) or
67
+ Envoy: answer them where they will read it, a Dispatch message or reply, since text left only in
68
+ your session reaches no one. `legion state` and `legion status <KEY> <status>` work as below. There
69
+ is no `legion controller start` against this daemon, and nobody can replace you with one.
70
+
71
+ ### Started by the operator
72
+
73
+ Under `controller: operator`, the default, the daemon launches no controller: the operator ran
74
+ `legion controller start --config controller.yaml [--daemon-url <url>]` on
75
+ their own machine, and you are that foreground OMP session. The command fetched a fresh controller
76
+ secret from the daemon with the operator's token, wrote it to a 0600 file under `LEGION_STATE_DIR`
77
+ (`~/.local/state/legion/<project>-controller` by default) beside the `gh` shim and the `legion`
78
+ launcher, and started you with `LEGION_CONTROLLER=1` and the controller's environment. The
79
+ extension registers on `/legion/v1/claims/register` with the
80
+ secret, claims the role, then subscribes to `notifications.legion.<project>.controller`, where the
81
+ daemon publishes the rows marked from the Go daemon in the wake routing table. The daemon records
82
+ you as `controllerLocator: {runtime, external: true, sessionId, registeredAt}`, `runtime` being the
83
+ daemon's own (`kubernetes` or `tmux`). The daemon reads your liveness from the Envoy role registry
84
+ (the holder of `legion-<project>-controller` and its `last_seen`): keep the session running.
85
+ Exiting it leaves the project without a controller until the operator runs the command again —
86
+ the daemon logs `controller not registered; run legion controller start` once per
87
+ boot-timeout interval and launches nothing itself. `legion state` and `legion status <KEY>
88
+ <status>` work here over `LEGION_DAEMON_URL`. A second `legion controller start` replaces you: it
89
+ mints a new secret, so your grants stop working and the role moves to the new session.
90
+
91
+ ### What happened before you started (Go daemon)
92
+
93
+ The Go daemon's controller topic is a wake for a session that is running when it is published.
94
+ Envoy hands an Oh My Pi session no retained copy of a notice published before it subscribed, so a
95
+ hold, a tree architect's failed claim, a new triage root, or a freed slot from while no controller
96
+ ran never arrives as a wake. Every launch opens your first turn with a start message
97
+ (`Legion controller start: …`) — `legion controller start` passes it, and the daemon sends it to a
98
+ controller it launched once that controller is ready — so every start and restart runs this
99
+ procedure with nothing typed.
100
+ At every start, after the claim recheck ([Turn discipline](#turn-discipline)) and before anything
101
+ else:
102
+
103
+ 1. Read `legion state --json` and handle each issue whose `issues.<KEY>.phase` is `held` (its
104
+ `issues.<KEY>.holdReason` is `escalated` when its architect sent it to you, and absent while the
105
+ architect is still deciding or while its tree lingers or is closed, where the hold waits for the
106
+ tree's re-admission and needs nothing from you), and each tree root whose
107
+ `issues.<KEY>.architect.state` is `failed` and whose `issues.<KEY>.phase` is not `done`, exactly
108
+ as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
109
+ nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
110
+ 2. List the project's triage issues handed to Legion with
111
+ `dispatch_issues({project, status: "triage", label: "legion", limit: 250, offset: 0})`.
112
+ When its first line ends `(showing 1-250 of N)`, read the next page with `offset: 250`, and so
113
+ on until you have all N rows. The rows show no parent, so open each row with `dispatch_read`
114
+ and follow `Links:` up through each `child_of` parent (a `child_of` under `Referenced by:` is a
115
+ child of this issue, not its parent). Leave a child that has an ancestor in `admission.active`
116
+ or `admission.waiting`: that tree's architect owns it. Triage every other row as a root,
117
+ children outside a live tree included, when `legion state --json` does not record it under
118
+ `issues`. A root recorded there and now in `triage` is work the daemon holds that a human
119
+ pulled back: never re-admit it yourself; name it in your summary to the human ("<KEY> was
120
+ pulled back to triage; what do you want?").
121
+ 3. Fill the free admission slots ([Keeping the slots full](#keeping-the-slots-full-go-daemon)).
122
+ 4. Post the day's report when this is your first turn of the UTC day
123
+ ([Daily report](#daily-report-go-daemon)).
124
+
125
+ The issue record and Dispatch are the truth; the topic is the wake.
126
+
127
+ ### Issues handed to Legion (Go daemon)
128
+
129
+ The Go daemon works only the issues handed to it with the Dispatch label `legion` (in any case),
130
+ since its project may be shared with humans and other agents. It never admits a root in `todo`
131
+ without the label, and wakes you for a root in `triage` only while the root carries it and is
132
+ unrecorded: on its creation with the label, and on each change to it after that while it stays
133
+ in triage, the change that adds the label included (the dashboard creates an issue without
134
+ labels, so a human adds it from the issue header). Two parties hand work over: a person, who sets
135
+ the label from the issue header, and you, when you fill a free slot (below). You label an issue
136
+ only when you take it or file it for Legion, so the label means Legion has the issue or had it.
137
+ A person's label is their decision: never take it off. A child needs no label: it runs under its
138
+ tree's architect once its root is admitted. `legion status <KEY> todo` admits a root, or a child
139
+ outside a live tree (admitted as a root of its own), only while it carries the label, so an issue
140
+ you hand over or file for Legion to run carries it first (`labels` in `dispatch_issue_update` or
141
+ `dispatch_issue`). Taking the label off a waiting root drops it from the waiting line; taking it
142
+ off an admitted tree does not stop it.
143
+
144
+ ## Trees waiting on a root claim (Go daemon)
145
+
146
+ A Go root architect whose claim on its root issue was refused starts nothing and waits, holding
147
+ its slot, until the claim is free; nothing tells it when a session holder lets go without
148
+ replying. So every turn rechecks them ([Turn discipline](#turn-discipline)), the daemon's `tick`
149
+ included, which comes on its interval even with every slot taken: read each root in
150
+ `admission.active` whose `issues.<KEY>.phase` is still `admitted` with `dispatch_read`. When its
151
+ `Claimed by:` line is `nobody` or ends `· not running`, tell that tree's architect to claim again
152
+ with `envoy_publish` to `notifications.role.` followed by its claim token,
153
+ `issues.<KEY>.architect.locator.claim` in `legion state --json`. A claim that is its architect's
154
+ own, or one that still holds, needs nothing.
155
+
156
+ ## Keeping the slots full (Go daemon)
157
+
158
+ Picking the next work is your job: nobody hand-feeds issues to Legion. Keep every admission slot
159
+ filled with the highest-priority concrete issue Legion can take. The unit of Legion work is a
160
+ leaf, an issue with no children, never an umbrella that holds other issues.
161
+
162
+ **When.** At every start (step 3 above), and on each of the Go daemon's walk wakes. The daemon
163
+ sends each only while a controller is registered, and none while one of the same kind is still
164
+ unsent:
165
+
166
+ - `slot-free on <KEY>`: the daemon released `<KEY>`'s slot, because its tree finished or left the
167
+ workflow, and the slot is free by **How many** below.
168
+ - `todo on <KEY>`: `<KEY>`, an issue nobody handed to Legion, changed while in `todo` and a slot
169
+ was free, so it may be a new candidate. The daemon holds it back half a minute and folds the
170
+ events of that window into it. Walk the whole list, not only `<KEY>`.
171
+ - `tick on <PROJECT>`: the daemon's periodic wake, a minute after it starts and then every
172
+ `controller_wake_interval_seconds` (an hour by default), whatever the slots. An earlier walk
173
+ that found nothing, a day with no event, and [a tree waiting on a root
174
+ claim](#trees-waiting-on-a-root-claim-go-daemon) all get a turn from it.
175
+
176
+ **Scope first.** The scope the deployment instructions state decides which issues are candidates
177
+ at all, before anything below. When they say you hand Legion no issue yourself, or that Legion
178
+ runs only issues someone else sets to `todo`, the walk takes nothing: stop here, whatever slots
179
+ are free. When they narrow the scope (a repository, a kind of change), a candidate outside it is
180
+ skipped (the last row of the table below). With no scope stated, every issue of the project is in
181
+ scope.
182
+
183
+ **How many.** Read `legion state --json`. The free slots are `admission.cap` minus the roots in
184
+ `admission.active` and in `admission.waiting` (a waiting root takes the next slot before anything
185
+ you add). With none free, stop.
186
+
187
+ **Candidates.** The project's open `todo` issues, roots and children alike, that have no children
188
+ at all and do not carry the `legion` label. Ready work is `todo` (`skill://dispatch`, "Choosing
189
+ what to work on"): take the top ready issue, highest priority first, then board rank. An issue that
190
+ waits on a deploy or a decision belongs in `backlog`, so the walk takes nothing from `backlog` or
191
+ `triage`. The Go daemon runs
192
+ only labelled roots, so every root it ran since the daemon required the label carries it: a
193
+ labelled root in `todo` is the daemon's to admit or queue, one in `triage` is yours to triage
194
+ (step 2 above), and one anywhere else was parked by Legion or by a person. A child you take becomes
195
+ a root of its own: the daemon admits a labelled `todo` child whose tree Legion does not run as a
196
+ new tree. The `dispatch_issues` rows show no labels and no children, so the listing below filters
197
+ neither: both are checked on each candidate (the table's first rows). List the `todo` issues one
198
+ priority at a time, `priority: [0]` first, then `[1]`, `[2]`, `[3]`, and `[null]` (no priority)
199
+ last, keeping the listing's order within each, which is the board's rank:
200
+
201
+ ```text
202
+ dispatch_issues({ project: "<PROJECT>", status: "todo", priority: [0], limit: 250, offset: 0 })
203
+ ```
204
+
205
+ When the first line ends `(showing 1-250 of N)`, the next page is `offset: 250`, then `500`. Read
206
+ pages only as far as you need: stop listing once the free slots are filled. `<PROJECT>` is the
207
+ Dispatch project key, the prefix of this deployment's issue keys (`PROJ-12` → `PROJ`), which is
208
+ also `daemon.project` in `legion state --json`: the project key exactly as `legion.yaml` writes it.
209
+ A row that shows `claimed by …` and does not end its claim with `· not running` (the route, when
210
+ the row shows one, comes after the claim) is claimed, as the table below says: skip it without
211
+ reading it.
212
+
213
+ **Walk.** Take the remaining rows in that order until the free slots are filled. Read each one with
214
+ `dispatch_read({ issue: "<KEY>" })` and skip it when any of these holds. `dispatch_read` shows only
215
+ the issue's last 10 events, so the rows that read `Events:` are best-effort: the pull-request row
216
+ leans on `External links:`, and the label row is the one that never depends on history.
217
+
218
+ | Skip when | How you check it |
219
+ |---|---|
220
+ | It carries the `legion` label | `Labels:` lists `legion`, in any case. Legion has the issue or had it, as **Candidates** above says. |
221
+ | 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. |
222
+ | An ancestor is Legion's | Follow `Links:` up through each `child_of` parent, reading each one, and skip when any ancestor carries the `legion` label or is recorded under `issues` in `legion state --json`, whatever its status: a Legion tree, running or parked, owns its children. An ancestor's claim or route does not skip the issue: a coordinator holding an umbrella files `todo` leaves for others to pick up, and the claim on the issue itself is what keeps two sessions off the same work. A `child_of` under `Referenced by:` is a child of this issue, not its parent. |
223
+ | 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. |
224
+ | Legion ran it without the label now on it | `legion state --json` records it under `issues`, whatever its status, or `Events:` show a status write by `session legion-daemon:<PROJECT>`, the daemon's actor on every `legion status` (yours included) and on its own `in_progress` at admission. That covers a root a person took the label off, and one that ran before the daemon required the label and never had it. Name each one you skip for this in your summary. The walk never sends a root Legion already ran back into Legion: a person does that with the label and `todo`, and you do it only when a wake below says to (`worker-died`). |
225
+ | 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. |
226
+ | 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. |
227
+ | 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`. |
228
+ | Its assignee is working it | `Assignee:` names a person who holds the claim (the row above), or whose own comment or message among `Events:` says they are working on it. The assignee alone is who answers the issue's questions, not who works it. |
229
+ | It is outside this deployment's scope | Read the scope the deployment instructions state against the title and, when the title does not settle it, the spec (`dispatch_doc_read({ issue: "<KEY>" })`). When in doubt, skip it. With no scope stated, every issue of the project is in scope. |
230
+
231
+ **Take.** For each candidate that passes, in order:
232
+
233
+ 1. Add the label and keep the labels it has, which `Labels:` lists (`none` is no labels). `labels`
234
+ replaces the whole set, so a label you leave out is removed.
235
+
236
+ ```text
237
+ dispatch_issue_update({ issue: "<KEY>", labels: ["<each current label>", "legion"] })
238
+ ```
239
+
240
+ 2. It is already in `todo`, so the label admits it: the daemon records it and gives it the free
241
+ slot, or queues it in `admission.waiting`. It needs no status write.
242
+ 3. Post one short comment that says Legion took it, who is asked at its design gate, and how to
243
+ undo the take, naming the assignee with the sentence [New issue triage](#new-issue-triage)
244
+ step 4 gives (which follows the design gate policy):
245
+
246
+ ```text
247
+ dispatch_comment({ issue: "<KEY>", body: "Legion took this issue: it was the highest-priority open issue nobody else was working on. Assigned to <login>, who will get this tree's questions and its design approval. To stop Legion, move the issue to backlog. To keep Legion off it for good, also take the legion label off; taking the label off alone does not stop a tree that has started." })
248
+ ```
249
+
250
+ The daemon admits each root when Dispatch's event reaches it; the next `legion state --json`
251
+ shows it in `admission.active`. When the candidates run out with slots still free, leave those
252
+ slots free and say so in your summary.
253
+
254
+ ## Daily report (Go daemon)
255
+
256
+ Once a day, post one Dispatch message that says what Legion finished, what it closed without a
257
+ change and why, and what is running.
258
+
259
+ **Where.** On the issue the deployment instructions name for Legion's reports. With none named,
260
+ on the project's `Legion daily report` issue:
261
+ `dispatch_search({ query: "\"Legion daily report\"", project: "<PROJECT>" })` finds it. When it
262
+ does not exist, create it once and park it in `icebox`, so nobody takes it as work; it never
263
+ carries the `legion` label:
264
+
265
+ ```text
266
+ dispatch_issue({ project: "<PROJECT>", title: "Legion daily report", spec: "## Summary\n\nLegion'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." })
267
+ legion status <report KEY> icebox
268
+ ```
269
+
270
+ **When.** On your first turn of each UTC day, whatever it is: your start, or a wake of any kind.
271
+ While you are registered, the daemon's `tick` gives you a turn at least every
272
+ `controller_wake_interval_seconds`. After the turn's own work, read the report issue with
273
+ `dispatch_read`, and post when its `Events:` show no `message.created` from today.
274
+
275
+ **What.** One `dispatch_message({ issue: "<report KEY>", body })` of at most 2,000 characters,
276
+ written as `skill://dispatch`'s "Writing for the human" says: every issue by its key and title,
277
+ every pull request by its URL.
278
+
279
+ - **Finished.** `dispatch_issues({ project: "<PROJECT>", status: "done", updated_since: "<the
280
+ previous report's time, or 24 hours ago>", limit: 250 })`, read every page, and keep the issues
281
+ `legion state --json` records under `issues` whose `issue.closed` event is after the previous
282
+ report. For each, `dispatch_read` it: the pull request under `External links:` is the one that
283
+ merged.
284
+ - **Closed without a change.** Those with no pull request, each with the reason its closing
285
+ message gave (the `message.created` just before `issue.closed` among `Events:`).
286
+ - **Running.** Each root in `admission.active` with its `issues.<KEY>.phase`, and the roots in
287
+ `admission.waiting`. A root whose architect told you its claim was refused is named as waiting
288
+ on that holder: its architect started nothing and asked them to release it or take the issue
289
+ back. Nothing in `legion state` records that, so read the tree's issue (its `Claimed by:` line
290
+ and the ask or message the architect opened) before you name it.
291
+ - **The slots and the walk.** The free slots, as **How many** counts them, then this turn's walk
292
+ when it made one: what it took, and how many candidates each row of the table skipped, so a
293
+ reader can see why a free slot stays empty.
294
+
295
+ A day with nothing finished says so in one sentence. When the lists do not fit, keep the counts
296
+ and the highest-priority issues.
297
+
298
+ ## Deployment instructions
299
+
300
+ Deployment instructions, when present, are the operator's standing rules for this repository —
301
+ required checks, deploy/smoke commands, code-owner expectations, and standing roles you may
302
+ consult. They override this skill's defaults where they conflict, and may narrow which issues the
303
+ walk takes, but never widen it past `todo` issues or change the order it takes them in: highest
304
+ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-full-go-daemon)).
305
+
306
+ ## Turn discipline
307
+
308
+ - **Direct user message always first.** If this turn includes a direct user message, answer
309
+ it before handling every other wake.
310
+ - **One wake = one turn.** Handle exactly the wake's implication, then end the turn. Never
311
+ poll, idle-loop, or wait for another event. Two additions, after any direct user message: every
312
+ turn first rechecks the [trees waiting on a root
313
+ claim](#trees-waiting-on-a-root-claim-go-daemon), and your first turn of each UTC day, whatever
314
+ woke you, also posts the day's report ([Daily report](#daily-report-go-daemon)).
315
+ - **Wakes are advisory.** Before any side effect, verify the current daemon state and the
316
+ relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
317
+ - **Controller state is disposable.** Do not reconstruct or preserve local controller
318
+ bookkeeping between turns.
319
+ - **Write for a human.** Every `dispatch_comment`, `dispatch_message`, and `dispatch_ask` you
320
+ post follows `skill://dispatch`'s "Writing for the human" rules: plain sentences, every
321
+ identifier expanded on first use, no coined shorthand. A triage note that reads like a log
322
+ line is not a triage note.
323
+
324
+ ## Wake routing table
325
+
326
+ | Wake | Content | Controller action |
327
+ |---|---|---|
328
+ | New issue created in the Dispatch project, status `triage`: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above); the start procedure above catches one sent while no controller ran | issue key + triage context (incl. pre-existing children) | Triage: `legion status <KEY> todo` to admit, or set `backlog`/`icebox` to park |
329
+ | `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `legion state --json`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
330
+ | `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)) |
331
+ | `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick"}`) | the project key; the daemon's periodic wake, whatever the slots | Recheck the trees waiting on a claim, then walk if a slot is free; post the day's report if this is the day's first turn |
332
+ | 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 |
333
+ | Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
334
+ | `held on <KEY>` from the Go daemon (payload `{kind: "held", phase, role?, reason?}`) | the held issue, the phase it left, and the role whose claim failed, or `reason: "escalated"` | Verify the hold in `legion state` (the issue's phase is `held`). Without `reason`, a phase worker's launches or prompts ran out and the tree's architect decides retry or escalate: no action. With `reason: "escalated"` (on the record, `issues.<KEY>.holdReason` is `escalated`), the architect sent it to you: handle it as an architect escalation below. Parking the tree is `legion status <root> backlog`; setting the root back to `todo` later re-admits it as a new generation, which starts again from its architect |
335
+ | `worker-died on <KEY>` from the Go daemon with `role: "architect"` | the tree root whose architect's claim failed, and the phase the root was in | The tree's architect ran out of launches or prompts and the daemon relaunches nothing; every other notice of the tree goes to that architect, so nobody inside the tree can act. Verify in `legion state` (`issues.<KEY>.architect.state` is `failed`); if the root's phase is `done`, the tree is already parked: no action. Otherwise re-admit the tree (`legion status <root> backlog`, then `todo`: a new generation, whose architect starts again with fresh budgets) or leave it parked and say why on the issue |
336
+ | Direct user message | — | Always first |
337
+
338
+ ## New issue triage
339
+
340
+ 1. Read `legion state --json`, then inspect the reported Dispatch issue with `dispatch_read`.
341
+ Verify the issue is in this project, is eligible for a root process, and whether it
342
+ has pre-existing children. Dispatch and daemon state, not the wake text, decide triage.
343
+ Note the `Assignee:` line: that human answers the tree's asks, and their Inbox opens on
344
+ the issues they hold. Never reassign during triage — who holds an issue is the humans'
345
+ decision, made from the issue header.
346
+ 2. If it should run now, admit the root issue:
347
+
348
+ ```text
349
+ legion status <issue> todo
350
+ ```
351
+
352
+ 3. If it should deliberately wait, move it to a parked status instead of leaving it in
353
+ `triage`:
354
+
355
+ ```text
356
+ legion status <issue> backlog
357
+ ```
358
+
359
+ (or `icebox` for longer-term deferral). Dispatch status is the durable record; there is no
360
+ other marker to maintain, beyond the `legion` label, which parking leaves in
361
+ place. A root never waits for capacity in `backlog`: in `todo` the
362
+ daemon's admission queue holds it (`admission.waiting`), so park only a root that should not
363
+ run now. Do not triage a system-created child as a root issue.
364
+ 4. When you post a triage note (a `dispatch_comment` on the issue saying what you decided and
365
+ why), name who will be asked with the assignee sentence: `Assigned to <login>, who will get
366
+ this tree's questions and its design approval`, or, when the `Assignee:` line says
367
+ `unassigned`, `Unassigned — nobody's Inbox shows this tree's questions or its design approval
368
+ until someone takes it from the issue header (Assignee, beside Priority)`. The design approval
369
+ is promised only when the `Design gate policy:` line of your system prompt, which
370
+ whoever launched you writes from the daemon's own configuration, says
371
+ `gates.design: root-issues`. Under `gates.design: off` nobody approves a design, so drop "and
372
+ its design approval" (and "or its design approval") from the sentence. An unassigned root
373
+ still runs; the architect's asks wait in every Inbox's Unassigned band.
374
+
375
+ ## Backlog eligibility
376
+
377
+ Nothing reconsiders `backlog` or `icebox` on its own: [Keeping the slots
378
+ full](#keeping-the-slots-full-go-daemon) takes only `todo` issues, since an issue that waits on a
379
+ deploy or a decision belongs in `backlog` (`skill://dispatch`, "Choosing what to work on"). A
380
+ handed-over root waits for a slot in `todo`, where the daemon's
381
+ admission queue holds it (`admission.waiting`), so a park means "should not run now", and a
382
+ parked root keeps its `legion` label. A parked issue runs again when a person sets it to `todo`,
383
+ or when a wake tells you to re-admit it (`worker-died`).
384
+
385
+ ## Architect escalation
386
+
387
+ Only decide controller-actionable escalations: re-filing independent work, capacity, and
388
+ cross-tree conflicts. The owning architect writes an issue-design decision as a decision block and
389
+ uses `dispatch_ask` only for a human to-do, not the controller.
390
+
391
+ For an independence judgment, verify the child and its parent against current daemon state
392
+ and the Dispatch issue. If the work belongs in an independent root:
393
+
394
+ 1. File a **fresh root issue** with `dispatch_issue({ project, title, spec })` (no `parent`;
395
+ add `labels: ["legion"]`, without which it is never admitted).
396
+ `project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`): the
397
+ project key exactly as `legion.yaml` writes it, which `legion state --json` shows as
398
+ `daemon.project`. It is not the lowercase project token in role names such as
399
+ `legion-<project>-controller`.
400
+ 2. Park the child (`legion status <child> icebox`) and leave
401
+ a pointer to the new root issue. The controller's capability is `todo`/`backlog`/`icebox`
402
+ only — only the owning architect or the daemon closes an issue as `done`.
403
+ 3. Admit or deliberately backlog the new root through the normal triage procedure.
404
+
405
+ Never promote a child in place. Resolve capacity and cross-tree conflicts from verified
406
+ state, routing design decisions back to the owning architect when they are not controller
407
+ judgments.
408
+
409
+ ## Mentions
410
+
411
+ Read the mention and its artifact. Answer it when it asks the controller for triage or
412
+ human-facing information. A human asking how to let a root proceed past its design gate
413
+ approves the root issue's spec document in Dispatch — the `Approve` control in the document's
414
+ header, or the approval question the architect's request opened in the Inbox. The controller
415
+ never opens a gate and there is no operator command for it; a project that does not want the
416
+ gate at all runs `gates.design: off` in its `legion.yaml`. Otherwise resolve the authoritative
417
+ owning architect role and route the verified context with `envoy_publish`. Do not route raw
418
+ event traffic or invent a role token from a partial issue reference.
419
+
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: legion-oracle
3
+ description: Research institutional knowledge before escalating questions to users. Check docs/solutions/ and codebase patterns before asking humans.
4
+ ---
5
+
6
+ # Legion Oracle
7
+
8
+ Research institutional knowledge before escalating questions to users.
9
+
10
+ ## Core Principle
11
+
12
+ **Check docs/solutions/ first.** This codebase captures learnings from past work.
13
+
14
+ ## When to Use
15
+
16
+ ```dot
17
+ digraph oracle_decision {
18
+ "About to ask user a question?" [shape=diamond];
19
+ "Is it a preference/requirement?" [shape=diamond];
20
+ "Might be documented?" [shape=diamond];
21
+ "Ask user directly" [shape=box];
22
+ "Use oracle" [shape=box];
23
+
24
+ "About to ask user a question?" -> "Is it a preference/requirement?" [label="yes"];
25
+ "About to ask user a question?" -> "Ask user directly" [label="no - not asking"];
26
+ "Is it a preference/requirement?" -> "Ask user directly" [label="yes"];
27
+ "Is it a preference/requirement?" -> "Might be documented?" [label="no"];
28
+ "Might be documented?" -> "Use oracle" [label="yes"];
29
+ "Might be documented?" -> "Ask user directly" [label="no"];
30
+ }
31
+ ```
32
+
33
+ **Use oracle for:** patterns, conventions, solved problems, technical approaches
34
+
35
+ **Ask directly for:** preferences, requirements, scope decisions, human judgment
36
+
37
+ ## Research Strategy
38
+
39
+ If the deployment instructions name a librarian (or oracle) role, publish your question to
40
+ `notifications.role.<name>` with `expects_reply: required` and wait for the reply before
41
+ researching yourself. Then run steps 1-2 (parallel OK), and 3-4 if needed, with tools a Legion
42
+ pane actually has: `read`, `grep`, `web_search`, and `task(agent="scout")` (fast read-only
43
+ codebase search) or `task(agent="oracle")` (deeper read-only analysis when the answer needs
44
+ judgment across many files). Do not name any other agent.
45
+
46
+ | Step | Tool | Query |
47
+ |------|------|-------|
48
+ | 1. Institutional learnings | `grep` then `read` over `docs/solutions/` (front-matter `tags`, then the body) | [question]'s keywords |
49
+ | 2. Codebase patterns | `task(agent="scout")`; `task(agent="oracle")` when judgment across many files is needed | Find how [module] handles [topic] |
50
+ | 3. Framework docs | `read` the library's documentation URL | [library] [topic] |
51
+ | 4. External practices | `web_search`, then `read` the primary source | Current best practices for [topic] |
52
+
53
+ Before relying on a step 1 hit, also search for `supersedes: docs/solutions/<its-path>` and
54
+ `Extends docs/solutions/<its-path>` naming it: a newer file may extend or supersede what you
55
+ found, and Legion runs no pass that reconciles these links, so following them is the only way to
56
+ know the hit is still current.
57
+
58
+ ## Output
59
+
60
+ **Found:** Answer with source (file:line or URL)
61
+
62
+ **Not found:** "Checked docs/solutions/ and codebase - no relevant learnings found" → search externally OR escalate to user
63
+
64
+ ## Example
65
+
66
+ ```
67
+ Question: How should I paginate a GraphQL connection?
68
+
69
+ grep pagination docs/solutions/ → no matches
70
+ task(agent="scout") → packages/daemon/src/state/fetch.ts loops on
71
+ pageInfo.hasNextPage / endCursor (contextsPage)
72
+
73
+ Answer: cursor-based, per the `while (page.hasNextPage)` loop in packages/daemon/src/state/fetch.ts
74
+ ```