@sjawhar/opencode-legion-envoy 3.16.0 → 3.16.2
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 +40 -11
- package/package.json +1 -1
- package/skills/dispatch/references/api.md +11 -0
- package/skills/dispatch/references/document-edits.md +9 -1
- package/skills/dispatch/references/documents.md +8 -0
- package/skills/dispatch/references/issues.md +9 -4
- package/skills/legion-worker/SKILL.md +1 -1
package/dist/src/server.js
CHANGED
|
@@ -13559,6 +13559,8 @@ function serviceSubjectLabel(subject) {
|
|
|
13559
13559
|
}
|
|
13560
13560
|
return subject;
|
|
13561
13561
|
}
|
|
13562
|
+
var MAX_ISSUE_PAGE_LIMIT = 250;
|
|
13563
|
+
var DEFAULT_ISSUE_PAGE_LIMIT = 50;
|
|
13562
13564
|
var ASK_TURNS = ["human", "agent"];
|
|
13563
13565
|
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
13564
13566
|
var DELIVERY_DUPLICATE_WINDOW_MS = 72 * 60 * 60 * 1000;
|
|
@@ -13960,6 +13962,7 @@ var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
|
13960
13962
|
var ASK_QUESTION_MAX = 800;
|
|
13961
13963
|
var SEARCH_QUERY_MAX = 1000;
|
|
13962
13964
|
var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passage";
|
|
13965
|
+
var PROJECT_KEY_PATTERN = /^[A-Z][A-Z0-9]{1,9}$/;
|
|
13963
13966
|
var ISSUE_STATUSES = [
|
|
13964
13967
|
"triage",
|
|
13965
13968
|
"icebox",
|
|
@@ -14279,12 +14282,19 @@ var dispatchToolSpecs = [
|
|
|
14279
14282
|
}).describe(`Keyword, phrase, or websearch expression; 2 to ${SEARCH_QUERY_MAX} characters.`),
|
|
14280
14283
|
project: z2.string().describe("Optional project key to search within.").optional(),
|
|
14281
14284
|
limit: z2.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
|
|
14282
|
-
})
|
|
14285
|
+
}),
|
|
14286
|
+
validation: {
|
|
14287
|
+
check: (value) => {
|
|
14288
|
+
const { project } = value;
|
|
14289
|
+
return typeof project !== "string" || project === "" || PROJECT_KEY_PATTERN.test(project);
|
|
14290
|
+
},
|
|
14291
|
+
message: "project must be a project key such as CORE"
|
|
14292
|
+
}
|
|
14283
14293
|
},
|
|
14284
14294
|
{
|
|
14285
14295
|
name: "dispatch_issues",
|
|
14286
14296
|
example: { project: "AGENTC", limit: 250, offset: 250 },
|
|
14287
|
-
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. " + "
|
|
14297
|
+
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.",
|
|
14288
14298
|
arguments: (z2) => ({
|
|
14289
14299
|
project: z2.string().describe("Project key to list issues from."),
|
|
14290
14300
|
status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
|
|
@@ -14293,7 +14303,7 @@ var dispatchToolSpecs = [
|
|
|
14293
14303
|
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(),
|
|
14294
14304
|
updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
|
|
14295
14305
|
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(),
|
|
14296
|
-
limit: z2.number({ int: true, min: 1, max:
|
|
14306
|
+
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(),
|
|
14297
14307
|
offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional()
|
|
14298
14308
|
})
|
|
14299
14309
|
},
|
|
@@ -15118,8 +15128,26 @@ class DispatchClient {
|
|
|
15118
15128
|
async resolveIssue(issueReference) {
|
|
15119
15129
|
return this.#resolveIssue(issueReference);
|
|
15120
15130
|
}
|
|
15121
|
-
async
|
|
15122
|
-
|
|
15131
|
+
async listIssuePage(options, page) {
|
|
15132
|
+
const answer = await this.#json("GET", ["api", "v1", "issues"], undefined, {
|
|
15133
|
+
...options,
|
|
15134
|
+
limit: page.limit,
|
|
15135
|
+
offset: page.offset
|
|
15136
|
+
});
|
|
15137
|
+
if (Array.isArray(answer)) {
|
|
15138
|
+
const issues = answer;
|
|
15139
|
+
return {
|
|
15140
|
+
issues: issues.slice(page.offset, page.offset + page.limit),
|
|
15141
|
+
total: issues.length,
|
|
15142
|
+
limit: page.limit,
|
|
15143
|
+
offset: page.offset
|
|
15144
|
+
};
|
|
15145
|
+
}
|
|
15146
|
+
const served = answer;
|
|
15147
|
+
if (typeof served !== "object" || served === null || !Array.isArray(served.issues) || typeof served.total !== "number" || typeof served.limit !== "number" || typeof served.offset !== "number") {
|
|
15148
|
+
throw new Error("GET /api/v1/issues answered neither a page ({issues, total, limit, offset}) nor an array of issues");
|
|
15149
|
+
}
|
|
15150
|
+
return served;
|
|
15123
15151
|
}
|
|
15124
15152
|
async listProjectArtifacts(project, unlinked = false) {
|
|
15125
15153
|
return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
|
|
@@ -16027,7 +16055,7 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
|
|
|
16027
16055
|
problems.push("exactly one of issue and project is required");
|
|
16028
16056
|
}
|
|
16029
16057
|
if (typeof projectArgument === "string") {
|
|
16030
|
-
if (
|
|
16058
|
+
if (!PROJECT_KEY_PATTERN.test(projectArgument)) {
|
|
16031
16059
|
problems.push("project must be a project key such as CORE");
|
|
16032
16060
|
}
|
|
16033
16061
|
const refDocument = ref?.owner.kind === "project" ? ref.artifact ?? ref.id : undefined;
|
|
@@ -16789,9 +16817,7 @@ async function executeDispatchTool(input) {
|
|
|
16789
16817
|
const priority = optionalPriorityFilter(args, "priority");
|
|
16790
16818
|
const updatedSince = optionalString(args, "updated_since");
|
|
16791
16819
|
const routeStatus = optionalString(args, "route_status");
|
|
16792
|
-
const
|
|
16793
|
-
const offset = Math.max(optionalNumber(args, "offset") ?? 0, 0);
|
|
16794
|
-
const issues = await client.listIssues({
|
|
16820
|
+
const page = await client.listIssuePage({
|
|
16795
16821
|
project,
|
|
16796
16822
|
...status === undefined ? {} : { status },
|
|
16797
16823
|
...parent === undefined ? {} : { parent },
|
|
@@ -16799,9 +16825,12 @@ async function executeDispatchTool(input) {
|
|
|
16799
16825
|
...priority === undefined ? {} : { priority },
|
|
16800
16826
|
...updatedSince === undefined ? {} : { updated_since: updatedSince },
|
|
16801
16827
|
...routeStatus === undefined ? {} : { route_status: routeStatus }
|
|
16828
|
+
}, {
|
|
16829
|
+
limit: optionalNumber(args, "limit") ?? DEFAULT_ISSUE_PAGE_LIMIT,
|
|
16830
|
+
offset: optionalNumber(args, "offset") ?? 0
|
|
16802
16831
|
});
|
|
16803
|
-
const total =
|
|
16804
|
-
const rows = issues.
|
|
16832
|
+
const { total, limit, offset } = page;
|
|
16833
|
+
const rows = page.issues.map((row) => ({
|
|
16805
16834
|
key: row.key,
|
|
16806
16835
|
title: row.title,
|
|
16807
16836
|
status: row.status,
|
package/package.json
CHANGED
|
@@ -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
|
+
|
|
@@ -43,7 +43,15 @@ within one textblock; split changes that span separate blocks into separate oper
|
|
|
43
43
|
`block` plus a zero-based `index`. An insert or move anchor is a quote, `"start"`, `"end"`, `"heading:Title"`, or `"block:<id>"`.
|
|
44
44
|
Ordinary inserts create a sibling block before or after the quote, heading, or block's enclosing document block, and a move lands the
|
|
45
45
|
block at that same boundary; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no header or
|
|
46
|
-
delimiter rows) extends that table before or after the matched row instead
|
|
46
|
+
delimiter rows) extends that table before or after the matched row instead, decided in three steps. A fragment whose every line yields
|
|
47
|
+
a cell and an unescaped `|` (a lone `|` yields no cell, and a line whose only pipe is `\|` has no separator), with no line a delimiter
|
|
48
|
+
row of three hyphens or more a cell, is parsed as rows of that table. What that parse refuses is refused: a row holding text past the
|
|
49
|
+
table's width as `TABLE_WIDTH` (blank cells there are dropped), a line the table cannot hold as a row (indented code, `2. | a | b |`)
|
|
50
|
+
as `INVALID_OP`. When it refuses nothing and reads every line as a row, the rows are inserted, short ones padded. Any other fragment is
|
|
51
|
+
read on its own as blocks, which are inserted after the table or refused as such blocks would be: a whole table you paste there, its
|
|
52
|
+
delimiter row three hyphens or more a cell, becomes a second table, while one with any delimiter cell of one or two hyphens (`| - |`,
|
|
53
|
+
`| --- | - |`) passes the first step, so its header and delimiter rows are inserted as rows of the table, or refused as `TABLE_WIDTH`
|
|
54
|
+
where it is wider than the table. Deleting
|
|
47
55
|
a cell's quoted text removes only that text. `delete_row` / `delete_column` instead mutate their named table in place, keeping the
|
|
48
56
|
table's block id. A row index includes the header: row `0` is the header and its deletion promotes the first body row. The last body
|
|
49
57
|
row and any row's last column cannot be deleted. An index is required. A missing, non-integer, negative, or out-of-range index is
|
|
@@ -128,6 +128,14 @@ dropped and the URL keeps `|text`. A backslash-escaped `\<https://example.com|te
|
|
|
128
128
|
document but comes back re-escaped (`\<`) from `dispatch_doc_read`. A Slack mrkdwn draft, or any other payload that is not Markdown,
|
|
129
129
|
still belongs inside a fenced code block, where it survives verbatim both ways.
|
|
130
130
|
|
|
131
|
+
A table cell ends at every `|` not written `\|`, inside inline code and links too, so write
|
|
132
|
+
`` `x: Promise<void> \| undefined` ``, never `` `x: Promise<void> | undefined` ``, in a cell. A row
|
|
133
|
+
holding text in a cell past its table's width is refused rather than stored without it, naming the
|
|
134
|
+
row: write a `|` inside a cell as `\|`, or, where the row really has more cells, give the header and
|
|
135
|
+
delimiter rows as many. Blank cells past the width are dropped, on every path. A spec, an upload or
|
|
136
|
+
a version answers `INVALID_MARKDOWN`, an insert of blocks `INVALID_OP` on `markdown`, and an insert
|
|
137
|
+
of bare table rows `TABLE_WIDTH`, which names the cell counts only.
|
|
138
|
+
|
|
131
139
|
## A document that is reloading
|
|
132
140
|
|
|
133
141
|
These calls can answer `DOC_SERVICE_UNAVAILABLE` (HTTP 503), because each writes a document inside its
|
|
@@ -24,8 +24,10 @@ In-flight issues with no owner at all go
|
|
|
24
24
|
back to `backlog` as well: no claim or route held by a live session, no Dispatch activity in the
|
|
25
25
|
last day, and no pull request moving on GitHub (an owner working there leaves no Dispatch trace).
|
|
26
26
|
The order keeper sweeps those. Never write the status of an issue that carries the `legion`
|
|
27
|
-
label, or of any issue under one: the Legion daemon writes those statuses
|
|
28
|
-
|
|
27
|
+
label, or of any issue under one: the Legion daemon writes those statuses. A status your session
|
|
28
|
+
writes on the root of a tree Legion is running is set back and the tree's architect told who wrote
|
|
29
|
+
it; only a person in the dashboard, or `legion status`, stops that tree. On an issue under one, a
|
|
30
|
+
status that takes it out of the flow parks that issue and stops its workers.
|
|
29
31
|
|
|
30
32
|
One agent keeps the backlog's order against those priorities, with Sami
|
|
31
33
|
(dispatch://AGENTC-34/ask/f6780f9e-8b96-49eb-9be7-7c7f2036d5cc). Setting an issue's priority
|
|
@@ -144,8 +146,11 @@ when it last changed — a roadmap or backlog pass without opening every issue.
|
|
|
144
146
|
(a lifecycle status), `parent` (one issue's children), `label`, `priority` (a list of `0`–`3`, with
|
|
145
147
|
`null` for an issue with no priority: `[0, 1]` is every P0 and P1), `route_status` (below), or
|
|
146
148
|
`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
|
-
|
|
149
|
+
default and 250 at most, and `offset` is where the page starts; the answer says how many issues
|
|
150
|
+
match. Repeating with the next offset walks every matching issue only while the list does not
|
|
151
|
+
change: an issue that enters or leaves what your filters match, or whose status or rank changes,
|
|
152
|
+
between two pages shifts rows across a page boundary, and the walk then shows one issue twice and
|
|
153
|
+
misses another.
|
|
149
154
|
|
|
150
155
|
This is not search: it matches no text. Use `dispatch_search` for a keyword or phrase, and
|
|
151
156
|
`dispatch_issues` when you want every issue in a project and its current state.
|
|
@@ -59,7 +59,7 @@ only on this phase's artifact.
|
|
|
59
59
|
|
|
60
60
|
You never spawn another Legion role: spawning a worker
|
|
61
61
|
(`legion({op: "spawn_worker", ... })`) is architect-only. You may still use ordinary `task`
|
|
62
|
-
|
|
62
|
+
subagents for your own phase work; none of them is a Legion role.
|
|
63
63
|
Escalate a product, scope, cross-phase, or lifecycle decision to the owning architect with
|
|
64
64
|
`envoy_publish` to its role topic (`notifications.role.` followed by its encoded token, see
|
|
65
65
|
above), carrying the verified facts and the decision needed. `hub` only reaches subagents
|