@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.
- package/README.md +74 -0
- package/agents/deep-worker.md +64 -0
- package/agents/oracle.md +38 -0
- package/agents/plan-gap-analyst.md +59 -0
- package/agents/plan-reviewer.md +61 -0
- package/agents/thermonuclear-code-quality.md +28 -0
- package/agents/thermonuclear-deep-review.md +28 -0
- package/dist/THIRD_PARTY_NOTICES +30 -0
- package/dist/legion.js +16807 -0
- package/dist/skills/ce-simplify-code/LICENSE +21 -0
- package/dist/skills/ce-simplify-code/SKILL.md +64 -0
- package/dist/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
- package/dist/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
- package/dist/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
- package/dist/skills/legion-architect/SKILL.md +370 -0
- package/dist/skills/legion-controller/SKILL.md +419 -0
- package/dist/skills/legion-oracle/SKILL.md +74 -0
- package/dist/skills/legion-retro/SKILL.md +196 -0
- package/dist/skills/legion-worker/SKILL.md +482 -0
- package/dist/skills/legion-worker/references/cleanup-deletion.md +22 -0
- package/dist/skills/legion-worker/references/conflicts-and-rewrites.md +126 -0
- package/dist/skills/legion-worker/references/merge-gate.md +117 -0
- package/dist/skills/legion-worker/references/pr-body.md +146 -0
- package/dist/skills/legion-worker/references/review-threads.md +101 -0
- package/dist/skills/legion-worker/references/systematic-rename.md +19 -0
- package/dist/skills/thermonuclear-code-quality/LICENSE +21 -0
- package/dist/skills/thermonuclear-code-quality/SKILL.md +192 -0
- package/dist/skills/thermonuclear-deep-review/LICENSE +21 -0
- package/dist/skills/thermonuclear-deep-review/SKILL.md +98 -0
- 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
|
+
```
|