@sjawhar/opencode-legion-envoy 3.10.1 → 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.
@@ -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", priority: [0, 1] },
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, and open-ask count, so you can see backlog shape without " + "opening every issue. Optionally filter by status, parent, label, priority, 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]. 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.",
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
  },
@@ -16185,6 +16187,20 @@ async function liveSessionTitles(client, needed) {
16185
16187
  function holdsSession(claim) {
16186
16188
  return claim?.actor.kind === "session";
16187
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
+ }
16188
16204
  function issueSummary(issue2, events, references, graph, titles) {
16189
16205
  const asks = issue2.open_asks;
16190
16206
  const spec = issue2.artifacts?.find((artifact) => artifact.primary);
@@ -16206,7 +16222,7 @@ function issueSummary(issue2, events, references, graph, titles) {
16206
16222
  ...issue2.priority === null ? [] : [`Priority: P${issue2.priority}`],
16207
16223
  `Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
16208
16224
  componentsLine(issue2.components),
16209
- `Route: ${issue2.route ?? "none"}`,
16225
+ `Route: ${routeText(issue2, titles)}`,
16210
16226
  ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
16211
16227
  "Open asks:",
16212
16228
  ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
@@ -16718,6 +16734,7 @@ async function executeDispatchTool(input) {
16718
16734
  const label = optionalString(args, "label");
16719
16735
  const priority = optionalPriorityFilter(args, "priority");
16720
16736
  const updatedSince = optionalString(args, "updated_since");
16737
+ const routeStatus = optionalString(args, "route_status");
16721
16738
  const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
16722
16739
  const issues = await client.listIssues({
16723
16740
  project,
@@ -16725,7 +16742,8 @@ async function executeDispatchTool(input) {
16725
16742
  ...parent === undefined ? {} : { parent },
16726
16743
  ...label === undefined ? {} : { label },
16727
16744
  ...priority === undefined ? {} : { priority },
16728
- ...updatedSince === undefined ? {} : { updated_since: updatedSince }
16745
+ ...updatedSince === undefined ? {} : { updated_since: updatedSince },
16746
+ ...routeStatus === undefined ? {} : { route_status: routeStatus }
16729
16747
  });
16730
16748
  const rows = issues.slice(0, limit).map((row) => ({
16731
16749
  key: row.key,
@@ -16736,13 +16754,16 @@ async function executeDispatchTool(input) {
16736
16754
  labels: row.labels ?? [],
16737
16755
  open_asks: row.open_asks,
16738
16756
  claim: row.claim ?? null,
16757
+ route: row.route ?? null,
16758
+ route_status: row.route_status ?? null,
16759
+ route_holder: row.route_holder ?? null,
16739
16760
  updated_at: row.updated_at
16740
16761
  }));
16741
16762
  const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
16742
16763
  return {
16743
16764
  text: rows.length === 0 ? `No issues in ${project}.` : [
16744
16765
  `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (issues.length > rows.length ? ` (showing ${rows.length} of ${issues.length})` : ""),
16745
- ...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)}`))
16746
16767
  ].join(`
16747
16768
  `),
16748
16769
  details: { issues: rows }
@@ -17178,7 +17199,7 @@ ${trailer.join(`
17178
17199
  const [references, graph, titles] = await Promise.all([
17179
17200
  referencesPromise,
17180
17201
  graphSections(client, dispatchIssueRef(read.issue.key)),
17181
- liveSessionTitles(client, holdsSession(read.issue.claim))
17202
+ liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
17182
17203
  ]);
17183
17204
  return {
17184
17205
  text: issueSummary(read.issue, read.events, references, graph, titles),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "3.10.1",
3
+ "version": "3.11.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -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), or `updated_since` (an RFC3339
295
- timestamp, for "what moved this week"). `limit` caps the rows at 50 by default and 250 at most.
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 three shapes:
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.