@sjawhar/pi-legion-envoy 5.18.1 → 5.20.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/envoy.js +20 -3
- package/dist/legion.js +21 -4
- package/dist/skills/AGENTS.md +1 -1
- package/dist/skills/dispatch/SKILL.md +7 -3
- package/dist/skills/legion-architect/SKILL.md +2 -2
- package/dist/skills/legion-controller/SKILL.md +183 -34
- package/dist/skills/legion-worker/SKILL.md +65 -365
- package/dist/skills/legion-worker/references/conflicts-and-rewrites.md +129 -0
- package/dist/skills/legion-worker/references/merge-gate.md +101 -0
- package/dist/skills/legion-worker/references/pr-body.md +125 -0
- package/dist/skills/legion-worker/references/review-threads.md +71 -0
- package/package.json +1 -1
- package/dist/skills/legion-worker/references/knowledge-injection.md +0 -98
- /package/dist/skills/legion-worker/{resources/strategies → references}/cleanup-deletion.md +0 -0
- /package/dist/skills/legion-worker/{resources/strategies → references}/systematic-rename.md +0 -0
package/dist/envoy.js
CHANGED
|
@@ -29904,6 +29904,16 @@ function capAgentStreamText(text, limit) {
|
|
|
29904
29904
|
return text;
|
|
29905
29905
|
return text.slice(0, limit) + AGENT_STREAM_TRUNCATION_SUFFIX;
|
|
29906
29906
|
}
|
|
29907
|
+
// ../contracts/src/claim-holds.ts
|
|
29908
|
+
function claimHolds(claim, titles) {
|
|
29909
|
+
if (claim.actor.kind !== "session") {
|
|
29910
|
+
return "holds";
|
|
29911
|
+
}
|
|
29912
|
+
if (titles === undefined) {
|
|
29913
|
+
return "unknown";
|
|
29914
|
+
}
|
|
29915
|
+
return titles.has(claim.actor.id) ? "holds" : "lapsed";
|
|
29916
|
+
}
|
|
29907
29917
|
// ../contracts/src/dispatch-href.ts
|
|
29908
29918
|
function itemFromSearch(search) {
|
|
29909
29919
|
const params = new URLSearchParams(search);
|
|
@@ -33414,17 +33424,19 @@ function componentsLine(components) {
|
|
|
33414
33424
|
}
|
|
33415
33425
|
}
|
|
33416
33426
|
function claimText(claim, titles) {
|
|
33417
|
-
|
|
33427
|
+
const holding = claimHolds(claim, titles);
|
|
33428
|
+
const marker = holding === "unknown" ? " \xB7 liveness unknown" : holding === "lapsed" ? " \xB7 not running" : "";
|
|
33429
|
+
return `${actorLabel(claim.actor, titles)} since ${claim.at}${marker}`;
|
|
33418
33430
|
}
|
|
33419
33431
|
async function liveSessionTitles(client, needed) {
|
|
33420
33432
|
if (!needed) {
|
|
33421
|
-
return
|
|
33433
|
+
return;
|
|
33422
33434
|
}
|
|
33423
33435
|
try {
|
|
33424
33436
|
const agents = await client.listAgents();
|
|
33425
33437
|
return new Map(agents.map((agent) => [agent.session_id, agent.title]));
|
|
33426
33438
|
} catch {
|
|
33427
|
-
return
|
|
33439
|
+
return;
|
|
33428
33440
|
}
|
|
33429
33441
|
}
|
|
33430
33442
|
function holdsSession(claim) {
|
|
@@ -33456,6 +33468,9 @@ function issueSummary(issue2, events, references, graph, titles) {
|
|
|
33456
33468
|
throw new Error("Dispatch issue is missing components");
|
|
33457
33469
|
if (issue2.claim === undefined)
|
|
33458
33470
|
throw new Error("Dispatch issue is missing claim");
|
|
33471
|
+
if (issue2.external_links === undefined) {
|
|
33472
|
+
throw new Error("Dispatch issue is missing external_links");
|
|
33473
|
+
}
|
|
33459
33474
|
return [
|
|
33460
33475
|
`Title: ${issue2.title}`,
|
|
33461
33476
|
`Key: ${issue2.key}`,
|
|
@@ -33467,6 +33482,8 @@ function issueSummary(issue2, events, references, graph, titles) {
|
|
|
33467
33482
|
componentsLine(issue2.components),
|
|
33468
33483
|
`Route: ${routeText(issue2, titles)}`,
|
|
33469
33484
|
...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
|
|
33485
|
+
"External links:",
|
|
33486
|
+
...issue2.external_links.length === 0 ? ["- none"] : issue2.external_links.map((link) => `- ${link.url}${link.kind === undefined ? "" : ` (${link.kind})`}`),
|
|
33470
33487
|
"Open asks:",
|
|
33471
33488
|
...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
|
|
33472
33489
|
"References:",
|
package/dist/legion.js
CHANGED
|
@@ -30073,6 +30073,16 @@ function capAgentStreamText(text, limit) {
|
|
|
30073
30073
|
return text;
|
|
30074
30074
|
return text.slice(0, limit) + AGENT_STREAM_TRUNCATION_SUFFIX;
|
|
30075
30075
|
}
|
|
30076
|
+
// ../contracts/src/claim-holds.ts
|
|
30077
|
+
function claimHolds(claim, titles) {
|
|
30078
|
+
if (claim.actor.kind !== "session") {
|
|
30079
|
+
return "holds";
|
|
30080
|
+
}
|
|
30081
|
+
if (titles === undefined) {
|
|
30082
|
+
return "unknown";
|
|
30083
|
+
}
|
|
30084
|
+
return titles.has(claim.actor.id) ? "holds" : "lapsed";
|
|
30085
|
+
}
|
|
30076
30086
|
// ../contracts/src/dispatch-href.ts
|
|
30077
30087
|
function itemFromSearch(search) {
|
|
30078
30088
|
const params = new URLSearchParams(search);
|
|
@@ -32353,17 +32363,19 @@ function componentsLine(components) {
|
|
|
32353
32363
|
}
|
|
32354
32364
|
}
|
|
32355
32365
|
function claimText(claim, titles) {
|
|
32356
|
-
|
|
32366
|
+
const holding = claimHolds(claim, titles);
|
|
32367
|
+
const marker = holding === "unknown" ? " \xB7 liveness unknown" : holding === "lapsed" ? " \xB7 not running" : "";
|
|
32368
|
+
return `${actorLabel(claim.actor, titles)} since ${claim.at}${marker}`;
|
|
32357
32369
|
}
|
|
32358
32370
|
async function liveSessionTitles(client, needed) {
|
|
32359
32371
|
if (!needed) {
|
|
32360
|
-
return
|
|
32372
|
+
return;
|
|
32361
32373
|
}
|
|
32362
32374
|
try {
|
|
32363
32375
|
const agents = await client.listAgents();
|
|
32364
32376
|
return new Map(agents.map((agent) => [agent.session_id, agent.title]));
|
|
32365
32377
|
} catch {
|
|
32366
|
-
return
|
|
32378
|
+
return;
|
|
32367
32379
|
}
|
|
32368
32380
|
}
|
|
32369
32381
|
function holdsSession(claim) {
|
|
@@ -32395,6 +32407,9 @@ function issueSummary(issue2, events, references, graph, titles) {
|
|
|
32395
32407
|
throw new Error("Dispatch issue is missing components");
|
|
32396
32408
|
if (issue2.claim === undefined)
|
|
32397
32409
|
throw new Error("Dispatch issue is missing claim");
|
|
32410
|
+
if (issue2.external_links === undefined) {
|
|
32411
|
+
throw new Error("Dispatch issue is missing external_links");
|
|
32412
|
+
}
|
|
32398
32413
|
return [
|
|
32399
32414
|
`Title: ${issue2.title}`,
|
|
32400
32415
|
`Key: ${issue2.key}`,
|
|
@@ -32406,6 +32421,8 @@ function issueSummary(issue2, events, references, graph, titles) {
|
|
|
32406
32421
|
componentsLine(issue2.components),
|
|
32407
32422
|
`Route: ${routeText(issue2, titles)}`,
|
|
32408
32423
|
...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
|
|
32424
|
+
"External links:",
|
|
32425
|
+
...issue2.external_links.length === 0 ? ["- none"] : issue2.external_links.map((link) => `- ${link.url}${link.kind === undefined ? "" : ` (${link.kind})`}`),
|
|
32409
32426
|
"Open asks:",
|
|
32410
32427
|
...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
|
|
32411
32428
|
"References:",
|
|
@@ -33478,7 +33495,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
33478
33495
|
// package.json
|
|
33479
33496
|
var package_default = {
|
|
33480
33497
|
name: "@sjawhar/pi-legion-envoy",
|
|
33481
|
-
version: "5.
|
|
33498
|
+
version: "5.20.0",
|
|
33482
33499
|
type: "module",
|
|
33483
33500
|
omp: {
|
|
33484
33501
|
extensions: [
|
package/dist/skills/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ event intake, process lifecycle, credentials, and role delivery.
|
|
|
10
10
|
| `dispatch/` | every role, and any session writing to Dispatch | specs, asks, comments, artifacts, and messages on native Dispatch |
|
|
11
11
|
| `envoy/` | every role | subscriptions, agent-to-agent messages, and topic formats |
|
|
12
12
|
| `legion-architect/` | root and sub-architects | tree ownership, decomposition, waves, gates, integration, sign-off |
|
|
13
|
-
| `legion-controller/` | the controller root process | wake routing,
|
|
13
|
+
| `legion-controller/` | the controller root process | wake routing, keeping the admission slots full from `todo`, the daily report, escalation |
|
|
14
14
|
| `legion-oracle/` | any role doing research | repository-grounded research |
|
|
15
15
|
| `legion-retro/` | the implementer, at retro | the pre-merge retrospective and its Dispatch message |
|
|
16
16
|
| `legion-worker/` | planner, implementer, tester, reviewer, merger | the phase contracts: handoffs, GitHub identity, PR body and READY discipline, the merge-gate order |
|
|
@@ -215,8 +215,11 @@ dispatch_claim({ issue: "LEGION-234", release: true }) // I have stopped; it i
|
|
|
215
215
|
|
|
216
216
|
A claim records **your** session — the one making the call, never another — and shows on every
|
|
217
217
|
read of the issue: the dashboard header, the issue list and board, `dispatch_read` (a
|
|
218
|
-
`Claimed by:` line) and `dispatch_issues` (a claim on the row).
|
|
219
|
-
|
|
218
|
+
`Claimed by:` line) and `dispatch_issues` (a claim on the row). A session's claim there ends
|
|
219
|
+
`· not running` when the live agent registry does not list that session, as the dashboard's chip
|
|
220
|
+
does — that claim is free to take — and `· liveness unknown` when the registry could not be read,
|
|
221
|
+
which says nothing either way. `dispatch_issues` plus the dashboard's **Unclaimed** filter is how
|
|
222
|
+
you find work nobody is on.
|
|
220
223
|
|
|
221
224
|
- **`409 ISSUE_CLAIMED` means someone else holds this issue.** When it is another session, the
|
|
222
225
|
refusal names it and says it is still running: do not work the issue in parallel — message
|
|
@@ -274,7 +277,8 @@ The two clears differ: `priority` clears with `null`, while `parent` and `route`
|
|
|
274
277
|
Guessing the other one is a refusal either way.
|
|
275
278
|
|
|
276
279
|
Link the pull request that delivers the issue in `external_links` when you open it; the issue page
|
|
277
|
-
renders its state and checks from that link
|
|
280
|
+
renders its state and checks from that link, and `dispatch_read` lists it under `External links:`.
|
|
281
|
+
The call is authenticated with the same bearer as every
|
|
278
282
|
other `dispatch_*` tool: a Legion pane reads it from the `DISPATCH_TOKEN_FILE` path the daemon sets on
|
|
279
283
|
the pane; an OMP session outside Legion reads `dispatch.token` from `~/.config/opencode/envoy.json`.
|
|
280
284
|
|
|
@@ -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`.
|