@sjawhar/opencode-legion-envoy 3.10.0 → 3.11.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
CHANGED
|
@@ -13949,6 +13949,7 @@ var ISSUE_STATUSES = [
|
|
|
13949
13949
|
"retro",
|
|
13950
13950
|
"done"
|
|
13951
13951
|
];
|
|
13952
|
+
var ISSUE_ROUTE_STATUSES = ["live", "no_holder", "unknown"];
|
|
13952
13953
|
var DOC_EDIT_OPS = [
|
|
13953
13954
|
"replace",
|
|
13954
13955
|
"delete",
|
|
@@ -14255,8 +14256,8 @@ var dispatchToolSpecs = [
|
|
|
14255
14256
|
},
|
|
14256
14257
|
{
|
|
14257
14258
|
name: "dispatch_issues",
|
|
14258
|
-
example: { project: "AGENTC",
|
|
14259
|
-
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels,
|
|
14259
|
+
example: { project: "AGENTC", route_status: "no_holder" },
|
|
14260
|
+
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. " + "Rows are capped at limit (default 50, max 250), applied to the response here, not by the server.",
|
|
14260
14261
|
arguments: (z2) => ({
|
|
14261
14262
|
project: z2.string().describe("Project key to list issues from."),
|
|
14262
14263
|
status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
|
|
@@ -14264,6 +14265,7 @@ var dispatchToolSpecs = [
|
|
|
14264
14265
|
label: z2.string().describe("Optional label filter.").optional(),
|
|
14265
14266
|
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(),
|
|
14266
14267
|
updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
|
|
14268
|
+
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(),
|
|
14267
14269
|
limit: z2.number({ int: true, min: 1, max: 250 }).describe("Maximum rows, 1-250; default 50.").optional()
|
|
14268
14270
|
})
|
|
14269
14271
|
},
|
|
@@ -15150,7 +15152,14 @@ class DispatchClient {
|
|
|
15150
15152
|
return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
|
|
15151
15153
|
}
|
|
15152
15154
|
async getArchitectureSource(project) {
|
|
15153
|
-
|
|
15155
|
+
try {
|
|
15156
|
+
return await this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
|
|
15157
|
+
} catch (error48) {
|
|
15158
|
+
if (error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "SOURCE_NOT_FOUND") {
|
|
15159
|
+
return null;
|
|
15160
|
+
}
|
|
15161
|
+
throw error48;
|
|
15162
|
+
}
|
|
15154
15163
|
}
|
|
15155
15164
|
async resolveAsk(id, input) {
|
|
15156
15165
|
return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
|
|
@@ -16178,6 +16187,20 @@ async function liveSessionTitles(client, needed) {
|
|
|
16178
16187
|
function holdsSession(claim) {
|
|
16179
16188
|
return claim?.actor.kind === "session";
|
|
16180
16189
|
}
|
|
16190
|
+
function routeHeldBySession(issue2) {
|
|
16191
|
+
return issue2.route_status === "live" && issue2.route?.startsWith("role:") === true;
|
|
16192
|
+
}
|
|
16193
|
+
function routeText(issue2, titles) {
|
|
16194
|
+
if (issue2.route === null)
|
|
16195
|
+
return "none";
|
|
16196
|
+
const holder = issue2.route_holder ?? null;
|
|
16197
|
+
const reach = {
|
|
16198
|
+
live: routeHeldBySession(issue2) && holder !== null ? ` (held by ${titles?.get(holder) ?? holder})` : "",
|
|
16199
|
+
no_holder: issue2.route.startsWith("role:") ? " (nobody holds it right now)" : " (that session is not running right now)",
|
|
16200
|
+
unknown: " (the Envoy listener did not answer, so whether it reaches anyone is unknown)"
|
|
16201
|
+
};
|
|
16202
|
+
return issue2.route + (issue2.route_status == null ? "" : reach[issue2.route_status]);
|
|
16203
|
+
}
|
|
16181
16204
|
function issueSummary(issue2, events, references, graph, titles) {
|
|
16182
16205
|
const asks = issue2.open_asks;
|
|
16183
16206
|
const spec = issue2.artifacts?.find((artifact) => artifact.primary);
|
|
@@ -16199,7 +16222,7 @@ function issueSummary(issue2, events, references, graph, titles) {
|
|
|
16199
16222
|
...issue2.priority === null ? [] : [`Priority: P${issue2.priority}`],
|
|
16200
16223
|
`Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
|
|
16201
16224
|
componentsLine(issue2.components),
|
|
16202
|
-
`Route: ${issue2
|
|
16225
|
+
`Route: ${routeText(issue2, titles)}`,
|
|
16203
16226
|
...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
|
|
16204
16227
|
"Open asks:",
|
|
16205
16228
|
...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
|
|
@@ -16711,6 +16734,7 @@ async function executeDispatchTool(input) {
|
|
|
16711
16734
|
const label = optionalString(args, "label");
|
|
16712
16735
|
const priority = optionalPriorityFilter(args, "priority");
|
|
16713
16736
|
const updatedSince = optionalString(args, "updated_since");
|
|
16737
|
+
const routeStatus = optionalString(args, "route_status");
|
|
16714
16738
|
const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
|
|
16715
16739
|
const issues = await client.listIssues({
|
|
16716
16740
|
project,
|
|
@@ -16718,7 +16742,8 @@ async function executeDispatchTool(input) {
|
|
|
16718
16742
|
...parent === undefined ? {} : { parent },
|
|
16719
16743
|
...label === undefined ? {} : { label },
|
|
16720
16744
|
...priority === undefined ? {} : { priority },
|
|
16721
|
-
...updatedSince === undefined ? {} : { updated_since: updatedSince }
|
|
16745
|
+
...updatedSince === undefined ? {} : { updated_since: updatedSince },
|
|
16746
|
+
...routeStatus === undefined ? {} : { route_status: routeStatus }
|
|
16722
16747
|
});
|
|
16723
16748
|
const rows = issues.slice(0, limit).map((row) => ({
|
|
16724
16749
|
key: row.key,
|
|
@@ -16729,13 +16754,16 @@ async function executeDispatchTool(input) {
|
|
|
16729
16754
|
labels: row.labels ?? [],
|
|
16730
16755
|
open_asks: row.open_asks,
|
|
16731
16756
|
claim: row.claim ?? null,
|
|
16757
|
+
route: row.route ?? null,
|
|
16758
|
+
route_status: row.route_status ?? null,
|
|
16759
|
+
route_holder: row.route_holder ?? null,
|
|
16732
16760
|
updated_at: row.updated_at
|
|
16733
16761
|
}));
|
|
16734
16762
|
const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
|
|
16735
16763
|
return {
|
|
16736
16764
|
text: rows.length === 0 ? `No issues in ${project}.` : [
|
|
16737
16765
|
`${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (issues.length > rows.length ? ` (showing ${rows.length} of ${issues.length})` : ""),
|
|
16738
|
-
...rows.map((row) => `${row.key} [${row.status}]${row.priority === null ? "" : ` P${row.priority}`} ${row.title}` + (row.open_asks === 0 ? "" : ` \xB7 ${row.open_asks} open ${row.open_asks === 1 ? "ask" : "asks"}`) + (row.claim === null ? "" : ` \xB7 claimed by ${claimText(row.claim, titles)}`))
|
|
16766
|
+
...rows.map((row) => `${row.key} [${row.status}]${row.priority === null ? "" : ` P${row.priority}`} ${row.title}` + (row.open_asks === 0 ? "" : ` \xB7 ${row.open_asks} open ${row.open_asks === 1 ? "ask" : "asks"}`) + (row.claim === null ? "" : ` \xB7 claimed by ${claimText(row.claim, titles)}`) + (row.route === null || row.route_status === "live" || row.route_status === null ? "" : ` \xB7 route ${routeText(row)}`))
|
|
16739
16767
|
].join(`
|
|
16740
16768
|
`),
|
|
16741
16769
|
details: { issues: rows }
|
|
@@ -17171,7 +17199,7 @@ ${trailer.join(`
|
|
|
17171
17199
|
const [references, graph, titles] = await Promise.all([
|
|
17172
17200
|
referencesPromise,
|
|
17173
17201
|
graphSections(client, dispatchIssueRef(read.issue.key)),
|
|
17174
|
-
liveSessionTitles(client, holdsSession(read.issue.claim))
|
|
17202
|
+
liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
|
|
17175
17203
|
]);
|
|
17176
17204
|
return {
|
|
17177
17205
|
text: issueSummary(read.issue, read.events, references, graph, titles),
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -286,13 +286,14 @@ distinction from the named issue, citing it (`dispatch://KEY`), for whoever read
|
|
|
286
286
|
|
|
287
287
|
To see the shape of a project rather than find a phrase, list its issues:
|
|
288
288
|
```ts
|
|
289
|
-
dispatch_issues({ project, status?, parent?, label?, priority?, updated_since?, limit? })
|
|
289
|
+
dispatch_issues({ project, status?, parent?, label?, priority?, route_status?, updated_since?, limit? })
|
|
290
290
|
```
|
|
291
291
|
Each row carries the issue key, title, status, priority, parent, labels, its open-ask count, and
|
|
292
292
|
when it last changed — a roadmap or backlog pass without opening every issue. Filter with `status`
|
|
293
293
|
(a lifecycle status), `parent` (one issue's children), `label`, `priority` (a list of `0`–`3`, with
|
|
294
|
-
`null` for an issue with no priority: `[0, 1]` is every P0 and P1),
|
|
295
|
-
timestamp, for "what moved this week"). `limit` caps the rows at 50 by
|
|
294
|
+
`null` for an issue with no priority: `[0, 1]` is every P0 and P1), `route_status` (below), or
|
|
295
|
+
`updated_since` (an RFC3339 timestamp, for "what moved this week"). `limit` caps the rows at 50 by
|
|
296
|
+
default and 250 at most.
|
|
296
297
|
|
|
297
298
|
This is not search: it matches no text. Use `dispatch_search` for a keyword or phrase, and
|
|
298
299
|
`dispatch_issues` when you want every issue in a project and its current state.
|
|
@@ -311,7 +312,7 @@ and is not re-staffed. A todo with a finished spec reads as queued work that nob
|
|
|
311
312
|
(LEGION-173 sat in todo for two weeks with a complete spec; AGENTC-1010's v4 plan sat in backlog
|
|
312
313
|
with nobody building it).
|
|
313
314
|
|
|
314
|
-
The audit finds
|
|
315
|
+
The audit finds four shapes:
|
|
315
316
|
|
|
316
317
|
- **Unstaffed work.** A plan or measurement exists, and no one is building it.
|
|
317
318
|
- **Unrecorded delivery.** An issue not yet in `testing` or `done`, claimed or not, has a merged PR
|
|
@@ -321,6 +322,25 @@ The audit finds three shapes:
|
|
|
321
322
|
sits in backlog (OPS-132, done by hand on every migration merge). It leaves no plan and no PR to
|
|
322
323
|
find; the tell is your own messages. Doing something by hand more than once means an issue is
|
|
323
324
|
wearing the wrong status.
|
|
325
|
+
- **Unreachable route.** An open issue whose route names a role nobody holds, or a session that is
|
|
326
|
+
not running, reaches nobody, whatever its priority, and the priority filter above never finds
|
|
327
|
+
it. List it on its own:
|
|
328
|
+
```ts
|
|
329
|
+
dispatch_issues({ project, route_status: "no_holder", limit: 250 })
|
|
330
|
+
```
|
|
331
|
+
Each row reads `route role:sre (nobody holds it right now)` or `route session:<id> (that session
|
|
332
|
+
is not running right now)`. That is one read of the listener, and one read is a restart gap as
|
|
333
|
+
often as a vacancy: an agent box that restarts or resumes keeps the session id, but the session
|
|
334
|
+
is absent from the listener for minutes, and its role with it. On 2026-09-27, 58 of 63 session
|
|
335
|
+
routes one read showed as unreachable pointed at a single session that was moving between boxes.
|
|
336
|
+
So a route is unowned only when it is `no_holder` on two reads at least ten minutes apart: list
|
|
337
|
+
again after ten minutes and act on the issues both lists name. Confirm with the second
|
|
338
|
+
`dispatch_issues` read, not `envoy_role_get`: a role lookup releases the claim of a holder whose
|
|
339
|
+
session is absent from the registry as it answers. Then staff the role, re-route the
|
|
340
|
+
issue to a live holder, or clear the route and assign it (AGENTC-1065, a P2 production listener
|
|
341
|
+
503, sat routed to an unheld `role:sre` with no assignee). `route_status: "unknown"` means the
|
|
342
|
+
listener did not answer, so a route could not be judged; a `no_holder` filter refuses rather
|
|
343
|
+
than answer an empty list then.
|
|
324
344
|
|
|
325
345
|
Run the audit as a step of a coordinator's loop, at each checkpoint, not as a habit: these shapes
|
|
326
346
|
are found by running the check, not by noticing them.
|
|
@@ -255,16 +255,20 @@ Preserve this order exactly:
|
|
|
255
255
|
with options for its outcomes, and the issue waits for it.
|
|
256
256
|
|
|
257
257
|
What returns the tree to review: a changed diff — a commit above the approved head that
|
|
258
|
-
touches anything outside `docs/solutions/`, or a
|
|
258
|
+
touches anything outside `docs/solutions/`, or a conflict-resolution merge whose fingerprint
|
|
259
259
|
(`skill://legion-worker`'s unchanged-diff check) differs from the approved head's. What does not: retro's
|
|
260
|
-
`docs/solutions/` commit, and a
|
|
261
|
-
is unchanged. For that
|
|
262
|
-
`legion-worker`'s
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
`docs/solutions/` commit, and a merge forced by a GitHub-reported conflict whose fingerprint
|
|
261
|
+
is unchanged. For that merge the order is: the implementer merges the bookmark forward with the
|
|
262
|
+
destination (`legion-worker`'s forward-merge procedure — `jj new legion/<KEY> <destination>`,
|
|
263
|
+
never a rebase, since a rebase rewrites every descendant of the chain's fork point, including
|
|
264
|
+
another tree's branch stacked on it), pushes it with the ordinary push procedure (a genuine
|
|
265
|
+
fast-forward), and posts the before/after fingerprints; the tester re-runs the bare gates only;
|
|
266
|
+
the reviewer confirms and approves the new head by SHA (or continues its round if it had not
|
|
267
|
+
approved); the merger republishes READY. Retro does not re-run. This merge happens only when
|
|
268
|
+
GitHub reports `CONFLICTING`
|
|
265
269
|
(`legion gh -- pr view <n> --json mergeable,mergeStateStatus`); read that on every end-game
|
|
266
270
|
wake — `pr-ready`, `pr-review`, `phase-complete`, `catchup-overseer` — because a `CONFLICTING`
|
|
267
|
-
PR gets no CI and no wake announces it, and send the implementer to
|
|
271
|
+
PR gets no CI and no wake announces it, and send the implementer to resolve it the moment you see
|
|
268
272
|
it. Do not let the merger publish `READY` for an obsolete approval.
|
|
269
273
|
|
|
270
274
|
If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
|
|
@@ -280,7 +280,7 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
280
280
|
**Fast-follow:** <one named cleanup item and where it will land>, or "none".
|
|
281
281
|
|
|
282
282
|
**Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
|
|
283
|
-
**Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget,
|
|
283
|
+
**Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, merge the bookmark onto the new base (`jj new legion/<KEY> <new base> -m "<message>"`) and push with `legion-worker`'s ordinary push procedure — a genuine fast-forward, never the procedure for rewritten commits — the new head runs Tests against the new merge result — and cite that run in the PR body.
|
|
284
284
|
```
|
|
285
285
|
|
|
286
286
|
**A proof** is the changed behaviour exercised on the surface a user reaches it through, recorded
|
|
@@ -362,9 +362,30 @@ this proof.
|
|
|
362
362
|
the fingerprint at the current tip; after pushing the rebased branch, record it at the new
|
|
363
363
|
tip; post one PR comment (Legion footer):
|
|
364
364
|
`rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
Every issue workspace is a `jj workspace` of the same shared repository and operation log, and
|
|
366
|
+
jj always rebases every descendant of any commit it rewrites — a revset naming the root of your
|
|
367
|
+
own chain and rewriting it in place also rewrites whatever another tree has stacked on that root,
|
|
368
|
+
whichever selector chose it (`-s`, `-b`, and `-r` all rewrite descendants; `-r` only re-parents
|
|
369
|
+
them to fill the hole, which is worse). This is what happened in LEGION-118: one issue's own
|
|
370
|
+
conflict step moved a second issue's twelve commits and its bookmark onto a conflicted copy.
|
|
371
|
+
Resolve the conflict with a forward merge instead of a rewrite — merge the branch's own
|
|
372
|
+
bookmark with the destination in one new commit, so nothing existing is rewritten and nothing
|
|
373
|
+
built on your prior commits, in this tree or another, ever moves:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
jj -R "$LEGION_WORKSPACE" new legion/<KEY> main@origin -m "merge: resolve conflict against main@origin"
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Merge from the bookmark, never from `@`: a handoff split leaves `@` an empty, undescribed
|
|
380
|
+
commit above the described one the bookmark already names, and `jj git push` refuses to push
|
|
381
|
+
any commit without a description — merging from `@` drags that undescribed commit into the
|
|
382
|
+
ancestry and the push fails (`Won't push commit … since it has no description`); the bookmark
|
|
383
|
+
is always on a described, already-pushed commit. If the merge conflicts, resolve it in that
|
|
384
|
+
one commit — edit the markers directly; there is nothing to squash, since the merge is the
|
|
385
|
+
only new commit. Then `jj -R "$LEGION_WORKSPACE" new` to move off it, and push with the one
|
|
386
|
+
push procedure (*Every role pushes its own commits*, below): the merge descends from both the
|
|
387
|
+
bookmark's old position and the destination, so it is a genuine fast-forward and *Rewriting
|
|
388
|
+
pushed commits* never applies — nothing was rewritten, so there is no tip to record first.
|
|
368
389
|
- **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." The `Fast-follow:`
|
|
369
390
|
field names naming, duplication, or wording cleanup only; anything that changes behaviour,
|
|
370
391
|
hides an error, or breaks a gate lands in this PR.
|
|
@@ -586,8 +607,8 @@ remote branch sideways onto your commit and drops theirs (jj 0.45.1:
|
|
|
586
607
|
`bookmark: legion/K [move sideways from <theirs> to <yours>]`). A clone that has not seen the other
|
|
587
608
|
push is refused by jj itself (`unexpectedly moved on the remote`).
|
|
588
609
|
|
|
589
|
-
**Rewriting pushed commits** —
|
|
590
|
-
|
|
610
|
+
**Rewriting pushed commits** — a `jj squash --into` a commit already on GitHub, or any other
|
|
611
|
+
rewrite of a commit you already pushed — leaves the pushed tip outside `::@-`, so record
|
|
591
612
|
that tip first, after a fetch and while your chain still descends from it:
|
|
592
613
|
|
|
593
614
|
```bash
|