@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.
- package/dist/src/server.js +21 -0
- package/package.json +1 -1
- package/skills/AGENTS.md +2 -0
- package/skills/dispatch/SKILL.md +83 -663
- package/skills/dispatch/references/api.md +28 -0
- package/skills/dispatch/references/asks.md +109 -0
- package/skills/dispatch/references/document-edits.md +1 -1
- package/skills/dispatch/references/documents.md +148 -0
- package/skills/dispatch/references/examples.md +82 -0
- package/skills/dispatch/references/issues.md +213 -0
- package/skills/dispatch/references/messages.md +65 -0
- package/skills/dispatch/references/reading.md +55 -0
- package/skills/dispatch-first/SKILL.md +49 -0
- package/skills/legion-architect/SKILL.md +2 -2
- package/skills/legion-worker/SKILL.md +65 -365
- package/skills/legion-worker/references/conflicts-and-rewrites.md +129 -0
- package/skills/legion-worker/references/merge-gate.md +101 -0
- package/skills/legion-worker/references/pr-body.md +125 -0
- package/skills/legion-worker/references/review-threads.md +71 -0
- package/src/server.ts +22 -1
- package/skills/legion-worker/references/knowledge-injection.md +0 -98
- /package/skills/legion-worker/{resources/strategies → references}/cleanup-deletion.md +0 -0
- /package/skills/legion-worker/{resources/strategies → references}/systematic-rename.md +0 -0
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -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
|
-
[
|
|
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
|
-
|
|
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](
|
|
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
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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](
|
|
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
|
-
|
|
694
|
-
|
|
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
|
-
|
|
819
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|