@sjawhar/opencode-legion-envoy 3.12.1 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/server.js +41 -3
- package/package.json +1 -1
- package/skills/AGENTS.md +3 -1
- package/skills/dispatch/SKILL.md +83 -659
- 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-controller/SKILL.md +183 -34
- package/skills/legion-worker/SKILL.md +86 -383
- 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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Answering targeted and direct messages
|
|
2
|
+
|
|
3
|
+
`skill://dispatch` sends you here when a Dispatch frame targets you as BTW, Aside or Steer, or a
|
|
4
|
+
human messages you directly from the Agents page.
|
|
5
|
+
|
|
6
|
+
## Targeted agent messages
|
|
7
|
+
|
|
8
|
+
A human — or any bearer caller over HTTP, such as a test rig — can target the issue message at a
|
|
9
|
+
live Envoy session or role as **BTW**, **Aside**, or **Steer**. The incoming Dispatch frame names
|
|
10
|
+
the issue and includes a `reply_with` hint (`{ tool, args }`, ready to issue on any host); reply on the same open issue with the existing
|
|
11
|
+
tool, never a new targeted send:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
dispatch_message({
|
|
15
|
+
issue: "CORE-1",
|
|
16
|
+
in_reply_to: "<targeted-message-id>",
|
|
17
|
+
body: "The requested answer.",
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`in_reply_to` correlates the answer under the asker's message in its Conversation card. A BTW
|
|
22
|
+
delivery can post its answer automatically; use this call when the frame asks the primary agent to
|
|
23
|
+
reply. A human may reply to your message in turn — the follow-up arrives as a targeted frame whose
|
|
24
|
+
`in_reply_to` names your message and whose `reply_body` quotes it; answer it the same way,
|
|
25
|
+
`dispatch_message({ issue, in_reply_to: "<their reply id>", body })`, so the exchange reads as one
|
|
26
|
+
thread. `dispatch_message` itself never carries `target` or `delivery`: agent-to-agent traffic goes
|
|
27
|
+
through Envoy or the hub. A bearer that targets over HTTP names its own session in `actor`
|
|
28
|
+
(`{kind: "session", id}`), and the card shows that session as the author. `GET /api/v1/agents`
|
|
29
|
+
(any authenticated caller) lists live sessions with their capabilities (`aside`, `btw`, `steer`);
|
|
30
|
+
target only a session that advertises the mode you want. Sending to a session with no issue
|
|
31
|
+
(`POST /api/v1/agents/{session_id}/messages`) stays human-only.
|
|
32
|
+
|
|
33
|
+
## Answering a direct message
|
|
34
|
+
|
|
35
|
+
A human can also message you directly from the **Agents** page, with no issue at all. That frame
|
|
36
|
+
names no issue and its `reply_with` hint carries none either; answer it with the message's bare
|
|
37
|
+
id in `in_reply_to`, alone:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
dispatch_message({
|
|
41
|
+
in_reply_to: "<the direct message's id>",
|
|
42
|
+
body: "The requested answer.",
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Leave `issue` out — there is no issue to post into, and naming one would file your answer on
|
|
47
|
+
unrelated work. Dispatch threads the reply under their message in the same conversation, and the
|
|
48
|
+
human sees it in your conversation on the Agents page, where it shows as an unread reply until
|
|
49
|
+
they read it. Every other message still names its issue, so keep the `issue`
|
|
50
|
+
the frame gave you whenever it gave you one; a `dispatch://KEY/message/<id>` reference names the
|
|
51
|
+
issue its message lives on, so that form is a reply on that issue, not a direct message.
|
|
52
|
+
|
|
53
|
+
Have more to say after you answered? Call it again with the same `in_reply_to` and the new text:
|
|
54
|
+
Dispatch threads that follow-up under your first reply, and the tool result names the reply it
|
|
55
|
+
follows. The same text again posts nothing, so a retry is safe. Your host may already have
|
|
56
|
+
answered a **BTW** automatically before you got here; a second call is then your follow-up to
|
|
57
|
+
that answer, so read the result before writing again.
|
|
58
|
+
|
|
59
|
+
Read the whole conversation back — their message and every reply, yours included — with the
|
|
60
|
+
message id alone; it has no issue:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
dispatch_read({ message: "<the direct message's id, or any reply's>" })
|
|
64
|
+
```
|
|
65
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Reading Dispatch back, and the reference forms
|
|
2
|
+
|
|
3
|
+
`skill://dispatch` sends you here when you catch up after a restart, read an issue, ask, comment,
|
|
4
|
+
message or document, trace what cites a node, or need the exact `dispatch://` form for a reference.
|
|
5
|
+
|
|
6
|
+
## What comes back
|
|
7
|
+
|
|
8
|
+
After a restart, catch up with:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
dispatch_read({ issue?, project?, artifact?, ref? })
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
With an issue ref, it returns the issue summary, open asks, references, and recent events with `details` `{ issue }`. With a project
|
|
15
|
+
document owner or ref, it returns a document summary with `details` `{ project, document }`. With an ask ref, it returns that ask's
|
|
16
|
+
question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. With a
|
|
17
|
+
message ref, it returns that message and its reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
|
|
18
|
+
|
|
19
|
+
Every read ends with two sections from the reference graph. `Referenced by:` lists what points at the node — every document, ask,
|
|
20
|
+
comment, or message that cites it, plus its structure: child issues, attached documents, anchored and owned asks and comments, replies,
|
|
21
|
+
followers — and `Links:` lists what it cites. Each row is `- <edge kind> <node kind> dispatch://… (<excerpt> · <when>)`; for a
|
|
22
|
+
document source the excerpt is the start of the block holding the mention, and a whole list is one block, so every issue named in
|
|
23
|
+
one list previews the list's first item. When a document references many issues and each backlink should read right, give each
|
|
24
|
+
issue its own paragraph (or block), not an item of one list. Cross-project, always: a message on another project's issue that
|
|
25
|
+
cites an ask shows up under that ask. So "what led to this decision" is one `dispatch_read` on the ask, and "who relies on this
|
|
26
|
+
document" one read on the document. Cite with `dispatch://` references (below) whenever you name a node in a body — a bare id or
|
|
27
|
+
title is invisible to the graph.
|
|
28
|
+
|
|
29
|
+
## Reference forms
|
|
30
|
+
|
|
31
|
+
Use these in document, ask, comment, and message bodies. In the dashboard, a reference renders
|
|
32
|
+
as an inline link whose text is the target's title (an issue's title, an ask's question, a
|
|
33
|
+
comment's first line, a document's name) once it resolves; a body that is only a bare reference
|
|
34
|
+
still gets an unfurl card instead. Every `ref` argument below (and `issue`/`project`) accepts
|
|
35
|
+
either form — an issue key or a project key is never ambiguous, since a project key never
|
|
36
|
+
contains a dash:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
dispatch://KEY
|
|
40
|
+
dispatch://KEY/spec
|
|
41
|
+
dispatch://KEY/artifact/<slug>[@vN]
|
|
42
|
+
dispatch://KEY/ask/<id>
|
|
43
|
+
dispatch://KEY/comment/<id>
|
|
44
|
+
dispatch://KEY/message/<id>
|
|
45
|
+
dispatch://PROJECT/artifact/<document-ref>[@vN]
|
|
46
|
+
dispatch://PROJECT/artifact/<document-ref>/ask/<id>
|
|
47
|
+
dispatch://PROJECT/artifact/<document-ref>/comment/<id>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records. `dispatch_read` also
|
|
51
|
+
accepts the dashboard URL of an issue, spec, artifact, ask, comment, or project document on the configured server (it maps to the
|
|
52
|
+
`dispatch://` form above), and an ask or comment id may be a unique prefix of at least 8 hex characters; a message id is always the
|
|
53
|
+
full uuid. A non-uuid id on `GET /asks/{id}`, `/comments/{id}`, or `/issues/{key}/messages/{id}` is a 400 `ASK_ID_INPUT` /
|
|
54
|
+
`COMMENT_ID_INPUT` / `MESSAGE_ID_INPUT`, never a 500.
|
|
55
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dispatch-first
|
|
3
|
+
description: "Use in any session with Dispatch tools, before planning, filing an issue, asking a human, posting a finding, or starting work that someone may already track or have decided."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dispatch first
|
|
7
|
+
|
|
8
|
+
Dispatch already holds most of what you are about to plan, file or ask: the open issues, their
|
|
9
|
+
specs, and the answers humans gave. A second issue for tracked work, or a question a human already
|
|
10
|
+
answered, costs that human the time to notice it and splits the history across two places.
|
|
11
|
+
|
|
12
|
+
## Search before you act
|
|
13
|
+
|
|
14
|
+
Before you plan, file an issue, ask, post a finding or start work, search Dispatch. Every word of
|
|
15
|
+
the query must match, so each word you add can only lose hits: search with two or three words, the
|
|
16
|
+
thing and what is wrong with it, as a user would name them. Never paste a draft.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
dispatch_search({ query: "broadcast send order" })
|
|
20
|
+
dispatch_search({ query: "reviewer threads OR review comments" })
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When a query finds nothing, drop a word before you add one. Try two or three wordings (the
|
|
24
|
+
component's name, the symptom, the fix) before you conclude that nothing exists. Open every hit
|
|
25
|
+
that could be yours with `dispatch_read`, then its parent (the `child_of` row under `Links:`); the
|
|
26
|
+
parent's children and the issue's `Components:` line show where the rest of that work lives.
|
|
27
|
+
|
|
28
|
+
## What to do with what you find
|
|
29
|
+
|
|
30
|
+
- **The work is already tracked: extend that issue.** Put the finding on it (a comment, a message,
|
|
31
|
+
or an edit to its spec) instead of filing another. File a new issue only when no hit covers the
|
|
32
|
+
work, and cite the nearest one you ruled out (`dispatch://KEY`).
|
|
33
|
+
- **The question is already decided: cite the decision.** An answered ask is the record. Point to
|
|
34
|
+
it where you rely on it (`dispatch://KEY/ask/<id>`) instead of asking again.
|
|
35
|
+
- **You met a duplicate: close it.** Keep the issue that holds the spec and the discussion, carry
|
|
36
|
+
over anything only the duplicate has, then close the duplicate with a reason that names the
|
|
37
|
+
survivor: `dispatch_issue_update({ issue, status: "done", reason: "Duplicate of dispatch://KEY." })`.
|
|
38
|
+
When the duplicate carries the `legion` label, or another session or a human holds its claim,
|
|
39
|
+
comment on it naming the survivor instead of closing it.
|
|
40
|
+
- **Every ask and message stands on its own.** The person who answers sees only that text: put the
|
|
41
|
+
facts, the options and your recommendation in it, and link what you cite (`dispatch://…`)
|
|
42
|
+
instead of writing "see above" or "my earlier message".
|
|
43
|
+
|
|
44
|
+
## Load the full skill before you write
|
|
45
|
+
|
|
46
|
+
- **Before you write or change a spec, read `skill://dispatch`,** including its "Writing a spec"
|
|
47
|
+
section.
|
|
48
|
+
- Before any other write to Dispatch (an ask, a message, a comment, a document edit, a status
|
|
49
|
+
change, a claim), load `skill://dispatch` unless you already have in this session.
|
|
@@ -256,10 +256,10 @@ Preserve this order exactly:
|
|
|
256
256
|
|
|
257
257
|
What returns the tree to review: a changed diff — a commit above the approved head that
|
|
258
258
|
touches anything outside `docs/solutions/`, or a conflict-resolution merge whose fingerprint
|
|
259
|
-
(`skill://legion-worker`
|
|
259
|
+
(the unchanged-diff check, `skill://legion-worker/references/conflicts-and-rewrites.md`) differs from the approved head's. What does not: retro's
|
|
260
260
|
`docs/solutions/` commit, and a merge forced by a GitHub-reported conflict whose fingerprint
|
|
261
261
|
is unchanged. For that merge the order is: the implementer merges the bookmark forward with the
|
|
262
|
-
destination (
|
|
262
|
+
destination (the forward-merge procedure in `skill://legion-worker/references/conflicts-and-rewrites.md` — `jj new legion/<KEY> <destination>`,
|
|
263
263
|
never a rebase, since a rebase rewrites every descendant of the chain's fork point, including
|
|
264
264
|
another tree's branch stacked on it), pushes it with the ordinary push procedure (a genuine
|
|
265
265
|
fast-forward), and posts the before/after fingerprints; the tester re-runs the bare gates only;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: legion-controller
|
|
3
|
-
description: Use when handling Legion controller wakes for root-issue triage,
|
|
3
|
+
description: Use when handling Legion controller wakes for root-issue triage, keeping the admission slots full from `todo` issues, the daily report, architect escalation, resync healing, or human interaction.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Legion Controller
|
|
7
7
|
|
|
8
|
-
The controller is the one persistent, wake-driven session for a Legion project. It
|
|
9
|
-
|
|
10
|
-
routes raw events into an
|
|
8
|
+
The controller is the one persistent, wake-driven session for a Legion project. It keeps the
|
|
9
|
+
project's admission slots full with the highest-priority work, and makes triage, escalation, and
|
|
10
|
+
human-interaction judgments; it never does phase-worker work or routes raw events into an
|
|
11
|
+
architect.
|
|
11
12
|
|
|
12
13
|
## Start and claim the controller role
|
|
13
14
|
|
|
@@ -87,8 +88,8 @@ mints a new secret, so your grants stop working and the role moves to the new se
|
|
|
87
88
|
|
|
88
89
|
The Go daemon's controller topic is a wake for a session that is running when it is published.
|
|
89
90
|
Envoy hands an Oh My Pi session no retained copy of a notice published before it subscribed, so a
|
|
90
|
-
hold, a tree architect's failed claim,
|
|
91
|
-
arrives as a wake. At every start, before anything else:
|
|
91
|
+
hold, a tree architect's failed claim, a new triage root, or a freed slot from while no controller
|
|
92
|
+
ran never arrives as a wake. At every start, before anything else:
|
|
92
93
|
|
|
93
94
|
1. Read `legion state --json` and handle each issue whose `issues.<KEY>.phase` is `held` (its
|
|
94
95
|
`issues.<KEY>.holdReason` is `escalated` when its architect sent it to you, and absent while the
|
|
@@ -98,15 +99,19 @@ arrives as a wake. At every start, before anything else:
|
|
|
98
99
|
as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
|
|
99
100
|
nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
|
|
100
101
|
2. List the project's triage issues handed to Legion with
|
|
101
|
-
`dispatch_issues({project, status: "triage", label: "legion", limit: 250})`.
|
|
102
|
-
When its first line ends `(showing
|
|
103
|
-
|
|
104
|
-
`Links:`
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
`dispatch_issues({project, status: "triage", label: "legion", limit: 250, offset: 0})`.
|
|
103
|
+
When its first line ends `(showing 1-250 of N)`, read the next page with `offset: 250`, and so
|
|
104
|
+
on until you have all N rows. The rows show no parent, so open each row with `dispatch_read`
|
|
105
|
+
and follow `Links:` up through each `child_of` parent (a `child_of` under `Referenced by:` is a
|
|
106
|
+
child of this issue, not its parent). Leave a child that has an ancestor in `admission.active`
|
|
107
|
+
or `admission.waiting`: that tree's architect owns it. Triage every other row as a root,
|
|
108
|
+
children outside a live tree included, when `legion state --json` does not record it under
|
|
109
|
+
`issues`. A root recorded there and now in `triage` is work the daemon holds that a human
|
|
110
|
+
pulled back: never re-admit it yourself; name it in your summary to the human ("<KEY> was
|
|
111
|
+
pulled back to triage; what do you want?").
|
|
112
|
+
3. Fill the free admission slots ([Keeping the slots full](#keeping-the-slots-full-go-daemon)).
|
|
113
|
+
4. Post the day's report when this is your first turn of the UTC day
|
|
114
|
+
([Daily report](#daily-report-go-daemon)).
|
|
110
115
|
|
|
111
116
|
The issue record and Dispatch are the truth; the topic is the wake.
|
|
112
117
|
|
|
@@ -117,12 +122,141 @@ since its project may be shared with humans and other agents. It never admits a
|
|
|
117
122
|
without the label, and wakes you for a root in `triage` only while the root carries it and is
|
|
118
123
|
unrecorded: on its creation with the label, and on each change to it after that while it stays
|
|
119
124
|
in triage, the change that adds the label included (the dashboard creates an issue without
|
|
120
|
-
labels, so a human adds it from the issue header).
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
labels, so a human adds it from the issue header). Two parties hand work over: a person, who sets
|
|
126
|
+
the label from the issue header, and you, when you fill a free slot (below). You label an issue
|
|
127
|
+
only when you take it or file it for Legion, so the label means Legion has the issue or had it.
|
|
128
|
+
A person's label is their decision: never take it off. A child needs no label: it runs under its
|
|
129
|
+
tree's architect once its root is admitted. `legion status <KEY> todo` admits a root, or a child
|
|
130
|
+
outside a live tree (admitted as a root of its own), only while it carries the label, so an issue
|
|
131
|
+
you hand over or file for Legion to run carries it first (`labels` in `dispatch_issue_update` or
|
|
132
|
+
`dispatch_issue`). Taking the label off a waiting root drops it from the waiting line; taking it
|
|
133
|
+
off an admitted tree does not stop it.
|
|
134
|
+
|
|
135
|
+
## Keeping the slots full (Go daemon)
|
|
136
|
+
|
|
137
|
+
Picking the next work is your job: nobody hand-feeds issues to Legion. Keep every admission slot
|
|
138
|
+
filled with the highest-priority concrete issue Legion can take. The unit of Legion work is a
|
|
139
|
+
leaf, an issue with no children, never an umbrella that holds other issues.
|
|
140
|
+
|
|
141
|
+
**When.** At every start (step 3 above), and on each `slot-free on <KEY>` wake: the daemon
|
|
142
|
+
released `<KEY>`'s slot, because its tree finished or left the workflow, and no waiting root took
|
|
143
|
+
it.
|
|
144
|
+
|
|
145
|
+
**Scope first.** The scope the deployment instructions state decides which issues are candidates
|
|
146
|
+
at all, before anything below. When they say you hand Legion no issue yourself, or that Legion
|
|
147
|
+
runs only issues someone else sets to `todo`, the walk takes nothing: stop here, whatever slots
|
|
148
|
+
are free. When they narrow the scope (a repository, a kind of change), a candidate outside it is
|
|
149
|
+
skipped (the last row of the table below). With no scope stated, every issue of the project is in
|
|
150
|
+
scope.
|
|
151
|
+
|
|
152
|
+
**How many.** Read `legion state --json`. The free slots are `admission.cap` minus the roots in
|
|
153
|
+
`admission.active` and in `admission.waiting` (a waiting root takes the next slot before anything
|
|
154
|
+
you add). With none free, stop.
|
|
155
|
+
|
|
156
|
+
**Candidates.** The project's open `todo` issues, roots and children alike, that have no children
|
|
157
|
+
at all and do not carry the `legion` label. `todo` alone, as Sami ruled for choosing work on
|
|
158
|
+
2026-09-27 (`skill://dispatch`, "Choosing what to work on"): take the top ready issue, "status
|
|
159
|
+
`todo`, highest priority first, then board rank"; an issue that waits on a deploy or a decision
|
|
160
|
+
belongs in `backlog`, so the walk takes nothing from `backlog` or `triage`. The Go daemon runs
|
|
161
|
+
only labelled roots, so every root it ran since the daemon required the label carries it: a
|
|
162
|
+
labelled root in `todo` is the daemon's to admit or queue, one in `triage` is yours to triage
|
|
163
|
+
(step 2 above), and one anywhere else was parked by Legion or by a person. A child you take becomes
|
|
164
|
+
a root of its own: the daemon admits a labelled `todo` child whose tree Legion does not run as a
|
|
165
|
+
new tree. The `dispatch_issues` rows show no labels and no children, so the listing below filters
|
|
166
|
+
neither: both are checked on each candidate (the table's first rows). List the `todo` issues one
|
|
167
|
+
priority at a time, `priority: [0]` first, then `[1]`, `[2]`, `[3]`, and `[null]` (no priority)
|
|
168
|
+
last, keeping the listing's order within each, which is the board's rank:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
dispatch_issues({ project: "<PROJECT>", status: "todo", priority: [0], limit: 250, offset: 0 })
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
When the first line ends `(showing 1-250 of N)`, the next page is `offset: 250`, then `500`. Read
|
|
175
|
+
pages only as far as you need: stop listing once the free slots are filled. `<PROJECT>` is the
|
|
176
|
+
Dispatch project key, the prefix of this deployment's issue keys (`AGENTC-12` → `AGENTC`), which is
|
|
177
|
+
also `daemon.project` in `legion state --json`: the project key exactly as `legion.yaml` writes it.
|
|
178
|
+
A row that shows `claimed by …` and does not end its claim with `· not running` (the route, when
|
|
179
|
+
the row shows one, comes after the claim) is claimed, as the table below says: skip it without
|
|
180
|
+
reading it.
|
|
181
|
+
|
|
182
|
+
**Walk.** Take the remaining rows in that order until the free slots are filled. Read each one with
|
|
183
|
+
`dispatch_read({ issue: "<KEY>" })` and skip it when any of these holds. `dispatch_read` shows only
|
|
184
|
+
the issue's last 10 events, so the rows that read `Events:` are best-effort: the pull-request row
|
|
185
|
+
leans on `External links:`, and the label row is the one that never depends on history.
|
|
186
|
+
|
|
187
|
+
| Skip when | How you check it |
|
|
188
|
+
|---|---|
|
|
189
|
+
| It carries the `legion` label | `Labels:` lists `legion`, in any case. Legion has the issue or had it, as **Candidates** above says. |
|
|
190
|
+
| It has any child | `dispatch_read({ ref: "dispatch://<KEY>/children" })` lists any child, open or `done`. It is an umbrella, and a finished umbrella is still no leaf. That also skips an issue whose only child is done, which is accepted. Its open children are candidates themselves, each in its own place in the order. |
|
|
191
|
+
| An ancestor is Legion's | Follow `Links:` up through each `child_of` parent, reading each one, and skip when any ancestor carries the `legion` label or is recorded under `issues` in `legion state --json`, whatever its status: a Legion tree, running or parked, owns its children. An ancestor's claim or route does not skip the issue: a coordinator holding an umbrella files `todo` leaves for others to pick up, and the claim on the issue itself is what keeps two sessions off the same work. A `child_of` under `Referenced by:` is a child of this issue, not its parent. |
|
|
192
|
+
| Someone is designing it | `Open asks:` lists any ask, a `Spec approval: awaiting …` line shows the spec waits on a human, or `Events:` show an `artifact.version` or an `ask.opened` from the last seven days: a session or a person is shaping it even when nobody claims or routes it. |
|
|
193
|
+
| Legion ran it without the label now on it | `legion state --json` records it under `issues`, whatever its status, or `Events:` show a status write by `session legion-daemon:<PROJECT>`, the daemon's actor on every `legion status` (yours included) and on its own `in_progress` at admission. That covers a root a person took the label off, and one that ran before the daemon required the label and never had it. Name each one you skip for this in your summary. The walk never sends a root Legion already ran back into Legion: a person does that with the label and `todo`, and you do it only when a wake below says to (`worker-died`, closed-tree activity). |
|
|
194
|
+
| A running session or a person claims it | `Claimed by:` names anyone and does not end `· not running`. `· liveness unknown` counts as claimed: the agent registry could not be read, so nothing says the holder stopped. A claim ending `· not running` has lapsed, and the issue is free. |
|
|
195
|
+
| Its route reaches a running session | `Route:` names a route with nothing after it, or with `(held by …)`. `(nobody holds it right now)` and `(that session is not running right now)` reach nobody; `(the Envoy listener did not answer, …)` counts as reaching someone. `Route: none` is free. |
|
|
196
|
+
| A pull request is linked or named | `External links:` lists a pull request (kind `github_pr`, or a URL ending `/pull/<n>`), or a comment or message among `Events:` names one. You cannot read GitHub, so an open, merged, or closed pull request all count. A person who wants Legion on it anyway hands it over themselves: the label, then `todo`. |
|
|
197
|
+
| Its assignee is working it | `Assignee:` names a person who holds the claim (the row above), or whose own comment or message among `Events:` says they are working on it. The assignee alone is who answers the issue's questions, not who works it. |
|
|
198
|
+
| It is outside this deployment's scope | Read the scope the deployment instructions state against the title and, when the title does not settle it, the spec (`dispatch_doc_read({ issue: "<KEY>" })`). When in doubt, skip it. With no scope stated, every issue of the project is in scope. |
|
|
199
|
+
|
|
200
|
+
**Take.** For each candidate that passes, in order:
|
|
201
|
+
|
|
202
|
+
1. Add the label and keep the labels it has, which `Labels:` lists (`none` is no labels). `labels`
|
|
203
|
+
replaces the whole set, so a label you leave out is removed.
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
dispatch_issue_update({ issue: "<KEY>", labels: ["<each current label>", "legion"] })
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
2. It is already in `todo`, so the label admits it: the daemon records it and gives it the free
|
|
210
|
+
slot, or queues it in `admission.waiting`. It needs no status write.
|
|
211
|
+
3. Post one short comment that says Legion took it and names who is asked at its design gate, from
|
|
212
|
+
the `Assignee:` line, with the assignee sentence [New issue triage](#new-issue-triage) step 4
|
|
213
|
+
gives:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
dispatch_comment({ issue: "<KEY>", body: "Legion took this issue: it was the highest-priority open issue nobody else was working on. Assigned to <login>, who will get this tree's questions and its design approval." })
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The daemon admits each root when Dispatch's event reaches it; the next `legion state --json`
|
|
220
|
+
shows it in `admission.active`. When the candidates run out with slots still free, leave those
|
|
221
|
+
slots free and say so in your summary.
|
|
222
|
+
|
|
223
|
+
## Daily report (Go daemon)
|
|
224
|
+
|
|
225
|
+
Once a day, post one Dispatch message that says what Legion finished, what it closed without a
|
|
226
|
+
change and why, and what is running.
|
|
227
|
+
|
|
228
|
+
**Where.** On the issue the deployment instructions name for Legion's reports. With none named,
|
|
229
|
+
on the project's `Legion daily report` issue:
|
|
230
|
+
`dispatch_search({ query: "\"Legion daily report\"", project: "<PROJECT>" })` finds it. When it
|
|
231
|
+
does not exist, create it once and park it in `icebox`, so nobody takes it as work; it never
|
|
232
|
+
carries the `legion` label:
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
dispatch_issue({ project: "<PROJECT>", title: "Legion daily report", spec: "## Summary\n\nLegion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. This issue is not work, so it carries no `legion` label and stays in icebox." })
|
|
236
|
+
legion status <report KEY> icebox
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**When.** On your first turn of each UTC day, whatever it is: your start, or a wake of any kind.
|
|
240
|
+
Read the report issue with `dispatch_read`, and post when its `Events:` show no `message.created`
|
|
241
|
+
from today. You have no clock of your own, so a day with no turn has no report; the next one
|
|
242
|
+
covers it.
|
|
243
|
+
|
|
244
|
+
**What.** One `dispatch_message({ issue: "<report KEY>", body })` of at most 2,000 characters,
|
|
245
|
+
written as `skill://dispatch`'s "Writing for the human" says: every issue by its key and title,
|
|
246
|
+
every pull request by its URL.
|
|
247
|
+
|
|
248
|
+
- **Finished.** `dispatch_issues({ project: "<PROJECT>", status: "done", updated_since: "<the
|
|
249
|
+
previous report's time, or 24 hours ago>", limit: 250 })`, read every page, and keep the issues
|
|
250
|
+
`legion state --json` records under `issues` whose `issue.closed` event is after the previous
|
|
251
|
+
report. For each, `dispatch_read` it: the pull request under `External links:` is the one that
|
|
252
|
+
merged.
|
|
253
|
+
- **Closed without a change.** Those with no pull request, each with the reason its closing
|
|
254
|
+
message gave (the `message.created` just before `issue.closed` among `Events:`).
|
|
255
|
+
- **Running.** Each root in `admission.active` with its `issues.<KEY>.phase`, and the roots in
|
|
256
|
+
`admission.waiting`.
|
|
257
|
+
|
|
258
|
+
A day with nothing finished says so in one sentence. When the lists do not fit, keep the counts
|
|
259
|
+
and the highest-priority issues.
|
|
126
260
|
|
|
127
261
|
## Deployment instructions
|
|
128
262
|
|
|
@@ -136,7 +270,8 @@ quoted here.
|
|
|
136
270
|
- **Direct user message always first.** If this turn includes a direct user message, answer
|
|
137
271
|
it before handling every other wake.
|
|
138
272
|
- **One wake = one turn.** Handle exactly the wake's implication, then end the turn. Never
|
|
139
|
-
poll, idle-loop, or wait for another event.
|
|
273
|
+
poll, idle-loop, or wait for another event. The one addition: your first turn of each UTC day,
|
|
274
|
+
whatever woke you, also posts the day's report ([Daily report](#daily-report-go-daemon)).
|
|
140
275
|
- **Wakes are advisory.** Before any side effect, verify the current daemon state and the
|
|
141
276
|
relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
|
|
142
277
|
- **Controller state is disposable.** Do not reconstruct or preserve local controller
|
|
@@ -151,7 +286,8 @@ quoted here.
|
|
|
151
286
|
| Wake | Content | Controller action |
|
|
152
287
|
|---|---|---|
|
|
153
288
|
| New issue created in the Dispatch project (`issue.created`, status `triage`; under the TypeScript daemon resync heals misses, under the Go daemon the boot step above does). From the Go daemon: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above) | issue key + triage context (incl. pre-existing children) | Triage: `legion status <KEY> todo` to admit, or set `backlog`/`icebox` to park |
|
|
154
|
-
| Backlog eligibility | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
|
|
289
|
+
| Backlog eligibility (TypeScript daemon) | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
|
|
290
|
+
| `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `legion state --json`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
155
291
|
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `dispatch_ask` from the owning architect, not here |
|
|
156
292
|
| Resync report | artifact-driven anomaly list (zero-owner trees, untriaged-open, launch-failed, admission-drift) | Verify against fresh state, then heal |
|
|
157
293
|
| Resync report: `admission-drift` entry | issue key + whether the daemon added it to, or removed it from, its admission list (the detail says which) | No action: the daemon already repaired it in the same run. An issue that reappears in consecutive reports is a live leak — file a LEGION issue on Dispatch with both reports pasted as evidence (never a GitHub issue) |
|
|
@@ -187,19 +323,30 @@ quoted here.
|
|
|
187
323
|
|
|
188
324
|
(or `icebox` for longer-term deferral). Dispatch status is the durable record; there is no
|
|
189
325
|
other marker to maintain, beyond the Go daemon's `legion` label, which parking leaves in
|
|
190
|
-
place.
|
|
326
|
+
place. Under the Go daemon a root never waits for capacity in `backlog`: in `todo` the
|
|
327
|
+
daemon's admission queue holds it (`admission.waiting`), so park only a root that should not
|
|
328
|
+
run now. Do not triage a system-created child as a root issue.
|
|
191
329
|
4. When you post a triage note (a `dispatch_comment` on the issue saying what you decided and
|
|
192
|
-
why), name who will be asked: `Assigned to <login>, who will get
|
|
193
|
-
or, when the `Assignee:` line says
|
|
194
|
-
|
|
195
|
-
|
|
330
|
+
why), name who will be asked with the assignee sentence: `Assigned to <login>, who will get
|
|
331
|
+
this tree's questions and its design approval`, or, when the `Assignee:` line says
|
|
332
|
+
`unassigned`, `Unassigned — nobody's Inbox shows this tree's questions or its design approval
|
|
333
|
+
until someone takes it from the issue header (Assignee, beside Priority)`. An unassigned root
|
|
334
|
+
still runs; the architect's asks wait in every Inbox's Unassigned band.
|
|
196
335
|
|
|
197
336
|
## Backlog eligibility
|
|
198
337
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
338
|
+
Under the Go daemon nothing reconsiders `backlog` or `icebox` on its own: [Keeping the slots
|
|
339
|
+
full](#keeping-the-slots-full-go-daemon) takes only `todo` issues, since an issue that waits on a
|
|
340
|
+
deploy or a decision belongs in `backlog` (Sami's ruling of 2026-09-27, `skill://dispatch`,
|
|
341
|
+
"Choosing what to work on"). A handed-over root waits for a slot in `todo`, where the daemon's
|
|
342
|
+
admission queue holds it (`admission.waiting`), so a park means "should not run now", and a
|
|
343
|
+
parked root keeps its `legion` label. A parked issue runs again when a person sets it to `todo`,
|
|
344
|
+
or when a wake tells you to re-admit it (`worker-died`, closed-tree activity).
|
|
345
|
+
|
|
346
|
+
Under the TypeScript daemon, when a slot frees or priority changes, use `legion state --json` and
|
|
347
|
+
the current Dispatch issue to reconsider parked roots. Admit the selected root with
|
|
348
|
+
`legion status <KEY> todo`. Moving an item to or from `backlog`/`icebox` is a deliberate
|
|
349
|
+
controller decision, not a no-op.
|
|
203
350
|
|
|
204
351
|
## Architect escalation
|
|
205
352
|
|
|
@@ -212,8 +359,10 @@ and the Dispatch issue. If the work belongs in an independent root:
|
|
|
212
359
|
|
|
213
360
|
1. File a **fresh root issue** with `dispatch_issue({ project, title, spec })` (no `parent`;
|
|
214
361
|
under the Go daemon add `labels: ["legion"]`, without which it is never admitted).
|
|
215
|
-
`project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`)
|
|
216
|
-
|
|
362
|
+
`project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`): the
|
|
363
|
+
project key exactly as `legion.yaml` writes it, which `legion state --json` shows as
|
|
364
|
+
`daemon.project`. It is not the lowercase project token in role names such as
|
|
365
|
+
`legion-<project>-controller`.
|
|
217
366
|
2. Park the child (`legion status <child> icebox`) and leave
|
|
218
367
|
a pointer to the new root issue. The controller's capability is `todo`/`backlog`/`icebox`
|
|
219
368
|
only — only the owning architect or the daemon closes an issue as `done`.
|