@sjawhar/opencode-legion-envoy 3.12.1 → 3.14.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.
@@ -0,0 +1,65 @@
1
+ # Answering targeted and direct messages
2
+
3
+ `skill://dispatch` sends you here when a Dispatch frame targets you as BTW, Aside or Steer, or a
4
+ human messages you directly from the Agents page.
5
+
6
+ ## Targeted agent messages
7
+
8
+ A human — or any bearer caller over HTTP, such as a test rig — can target the issue message at a
9
+ live Envoy session or role as **BTW**, **Aside**, or **Steer**. The incoming Dispatch frame names
10
+ the issue and includes a `reply_with` hint (`{ tool, args }`, ready to issue on any host); reply on the same open issue with the existing
11
+ tool, never a new targeted send:
12
+
13
+ ```ts
14
+ dispatch_message({
15
+ issue: "CORE-1",
16
+ in_reply_to: "<targeted-message-id>",
17
+ body: "The requested answer.",
18
+ })
19
+ ```
20
+
21
+ `in_reply_to` correlates the answer under the asker's message in its Conversation card. A BTW
22
+ delivery can post its answer automatically; use this call when the frame asks the primary agent to
23
+ reply. A human may reply to your message in turn — the follow-up arrives as a targeted frame whose
24
+ `in_reply_to` names your message and whose `reply_body` quotes it; answer it the same way,
25
+ `dispatch_message({ issue, in_reply_to: "<their reply id>", body })`, so the exchange reads as one
26
+ thread. `dispatch_message` itself never carries `target` or `delivery`: agent-to-agent traffic goes
27
+ through Envoy or the hub. A bearer that targets over HTTP names its own session in `actor`
28
+ (`{kind: "session", id}`), and the card shows that session as the author. `GET /api/v1/agents`
29
+ (any authenticated caller) lists live sessions with their capabilities (`aside`, `btw`, `steer`);
30
+ target only a session that advertises the mode you want. Sending to a session with no issue
31
+ (`POST /api/v1/agents/{session_id}/messages`) stays human-only.
32
+
33
+ ## Answering a direct message
34
+
35
+ A human can also message you directly from the **Agents** page, with no issue at all. That frame
36
+ names no issue and its `reply_with` hint carries none either; answer it with the message's bare
37
+ id in `in_reply_to`, alone:
38
+
39
+ ```ts
40
+ dispatch_message({
41
+ in_reply_to: "<the direct message's id>",
42
+ body: "The requested answer.",
43
+ })
44
+ ```
45
+
46
+ Leave `issue` out — there is no issue to post into, and naming one would file your answer on
47
+ unrelated work. Dispatch threads the reply under their message in the same conversation, and the
48
+ human sees it in your conversation on the Agents page, where it shows as an unread reply until
49
+ they read it. Every other message still names its issue, so keep the `issue`
50
+ the frame gave you whenever it gave you one; a `dispatch://KEY/message/<id>` reference names the
51
+ issue its message lives on, so that form is a reply on that issue, not a direct message.
52
+
53
+ Have more to say after you answered? Call it again with the same `in_reply_to` and the new text:
54
+ Dispatch threads that follow-up under your first reply, and the tool result names the reply it
55
+ follows. The same text again posts nothing, so a retry is safe. Your host may already have
56
+ answered a **BTW** automatically before you got here; a second call is then your follow-up to
57
+ that answer, so read the result before writing again.
58
+
59
+ Read the whole conversation back — their message and every reply, yours included — with the
60
+ message id alone; it has no issue:
61
+
62
+ ```ts
63
+ dispatch_read({ message: "<the direct message's id, or any reply's>" })
64
+ ```
65
+
@@ -0,0 +1,55 @@
1
+ # Reading Dispatch back, and the reference forms
2
+
3
+ `skill://dispatch` sends you here when you catch up after a restart, read an issue, ask, comment,
4
+ message or document, trace what cites a node, or need the exact `dispatch://` form for a reference.
5
+
6
+ ## What comes back
7
+
8
+ After a restart, catch up with:
9
+
10
+ ```ts
11
+ dispatch_read({ issue?, project?, artifact?, ref? })
12
+ ```
13
+
14
+ With an issue ref, it returns the issue summary, open asks, references, and recent events with `details` `{ issue }`. With a project
15
+ document owner or ref, it returns a document summary with `details` `{ project, document }`. With an ask ref, it returns that ask's
16
+ question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. With a
17
+ message ref, it returns that message and its reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
18
+
19
+ Every read ends with two sections from the reference graph. `Referenced by:` lists what points at the node — every document, ask,
20
+ comment, or message that cites it, plus its structure: child issues, attached documents, anchored and owned asks and comments, replies,
21
+ followers — and `Links:` lists what it cites. Each row is `- <edge kind> <node kind> dispatch://… (<excerpt> · <when>)`; for a
22
+ document source the excerpt is the start of the block holding the mention, and a whole list is one block, so every issue named in
23
+ one list previews the list's first item. When a document references many issues and each backlink should read right, give each
24
+ issue its own paragraph (or block), not an item of one list. Cross-project, always: a message on another project's issue that
25
+ cites an ask shows up under that ask. So "what led to this decision" is one `dispatch_read` on the ask, and "who relies on this
26
+ document" one read on the document. Cite with `dispatch://` references (below) whenever you name a node in a body — a bare id or
27
+ title is invisible to the graph.
28
+
29
+ ## Reference forms
30
+
31
+ Use these in document, ask, comment, and message bodies. In the dashboard, a reference renders
32
+ as an inline link whose text is the target's title (an issue's title, an ask's question, a
33
+ comment's first line, a document's name) once it resolves; a body that is only a bare reference
34
+ still gets an unfurl card instead. Every `ref` argument below (and `issue`/`project`) accepts
35
+ either form — an issue key or a project key is never ambiguous, since a project key never
36
+ contains a dash:
37
+
38
+ ```text
39
+ dispatch://KEY
40
+ dispatch://KEY/spec
41
+ dispatch://KEY/artifact/<slug>[@vN]
42
+ dispatch://KEY/ask/<id>
43
+ dispatch://KEY/comment/<id>
44
+ dispatch://KEY/message/<id>
45
+ dispatch://PROJECT/artifact/<document-ref>[@vN]
46
+ dispatch://PROJECT/artifact/<document-ref>/ask/<id>
47
+ dispatch://PROJECT/artifact/<document-ref>/comment/<id>
48
+ ```
49
+
50
+ A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records. `dispatch_read` also
51
+ accepts the dashboard URL of an issue, spec, artifact, ask, comment, or project document on the configured server (it maps to the
52
+ `dispatch://` form above), and an ask or comment id may be a unique prefix of at least 8 hex characters; a message id is always the
53
+ full uuid. A non-uuid id on `GET /asks/{id}`, `/comments/{id}`, or `/issues/{key}/messages/{id}` is a 400 `ASK_ID_INPUT` /
54
+ `COMMENT_ID_INPUT` / `MESSAGE_ID_INPUT`, never a 500.
55
+
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: dispatch-first
3
+ description: "Use in any session with Dispatch tools, before planning, filing an issue, asking a human, posting a finding, or starting work that someone may already track or have decided."
4
+ ---
5
+
6
+ # Dispatch first
7
+
8
+ Dispatch already holds most of what you are about to plan, file or ask: the open issues, their
9
+ specs, and the answers humans gave. A second issue for tracked work, or a question a human already
10
+ answered, costs that human the time to notice it and splits the history across two places.
11
+
12
+ ## Search before you act
13
+
14
+ Before you plan, file an issue, ask, post a finding or start work, search Dispatch. Every word of
15
+ the query must match, so each word you add can only lose hits: search with two or three words, the
16
+ thing and what is wrong with it, as a user would name them. Never paste a draft.
17
+
18
+ ```ts
19
+ dispatch_search({ query: "broadcast send order" })
20
+ dispatch_search({ query: "reviewer threads OR review comments" })
21
+ ```
22
+
23
+ When a query finds nothing, drop a word before you add one. Try two or three wordings (the
24
+ component's name, the symptom, the fix) before you conclude that nothing exists. Open every hit
25
+ that could be yours with `dispatch_read`, then its parent (the `child_of` row under `Links:`); the
26
+ parent's children and the issue's `Components:` line show where the rest of that work lives.
27
+
28
+ ## What to do with what you find
29
+
30
+ - **The work is already tracked: extend that issue.** Put the finding on it (a comment, a message,
31
+ or an edit to its spec) instead of filing another. File a new issue only when no hit covers the
32
+ work, and cite the nearest one you ruled out (`dispatch://KEY`).
33
+ - **The question is already decided: cite the decision.** An answered ask is the record. Point to
34
+ it where you rely on it (`dispatch://KEY/ask/<id>`) instead of asking again.
35
+ - **You met a duplicate: close it.** Keep the issue that holds the spec and the discussion, carry
36
+ over anything only the duplicate has, then close the duplicate with a reason that names the
37
+ survivor: `dispatch_issue_update({ issue, status: "done", reason: "Duplicate of dispatch://KEY." })`.
38
+ When the duplicate carries the `legion` label, or another session or a human holds its claim,
39
+ comment on it naming the survivor instead of closing it.
40
+ - **Every ask and message stands on its own.** The person who answers sees only that text: put the
41
+ facts, the options and your recommendation in it, and link what you cite (`dispatch://…`)
42
+ instead of writing "see above" or "my earlier message".
43
+
44
+ ## Load the full skill before you write
45
+
46
+ - **Before you write or change a spec, read `skill://dispatch`,** including its "Writing a spec"
47
+ section.
48
+ - Before any other write to Dispatch (an ask, a message, a comment, a document edit, a status
49
+ change, a claim), load `skill://dispatch` unless you already have in this session.
@@ -256,10 +256,10 @@ Preserve this order exactly:
256
256
 
257
257
  What returns the tree to review: a changed diff — a commit above the approved head that
258
258
  touches anything outside `docs/solutions/`, or a conflict-resolution merge whose fingerprint
259
- (`skill://legion-worker`'s unchanged-diff check) differs from the approved head's. What does not: retro's
259
+ (the unchanged-diff check, `skill://legion-worker/references/conflicts-and-rewrites.md`) differs from the approved head's. What does not: retro's
260
260
  `docs/solutions/` commit, and a merge forced by a GitHub-reported conflict whose fingerprint
261
261
  is unchanged. For that merge the order is: the implementer merges the bookmark forward with the
262
- destination (`legion-worker`'s forward-merge procedure — `jj new legion/<KEY> <destination>`,
262
+ destination (the forward-merge procedure in `skill://legion-worker/references/conflicts-and-rewrites.md` — `jj new legion/<KEY> <destination>`,
263
263
  never a rebase, since a rebase rewrites every descendant of the chain's fork point, including
264
264
  another tree's branch stacked on it), pushes it with the ordinary push procedure (a genuine
265
265
  fast-forward), and posts the before/after fingerprints; the tester re-runs the bare gates only;
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: legion-controller
3
- description: Use when handling Legion controller wakes for root-issue triage, backlog admission, architect escalation, resync healing, or human interaction.
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 makes
9
- triage, escalation, and human-interaction judgments; it never does phase-worker work or
10
- routes raw events into an architect.
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, or a new triage root from while no controller ran never
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 N of M)`, it is one page: say in your summary how many rows
103
- it left unread. The rows show no parent, so open each row with `dispatch_read`: one whose
104
- `Links:` name a `child_of` issue is a child, which its parent's architect owns, so leave it,
105
- whether or not `legion state --json` records it (a `child_of` under `Referenced by:` is a child
106
- of this issue, not its parent). Of the rest, triage each that `legion state --json` does not
107
- record under `issues` as a new issue. A root recorded there and now in `triage` is work the
108
- daemon holds that a human pulled back: never re-admit it yourself; name it in your summary to
109
- the human ("<KEY> was pulled back to triage; what do you want?").
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). Leave an issue without the label alone:
121
- handing work to Legion is its owner's decision, so never triage, park, label, or comment on it.
122
- A child needs no label: it runs under its tree's architect once its root is admitted.
123
- `legion status <KEY> todo` admits a root only while it carries the label, so a root you file for
124
- Legion to run carries it (`labels: ["legion"]` in `dispatch_issue`). Taking the label off a
125
- waiting root drops it from the waiting line; taking it off an admitted tree does not stop it.
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. Do not triage a system-created child as a root issue.
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 this tree's questions`,
193
- or, when the `Assignee:` line says `unassigned`, `Unassigned — nobody's Inbox shows this
194
- tree's questions until someone takes it from the issue header (Assignee, beside Priority)`.
195
- An unassigned root still runs; the architect's asks wait in every Inbox's Unassigned band.
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
- When a slot frees or priority changes, use `legion state --json` and the current Dispatch
200
- issue to reconsider parked roots. Admit the selected root with
201
- `legion status <KEY> todo`. Moving an item to or from `backlog`/
202
- `icebox` is a deliberate controller decision, not a no-op.
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`) — not
216
- the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string.
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`.