@sjawhar/pi-legion-envoy 5.22.1 → 5.23.1
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 +30 -9
- package/dist/legion.js +33 -11
- package/dist/skills/dispatch/references/api.md +11 -0
- package/dist/skills/dispatch/references/issues.md +5 -2
- package/dist/skills/legion-architect/SKILL.md +1 -2
- package/dist/skills/legion-controller/SKILL.md +63 -23
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -29654,6 +29654,8 @@ function serviceSubjectLabel(subject) {
|
|
|
29654
29654
|
}
|
|
29655
29655
|
return subject;
|
|
29656
29656
|
}
|
|
29657
|
+
var MAX_ISSUE_PAGE_LIMIT = 250;
|
|
29658
|
+
var DEFAULT_ISSUE_PAGE_LIMIT = 50;
|
|
29657
29659
|
var ASK_TURNS = ["human", "agent"];
|
|
29658
29660
|
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
29659
29661
|
var DELIVERY_DUPLICATE_WINDOW_MS = 72 * 60 * 60 * 1000;
|
|
@@ -30398,7 +30400,7 @@ var dispatchToolSpecs = [
|
|
|
30398
30400
|
{
|
|
30399
30401
|
name: "dispatch_issues",
|
|
30400
30402
|
example: { project: "AGENTC", limit: 250, offset: 250 },
|
|
30401
|
-
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch_search remains the keyword surface. " + "
|
|
30403
|
+
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch_search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
|
|
30402
30404
|
arguments: (z2) => ({
|
|
30403
30405
|
project: z2.string().describe("Project key to list issues from."),
|
|
30404
30406
|
status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
|
|
@@ -30407,7 +30409,7 @@ var dispatchToolSpecs = [
|
|
|
30407
30409
|
priority: z2.array(z2.number({ int: true, min: 0, max: 3 }).nullable(), { min: 1, max: 5 }).describe("Optional priority filter: one or more of 0 (P0, highest) through 3 (P3, lowest), and null " + "for an issue with no priority; an issue matching any listed value is returned.").optional(),
|
|
30408
30410
|
updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
|
|
30409
30411
|
route_status: z2.enum(ISSUE_ROUTE_STATUSES).describe("Optional: only open issues whose route is in this state. live: a running session holds " + "the role or is the routed session. no_holder: nobody running holds the role, or the " + "session is not running, right now. unknown: the Envoy listener did not answer.").optional(),
|
|
30410
|
-
limit: z2.number({ int: true, min: 1, max:
|
|
30412
|
+
limit: z2.number({ int: true, min: 1, max: MAX_ISSUE_PAGE_LIMIT }).describe(`Maximum rows, 1-${MAX_ISSUE_PAGE_LIMIT}; default ${DEFAULT_ISSUE_PAGE_LIMIT}.`).optional(),
|
|
30411
30413
|
offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional()
|
|
30412
30414
|
})
|
|
30413
30415
|
},
|
|
@@ -32351,8 +32353,26 @@ class DispatchClient {
|
|
|
32351
32353
|
async resolveIssue(issueReference) {
|
|
32352
32354
|
return this.#resolveIssue(issueReference);
|
|
32353
32355
|
}
|
|
32354
|
-
async
|
|
32355
|
-
|
|
32356
|
+
async listIssuePage(options, page) {
|
|
32357
|
+
const answer = await this.#json("GET", ["api", "v1", "issues"], undefined, {
|
|
32358
|
+
...options,
|
|
32359
|
+
limit: page.limit,
|
|
32360
|
+
offset: page.offset
|
|
32361
|
+
});
|
|
32362
|
+
if (Array.isArray(answer)) {
|
|
32363
|
+
const issues = answer;
|
|
32364
|
+
return {
|
|
32365
|
+
issues: issues.slice(page.offset, page.offset + page.limit),
|
|
32366
|
+
total: issues.length,
|
|
32367
|
+
limit: page.limit,
|
|
32368
|
+
offset: page.offset
|
|
32369
|
+
};
|
|
32370
|
+
}
|
|
32371
|
+
const served = answer;
|
|
32372
|
+
if (typeof served !== "object" || served === null || !Array.isArray(served.issues) || typeof served.total !== "number" || typeof served.limit !== "number" || typeof served.offset !== "number") {
|
|
32373
|
+
throw new Error("GET /api/v1/issues answered neither a page ({issues, total, limit, offset}) nor an array of issues");
|
|
32374
|
+
}
|
|
32375
|
+
return served;
|
|
32356
32376
|
}
|
|
32357
32377
|
async listProjectArtifacts(project, unlinked = false) {
|
|
32358
32378
|
return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
|
|
@@ -34002,9 +34022,7 @@ async function executeDispatchTool(input) {
|
|
|
34002
34022
|
const priority = optionalPriorityFilter(args, "priority");
|
|
34003
34023
|
const updatedSince = optionalString(args, "updated_since");
|
|
34004
34024
|
const routeStatus = optionalString(args, "route_status");
|
|
34005
|
-
const
|
|
34006
|
-
const offset = Math.max(optionalNumber(args, "offset") ?? 0, 0);
|
|
34007
|
-
const issues = await client.listIssues({
|
|
34025
|
+
const page = await client.listIssuePage({
|
|
34008
34026
|
project,
|
|
34009
34027
|
...status === undefined ? {} : { status },
|
|
34010
34028
|
...parent === undefined ? {} : { parent },
|
|
@@ -34012,9 +34030,12 @@ async function executeDispatchTool(input) {
|
|
|
34012
34030
|
...priority === undefined ? {} : { priority },
|
|
34013
34031
|
...updatedSince === undefined ? {} : { updated_since: updatedSince },
|
|
34014
34032
|
...routeStatus === undefined ? {} : { route_status: routeStatus }
|
|
34033
|
+
}, {
|
|
34034
|
+
limit: optionalNumber(args, "limit") ?? DEFAULT_ISSUE_PAGE_LIMIT,
|
|
34035
|
+
offset: optionalNumber(args, "offset") ?? 0
|
|
34015
34036
|
});
|
|
34016
|
-
const total =
|
|
34017
|
-
const rows = issues.
|
|
34037
|
+
const { total, limit, offset } = page;
|
|
34038
|
+
const rows = page.issues.map((row) => ({
|
|
34018
34039
|
key: row.key,
|
|
34019
34040
|
title: row.title,
|
|
34020
34041
|
status: row.status,
|
package/dist/legion.js
CHANGED
|
@@ -29823,6 +29823,8 @@ function serviceSubjectLabel(subject) {
|
|
|
29823
29823
|
}
|
|
29824
29824
|
return subject;
|
|
29825
29825
|
}
|
|
29826
|
+
var MAX_ISSUE_PAGE_LIMIT = 250;
|
|
29827
|
+
var DEFAULT_ISSUE_PAGE_LIMIT = 50;
|
|
29826
29828
|
var ASK_TURNS = ["human", "agent"];
|
|
29827
29829
|
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
29828
29830
|
var DELIVERY_DUPLICATE_WINDOW_MS = 72 * 60 * 60 * 1000;
|
|
@@ -30567,7 +30569,7 @@ var dispatchToolSpecs = [
|
|
|
30567
30569
|
{
|
|
30568
30570
|
name: "dispatch_issues",
|
|
30569
30571
|
example: { project: "AGENTC", limit: 250, offset: 250 },
|
|
30570
|
-
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch_search remains the keyword surface. " + "
|
|
30572
|
+
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch_search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
|
|
30571
30573
|
arguments: (z2) => ({
|
|
30572
30574
|
project: z2.string().describe("Project key to list issues from."),
|
|
30573
30575
|
status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
|
|
@@ -30576,7 +30578,7 @@ var dispatchToolSpecs = [
|
|
|
30576
30578
|
priority: z2.array(z2.number({ int: true, min: 0, max: 3 }).nullable(), { min: 1, max: 5 }).describe("Optional priority filter: one or more of 0 (P0, highest) through 3 (P3, lowest), and null " + "for an issue with no priority; an issue matching any listed value is returned.").optional(),
|
|
30577
30579
|
updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
|
|
30578
30580
|
route_status: z2.enum(ISSUE_ROUTE_STATUSES).describe("Optional: only open issues whose route is in this state. live: a running session holds " + "the role or is the routed session. no_holder: nobody running holds the role, or the " + "session is not running, right now. unknown: the Envoy listener did not answer.").optional(),
|
|
30579
|
-
limit: z2.number({ int: true, min: 1, max:
|
|
30581
|
+
limit: z2.number({ int: true, min: 1, max: MAX_ISSUE_PAGE_LIMIT }).describe(`Maximum rows, 1-${MAX_ISSUE_PAGE_LIMIT}; default ${DEFAULT_ISSUE_PAGE_LIMIT}.`).optional(),
|
|
30580
30582
|
offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional()
|
|
30581
30583
|
})
|
|
30582
30584
|
},
|
|
@@ -31267,8 +31269,26 @@ class DispatchClient {
|
|
|
31267
31269
|
async resolveIssue(issueReference) {
|
|
31268
31270
|
return this.#resolveIssue(issueReference);
|
|
31269
31271
|
}
|
|
31270
|
-
async
|
|
31271
|
-
|
|
31272
|
+
async listIssuePage(options, page) {
|
|
31273
|
+
const answer = await this.#json("GET", ["api", "v1", "issues"], undefined, {
|
|
31274
|
+
...options,
|
|
31275
|
+
limit: page.limit,
|
|
31276
|
+
offset: page.offset
|
|
31277
|
+
});
|
|
31278
|
+
if (Array.isArray(answer)) {
|
|
31279
|
+
const issues = answer;
|
|
31280
|
+
return {
|
|
31281
|
+
issues: issues.slice(page.offset, page.offset + page.limit),
|
|
31282
|
+
total: issues.length,
|
|
31283
|
+
limit: page.limit,
|
|
31284
|
+
offset: page.offset
|
|
31285
|
+
};
|
|
31286
|
+
}
|
|
31287
|
+
const served = answer;
|
|
31288
|
+
if (typeof served !== "object" || served === null || !Array.isArray(served.issues) || typeof served.total !== "number" || typeof served.limit !== "number" || typeof served.offset !== "number") {
|
|
31289
|
+
throw new Error("GET /api/v1/issues answered neither a page ({issues, total, limit, offset}) nor an array of issues");
|
|
31290
|
+
}
|
|
31291
|
+
return served;
|
|
31272
31292
|
}
|
|
31273
31293
|
async listProjectArtifacts(project, unlinked = false) {
|
|
31274
31294
|
return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
|
|
@@ -32941,9 +32961,7 @@ async function executeDispatchTool(input) {
|
|
|
32941
32961
|
const priority = optionalPriorityFilter(args, "priority");
|
|
32942
32962
|
const updatedSince = optionalString(args, "updated_since");
|
|
32943
32963
|
const routeStatus = optionalString(args, "route_status");
|
|
32944
|
-
const
|
|
32945
|
-
const offset = Math.max(optionalNumber(args, "offset") ?? 0, 0);
|
|
32946
|
-
const issues = await client.listIssues({
|
|
32964
|
+
const page = await client.listIssuePage({
|
|
32947
32965
|
project,
|
|
32948
32966
|
...status === undefined ? {} : { status },
|
|
32949
32967
|
...parent === undefined ? {} : { parent },
|
|
@@ -32951,9 +32969,12 @@ async function executeDispatchTool(input) {
|
|
|
32951
32969
|
...priority === undefined ? {} : { priority },
|
|
32952
32970
|
...updatedSince === undefined ? {} : { updated_since: updatedSince },
|
|
32953
32971
|
...routeStatus === undefined ? {} : { route_status: routeStatus }
|
|
32972
|
+
}, {
|
|
32973
|
+
limit: optionalNumber(args, "limit") ?? DEFAULT_ISSUE_PAGE_LIMIT,
|
|
32974
|
+
offset: optionalNumber(args, "offset") ?? 0
|
|
32954
32975
|
});
|
|
32955
|
-
const total =
|
|
32956
|
-
const rows = issues.
|
|
32976
|
+
const { total, limit, offset } = page;
|
|
32977
|
+
const rows = page.issues.map((row) => ({
|
|
32957
32978
|
key: row.key,
|
|
32958
32979
|
title: row.title,
|
|
32959
32980
|
status: row.status,
|
|
@@ -33502,7 +33523,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
33502
33523
|
// package.json
|
|
33503
33524
|
var package_default = {
|
|
33504
33525
|
name: "@sjawhar/pi-legion-envoy",
|
|
33505
|
-
version: "5.
|
|
33526
|
+
version: "5.23.1",
|
|
33506
33527
|
type: "module",
|
|
33507
33528
|
omp: {
|
|
33508
33529
|
extensions: [
|
|
@@ -34035,7 +34056,8 @@ var LegionGoControllerRegisterResponse = exports_external.strictObject({
|
|
|
34035
34056
|
secret: nonEmptyString2
|
|
34036
34057
|
});
|
|
34037
34058
|
var LegionGoControllerSecretResponse = exports_external.strictObject({
|
|
34038
|
-
secret: nonEmptyString2
|
|
34059
|
+
secret: nonEmptyString2,
|
|
34060
|
+
designGate: exports_external.enum(["root-issues", "off"])
|
|
34039
34061
|
});
|
|
34040
34062
|
var LegionGoErrorResponse = exports_external.union([
|
|
34041
34063
|
exports_external.strictObject({ error: nonEmptyString2 }),
|
|
@@ -26,3 +26,14 @@ A path Dispatch does not serve under `/api` or `/v1` answers
|
|
|
26
26
|
route"}`; when you see that, you typed the path wrong — read the index rather than guessing. Every
|
|
27
27
|
`/api/v1` error body carries a `code`; branch on the code, never on the text.
|
|
28
28
|
|
|
29
|
+
`GET /api/v1/issues` answers every matching issue as an array, or one page when you name `limit`
|
|
30
|
+
(1–250) or `offset` (alone it pages 50): `{issues, total, limit, offset}`, where `total` counts
|
|
31
|
+
every issue your filters match. Walking the pages with the next `offset` is exact only while the
|
|
32
|
+
listing does not change: an issue that enters or leaves what the filters match, or whose status or
|
|
33
|
+
rank changes, between two reads shifts rows across a page boundary, and one issue comes back twice
|
|
34
|
+
and another never, even when you stop at `total`. The unpaged array is the only exact set one read
|
|
35
|
+
gives. `cursor` is `400 INVALID_QUERY`. `dispatch_issues` pages for you.
|
|
36
|
+
The event logs page with `after` or `before` and `limit`, and `GET /api/v1/search` takes a `limit`
|
|
37
|
+
of at most 50 and has no next page. Every other route answers without paging and ignores a paging
|
|
38
|
+
parameter.
|
|
39
|
+
|
|
@@ -144,8 +144,11 @@ when it last changed — a roadmap or backlog pass without opening every issue.
|
|
|
144
144
|
(a lifecycle status), `parent` (one issue's children), `label`, `priority` (a list of `0`–`3`, with
|
|
145
145
|
`null` for an issue with no priority: `[0, 1]` is every P0 and P1), `route_status` (below), or
|
|
146
146
|
`updated_since` (an RFC3339 timestamp, for "what moved this week"). `limit` is the page size, 50 by
|
|
147
|
-
default and 250 at most, and `offset` is where the page starts
|
|
148
|
-
|
|
147
|
+
default and 250 at most, and `offset` is where the page starts; the answer says how many issues
|
|
148
|
+
match. Repeating with the next offset walks every matching issue only while the list does not
|
|
149
|
+
change: an issue that enters or leaves what your filters match, or whose status or rank changes,
|
|
150
|
+
between two pages shifts rows across a page boundary, and the walk then shows one issue twice and
|
|
151
|
+
misses another.
|
|
149
152
|
|
|
150
153
|
This is not search: it matches no text. Use `dispatch_search` for a keyword or phrase, and
|
|
151
154
|
`dispatch_issues` when you want every issue in a project and its current state.
|
|
@@ -43,8 +43,7 @@ separate coordinator to finish necessary work.
|
|
|
43
43
|
|
|
44
44
|
Deployment instructions, when present, are the operator's standing rules for this repository —
|
|
45
45
|
required checks, deploy/smoke commands, code-owner expectations, standing roles you may consult,
|
|
46
|
-
the merge credential. They override this skill's defaults where they conflict
|
|
47
|
-
override a Sami ruling quoted here.
|
|
46
|
+
the merge credential. They override this skill's defaults where they conflict.
|
|
48
47
|
|
|
49
48
|
## 1. Decompose or adopt
|
|
50
49
|
|
|
@@ -89,7 +89,10 @@ mints a new secret, so your grants stop working and the role moves to the new se
|
|
|
89
89
|
The Go daemon's controller topic is a wake for a session that is running when it is published.
|
|
90
90
|
Envoy hands an Oh My Pi session no retained copy of a notice published before it subscribed, so a
|
|
91
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.
|
|
92
|
+
ran never arrives as a wake. `legion controller start` opens your first turn with a start message
|
|
93
|
+
(`Legion controller start: …`), so every start and restart runs this procedure with nothing typed.
|
|
94
|
+
At every start, after the claim recheck ([Turn discipline](#turn-discipline)) and before anything
|
|
95
|
+
else:
|
|
93
96
|
|
|
94
97
|
1. Read `legion state --json` and handle each issue whose `issues.<KEY>.phase` is `held` (its
|
|
95
98
|
`issues.<KEY>.holdReason` is `escalated` when its architect sent it to you, and absent while the
|
|
@@ -132,15 +135,37 @@ you hand over or file for Legion to run carries it first (`labels` in `dispatch_
|
|
|
132
135
|
`dispatch_issue`). Taking the label off a waiting root drops it from the waiting line; taking it
|
|
133
136
|
off an admitted tree does not stop it.
|
|
134
137
|
|
|
138
|
+
## Trees waiting on a root claim (Go daemon)
|
|
139
|
+
|
|
140
|
+
A Go root architect whose claim on its root issue was refused starts nothing and waits, holding
|
|
141
|
+
its slot, until the claim is free; nothing tells it when a session holder lets go without
|
|
142
|
+
replying. So every turn rechecks them ([Turn discipline](#turn-discipline)), the daemon's `tick`
|
|
143
|
+
included, which comes on its interval even with every slot taken: read each root in
|
|
144
|
+
`admission.active` whose `issues.<KEY>.phase` is still `admitted` with `dispatch_read`. When its
|
|
145
|
+
`Claimed by:` line is `nobody` or ends `· not running`, tell that tree's architect to claim again
|
|
146
|
+
with `envoy_publish` to `notifications.role.` followed by its claim token,
|
|
147
|
+
`issues.<KEY>.architect.locator.claim` in `legion state --json`. A claim that is its architect's
|
|
148
|
+
own, or one that still holds, needs nothing.
|
|
149
|
+
|
|
135
150
|
## Keeping the slots full (Go daemon)
|
|
136
151
|
|
|
137
152
|
Picking the next work is your job: nobody hand-feeds issues to Legion. Keep every admission slot
|
|
138
153
|
filled with the highest-priority concrete issue Legion can take. The unit of Legion work is a
|
|
139
154
|
leaf, an issue with no children, never an umbrella that holds other issues.
|
|
140
155
|
|
|
141
|
-
**When.** At every start (step 3 above), and on each
|
|
142
|
-
|
|
143
|
-
|
|
156
|
+
**When.** At every start (step 3 above), and on each of the Go daemon's walk wakes. The daemon
|
|
157
|
+
sends each only while a controller is registered, and none while one of the same kind is still
|
|
158
|
+
unsent:
|
|
159
|
+
|
|
160
|
+
- `slot-free on <KEY>`: the daemon released `<KEY>`'s slot, because its tree finished or left the
|
|
161
|
+
workflow, and the slot is free by **How many** below.
|
|
162
|
+
- `todo on <KEY>`: `<KEY>`, an issue nobody handed to Legion, changed while in `todo` and a slot
|
|
163
|
+
was free, so it may be a new candidate. The daemon holds it back half a minute and folds the
|
|
164
|
+
events of that window into it. Walk the whole list, not only `<KEY>`.
|
|
165
|
+
- `tick on <PROJECT>`: the daemon's periodic wake, a minute after it starts and then every
|
|
166
|
+
`controller_wake_interval_seconds` (an hour by default), whatever the slots. An earlier walk
|
|
167
|
+
that found nothing, a day with no event, and [a tree waiting on a root
|
|
168
|
+
claim](#trees-waiting-on-a-root-claim-go-daemon) all get a turn from it.
|
|
144
169
|
|
|
145
170
|
**Scope first.** The scope the deployment instructions state decides which issues are candidates
|
|
146
171
|
at all, before anything below. When they say you hand Legion no issue yourself, or that Legion
|
|
@@ -154,10 +179,10 @@ scope.
|
|
|
154
179
|
you add). With none free, stop.
|
|
155
180
|
|
|
156
181
|
**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`
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
at all and do not carry the `legion` label. Ready work is `todo` (`skill://dispatch`, "Choosing
|
|
183
|
+
what to work on"): take the top ready issue, highest priority first, then board rank. An issue that
|
|
184
|
+
waits on a deploy or a decision belongs in `backlog`, so the walk takes nothing from `backlog` or
|
|
185
|
+
`triage`. The Go daemon runs
|
|
161
186
|
only labelled roots, so every root it ran since the daemon required the label carries it: a
|
|
162
187
|
labelled root in `todo` is the daemon's to admit or queue, one in `triage` is yours to triage
|
|
163
188
|
(step 2 above), and one anywhere else was parked by Legion or by a person. A child you take becomes
|
|
@@ -208,12 +233,12 @@ leans on `External links:`, and the label row is the one that never depends on h
|
|
|
208
233
|
|
|
209
234
|
2. It is already in `todo`, so the label admits it: the daemon records it and gives it the free
|
|
210
235
|
slot, or queues it in `admission.waiting`. It needs no status write.
|
|
211
|
-
3. Post one short comment that says Legion took it
|
|
212
|
-
the
|
|
213
|
-
gives:
|
|
236
|
+
3. Post one short comment that says Legion took it, who is asked at its design gate, and how to
|
|
237
|
+
undo the take, naming the assignee with the sentence [New issue triage](#new-issue-triage)
|
|
238
|
+
step 4 gives (which follows the design gate policy):
|
|
214
239
|
|
|
215
240
|
```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." })
|
|
241
|
+
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. To stop Legion, move the issue to backlog. To keep Legion off it for good, also take the legion label off; taking the label off alone does not stop a tree that has started." })
|
|
217
242
|
```
|
|
218
243
|
|
|
219
244
|
The daemon admits each root when Dispatch's event reaches it; the next `legion state --json`
|
|
@@ -237,9 +262,9 @@ legion status <report KEY> icebox
|
|
|
237
262
|
```
|
|
238
263
|
|
|
239
264
|
**When.** On your first turn of each UTC day, whatever it is: your start, or a wake of any kind.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
265
|
+
While you are registered, the daemon's `tick` gives you a turn at least every
|
|
266
|
+
`controller_wake_interval_seconds`. After the turn's own work, read the report issue with
|
|
267
|
+
`dispatch_read`, and post when its `Events:` show no `message.created` from today.
|
|
243
268
|
|
|
244
269
|
**What.** One `dispatch_message({ issue: "<report KEY>", body })` of at most 2,000 characters,
|
|
245
270
|
written as `skill://dispatch`'s "Writing for the human" says: every issue by its key and title,
|
|
@@ -253,7 +278,13 @@ every pull request by its URL.
|
|
|
253
278
|
- **Closed without a change.** Those with no pull request, each with the reason its closing
|
|
254
279
|
message gave (the `message.created` just before `issue.closed` among `Events:`).
|
|
255
280
|
- **Running.** Each root in `admission.active` with its `issues.<KEY>.phase`, and the roots in
|
|
256
|
-
`admission.waiting`.
|
|
281
|
+
`admission.waiting`. A root whose architect told you its claim was refused is named as waiting
|
|
282
|
+
on that holder: its architect started nothing and asked them to release it or take the issue
|
|
283
|
+
back. Nothing in `legion state` records that, so read the tree's issue (its `Claimed by:` line
|
|
284
|
+
and the ask or message the architect opened) before you name it.
|
|
285
|
+
- **The slots and the walk.** The free slots, as **How many** counts them, then this turn's walk
|
|
286
|
+
when it made one: what it took, and how many candidates each row of the table skipped, so a
|
|
287
|
+
reader can see why a free slot stays empty.
|
|
257
288
|
|
|
258
289
|
A day with nothing finished says so in one sentence. When the lists do not fit, keep the counts
|
|
259
290
|
and the highest-priority issues.
|
|
@@ -262,16 +293,19 @@ and the highest-priority issues.
|
|
|
262
293
|
|
|
263
294
|
Deployment instructions, when present, are the operator's standing rules for this repository —
|
|
264
295
|
required checks, deploy/smoke commands, code-owner expectations, and standing roles you may
|
|
265
|
-
consult. They override this skill's defaults where they conflict
|
|
266
|
-
|
|
296
|
+
consult. They override this skill's defaults where they conflict, and may narrow which issues the
|
|
297
|
+
walk takes, but never widen it past `todo` issues or change the order it takes them in: highest
|
|
298
|
+
priority first, then board rank ([Keeping the slots full](#keeping-the-slots-full-go-daemon)).
|
|
267
299
|
|
|
268
300
|
## Turn discipline
|
|
269
301
|
|
|
270
302
|
- **Direct user message always first.** If this turn includes a direct user message, answer
|
|
271
303
|
it before handling every other wake.
|
|
272
304
|
- **One wake = one turn.** Handle exactly the wake's implication, then end the turn. Never
|
|
273
|
-
poll, idle-loop, or wait for another event.
|
|
274
|
-
|
|
305
|
+
poll, idle-loop, or wait for another event. Two additions, after any direct user message: every
|
|
306
|
+
turn under the Go daemon first rechecks the [trees waiting on a root
|
|
307
|
+
claim](#trees-waiting-on-a-root-claim-go-daemon), and your first turn of each UTC day, whatever
|
|
308
|
+
woke you, also posts the day's report ([Daily report](#daily-report-go-daemon)).
|
|
275
309
|
- **Wakes are advisory.** Before any side effect, verify the current daemon state and the
|
|
276
310
|
relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
|
|
277
311
|
- **Controller state is disposable.** Do not reconstruct or preserve local controller
|
|
@@ -288,6 +322,8 @@ quoted here.
|
|
|
288
322
|
| 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 |
|
|
289
323
|
| Backlog eligibility (TypeScript daemon) | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
|
|
290
324
|
| `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)) |
|
|
325
|
+
| `todo on <KEY>` from the Go daemon (payload `{kind: "todo"}`) | an issue not handed to Legion that changed while in `todo` and a slot stood free, sent half a minute later | Verify a free slot, then walk the whole `todo` list ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
326
|
+
| `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick"}`) | the project key; the daemon's periodic wake, whatever the slots | Recheck the trees waiting on a claim, then walk if a slot is free; post the day's report if this is the day's first turn |
|
|
291
327
|
| 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 |
|
|
292
328
|
| Resync report | artifact-driven anomaly list (zero-owner trees, untriaged-open, launch-failed, admission-drift) | Verify against fresh state, then heal |
|
|
293
329
|
| 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) |
|
|
@@ -330,15 +366,19 @@ quoted here.
|
|
|
330
366
|
why), name who will be asked with the assignee sentence: `Assigned to <login>, who will get
|
|
331
367
|
this tree's questions and its design approval`, or, when the `Assignee:` line says
|
|
332
368
|
`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)`.
|
|
369
|
+
until someone takes it from the issue header (Assignee, beside Priority)`. The design approval
|
|
370
|
+
is promised only when the `Design gate policy:` line of your system prompt, which
|
|
371
|
+
`legion controller start` writes from the daemon's own configuration, says
|
|
372
|
+
`gates.design: root-issues`. Under `gates.design: off` nobody approves a design, so drop "and
|
|
373
|
+
its design approval" (and "or its design approval") from the sentence. An unassigned root
|
|
334
374
|
still runs; the architect's asks wait in every Inbox's Unassigned band.
|
|
335
375
|
|
|
336
376
|
## Backlog eligibility
|
|
337
377
|
|
|
338
378
|
Under the Go daemon nothing reconsiders `backlog` or `icebox` on its own: [Keeping the slots
|
|
339
379
|
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` (
|
|
341
|
-
|
|
380
|
+
deploy or a decision belongs in `backlog` (`skill://dispatch`, "Choosing what to work on"). A
|
|
381
|
+
handed-over root waits for a slot in `todo`, where the daemon's
|
|
342
382
|
admission queue holds it (`admission.waiting`), so a park means "should not run now", and a
|
|
343
383
|
parked root keeps its `legion` label. A parked issue runs again when a person sets it to `todo`,
|
|
344
384
|
or when a wake tells you to re-admit it (`worker-died`, closed-tree activity).
|