@sjawhar/opencode-legion-envoy 3.12.1 → 3.13.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/dist/src/server.js +20 -3
- package/package.json +1 -1
- package/skills/AGENTS.md +1 -1
- package/skills/dispatch/SKILL.md +7 -3
- package/skills/legion-controller/SKILL.md +183 -34
- package/skills/legion-worker/SKILL.md +21 -18
package/dist/src/server.js
CHANGED
|
@@ -13793,6 +13793,16 @@ var AGENT_STREAM_LIMITS = {
|
|
|
13793
13793
|
historyBytes: 512 * 1024,
|
|
13794
13794
|
snapshotIntervalMs: 100
|
|
13795
13795
|
};
|
|
13796
|
+
// ../contracts/src/claim-holds.ts
|
|
13797
|
+
function claimHolds(claim, titles) {
|
|
13798
|
+
if (claim.actor.kind !== "session") {
|
|
13799
|
+
return "holds";
|
|
13800
|
+
}
|
|
13801
|
+
if (titles === undefined) {
|
|
13802
|
+
return "unknown";
|
|
13803
|
+
}
|
|
13804
|
+
return titles.has(claim.actor.id) ? "holds" : "lapsed";
|
|
13805
|
+
}
|
|
13796
13806
|
// ../contracts/src/dispatch-href.ts
|
|
13797
13807
|
function itemFromSearch(search) {
|
|
13798
13808
|
const params = new URLSearchParams(search);
|
|
@@ -16201,17 +16211,19 @@ function componentsLine(components) {
|
|
|
16201
16211
|
}
|
|
16202
16212
|
}
|
|
16203
16213
|
function claimText(claim, titles) {
|
|
16204
|
-
|
|
16214
|
+
const holding = claimHolds(claim, titles);
|
|
16215
|
+
const marker = holding === "unknown" ? " \xB7 liveness unknown" : holding === "lapsed" ? " \xB7 not running" : "";
|
|
16216
|
+
return `${actorLabel(claim.actor, titles)} since ${claim.at}${marker}`;
|
|
16205
16217
|
}
|
|
16206
16218
|
async function liveSessionTitles(client, needed) {
|
|
16207
16219
|
if (!needed) {
|
|
16208
|
-
return
|
|
16220
|
+
return;
|
|
16209
16221
|
}
|
|
16210
16222
|
try {
|
|
16211
16223
|
const agents = await client.listAgents();
|
|
16212
16224
|
return new Map(agents.map((agent) => [agent.session_id, agent.title]));
|
|
16213
16225
|
} catch {
|
|
16214
|
-
return
|
|
16226
|
+
return;
|
|
16215
16227
|
}
|
|
16216
16228
|
}
|
|
16217
16229
|
function holdsSession(claim) {
|
|
@@ -16243,6 +16255,9 @@ function issueSummary(issue2, events, references, graph, titles) {
|
|
|
16243
16255
|
throw new Error("Dispatch issue is missing components");
|
|
16244
16256
|
if (issue2.claim === undefined)
|
|
16245
16257
|
throw new Error("Dispatch issue is missing claim");
|
|
16258
|
+
if (issue2.external_links === undefined) {
|
|
16259
|
+
throw new Error("Dispatch issue is missing external_links");
|
|
16260
|
+
}
|
|
16246
16261
|
return [
|
|
16247
16262
|
`Title: ${issue2.title}`,
|
|
16248
16263
|
`Key: ${issue2.key}`,
|
|
@@ -16254,6 +16269,8 @@ function issueSummary(issue2, events, references, graph, titles) {
|
|
|
16254
16269
|
componentsLine(issue2.components),
|
|
16255
16270
|
`Route: ${routeText(issue2, titles)}`,
|
|
16256
16271
|
...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
|
|
16272
|
+
"External links:",
|
|
16273
|
+
...issue2.external_links.length === 0 ? ["- none"] : issue2.external_links.map((link) => `- ${link.url}${link.kind === undefined ? "" : ` (${link.kind})`}`),
|
|
16257
16274
|
"Open asks:",
|
|
16258
16275
|
...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
|
|
16259
16276
|
"References:",
|
package/package.json
CHANGED
package/skills/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ event intake, process lifecycle, credentials, and role delivery.
|
|
|
10
10
|
| `dispatch/` | every role, and any session writing to Dispatch | specs, asks, comments, artifacts, and messages on native Dispatch |
|
|
11
11
|
| `envoy/` | every role | subscriptions, agent-to-agent messages, and topic formats |
|
|
12
12
|
| `legion-architect/` | root and sub-architects | tree ownership, decomposition, waves, gates, integration, sign-off |
|
|
13
|
-
| `legion-controller/` | the controller root process | wake routing,
|
|
13
|
+
| `legion-controller/` | the controller root process | wake routing, keeping the admission slots full from `todo`, the daily report, escalation |
|
|
14
14
|
| `legion-oracle/` | any role doing research | repository-grounded research |
|
|
15
15
|
| `legion-retro/` | the implementer, at retro | the pre-merge retrospective and its Dispatch message |
|
|
16
16
|
| `legion-worker/` | planner, implementer, tester, reviewer, merger | the phase contracts: handoffs, GitHub identity, PR body and READY discipline, the merge-gate order |
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -215,8 +215,11 @@ dispatch_claim({ issue: "LEGION-234", release: true }) // I have stopped; it i
|
|
|
215
215
|
|
|
216
216
|
A claim records **your** session — the one making the call, never another — and shows on every
|
|
217
217
|
read of the issue: the dashboard header, the issue list and board, `dispatch_read` (a
|
|
218
|
-
`Claimed by:` line) and `dispatch_issues` (a claim on the row).
|
|
219
|
-
|
|
218
|
+
`Claimed by:` line) and `dispatch_issues` (a claim on the row). A session's claim there ends
|
|
219
|
+
`· not running` when the live agent registry does not list that session, as the dashboard's chip
|
|
220
|
+
does — that claim is free to take — and `· liveness unknown` when the registry could not be read,
|
|
221
|
+
which says nothing either way. `dispatch_issues` plus the dashboard's **Unclaimed** filter is how
|
|
222
|
+
you find work nobody is on.
|
|
220
223
|
|
|
221
224
|
- **`409 ISSUE_CLAIMED` means someone else holds this issue.** When it is another session, the
|
|
222
225
|
refusal names it and says it is still running: do not work the issue in parallel — message
|
|
@@ -274,7 +277,8 @@ The two clears differ: `priority` clears with `null`, while `parent` and `route`
|
|
|
274
277
|
Guessing the other one is a refusal either way.
|
|
275
278
|
|
|
276
279
|
Link the pull request that delivers the issue in `external_links` when you open it; the issue page
|
|
277
|
-
renders its state and checks from that link
|
|
280
|
+
renders its state and checks from that link, and `dispatch_read` lists it under `External links:`.
|
|
281
|
+
The call is authenticated with the same bearer as every
|
|
278
282
|
other `dispatch_*` tool: a Legion pane reads it from the `DISPATCH_TOKEN_FILE` path the daemon sets on
|
|
279
283
|
the pane; an OMP session outside Legion reads `dispatch.token` from `~/.config/opencode/envoy.json`.
|
|
280
284
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: legion-controller
|
|
3
|
-
description: Use when handling Legion controller wakes for root-issue triage,
|
|
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, resync healing, or human interaction.
|
|
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
|
|
9
|
-
|
|
10
|
-
routes raw events into an
|
|
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.
|
|
11
12
|
|
|
12
13
|
## Start and claim the controller role
|
|
13
14
|
|
|
@@ -87,8 +88,8 @@ mints a new secret, so your grants stop working and the role moves to the new se
|
|
|
87
88
|
|
|
88
89
|
The Go daemon's controller topic is a wake for a session that is running when it is published.
|
|
89
90
|
Envoy hands an Oh My Pi session no retained copy of a notice published before it subscribed, so a
|
|
90
|
-
hold, a tree architect's failed claim,
|
|
91
|
-
arrives as a wake. At every start, before anything else:
|
|
91
|
+
hold, a tree architect's failed claim, a new triage root, or a freed slot from while no controller
|
|
92
|
+
ran never arrives as a wake. At every start, before anything else:
|
|
92
93
|
|
|
93
94
|
1. Read `legion state --json` and handle each issue whose `issues.<KEY>.phase` is `held` (its
|
|
94
95
|
`issues.<KEY>.holdReason` is `escalated` when its architect sent it to you, and absent while the
|
|
@@ -98,15 +99,19 @@ arrives as a wake. At every start, before anything else:
|
|
|
98
99
|
as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
|
|
99
100
|
nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
|
|
100
101
|
2. List the project's triage issues handed to Legion with
|
|
101
|
-
`dispatch_issues({project, status: "triage", label: "legion", limit: 250})`.
|
|
102
|
-
When its first line ends `(showing
|
|
103
|
-
|
|
104
|
-
`Links:`
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
`dispatch_issues({project, status: "triage", label: "legion", limit: 250, offset: 0})`.
|
|
103
|
+
When its first line ends `(showing 1-250 of N)`, read the next page with `offset: 250`, and so
|
|
104
|
+
on until you have all N rows. The rows show no parent, so open each row with `dispatch_read`
|
|
105
|
+
and follow `Links:` up through each `child_of` parent (a `child_of` under `Referenced by:` is a
|
|
106
|
+
child of this issue, not its parent). Leave a child that has an ancestor in `admission.active`
|
|
107
|
+
or `admission.waiting`: that tree's architect owns it. Triage every other row as a root,
|
|
108
|
+
children outside a live tree included, when `legion state --json` does not record it under
|
|
109
|
+
`issues`. A root recorded there and now in `triage` is work the daemon holds that a human
|
|
110
|
+
pulled back: never re-admit it yourself; name it in your summary to the human ("<KEY> was
|
|
111
|
+
pulled back to triage; what do you want?").
|
|
112
|
+
3. Fill the free admission slots ([Keeping the slots full](#keeping-the-slots-full-go-daemon)).
|
|
113
|
+
4. Post the day's report when this is your first turn of the UTC day
|
|
114
|
+
([Daily report](#daily-report-go-daemon)).
|
|
110
115
|
|
|
111
116
|
The issue record and Dispatch are the truth; the topic is the wake.
|
|
112
117
|
|
|
@@ -117,12 +122,141 @@ since its project may be shared with humans and other agents. It never admits a
|
|
|
117
122
|
without the label, and wakes you for a root in `triage` only while the root carries it and is
|
|
118
123
|
unrecorded: on its creation with the label, and on each change to it after that while it stays
|
|
119
124
|
in triage, the change that adds the label included (the dashboard creates an issue without
|
|
120
|
-
labels, so a human adds it from the issue header).
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
labels, so a human adds it from the issue header). Two parties hand work over: a person, who sets
|
|
126
|
+
the label from the issue header, and you, when you fill a free slot (below). You label an issue
|
|
127
|
+
only when you take it or file it for Legion, so the label means Legion has the issue or had it.
|
|
128
|
+
A person's label is their decision: never take it off. A child needs no label: it runs under its
|
|
129
|
+
tree's architect once its root is admitted. `legion status <KEY> todo` admits a root, or a child
|
|
130
|
+
outside a live tree (admitted as a root of its own), only while it carries the label, so an issue
|
|
131
|
+
you hand over or file for Legion to run carries it first (`labels` in `dispatch_issue_update` or
|
|
132
|
+
`dispatch_issue`). Taking the label off a waiting root drops it from the waiting line; taking it
|
|
133
|
+
off an admitted tree does not stop it.
|
|
134
|
+
|
|
135
|
+
## Keeping the slots full (Go daemon)
|
|
136
|
+
|
|
137
|
+
Picking the next work is your job: nobody hand-feeds issues to Legion. Keep every admission slot
|
|
138
|
+
filled with the highest-priority concrete issue Legion can take. The unit of Legion work is a
|
|
139
|
+
leaf, an issue with no children, never an umbrella that holds other issues.
|
|
140
|
+
|
|
141
|
+
**When.** At every start (step 3 above), and on each `slot-free on <KEY>` wake: the daemon
|
|
142
|
+
released `<KEY>`'s slot, because its tree finished or left the workflow, and no waiting root took
|
|
143
|
+
it.
|
|
144
|
+
|
|
145
|
+
**Scope first.** The scope the deployment instructions state decides which issues are candidates
|
|
146
|
+
at all, before anything below. When they say you hand Legion no issue yourself, or that Legion
|
|
147
|
+
runs only issues someone else sets to `todo`, the walk takes nothing: stop here, whatever slots
|
|
148
|
+
are free. When they narrow the scope (a repository, a kind of change), a candidate outside it is
|
|
149
|
+
skipped (the last row of the table below). With no scope stated, every issue of the project is in
|
|
150
|
+
scope.
|
|
151
|
+
|
|
152
|
+
**How many.** Read `legion state --json`. The free slots are `admission.cap` minus the roots in
|
|
153
|
+
`admission.active` and in `admission.waiting` (a waiting root takes the next slot before anything
|
|
154
|
+
you add). With none free, stop.
|
|
155
|
+
|
|
156
|
+
**Candidates.** The project's open `todo` issues, roots and children alike, that have no children
|
|
157
|
+
at all and do not carry the `legion` label. `todo` alone, as Sami ruled for choosing work on
|
|
158
|
+
2026-09-27 (`skill://dispatch`, "Choosing what to work on"): take the top ready issue, "status
|
|
159
|
+
`todo`, highest priority first, then board rank"; an issue that waits on a deploy or a decision
|
|
160
|
+
belongs in `backlog`, so the walk takes nothing from `backlog` or `triage`. The Go daemon runs
|
|
161
|
+
only labelled roots, so every root it ran since the daemon required the label carries it: a
|
|
162
|
+
labelled root in `todo` is the daemon's to admit or queue, one in `triage` is yours to triage
|
|
163
|
+
(step 2 above), and one anywhere else was parked by Legion or by a person. A child you take becomes
|
|
164
|
+
a root of its own: the daemon admits a labelled `todo` child whose tree Legion does not run as a
|
|
165
|
+
new tree. The `dispatch_issues` rows show no labels and no children, so the listing below filters
|
|
166
|
+
neither: both are checked on each candidate (the table's first rows). List the `todo` issues one
|
|
167
|
+
priority at a time, `priority: [0]` first, then `[1]`, `[2]`, `[3]`, and `[null]` (no priority)
|
|
168
|
+
last, keeping the listing's order within each, which is the board's rank:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
dispatch_issues({ project: "<PROJECT>", status: "todo", priority: [0], limit: 250, offset: 0 })
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
When the first line ends `(showing 1-250 of N)`, the next page is `offset: 250`, then `500`. Read
|
|
175
|
+
pages only as far as you need: stop listing once the free slots are filled. `<PROJECT>` is the
|
|
176
|
+
Dispatch project key, the prefix of this deployment's issue keys (`AGENTC-12` → `AGENTC`), which is
|
|
177
|
+
also `daemon.project` in `legion state --json`: the project key exactly as `legion.yaml` writes it.
|
|
178
|
+
A row that shows `claimed by …` and does not end its claim with `· not running` (the route, when
|
|
179
|
+
the row shows one, comes after the claim) is claimed, as the table below says: skip it without
|
|
180
|
+
reading it.
|
|
181
|
+
|
|
182
|
+
**Walk.** Take the remaining rows in that order until the free slots are filled. Read each one with
|
|
183
|
+
`dispatch_read({ issue: "<KEY>" })` and skip it when any of these holds. `dispatch_read` shows only
|
|
184
|
+
the issue's last 10 events, so the rows that read `Events:` are best-effort: the pull-request row
|
|
185
|
+
leans on `External links:`, and the label row is the one that never depends on history.
|
|
186
|
+
|
|
187
|
+
| Skip when | How you check it |
|
|
188
|
+
|---|---|
|
|
189
|
+
| It carries the `legion` label | `Labels:` lists `legion`, in any case. Legion has the issue or had it, as **Candidates** above says. |
|
|
190
|
+
| 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. |
|
|
191
|
+
| 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. |
|
|
192
|
+
| 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. |
|
|
193
|
+
| 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`, closed-tree activity). |
|
|
194
|
+
| 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. |
|
|
195
|
+
| 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. |
|
|
196
|
+
| 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`. |
|
|
197
|
+
| 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. |
|
|
198
|
+
| 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. |
|
|
199
|
+
|
|
200
|
+
**Take.** For each candidate that passes, in order:
|
|
201
|
+
|
|
202
|
+
1. Add the label and keep the labels it has, which `Labels:` lists (`none` is no labels). `labels`
|
|
203
|
+
replaces the whole set, so a label you leave out is removed.
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
dispatch_issue_update({ issue: "<KEY>", labels: ["<each current label>", "legion"] })
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
2. It is already in `todo`, so the label admits it: the daemon records it and gives it the free
|
|
210
|
+
slot, or queues it in `admission.waiting`. It needs no status write.
|
|
211
|
+
3. Post one short comment that says Legion took it and names who is asked at its design gate, from
|
|
212
|
+
the `Assignee:` line, with the assignee sentence [New issue triage](#new-issue-triage) step 4
|
|
213
|
+
gives:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
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." })
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The daemon admits each root when Dispatch's event reaches it; the next `legion state --json`
|
|
220
|
+
shows it in `admission.active`. When the candidates run out with slots still free, leave those
|
|
221
|
+
slots free and say so in your summary.
|
|
222
|
+
|
|
223
|
+
## Daily report (Go daemon)
|
|
224
|
+
|
|
225
|
+
Once a day, post one Dispatch message that says what Legion finished, what it closed without a
|
|
226
|
+
change and why, and what is running.
|
|
227
|
+
|
|
228
|
+
**Where.** On the issue the deployment instructions name for Legion's reports. With none named,
|
|
229
|
+
on the project's `Legion daily report` issue:
|
|
230
|
+
`dispatch_search({ query: "\"Legion daily report\"", project: "<PROJECT>" })` finds it. When it
|
|
231
|
+
does not exist, create it once and park it in `icebox`, so nobody takes it as work; it never
|
|
232
|
+
carries the `legion` label:
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
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." })
|
|
236
|
+
legion status <report KEY> icebox
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**When.** On your first turn of each UTC day, whatever it is: your start, or a wake of any kind.
|
|
240
|
+
Read the report issue with `dispatch_read`, and post when its `Events:` show no `message.created`
|
|
241
|
+
from today. You have no clock of your own, so a day with no turn has no report; the next one
|
|
242
|
+
covers it.
|
|
243
|
+
|
|
244
|
+
**What.** One `dispatch_message({ issue: "<report KEY>", body })` of at most 2,000 characters,
|
|
245
|
+
written as `skill://dispatch`'s "Writing for the human" says: every issue by its key and title,
|
|
246
|
+
every pull request by its URL.
|
|
247
|
+
|
|
248
|
+
- **Finished.** `dispatch_issues({ project: "<PROJECT>", status: "done", updated_since: "<the
|
|
249
|
+
previous report's time, or 24 hours ago>", limit: 250 })`, read every page, and keep the issues
|
|
250
|
+
`legion state --json` records under `issues` whose `issue.closed` event is after the previous
|
|
251
|
+
report. For each, `dispatch_read` it: the pull request under `External links:` is the one that
|
|
252
|
+
merged.
|
|
253
|
+
- **Closed without a change.** Those with no pull request, each with the reason its closing
|
|
254
|
+
message gave (the `message.created` just before `issue.closed` among `Events:`).
|
|
255
|
+
- **Running.** Each root in `admission.active` with its `issues.<KEY>.phase`, and the roots in
|
|
256
|
+
`admission.waiting`.
|
|
257
|
+
|
|
258
|
+
A day with nothing finished says so in one sentence. When the lists do not fit, keep the counts
|
|
259
|
+
and the highest-priority issues.
|
|
126
260
|
|
|
127
261
|
## Deployment instructions
|
|
128
262
|
|
|
@@ -136,7 +270,8 @@ quoted here.
|
|
|
136
270
|
- **Direct user message always first.** If this turn includes a direct user message, answer
|
|
137
271
|
it before handling every other wake.
|
|
138
272
|
- **One wake = one turn.** Handle exactly the wake's implication, then end the turn. Never
|
|
139
|
-
poll, idle-loop, or wait for another event.
|
|
273
|
+
poll, idle-loop, or wait for another event. The one addition: your first turn of each UTC day,
|
|
274
|
+
whatever woke you, also posts the day's report ([Daily report](#daily-report-go-daemon)).
|
|
140
275
|
- **Wakes are advisory.** Before any side effect, verify the current daemon state and the
|
|
141
276
|
relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
|
|
142
277
|
- **Controller state is disposable.** Do not reconstruct or preserve local controller
|
|
@@ -151,7 +286,8 @@ quoted here.
|
|
|
151
286
|
| Wake | Content | Controller action |
|
|
152
287
|
|---|---|---|
|
|
153
288
|
| New issue created in the Dispatch project (`issue.created`, status `triage`; under the TypeScript daemon resync heals misses, under the Go daemon the boot step above does). From the Go daemon: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above) | issue key + triage context (incl. pre-existing children) | Triage: `legion status <KEY> todo` to admit, or set `backlog`/`icebox` to park |
|
|
154
|
-
| Backlog eligibility | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
|
|
289
|
+
| Backlog eligibility (TypeScript daemon) | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
|
|
290
|
+
| `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)) |
|
|
155
291
|
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `dispatch_ask` from the owning architect, not here |
|
|
156
292
|
| Resync report | artifact-driven anomaly list (zero-owner trees, untriaged-open, launch-failed, admission-drift) | Verify against fresh state, then heal |
|
|
157
293
|
| Resync report: `admission-drift` entry | issue key + whether the daemon added it to, or removed it from, its admission list (the detail says which) | No action: the daemon already repaired it in the same run. An issue that reappears in consecutive reports is a live leak — file a LEGION issue on Dispatch with both reports pasted as evidence (never a GitHub issue) |
|
|
@@ -187,19 +323,30 @@ quoted here.
|
|
|
187
323
|
|
|
188
324
|
(or `icebox` for longer-term deferral). Dispatch status is the durable record; there is no
|
|
189
325
|
other marker to maintain, beyond the Go daemon's `legion` label, which parking leaves in
|
|
190
|
-
place.
|
|
326
|
+
place. Under the Go daemon a root never waits for capacity in `backlog`: in `todo` the
|
|
327
|
+
daemon's admission queue holds it (`admission.waiting`), so park only a root that should not
|
|
328
|
+
run now. Do not triage a system-created child as a root issue.
|
|
191
329
|
4. When you post a triage note (a `dispatch_comment` on the issue saying what you decided and
|
|
192
|
-
why), name who will be asked: `Assigned to <login>, who will get
|
|
193
|
-
or, when the `Assignee:` line says
|
|
194
|
-
|
|
195
|
-
|
|
330
|
+
why), name who will be asked with the assignee sentence: `Assigned to <login>, who will get
|
|
331
|
+
this tree's questions and its design approval`, or, when the `Assignee:` line says
|
|
332
|
+
`unassigned`, `Unassigned — nobody's Inbox shows this tree's questions or its design approval
|
|
333
|
+
until someone takes it from the issue header (Assignee, beside Priority)`. An unassigned root
|
|
334
|
+
still runs; the architect's asks wait in every Inbox's Unassigned band.
|
|
196
335
|
|
|
197
336
|
## Backlog eligibility
|
|
198
337
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
338
|
+
Under the Go daemon nothing reconsiders `backlog` or `icebox` on its own: [Keeping the slots
|
|
339
|
+
full](#keeping-the-slots-full-go-daemon) takes only `todo` issues, since an issue that waits on a
|
|
340
|
+
deploy or a decision belongs in `backlog` (Sami's ruling of 2026-09-27, `skill://dispatch`,
|
|
341
|
+
"Choosing what to work on"). A handed-over root waits for a slot in `todo`, where the daemon's
|
|
342
|
+
admission queue holds it (`admission.waiting`), so a park means "should not run now", and a
|
|
343
|
+
parked root keeps its `legion` label. A parked issue runs again when a person sets it to `todo`,
|
|
344
|
+
or when a wake tells you to re-admit it (`worker-died`, closed-tree activity).
|
|
345
|
+
|
|
346
|
+
Under the TypeScript daemon, when a slot frees or priority changes, use `legion state --json` and
|
|
347
|
+
the current Dispatch issue to reconsider parked roots. Admit the selected root with
|
|
348
|
+
`legion status <KEY> todo`. Moving an item to or from `backlog`/`icebox` is a deliberate
|
|
349
|
+
controller decision, not a no-op.
|
|
203
350
|
|
|
204
351
|
## Architect escalation
|
|
205
352
|
|
|
@@ -212,8 +359,10 @@ and the Dispatch issue. If the work belongs in an independent root:
|
|
|
212
359
|
|
|
213
360
|
1. File a **fresh root issue** with `dispatch_issue({ project, title, spec })` (no `parent`;
|
|
214
361
|
under the Go daemon add `labels: ["legion"]`, without which it is never admitted).
|
|
215
|
-
`project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`)
|
|
216
|
-
|
|
362
|
+
`project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`): the
|
|
363
|
+
project key exactly as `legion.yaml` writes it, which `legion state --json` shows as
|
|
364
|
+
`daemon.project`. It is not the lowercase project token in role names such as
|
|
365
|
+
`legion-<project>-controller`.
|
|
217
366
|
2. Park the child (`legion status <child> icebox`) and leave
|
|
218
367
|
a pointer to the new root issue. The controller's capability is `todo`/`backlog`/`icebox`
|
|
219
368
|
only — only the owning architect or the daemon closes an issue as `done`.
|
|
@@ -145,22 +145,6 @@ Anything else, stop and send the owning architect the `jj -R "$LEGION_WORKSPACE"
|
|
|
145
145
|
evidence; the architect decides, and an operator performs any operation-log restore with every
|
|
146
146
|
other tree paused.
|
|
147
147
|
|
|
148
|
-
**Filesystem and process safety:** Your pane runs as the operator's own user, so one mistaken
|
|
149
|
-
path can destroy the machine every agent shares (on 2026-09-13 a probe script's leftover
|
|
150
|
-
`rm -rf "$work" "$HOME"` deleted the operator's SSH and signing keys and stopped every worker).
|
|
151
|
-
The extension refuses, before it runs, a `bash` command, `eval` code, or `hub` process start
|
|
152
|
-
(yours or a `task` subagent's) that would delete, move, truncate, overwrite an existing file by
|
|
153
|
-
redirection or `tee`, or `chmod -R`/`chown -R` anything outside `$LEGION_WORKSPACE` and any
|
|
154
|
-
directory below `/tmp` except `/tmp` itself, a glob over it, and its tmux and ssh socket
|
|
155
|
-
directories. It cannot tell which allowed `/tmp` directory belongs to your pane. It follows
|
|
156
|
-
`$HOME`, `~`, variables, `cd`, and the scripts a command runs. A target with no proven path prefix
|
|
157
|
-
is refused; an unknown trailing component under a prefix already proven inside your workspace or
|
|
158
|
-
permitted `/tmp` remains allowed. `pkill` and `killall` are refused, and `kill` only reaches a
|
|
159
|
-
process you started (a descendant of your Oh My Pi process): stop your own long-running processes
|
|
160
|
-
through the hub tool. The refusal names the target and the rule; do not rewrite a script just to
|
|
161
|
-
silence it. This is a mistake-guard rather than a sandbox. What it cannot read, a compiled program
|
|
162
|
-
or code whose paths are only known at run time, is still yours to keep inside the workspace.
|
|
163
|
-
|
|
164
148
|
## Phase work
|
|
165
149
|
|
|
166
150
|
Specifications written into Dispatch follow `skill://dispatch`'s [Writing a spec](../dispatch/SKILL.md#writing-a-spec).
|
|
@@ -628,12 +612,31 @@ cd -- "$LEGION_WORKSPACE" && \
|
|
|
628
612
|
**Every role pushes its own commits.** After the handoff commit — and, for the tester, the red
|
|
629
613
|
tests it wrote — advance the issue bookmark and push it with the provisioned credential helper,
|
|
630
614
|
which authenticates as your role's App (`appRoleForLegionRole` in
|
|
631
|
-
`packages/daemon/src/daemon/github-apps.ts`).
|
|
615
|
+
`packages/daemon/src/daemon/github-apps.ts`).
|
|
616
|
+
|
|
617
|
+
**Under the Go daemon, every push is `legion push`,** run from bash in your workspace in place of
|
|
618
|
+
the commands below. It runs this same procedure on `@-`: the ancestry check, against the remote
|
|
619
|
+
branch or the tip you recorded before rewriting pushed commits (below), then the bookmark and the
|
|
620
|
+
push. It also decides whether the push skips CI. A push skips CI only when none of its commits
|
|
621
|
+
touches anything but handoffs whose phase guarantees a later push: the planner's
|
|
622
|
+
`.legion/plan.json`, the tester's `.legion/test.json`, and a reviewer's `.legion/review.json` whose
|
|
623
|
+
`verdict` is `"changes_requested"`. Its head then ends with GitHub's `skip-checks: true` trailer,
|
|
624
|
+
and the Go daemon carries the code head's verdict to it. Every other push runs CI in full. Never
|
|
625
|
+
add or remove that trailer yourself, never write one of GitHub's bracket keywords (`[skip ci]`,
|
|
626
|
+
`[ci skip]`, `[no ci]`, `[skip actions]`, `[actions skip]`) into a commit message, and never push
|
|
627
|
+
the issue branch with the commands below under the Go daemon: a hand-run push of a handoff that
|
|
628
|
+
could skip CI runs it in full, and a hand-added trailer or keyword on any other push skips CI on a
|
|
629
|
+
head a human may merge. `legion push` refuses a head whose message carries a keyword and pushes
|
|
630
|
+
nothing until you take it out. Under the TypeScript daemon, whose `legion` has no `push` command,
|
|
631
|
+
run the commands below yourself.
|
|
632
|
+
|
|
633
|
+
`-r @-` puts the bookmark on the commit you just
|
|
632
634
|
split off: the working copy left above it has no description, and `jj git push` refuses a
|
|
633
635
|
commit without one. `--allow-backwards` is for that local step alone: after a split the bookmark
|
|
634
636
|
can sit on the undescribed working copy above `@-`. `--bookmark` also publishes the locally
|
|
635
637
|
provisioned bookmark on its first push — a bookmark not yet tracking a remote one is tracked
|
|
636
|
-
automatically. This is the one push procedure
|
|
638
|
+
automatically. This is the one push procedure, run as `legion push` under the Go daemon and by
|
|
639
|
+
hand under the TypeScript daemon; every push of the issue branch uses it:
|
|
637
640
|
|
|
638
641
|
```bash
|
|
639
642
|
cd -- "$LEGION_WORKSPACE" && \
|