@sjawhar/opencode-legion-envoy 3.13.0 → 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.
@@ -15,6 +15,23 @@ the 800-character limit (850/800)`); it never truncates it. A tool call with sev
15
15
  (`<tool> was not called: N problems`), so one corrected call lands. GitHub threads and markers no
16
16
  longer exist.
17
17
 
18
+ ## Where the detail lives
19
+
20
+ This file is the workflow. The detail for a step is in a reference file: open it when you reach
21
+ that step, not before. On a host with no `skill://` scheme (Claude Code, OpenCode), a link's path
22
+ after `skill://dispatch/` is relative to this skill's base directory.
23
+
24
+ | When you are about to | Read |
25
+ | --- | --- |
26
+ | call `dispatch_doc_edit`: rewrite a paragraph, insert or move a block, change a table's cells, rows or columns | [Editing a document](skill://dispatch/references/document-edits.md) |
27
+ | write a typed block (an `:::ask`, a callout), comment on or suggest a change to a document, upload an artifact, or retry after `DOC_SERVICE_UNAVAILABLE` | [Documents](skill://dispatch/references/documents.md) |
28
+ | choose your next issue, claim one, move its status or priority, reorder a board, or audit a project's backlog | [Working an issue](skill://dispatch/references/issues.md) |
29
+ | find who answers an ask, edit, retract or resolve one, reply with the turn, or follow a thread | [Asks after they open](skill://dispatch/references/asks.md) |
30
+ | catch up after a restart, trace what cites a node, or write a `dispatch://` reference | [Reading back](skill://dispatch/references/reading.md) |
31
+ | answer a BTW, Aside or Steer frame, or a message from the Agents page | [Targeted and direct messages](skill://dispatch/references/messages.md) |
32
+ | see a worked ask, a message not to send, and where a draft goes | [Before and after](skill://dispatch/references/examples.md) |
33
+ | set up a token, or call a route the tools do not cover | [Authentication and the HTTP API](skill://dispatch/references/api.md) |
34
+
18
35
  ## Design changes are brainstormed here
19
36
 
20
37
  Sami, 2026-09-17, verbatim: "Make sure your agents know that they should be doing brainstorming with me
@@ -59,29 +76,6 @@ vocabulary, and is often on a phone. Write for that person.
59
76
  - When an ask or message communicates a judgment, lead with that judgment in one sentence and put the mechanism underneath it. Do not make the reader ask a second time whether the result is a win. This shapes communication only when a judgment exists; it does not pre-decide an open question or remove its genuine options. Inferred from the AGENTC-186 12-hour-cap incident (platform PO, 2026-09-17).
60
77
  - When a Dispatch message states a root cause, include the reproducing command or test in that same message. Without it, label the diagnosis a hypothesis; a diagnosis still in progress may say so plainly. This boundary applies to causal claims, not to reporting that an investigation has started. Inferred from the astro lane's 2026-09-16 retro (platform PO, 2026-09-17).
61
78
 
62
- ## Agent authentication
63
-
64
- Use a personal Dispatch token: a human mints it in Dispatch **Settings → Agent tokens** and supplies
65
- it to the agent through `dispatch.token` in `~/.config/opencode/envoy.json` or `DISPATCH_TOKEN`.
66
- The server records the minting human as the owner of that session's writes. `DISPATCH_AGENT_TOKEN`
67
- is the shared devbox fallback; do not configure it for an individual agent.
68
-
69
- The deployed Dispatch server's browser origin is configured separately with
70
- `DISPATCH_SERVER_URL` in the deployment `compose/.env`. Do not change an
71
- agent's `envoy.json` to set the GitHub OAuth callback origin: the value must
72
- be the exact URL humans type in their browser, and the GitHub App callback is
73
- `<DISPATCH_SERVER_URL>/auth/callback`.
74
-
75
- ### Finding a route
76
-
77
- The tools cover the everyday surface. For anything else, ask the server: `GET /api/v1` (no
78
- credential) returns every route as `{method, path, auth, description}` sorted by path — `auth`
79
- is `public`, `any` (a human or a bearer), `human` (a bearer gets `403 HUMAN_ONLY`), or `bearer`.
80
- A path Dispatch does not serve under `/api` or `/v1` answers
81
- `404 {"code":"NOT_FOUND","error":"no route for GET /v1/issues","hint":"GET /api/v1 lists every
82
- route"}`; when you see that, you typed the path wrong — read the index rather than guessing. Every
83
- `/api/v1` error body carries a `code`; branch on the code, never on the text.
84
-
85
79
  ## Writing a spec
86
80
 
87
81
  A spec has two readers: the human who decides reads the **Summary** and **New since we talked** at the top, then each decision through its [`:::ask` block](#decision-blocks) where it arises; the implementer who builds reads the rest. Use these headings in this order.
@@ -150,164 +144,31 @@ See [Typed blocks](#typed-blocks) for the syntax and [Before you ask](#before-yo
150
144
  Every session works on an issue or project document. Legion pre-fills `issue` from `LEGION_ISSUE`: use a native issue key such as
151
145
  `LEGION-3`, an external `owner/repo#n` reference, or a bare positive number (resolved against the cwd repository). Otherwise pass
152
146
  exactly one owner to every owner-scoped tool: `issue` for an issue, or `project` and `artifact` for an unlinked project document (see
153
- [References](#references) for the resulting ref shape). An external issue reference addresses the existing Dispatch issue linked to
147
+ [Reference forms](skill://dispatch/references/reading.md) for the resulting ref shape). An external issue reference addresses the existing Dispatch issue linked to
154
148
  that GitHub issue or pull request. Only `dispatch_issue` with `external` creates a native issue; if no issue is linked, call
155
149
  `dispatch_issue({ external: "owner/repo#n", project: "<project>", title: "<title>" })` before addressing it.
156
150
 
157
- Issue reads include `rank`, the server-owned ordering key used by project boards; reorder through `PATCH /api/v1/issues/{key}` with `{"rank": {"before": "<key>", "after": "<key>"}}`, either neighbor optional and both in the issue's project. A bearer caller also names its own session in that body, `"actor": {"kind": "session", "id": "<your session id>"}`, or the server refuses with `ACTOR_KIND`. They also include nullable coarse priority (`P0` highest through `P3` lowest) and `assignee`: the lowercase GitHub login of the human who answers the issue's asks, or `null` when nobody holds it. `dispatch_read` of an issue prints it as `Assignee: <login>` or `Assignee: unassigned`.
158
-
159
- ### Who answers an ask
160
-
161
- An ask goes to the issue's assignee: their Inbox opens on **Mine**, which lists asks on the issues they hold plus an Unassigned band; an ask on an unassigned issue waits in that band for someone to take it. Find out who Dispatch takes you for with:
162
- ```ts
163
- dispatch_whoami({})
164
- ```
165
- It returns `details` `{ session, owner, service }`: `owner` is the lowercase login of the human whose personal token you run under, or `null` under the shared token or a verified service token, in which case `service` is that token's subject (`system:serviceaccount:<namespace>:<name>`, `null` otherwise) and every write you make is rendered `(as <namespace>/<name>)` — the namespace is kept because every namespace has a `default` service account, and a subject that is not a Kubernetes one is shown whole. An issue you create without `assignee` goes to your owner; with no owner it inherits its parent's assignee, or stays unassigned without a parent. If an issue you are asking on is unassigned and the answer matters, assign it to your owner (`PATCH /api/v1/issues/{key}` with `{"assignee": "<login>"}`; any authenticated caller may reassign, and an unlisted login is refused with `ASSIGNEE_NOT_ALLOWED`) or name in the question who should answer it. Never reassign an issue a human holds to get an answer faster: that is the human's call.
166
-
167
- Architects create newly tracked child work with:
151
+ Create an issue for newly tracked work with:
168
152
  ```ts
169
153
  dispatch_issue({ project, title, parent?, external?, spec?, force?, labels?: string[], priority?: 0 | 1 | 2 | 3, assignee?: string })
170
154
  ```
171
- `labels` are optional initial labels: Dispatch trims them, preserves their case, and removes case-insensitive duplicates. `priority` is yours on creation too — see [Priority is yours to set](#priority-is-yours-to-set). Set `assignee` (a GitHub login on the sign-in allowlist) only when the human said who owns the work; otherwise the default above applies, so a child inherits its parent's assignee. It returns
155
+ `labels` are optional initial labels: Dispatch trims them, preserves their case, and removes case-insensitive duplicates. `priority` is yours on creation too — see [Priority is yours to set](skill://dispatch/references/issues.md). Set `assignee` (a GitHub login on the sign-in allowlist) only when the human said who owns the work; otherwise the default above applies, so a child inherits its parent's assignee. It returns
172
156
  `details` `{ issue }`; creating an issue does not subscribe you to it (see [Following](#following)). Use `dispatch_issue` only to create an issue; never use it to park a question. When `spec` is supplied,
173
157
  follow [Writing a spec](#writing-a-spec).
174
158
 
175
- ## Choosing what to work on
176
-
177
- When you finish an issue, or are told to work on the next thing, take the top ready issue of the
178
- whole backlog, across every project: status `todo`, highest priority first, then board rank. There
179
- are no areas: a standing role, a product owner and a lane each take the top issue like everyone
180
- else (Sami, 2026-09-27, dispatch://AGENTC-34/ask/01ed2956-73cc-48d2-8ed4-7a86c6d439b1). `todo`
181
- means ready: specced, unblocked, and waiting on neither a deploy nor a decision. An issue that
182
- waits on one belongs in `backlog`, with what it waits on said on the issue.
183
-
184
- Hold at most three issues in flight (`in_progress`, `testing`, `needs_review` or `retro`), of any
185
- kind (Sami, 2026-09-27, answering dispatch://AGENTC-34/ask/1aeb8f2e-0950-4eaa-aaac-24286c9dd3ca;
186
- the question proposed two, and his answer set three). The limit is per agent and has nothing to do
187
- with the week's priorities (Sami, 2026-09-28, reply a7647eb0 on
188
- dispatch://AGENTC-393/ask/b773d9f6): the priorities decide only what you pull next. Past three:
189
- push any unfinished work, say where in one comment on the issue, move it to `backlog` and clear
190
- its route. Each issue counts on its own; a child does not ride under its parent's slot.
191
- In-flight issues with no owner at all go
192
- back to `backlog` as well: no claim or route held by a live session, no Dispatch activity in the
193
- last day, and no pull request moving on GitHub (an owner working there leaves no Dispatch trace).
194
- The order keeper sweeps those. Never write the status of an issue that carries the `legion`
195
- label, or of any issue under one: the Legion daemon writes those statuses, and moving one of its
196
- admitted roots out of its flow parks the tree and stops its workers.
197
-
198
- One agent keeps the backlog's order against those priorities, with Sami
199
- (dispatch://AGENTC-34/ask/f6780f9e-8b96-49eb-9be7-7c7f2036d5cc). Setting an issue's priority
200
- stays yours ([Priority is yours to set](#priority-is-yours-to-set)); reordering the board does not.
201
- When the top of the backlog looks wrong, or a priority's next step is not yet a ready issue,
202
- publish it to `notifications.role.backlog-order`, which the order keeper holds, instead of
203
- reordering the board yourself.
204
-
205
- ## Claim the issue before you work it
206
-
207
- Two sessions once spent a night implementing the same issue, because nothing on it said who was
208
- on it (Sami, 2026-09-24, verbatim: "It seems like we need a better way of tracking what's already
209
- in progress"). So before you start implementing an issue, claim it:
210
-
211
- ```ts
212
- dispatch_claim({ issue: "LEGION-234" }) // I am implementing this
213
- dispatch_claim({ issue: "LEGION-234", release: true }) // I have stopped; it is free
214
- ```
215
-
216
- A claim records **your** session — the one making the call, never another — and shows on every
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). 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.
223
-
224
- - **`409 ISSUE_CLAIMED` means someone else holds this issue.** When it is another session, the
225
- refusal names it and says it is still running: do not work the issue in parallel — message
226
- that session (its id is in the message; `envoy_send` reaches it) or pick up something else,
227
- and tell the human if you believe the work should be yours. When a **human** holds it, the
228
- refusal names the person and says nothing about a session running, because there is none to
229
- message: ask them with `dispatch_ask` instead, so the open ask appears in their Inbox, and
230
- never assume their claim has lapsed — only a human releases or forces a human's claim.
231
- - **`409 CLAIM_CONTENDED` means the issue changed hands twice while your call ran**, so nothing
232
- was applied and nobody's liveness was checked. Read the issue and decide again; it is not a
233
- refusal by a live holder.
234
- - **A claim whose session has ended is yours to take.** If the Envoy listener no longer lists
235
- the holder, your claim simply succeeds; the takeover is recorded on the issue and the session
236
- that lost it is told.
237
- - **Release it when you stop** — finished, handing over, or moving to something else. A claim is
238
- released by its holder or any human — and by any agent once the holder's session is no longer
239
- running, the same rule that lets you take it. Closing the issue releases it for you.
240
- - A claim is intent to implement, not contact: reading the issue, commenting, asking, or gating
241
- its pull request claims nothing, so a coordinator never collides with an implementer.
242
-
243
- **Claiming and moving the status are two separate actions, and you do both.** A claim says which
244
- session is on the work; the status says where the work has got to, and humans use it to track
245
- that too (Sami, 2026-09-24, verbatim: "Keep them separate — Separate because humans might be
246
- using them to keep track of work"). So when you start: `dispatch_claim({ issue })` **and**
247
- `dispatch_issue_update({ issue, status: "in_progress" })`.
248
-
249
- ## Issue status is yours to move
250
-
251
- The issue's status is how a human sees delivery without asking a session. Outside Legion (where
252
- the daemon writes it), the session doing the work moves it, the way a person moves a card:
253
- `in_progress` when implementation starts, `testing` when the change is being proven on a
254
- production-like surface, `needs_review` when its pull request is open and waiting on the merge
255
- queue, `done` when the change has been driven in production (a merge is not `done`). Move child
256
- issues you own as well as the root. An issue left at `triage` while work is underway is a defect:
257
- Sami, 2026-09-15, on the roadmap he could not read — "I'm not even sure what their development
258
- status is." Waiting for the deploy lane is not a status and is never announced.
259
-
260
- ```ts
261
- // PATCH /api/v1/issues/{key} — status, title, labels, priority, external_links (merged by URL), route, parent
262
- dispatch_issue_update({ issue: "AGENTC-175", status: "testing" })
263
- dispatch_issue_update({ issue: "AGENTC-175", status: "done", reason: "Shipped in owner/repo#7; verified on the production dashboard." })
264
- dispatch_issue_update({ issue: "AGENTC-175", priority: 1 }) // 0–3; see Priority is yours to set
265
- dispatch_issue_update({ issue: "AGENTC-175", external_links: ["https://github.com/owner/repo/pull/7"] })
266
- dispatch_issue_update({ issue: "AGENTC-175", parent: "AGENTC-170" }) // same-project key; "" clears the parent
267
- ```
268
-
269
- Closing takes a `reason`, and the tool refuses `status: "done"` without one: it posts the reason on
270
- the issue as a message, then closes it, because a closed issue refuses messages, comments, and
271
- artifacts, so a reason left for later has nowhere to go. When the close fails after the post, the
272
- error names the posted message; after a timeout or a server error it also says the close may have
273
- landed, so read the issue's status first. A retry points its reason at the posted message rather
274
- than repeating it.
275
-
276
- The two clears differ: `priority` clears with `null`, while `parent` and `route` clear with `""`.
277
- Guessing the other one is a refusal either way.
278
-
279
- Link the pull request that delivers the issue in `external_links` when you open it; the issue page
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
282
- other `dispatch_*` tool: a Legion pane reads it from the `DISPATCH_TOKEN_FILE` path the daemon sets on
283
- the pane; an OMP session outside Legion reads `dispatch.token` from `~/.config/opencode/envoy.json`.
284
-
285
- A write to an issue still in `triage` answers once with `… is still in triage …`; move the status
286
- when work has started.
287
-
288
- ## Priority is yours to set
289
-
290
- Priority is the coarse bucket a backlog is read by: `0` is P0, the highest, through `3`, P3, the
291
- lowest, and `null` clears it. Sami ruled on 2026-09-24, answering "may agents set issue priority
292
- (P0–P3), or only propose it for you?" on `dispatch://LEGION/artifact/issue-status-conventions-md`:
293
- **"Agents may set"**. So set it — on creation, and on a grooming pass over issues that have none —
294
- and say what you set and why; he overrides anything he disagrees with from the dashboard. A closed
295
- issue takes only `rank`, `components`, and a reopening `status` (any status but `done`);
296
- everything else, `priority` included, waits for the reopen (`409 ISSUE_CLOSED`). So reopen it
297
- first, then set the priority — the two cannot go in one call. `rank` itself is not a tool field:
298
- reorder it the way [Issue reads](#your-owner) describes, through `PATCH /api/v1/issues/{key}`.
299
-
300
159
  ## Search first
301
160
 
302
- Before you create an issue or start a design document, search:
161
+ `skill://dispatch-first`, which every session with Dispatch carries, says how to search before
162
+ you plan, start a design document, file an issue, ask, post a finding or start work, and what to
163
+ do with each hit. What it leaves out:
303
164
  ```ts
304
165
  dispatch_search({ query, project?, limit? })
305
166
  ```
306
- It returns every issue, document, comment, ask, and message that contains the words. Issue-owned hit lines
307
- start with the issue key; standalone project-document hit lines start with
167
+ Websearch syntax applies: `"merge queue"`, `-daemon`, `OR`. It returns the best `limit` hits (20
168
+ by default, 50 at most) across issues, documents, comments, asks, and messages. Issue-owned hit
169
+ lines start with the issue key; standalone project-document hit lines start with
308
170
  `dispatch://PROJECT/artifact/<slug>`, followed by the absolute link. Cite the hit you build on
309
- (`dispatch://KEY` or the document reference), or state "no prior issue" in the spec. Websearch syntax applies:
310
- `"merge queue"`, `-daemon`, `OR`.
171
+ (`dispatch://KEY` or the document reference), or state "no prior issue" in the spec.
311
172
 
312
173
  `dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns the candidates (`POSSIBLE_DUPLICATE`).
313
174
  Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
@@ -316,75 +177,6 @@ merge" pairs with "four CI gates that cannot fail a merge". So when you force pa
316
177
  the new issue a title that names what differs where you can, and open its spec's Summary with the
317
178
  distinction from the named issue, citing it (`dispatch://KEY`), for whoever reads the next pairing.
318
179
 
319
- ## Reading a project's backlog
320
-
321
- To see the shape of a project rather than find a phrase, list its issues:
322
- ```ts
323
- dispatch_issues({ project, status?, parent?, label?, priority?, route_status?, updated_since?, limit? })
324
- ```
325
- Each row carries the issue key, title, status, priority, parent, labels, its open-ask count, and
326
- when it last changed — a roadmap or backlog pass without opening every issue. Filter with `status`
327
- (a lifecycle status), `parent` (one issue's children), `label`, `priority` (a list of `0`–`3`, with
328
- `null` for an issue with no priority: `[0, 1]` is every P0 and P1), `route_status` (below), or
329
- `updated_since` (an RFC3339 timestamp, for "what moved this week"). `limit` caps the rows at 50 by
330
- default and 250 at most.
331
-
332
- This is not search: it matches no text. Use `dispatch_search` for a keyword or phrase, and
333
- `dispatch_issues` when you want every issue in a project and its current state.
334
-
335
- ### The owner audit
336
-
337
- As the owner of a surface, list the project's P0 and P1 issues and staff or close each one nobody
338
- has started:
339
- ```ts
340
- dispatch_issues({ project, priority: [0, 1], limit: 250 })
341
- ```
342
- Every unclaimed row in `triage`, `icebox`, `backlog` or `todo` is a decision: someone takes it and
343
- builds it, or it closes. A row in `in_progress`, `testing`, `needs_review` or `retro`, or one that
344
- carries a claim, is work under way ([Issue status is yours to move](#issue-status-is-yours-to-move))
345
- and is not re-staffed. A todo with a finished spec reads as queued work that nobody is doing
346
- (LEGION-173 sat in todo for two weeks with a complete spec; AGENTC-1010's v4 plan sat in backlog
347
- with nobody building it).
348
-
349
- A close that says the defect cannot happen cites the code that makes it impossible. An issue
350
- closed because a rewrite forecloses it names the file and line in the rewrite that does so; a
351
- close that cannot name one is not foreclosed, it is unread. The cheapest way for a rewrite to reach
352
- parity is to port the code, defect included: LEGION-211's bare `git worktree prune`, filed against
353
- the TypeScript daemon, had been ported into the Go coordinator and was live in production.
354
-
355
- The audit finds four shapes:
356
-
357
- - **Unstaffed work.** A plan or measurement exists, and no one is building it.
358
- - **Unrecorded delivery.** An issue not yet in `testing` or `done`, claimed or not, has a merged PR
359
- naming it. Check the change live, then move the issue (AGENTC-1033 sat at `triage` after its fix,
360
- agent-c #20367, merged).
361
- - **Unrecorded practice.** Someone does the issue's work by hand, more than once, while the issue
362
- sits in backlog (OPS-132, done by hand on every migration merge). It leaves no plan and no PR to
363
- find; the tell is your own messages. Doing something by hand more than once means an issue is
364
- wearing the wrong status.
365
- - **Unreachable route.** An open issue whose route names a role nobody holds, or a session that is
366
- not running, reaches nobody, whatever its priority, and the priority filter above never finds
367
- it. List it on its own:
368
- ```ts
369
- dispatch_issues({ project, route_status: "no_holder", limit: 250 })
370
- ```
371
- Each row reads `route role:sre (nobody holds it right now)` or `route session:<id> (that session
372
- is not running right now)`. That is one read of the listener, and one read is a restart gap as
373
- often as a vacancy: an agent box that restarts or resumes keeps the session id, but the session
374
- is absent from the listener for minutes, and its role with it. On 2026-09-27, 58 of 63 session
375
- routes one read showed as unreachable pointed at a single session that was moving between boxes.
376
- So a route is unowned only when it is `no_holder` on two reads at least ten minutes apart: list
377
- again after ten minutes and act on the issues both lists name. Confirm with the second
378
- `dispatch_issues` read, not `envoy_role_get`: a role lookup releases the claim of a holder whose
379
- session is absent from the registry as it answers. Then staff the role, re-route the
380
- issue to a live holder, or clear the route and assign it (AGENTC-1065, a P2 production listener
381
- 503, sat routed to an unheld `role:sre` with no assignee). `route_status: "unknown"` means the
382
- listener did not answer, so a route could not be judged; a `no_holder` filter refuses rather
383
- than answer an empty list then.
384
-
385
- Run the audit as a step of a coordinator's loop, at each checkpoint, not as a habit: these shapes
386
- are found by running the check, not by noticing them.
387
-
388
180
  ### Symptom versus cause
389
181
 
390
182
  When a symptom and its cause sit on different issues, the work accrues to the cause's issue, and
@@ -394,6 +186,30 @@ the fix, not the one naming the symptom. A symptom issue gathering messages with
394
186
  is the tell. (The production freeze was iterated on AGENTC-546, the failing gate, while its cause
395
187
  and answer sat on AGENTC-1010.)
396
188
 
189
+ ## Claim the issue before you work it
190
+
191
+ Before you start implementing an issue, claim it, and release the claim when you stop: finished,
192
+ handing over, or moving to something else. Closing the issue releases it for you.
193
+ ```ts
194
+ dispatch_claim({ issue: "LEGION-234" }) // I am implementing this
195
+ dispatch_claim({ issue: "LEGION-234", release: true }) // I have stopped; it is free
196
+ ```
197
+ A claim is intent to implement: reading, commenting, asking, or gating a pull request claims
198
+ nothing. `409 ISSUE_CLAIMED` means someone else holds the issue; never work it in parallel. What
199
+ each refusal means, and when a claim is yours to take, is in
200
+ [Working an issue](skill://dispatch/references/issues.md).
201
+
202
+ ## Issue status is yours to move
203
+
204
+ Claiming and moving the status are two actions, and you do both: when you start,
205
+ `dispatch_claim({ issue })` and `dispatch_issue_update({ issue, status: "in_progress" })`. Outside
206
+ Legion the session doing the work moves its issue and the children it owns: `in_progress` when
207
+ implementation starts, `testing` while the change is proven on a production-like surface,
208
+ `needs_review` when its pull request waits on the merge queue, and `done`, with a `reason`, once
209
+ the change has been driven in production. Never write the status of an issue that carries the
210
+ `legion` label, or of any issue under one: the Legion daemon writes those. Closing, clearing a
211
+ field, and choosing your next issue are in [Working an issue](skill://dispatch/references/issues.md).
212
+
397
213
  ## Asking
398
214
 
399
215
  ### Before you ask
@@ -510,26 +326,7 @@ that follow from it:
510
326
  the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
511
327
  they must read to decide belongs in the spec in the first place — see [Artifacts](#artifacts).
512
328
 
513
- ## Architecture components
514
-
515
- Attach every architectural issue to the components it changes. Attach the root before decomposing it: children inherit the root's effective attachment unless they deliberately set their own, so one root attachment classifies the whole tree.
516
-
517
- ```
518
- dispatch_issue({ project: "CORE", title: "...", components: { mode: "explicit", ids: ["dispatch-server", "web"] } })
519
- dispatch_issue_update({ issue: "CORE-12", components: { mode: "explicit", ids: ["web"] } }) // this issue's own set, replacing what it inherited
520
- dispatch_issue_update({ issue: "CORE-13", components: { mode: "none", reason: "hiring, not code" } })
521
- dispatch_issue_update({ issue: "CORE-14", components: { mode: "inherit" } }) // back to the parent chain's attachment
522
- ```
523
-
524
- No native Dispatch tool lists component ids. Read the configured model with `GET /api/v1/projects/{key}/architecture` and use each non-external `components[].id`; when its source repository is your checkout, those ids are the file names under `.dispatch/architecture/` (`web.md` → `web`). An id absent from the model, or an `external` component, is refused with `COMPONENTS_INPUT`.
525
-
526
- Use `mode: "none"` with a concrete reason only when the work is genuinely non-architectural (for example hiring, process, or operations work). A closed issue can be classified without reopening. Change code and its architecture description (`.dispatch/architecture/<id>.md`) in the same review.
527
-
528
- Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
529
- ```ts
530
- dispatch_architecture_sync({ project: "CORE" })
531
- ```
532
- It returns the imported commit, or the recorded error when the model was rejected — the previous model stays up. Without a configured source it answers 404 `SOURCE_NOT_FOUND`.
329
+ ### When you need a human
533
330
 
534
331
  Before saying you are waiting for human input, call `dispatch_open_asks`. With no arguments it lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply. With `dispatch_open_asks({ project })` it lists every open ask in that project — on its issues and on its documents, whoever authored them — which is how you audit what a whole project is waiting on rather than just your own asks.
535
332
 
@@ -546,73 +343,24 @@ blocker. Before asking, try to remove the step: a value already on the machine,
546
343
  already hold, an API that replaces the click. One ask per item, `urgency: "high"` when work is
547
344
  stopped on it; while it is open, keep working on everything that is not.
548
345
 
549
- A to-do handed to a human is an ordinary question: phrase the to-do as the question and give it
550
- the options that name its outcomes, in the human's words - there is no fixed vocabulary and the
551
- server treats no label specially. If an outcome needs a reason, say so in that option's
552
- description, and the human's free-text answer carries it:
553
- ```ts
554
- dispatch_ask({ issue: "DSP-42",
555
- question: "Run the production deploy for #19125?",
556
- options: [{ label: "Deployed" }, { label: "Blocked", description: "Say what is missing." }] })
557
- ```
346
+ Once an ask is open (who answers it, handing a human a to-do, editing, retracting or resolving it,
347
+ answering a clarification, whose turn a reply gives), see
348
+ [Asks after they open](skill://dispatch/references/asks.md).
349
+
350
+ ## Architecture components
351
+
352
+ Attach every architectural issue to the components it changes. Attach the root before decomposing it: children inherit the root's effective attachment unless they deliberately set their own, so one root attachment classifies the whole tree.
558
353
 
559
- Correct or refine an open ask in place instead of opening a second question:
560
- ```ts
561
- dispatch_edit_ask({
562
- ask,
563
- question?,
564
- options?: { label, description? }[],
565
- multiple?,
566
- urgency?,
567
- })
568
354
  ```
569
- At least one field besides `ask` is required. Use this only while the same decision remains open: it keeps the prior text in the event
570
- log and invalidates any answer draft against the prior `edited_at` revision, so the human sees the new wording and explicitly reconfirms.
571
- An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
572
-
573
- An ask that lives as an `ask` block in a document keeps its question and options in the block, and `dispatch_edit_ask` writes the
574
- block along with the row, so the edit stands and the document reads the same. It changes only the fields you name: pass `urgency`
575
- alone and the question's own wording, formatting, links and comment anchors are untouched. Pass `question` or `options` and that part
576
- is rewritten, so anchors inside the text you replaced move as they would for any document edit. Either way it is a document edit: it
577
- writes a new version, which on a spec awaiting approval closes the design gate until the new version is approved. Editing the block
578
- with `dispatch_doc_edit` works too and is the way to change anything else about it, including adding formatting to a question.
579
- Re-sending a field unchanged rewrites nothing, so retrying the whole ask is safe.
580
- Text the block cannot carry back unchanged is refused outright, naming the field and writing nothing - an option label containing
581
- `": "`, the separator between a label and its description, is one example of text that cannot survive the round trip. Blank lines separate paragraphs;
582
- a single newline is kept as a line break.
583
-
584
- An ask stays open until a human answers, unless its question no longer needs that answer. Retract a moot or superseded question, or
585
- self-resolve one after finding the answer:
586
- ```ts
587
- dispatch_resolve_ask({
588
- ask,
589
- kind: "retracted",
590
- reason: "A newer ask supersedes this question.",
591
- })
355
+ dispatch_issue({ project: "CORE", title: "...", components: { mode: "explicit", ids: ["dispatch-server", "web"] } })
356
+ dispatch_issue_update({ issue: "CORE-12", components: { mode: "explicit", ids: ["web"] } }) // this issue's own set, replacing what it inherited
357
+ dispatch_issue_update({ issue: "CORE-13", components: { mode: "none", reason: "hiring, not code" } })
358
+ dispatch_issue_update({ issue: "CORE-14", components: { mode: "inherit" } }) // back to the parent chain's attachment
592
359
  ```
593
- Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the reason because the question remains
594
- in its Conversation card and reply thread; a reason beginning `removed from the document in version` is refused, because that is how a
595
- retraction the document's own settlement wrote is recognised. Resolving a block ask records it in the block too, so it stays resolved
596
- however the document moves afterwards — while deleting the block from the document is the other way to close one, and putting the block
597
- back reopens it. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
598
- resolved. A human may reply to an open or answered ask; so may you, e.g. after finding the answer — use `reply_to_ask` on
599
- `dispatch_comment` (mutually exclusive with `reply_to`).
600
- A review comment you opened has its own closer, `dispatch_resolve_comment` — see
601
- [Comments and suggestions](#comments-and-suggestions).
602
-
603
- A human answers or asks back from the same field; a question-shaped free-text answer is offered as a clarification first. A human
604
- reply while your ask is still open (the delivered `comment.created` carries `ask_state: open`) is a request for clarification, not
605
- an answer: the human did not understand the question or needs more before choosing. The ask now waits on you in their Inbox. Answer
606
- in the same thread with `dispatch_comment({ reply_to_ask })`, or reword the question itself with `dispatch_edit_ask` when the wording
607
- was the problem; either puts the ask back in front of them. Do not open a second ask.
608
-
609
- Every reply to an open ask says whose turn it is next, and the Inbox files the ask by that, not by who spoke last. Your plain reply
610
- (`turn` omitted, or `turn: "human"`) hands the turn to the human: the ask returns to their `Waiting on you`. When you are not done
611
- yet — "dispatched two auditors, back with results", "checking the release branch, back shortly", any working-on-it note — reply with
612
- `turn: "agent"`: the note lands in the thread, the ask stays under `Waiting on agents`, and the human is not told to act. Use
613
- `turn: "agent"` for every progress note and `turn: "human"` (the default) only when you need them. A human's reply always hands the
614
- turn to you. The result names the state (`ask now waiting on agent` / `human`), the delivered `comment.created` carries it as
615
- `ask_waiting_on`, and every ask read carries it as `waiting_on`.
360
+
361
+ No native Dispatch tool lists component ids. Read the configured model with `GET /api/v1/projects/{key}/architecture` and use each non-external `components[].id`; when its source repository is your checkout, those ids are the file names under `.dispatch/architecture/` (`web.md` → `web`). An id absent from the model, or an `external` component, is refused with `COMPONENTS_INPUT`.
362
+
363
+ Use `mode: "none"` with a concrete reason only when the work is genuinely non-architectural (for example hiring, process, or operations work). A closed issue can be classified without reopening. Change code and its architecture description (`.dispatch/architecture/<id>.md`) in the same review. When the model you read is behind your checkout, `dispatch_architecture_sync` imports it now ([Syncing a project's architecture model](skill://dispatch/references/issues.md)).
616
364
 
617
365
  ## Close what you opened
618
366
 
@@ -632,7 +380,7 @@ supersedes one, retract the old one with `dispatch_resolve_ask` and kind `retrac
632
380
  turn.
633
381
 
634
382
  See [Following](#following) for why you receive what happens to asks you open and
635
- [What comes back](#what-comes-back) for finding them again with `dispatch_open_asks`.
383
+ [What comes back](skill://dispatch/references/reading.md) for finding them again with `dispatch_open_asks`.
636
384
 
637
385
  ## Approval of a spec
638
386
 
@@ -666,146 +414,17 @@ omitted `artifact` reads the issue specification; a project needs `artifact`; an
666
414
  `dispatch://PROJECT/artifact/<document-ref>` ref supplies both, where `document-ref` is the slug (an id or a
667
415
  filename resolves when no document has that slug).
668
416
 
669
- Editing one is [Editing a document](references/document-edits.md): the shape of `dispatch_doc_edit`,
417
+ Editing one is [Editing a document](skill://dispatch/references/document-edits.md): the shape of `dispatch_doc_edit`,
670
418
  how to quote the text you mean, one `replace` per paragraph, preconditions against a stale edit, and
671
419
  what each operation costs a block's id and its anchors.
672
420
 
673
- ## A document that is reloading
674
-
675
- These calls can answer `DOC_SERVICE_UNAVAILABLE` (HTTP 503), because each writes a document inside its
676
- transaction: `dispatch_doc_edit`; `dispatch_ask` and `dispatch_comment` on a quote; a `dispatch_comment` reply
677
- in a thread whose first comment is anchored; `dispatch_suggest`; `dispatch_resolve_comment` on an anchored
678
- comment; `dispatch_artifact` replacing a document that already exists; and, on an ask that lives in a `:::ask`
679
- block, `dispatch_edit_ask` and `dispatch_resolve_ask`, which write that block. Creating an issue with a spec,
680
- uploading a new document, `dispatch_request_approval` and `dispatch_message` never answer it, and neither do
681
- `dispatch_edit_ask` and `dispatch_resolve_ask` on an ask that has no block. It means that document's live room
682
- failed and is reloading from its durable copy, so the server refused rather than wait for it; your call wrote
683
- nothing and the document is intact. Nothing retries it for you: the Dispatch client hands a 503 straight back.
684
- Wait a few seconds and make the same call again. A second refusal in a row is worth telling your human about,
685
- with the document's reference.
686
-
687
- `dispatch_doc_read` can answer it too, though it writes nothing: a read never opens a live room, and waits out
688
- a room that is reloading, so it is refused only when the document's durable copy cannot be read or decoded.
689
- Retry it the same way.
690
-
691
421
  ## Typed blocks
692
422
 
693
- The server declares typed document blocks at `GET /api/v1/schema/blocks`. Write one only with the
694
- container-directive form `:::name{#block-id key="value"}` on its own line, ordinary block children,
695
- and a closing line of as many colons at the same nesting. A typed block directly inside another needs the outer
696
- one's fence a colon longer (`::::callout{…}` around a `:::callout{…}`), and so does one whose code holds a `:::` line;
697
- Dispatch writes its fences that way. An unclosed typed block at document level is rejected. For
698
- a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing typed block, retain
699
- its id and every rendered attribute. Never copy an existing block's id into new markdown: an id
700
- names one block, so an insert, upload or suggestion whose markdown names an id the document holds
701
- outside the text it replaces is refused naming the id: `INVALID_OP` for an insert,
702
- `INVALID_MARKDOWN` for any other write. To rewrite such a block whole, `delete` it and then
703
- `insert` the new one carrying its id, anchored on the block before or after it, in that order and
704
- in one batch: an insert carrying an id the document still holds is refused.
705
-
706
- Use only the type names, content rule, attributes, and enum values returned by the schema. Values are
707
- quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::: {.callout}`, leaf
708
- `::name` directives, or text `:name` directives; those strings are literal when quoted inside a code
709
- block. Do not set attributes the schema marks `server: true`; the server ignores them and reasserts
710
- its authoritative value at settlement.
711
-
712
- Questions about a document must be `ask` blocks, never an `Open questions` prose section. An ask
713
- body is one or more question paragraphs followed by an optional bullet list of options, where each
714
- item is `Label: description`. A spec, an uploaded document or an uploaded version holding an ask that
715
- breaks that shape - a code block, heading or quote in it, a paragraph after its options, a second
716
- list - is refused with `INVALID_ASK_BLOCK`; so is an edit that writes one. An ask someone left
717
- unreadable in the browser refuses nothing it is carried through unchanged by. For example:
718
-
719
- ```md
720
- :::ask{urgency="high" multiple="false"}
721
- Should we ship the migration?
722
-
723
- - Ship: Release the verified change.
724
- - Hold: Wait for another review.
725
- :::
726
- ```
727
-
728
- When a human answers a decision written as an ask block, the answer lives on that ask. Use
729
- `dispatch_resolve_ask` when the decision is resolved without a human response, or preserve the
730
- human's answer; never rewrite the question into its answer or blank its options. An edit that leaves
731
- an ask block without a question or with a blank option is rejected with `INVALID_ASK_BLOCK`.
732
-
733
- ## Comments and suggestions
734
-
735
- Add feedback with:
736
-
737
- ```ts
738
- dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask?, turn? })
739
- ```
740
-
741
- It returns issue or project-document owner details plus `comment`; a `reply_to_ask` reply also returns `ask` and `follows: { ask }`,
742
- because replying to an ask makes you one of its followers (see [Following](#following)).
743
- `ref` names the owner (an issue or project-document reference) in place of `issue`/`project`.
744
- `quote` requires `artifact`; its anchor is pinned to the containing block while retaining the quote
745
- for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
746
- `quote`; it belongs to its parent's anchor. Reply to any comment in a thread; the server keeps
747
- threads flat. A reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a
748
- question asked with `dispatch_ask`; `turn` (only with `reply_to_ask`) says who holds the turn after
749
- the reply — `agent` for a progress note that keeps the ask waiting on you, `human` (the default) when
750
- the human needs to act; see [Asking](#asking). Comments are edited only by their author from the
751
- dashboard. A delivered `comment.created` event carries the comment `id`; reply to it with
752
- `dispatch_comment({ reply_to: <id> })`.
753
-
754
- Resolve your own review comment once you have addressed it:
755
-
756
- ```ts
757
- dispatch_resolve_comment({ comment })
758
- ```
759
-
760
- `comment` is the comment id, or a `dispatch://KEY/comment/<id>` or `dispatch://PROJECT/artifact/<slug>/comment/<id>` reference (an
761
- 8+ character id prefix is resolved against the owner's comments). It returns the owner details plus `comment`, and takes no reason —
762
- say what you did in a `reply_to` first if the thread needs it. The server lets any session or human resolve any open comment, so
763
- resolve only threads you opened or were asked to close; reopening a resolved thread is human-only (from the dashboard), though your
764
- reply to it reopens it. Asks are closed with `dispatch_resolve_ask` instead.
765
-
766
- An exact replacement for document text is a suggestion (`dispatch_suggest`), never a comment; a
767
- comment is for a question or a note the human answers in words. A human accepts a suggestion with
768
- one click and cannot accept a comment, so propose the replacement instead of describing it:
769
-
770
- ```ts
771
- dispatch_suggest({ issue?, project?, artifact, ref?, quote, replace_with, body?, occurrence? })
772
- ```
773
-
774
- It returns issue or project-document owner details plus `comment`. A human accepts or rejects a suggestion.
775
- Errors: `TARGET_AMBIGUOUS` (add `occurrence`), `TARGET_NOT_FOUND` (re-read first), `INVALID_ANCHOR`/`ANCHOR_MISSING`/`ANCHOR_ORPHANED`
776
- (bad, unwritten, or stale quote), `INVALID_MARKDOWN`/`DOC_SCHEMA` (malformed content), `CAP_EXCEEDED`, `ISSUE_CLOSED`.
777
- Suggest only what the quoted block can hold. The accept, not the suggestion, checks that: an
778
- accept whose `replace_with` would break an ask block that was readable before it is refused with
779
- `INVALID_ASK_BLOCK` - a question given a code block, text after an ask's options, a second option
780
- list, or an emptied question; one that names an id the document holds outside the text it
781
- replaces, an ask under a held id included, with `INVALID_MARKDOWN`; and one no part of the
782
- document can hold where it sits (a code block over a table cell's whole text), or whose quote runs
783
- into an ask or callout from the text before it, with `INVALID_OP`. Either changes nothing: the human sees the reason with no Retry, and the suggestion
784
- stays open until someone rejects or replaces it.
423
+ A typed block (an `:::ask`, a callout) has its own syntax, attributes and refusals: write one only
424
+ as [Documents](skill://dispatch/references/documents.md) says.
785
425
 
786
426
  ## Artifacts
787
427
 
788
- Attach an image, diagram, or local file with:
789
-
790
- ```ts
791
- dispatch_artifact({ issue?, project?, name, path, summary? })
792
- ```
793
-
794
- Or, when the text is already in the call, post a Markdown document directly:
795
-
796
- ```ts
797
- dispatch_artifact({ issue?, project?, name: "load-test-results.md", content: "# Load test\n..." })
798
- ```
799
-
800
- Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
801
- Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact` and `version`.
802
- Uploading the same `name` creates its next version — so uploading `spec.md` **replaces the issue's own specification**
803
- with your text. Never do that: the spec is edited in place with `dispatch_doc_edit` (see [Editing a document](references/document-edits.md)). Address an existing
804
- artifact by the slug shown in the upload result or by its filename, and a project document by its artifact id, slug, or filename; the
805
- slug also arrives on `artifact.created` events. Dispatch suffixes a slug two documents would share, so one document's
806
- filename can be another's slug (`plan v2` takes `plan-v2`, then a document named `plan-v2` takes `plan-v2-2`): a bare
807
- `artifact` that names both is refused with each one's id, while a `dispatch://` reference's document part is always the slug.
808
-
809
428
  **Where a deliverable goes.** Text the human must read to decide — a draft message, a proposal,
810
429
  a summary — goes in the spec as a section: the spec is the one document they open. A separate
811
430
  artifact is for a real file: something sent as-is, a long report, a binary, a screenshot.
@@ -815,10 +434,8 @@ upload result; it renders as a link) at the place the reader needs it, and the a
815
434
  decision carries the same reference. A heading or a sentence naming the filename is not a
816
435
  reference.
817
436
 
818
- Documents are CommonMark. A bare `<https://example.com|text>` is a CommonMark autolink and is normalised: the angle brackets are
819
- dropped and the URL keeps `|text`. A backslash-escaped `\<https://example.com|text>` displays as `<https://example.com|text>` in the
820
- document but comes back re-escaped (`\<`) from `dispatch_doc_read`. A Slack mrkdwn draft, or any other payload that is not Markdown,
821
- still belongs inside a fenced code block, where it survives verbatim both ways.
437
+ Uploading one (`dispatch_artifact`, its slugs, versions and Markdown rules) is in
438
+ [Documents](skill://dispatch/references/documents.md).
822
439
 
823
440
  ## Structure over stream
824
441
 
@@ -832,7 +449,7 @@ high-signal, structured conversation"). The structure IS the product:
832
449
  carrying a draft, a spec, or anything over a couple of paragraphs, stop: that is an issue with a
833
450
  document, or an artifact on the issue it belongs to.
834
451
  - **Content lives in documents; decisions live in asks; messages only announce.** A draft the
835
- human must read goes in a document artifact the dashboard renders with versions and margins; the
452
+ human must read goes in the issue's spec (see [Artifacts](#artifacts)), which the dashboard renders with versions and margins; the
836
453
  ask that needs their word references it (`ref`, or the `dispatch://` link inline) instead of
837
454
  restating it. A message never carries a body a human has to scroll.
838
455
  - **Never split one deliverable across a message + an ask that points at it.** Ask the question
@@ -856,216 +473,19 @@ dispatch_message({ issue, body })
856
473
  It returns `details` `{ issue, message }`. `body` is capped at 2,000 characters. A message is not a decision
857
474
  (`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
858
475
 
859
- ### Targeted agent messages
860
-
861
- A human — or any bearer caller over HTTP, such as a test rig — can target the issue message at a
862
- live Envoy session or role as **BTW**, **Aside**, or **Steer**. The incoming Dispatch frame names
863
- 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
864
- tool, never a new targeted send:
865
-
866
- ```ts
867
- dispatch_message({
868
- issue: "CORE-1",
869
- in_reply_to: "<targeted-message-id>",
870
- body: "The requested answer.",
871
- })
872
- ```
873
-
874
- `in_reply_to` correlates the answer under the asker's message in its Conversation card. A BTW
875
- delivery can post its answer automatically; use this call when the frame asks the primary agent to
876
- reply. A human may reply to your message in turn — the follow-up arrives as a targeted frame whose
877
- `in_reply_to` names your message and whose `reply_body` quotes it; answer it the same way,
878
- `dispatch_message({ issue, in_reply_to: "<their reply id>", body })`, so the exchange reads as one
879
- thread. `dispatch_message` itself never carries `target` or `delivery`: agent-to-agent traffic goes
880
- through Envoy or the hub. A bearer that targets over HTTP names its own session in `actor`
881
- (`{kind: "session", id}`), and the card shows that session as the author. `GET /api/v1/agents`
882
- (any authenticated caller) lists live sessions with their capabilities (`aside`, `btw`, `steer`);
883
- target only a session that advertises the mode you want. Sending to a session with no issue
884
- (`POST /api/v1/agents/{session_id}/messages`) stays human-only.
885
-
886
- ### Answering a direct message
887
-
888
- A human can also message you directly from the **Agents** page, with no issue at all. That frame
889
- names no issue and its `reply_with` hint carries none either; answer it with the message's bare
890
- id in `in_reply_to`, alone:
891
-
892
- ```ts
893
- dispatch_message({
894
- in_reply_to: "<the direct message's id>",
895
- body: "The requested answer.",
896
- })
897
- ```
898
-
899
- Leave `issue` out — there is no issue to post into, and naming one would file your answer on
900
- unrelated work. Dispatch threads the reply under their message in the same conversation, and the
901
- human sees it in your conversation on the Agents page, where it shows as an unread reply until
902
- they read it. Every other message still names its issue, so keep the `issue`
903
- the frame gave you whenever it gave you one; a `dispatch://KEY/message/<id>` reference names the
904
- issue its message lives on, so that form is a reply on that issue, not a direct message.
905
-
906
- Have more to say after you answered? Call it again with the same `in_reply_to` and the new text:
907
- Dispatch threads that follow-up under your first reply, and the tool result names the reply it
908
- follows. The same text again posts nothing, so a retry is safe. Your host may already have
909
- answered a **BTW** automatically before you got here; a second call is then your follow-up to
910
- that answer, so read the result before writing again.
911
-
912
- Read the whole conversation back — their message and every reply, yours included — with the
913
- message id alone; it has no issue:
914
-
915
- ```ts
916
- dispatch_read({ message: "<the direct message's id, or any reply's>" })
917
- ```
476
+ A BTW, Aside or Steer frame, or a message from the Agents page, is answered as
477
+ [Targeted and direct messages](skill://dispatch/references/messages.md) says.
918
478
 
919
479
  ## Following
920
480
 
921
- An ask has followers: every session that wrote to it — the session that opened it and every session that replied with
922
- `dispatch_comment({ reply_to_ask })` — plus any session a human adds from the ask card. The ask's answer, edits, resolution, and
923
- every reply on it reach each follower's own agent topic directly, whether or not the writer was a human and whatever the issue's
924
- route. The tool result says so (`You follow this ask: its answer and replies reach you directly.`) and carries `details.follows.ask`;
925
- the host tells you once per ask. Leave a thread you no longer need, or rejoin one, with:
926
-
927
- ```ts
928
- dispatch_follow({ ask, action: "follow" | "unfollow" })
929
- ```
930
-
931
- `ask` is the full ask id or a `dispatch://KEY/ask/<id>` reference; `dispatch_follow`, `dispatch_edit_ask`, and
932
- `dispatch_resolve_ask` also take an 8+ hex prefix that is unique among your own open asks, and refuse anything shorter by naming
933
- those asks. A human may also remove you from the ask card; either way you are
934
- told with an `ask.follower_removed` notice, and a human adding you arrives as `ask.follower_added`.
935
-
936
- No write subscribes you to an issue or document. Following covers your own asks and the threads you joined; everything else on the
937
- owner — other sessions' asks, comments, messages, status changes — reaches you only if you subscribe to the owner topic yourself.
938
- Every write result names that line: `envoy_subscribe notifications.dispatch.issue.<KEY>.>` for an issue,
939
- `envoy_subscribe notifications.dispatch.document.<PROJECT>.<SLUG>.>` for a project document. The owner topic carries every Dispatch
940
- event; `notify` only controls agent wake and routed delivery. A human may unsubscribe you from the issue or document header; you
941
- are told with a `subscription.removed` notice when that happens.
942
-
943
- ## What comes back
944
-
945
- After a restart, catch up with:
946
-
947
- ```ts
948
- dispatch_read({ issue?, project?, artifact?, ref? })
949
- ```
950
-
951
- With an issue ref, it returns the issue summary, open asks, references, and recent events with `details` `{ issue }`. With a project
952
- document owner or ref, it returns a document summary with `details` `{ project, document }`. With an ask ref, it returns that ask's
953
- question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. With a
954
- message ref, it returns that message and its reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
955
-
956
- Every read ends with two sections from the reference graph. `Referenced by:` lists what points at the node — every document, ask,
957
- comment, or message that cites it, plus its structure: child issues, attached documents, anchored and owned asks and comments, replies,
958
- followers — and `Links:` lists what it cites. Each row is `- <edge kind> <node kind> dispatch://… (<excerpt> · <when>)`; for a
959
- 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
960
- one list previews the list's first item. When a document references many issues and each backlink should read right, give each
961
- issue its own paragraph (or block), not an item of one list. Cross-project, always: a message on another project's issue that
962
- 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
963
- document" one read on the document. Cite with `dispatch://` references (below) whenever you name a node in a body — a bare id or
964
- title is invisible to the graph.
481
+ You follow every ask you open or reply to: its answer, edits, resolution and replies reach you
482
+ directly. Nothing else on an issue reaches you unless you subscribe to its topic. Leaving or
483
+ rejoining a thread, and the topic to subscribe to, are in
484
+ [Asks after they open](skill://dispatch/references/asks.md).
965
485
 
966
486
  ## References
967
487
 
968
- **Every reference is a link, never an unlinked mention.** If you name a thing that has an address, link it: another issue, ask, comment, spec, or message (the `dispatch://` forms below), an artifact (`dispatch://KEY/artifact/<slug>`), an eval (its viewer URL), a Slack message (its permalink), a Drive file (its share link). Bare phrases like "see this eval", "his 09-04 run", "the comment above", or "per the spec" with no link are banned: they make the reader hunt for what you already had in hand, and nothing can be traversed from them. Linking every reference is what makes a body both consumable and navigable. If a thing genuinely has no linkable address, say so; otherwise the link is not optional.
969
-
970
- Use these in document, ask, comment, and message bodies. In the dashboard, a reference renders
971
- as an inline link whose text is the target's title (an issue's title, an ask's question, a
972
- comment's first line, a document's name) once it resolves; a body that is only a bare reference
973
- still gets an unfurl card instead. Every `ref` argument below (and `issue`/`project`) accepts
974
- either form — an issue key or a project key is never ambiguous, since a project key never
975
- contains a dash:
976
-
977
- ```text
978
- dispatch://KEY
979
- dispatch://KEY/spec
980
- dispatch://KEY/artifact/<slug>[@vN]
981
- dispatch://KEY/ask/<id>
982
- dispatch://KEY/comment/<id>
983
- dispatch://KEY/message/<id>
984
- dispatch://PROJECT/artifact/<document-ref>[@vN]
985
- dispatch://PROJECT/artifact/<document-ref>/ask/<id>
986
- dispatch://PROJECT/artifact/<document-ref>/comment/<id>
987
- ```
988
-
989
- A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records. `dispatch_read` also
990
- accepts the dashboard URL of an issue, spec, artifact, ask, comment, or project document on the configured server (it maps to the
991
- `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
992
- full uuid. A non-uuid id on `GET /asks/{id}`, `/comments/{id}`, or `/issues/{key}/messages/{id}` is a 400 `ASK_ID_INPUT` /
993
- `COMMENT_ID_INPUT` / `MESSAGE_ID_INPUT`, never a 500.
994
-
995
- ## Before / after
996
-
997
- Before — a wall of text hides the decision and makes the choices unclickable:
998
-
999
- ```text
1000
- We need to settle the release gate because the deploy branch has the migration and the
1001
- dashboard changes, I checked the staging result and it is fine except the release notes are
1002
- not reviewed, so should we ship today, wait for docs, or cut the dashboard from this release?
1003
- I think waiting is safest but the customer demo is tomorrow and the list above is probably stale.
1004
- ```
488
+ The `dispatch://` form for each kind of node, and what it resolves to, is under
489
+ [Reference forms](skill://dispatch/references/reading.md).
1005
490
 
1006
- After — anchor the decision and make each option a button:
1007
-
1008
- ```ts
1009
- dispatch_ask({
1010
- issue: "LEGION-815",
1011
- question:
1012
- "Choose the release gate. Recommendation: ship after release-note review, since the tested deployment is otherwise ready.",
1013
- options: [
1014
- { label: "Review notes, then ship", description: "Keeps the release intact and reviewed." },
1015
- { label: "Ship now", description: "Meets the demo deadline; release notes follow later." },
1016
- ],
1017
- urgency: "high",
1018
- anchor: { artifact: "spec", quote: "Release requires reviewed operator instructions before deployment." },
1019
- })
1020
- ```
1021
-
1022
- Before — a progress note that nobody needs, posted where humans look for decisions:
1023
-
1024
- ```ts
1025
- dispatch_message({ issue: "LEGION-815", body: "Merged the release PR, moving to docs next." })
1026
- ```
1027
-
1028
- After — nothing. The merge is visible on the pull request; the docs work shows up as its own deliverable. Post a message only when
1029
- a human must act or a deliverable is theirs to use:
1030
-
1031
- ```ts
1032
- dispatch_message({
1033
- issue: "LEGION-815",
1034
- body: "Release 1.4 is live on the devbox (dispatch://LEGION-815/artifact/release-notes). Nothing needed from you.",
1035
- })
1036
- ```
1037
-
1038
- Before — a draft the human must read is uploaded as a separate file, the spec only names it, and
1039
- the ask does not point at it, so the reader has to go looking:
1040
-
1041
- ```ts
1042
- dispatch_artifact({ issue: "OPS-52", name: "cu-update-2026-09-15.md", content: "Hi team, ..." })
1043
- dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
1044
- { op: "insert", after: "## Context", markdown: "## Draft (artifact cu-update-2026-09-15.md)" },
1045
- ]})
1046
- dispatch_ask({ issue: "OPS-52", question: "Send the customer update as drafted?", options: [...] })
1047
- ```
1048
-
1049
- After — the draft is a section of the spec, and the ask anchors there. If it really must be a
1050
- file (something to send as-is), the spec and the ask both link the slug from the upload result:
1051
-
1052
- ```ts
1053
- dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
1054
- { op: "insert", after: "## Context", markdown: "## Draft\n\nHi team, ..." },
1055
- ]})
1056
- dispatch_ask({
1057
- issue: "OPS-52",
1058
- question: "Send the customer update as drafted?",
1059
- options: [...],
1060
- anchor: { artifact: "spec", quote: "Hi team," },
1061
- })
1062
- // or, for a real file — the spec links it where the reader needs it, and so does the ask:
1063
- dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
1064
- { op: "insert", after: "## Context", markdown: "## Draft\n\nThe update to send as-is: dispatch://OPS-52/artifact/cu-update-2026-09-15-md" },
1065
- ]})
1066
- dispatch_ask({
1067
- issue: "OPS-52",
1068
- question: "Send this customer update as-is? dispatch://OPS-52/artifact/cu-update-2026-09-15-md",
1069
- options: [...],
1070
- })
1071
- ```
491
+ **Every reference is a link, never an unlinked mention.** If you name a thing that has an address, link it: another issue, ask, comment, spec, or message (the `dispatch://` forms in [Reading back](skill://dispatch/references/reading.md)), an artifact (`dispatch://KEY/artifact/<slug>`), an eval (its viewer URL), a Slack message (its permalink), a Drive file (its share link). Bare phrases like "see this eval", "his 09-04 run", "the comment above", or "per the spec" with no link are banned: they make the reader hunt for what you already had in hand, and nothing can be traversed from them. Linking every reference is what makes a body both consumable and navigable. If a thing genuinely has no linkable address, say so; otherwise the link is not optional.