@sjawhar/pi-legion-envoy 5.15.1 → 5.16.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 CHANGED
@@ -30063,6 +30063,7 @@ var ISSUE_STATUSES = [
30063
30063
  function isIssueStatus(value) {
30064
30064
  return ISSUE_STATUSES.includes(value);
30065
30065
  }
30066
+ var ISSUE_ROUTE_STATUSES = ["live", "no_holder", "unknown"];
30066
30067
  var DOC_EDIT_OPS = [
30067
30068
  "replace",
30068
30069
  "delete",
@@ -30369,8 +30370,8 @@ var dispatchToolSpecs = [
30369
30370
  },
30370
30371
  {
30371
30372
  name: "dispatch_issues",
30372
- example: { project: "AGENTC", priority: [0, 1] },
30373
- 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.",
30373
+ example: { project: "AGENTC", route_status: "no_holder" },
30374
+ 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.",
30374
30375
  arguments: (z2) => ({
30375
30376
  project: z2.string().describe("Project key to list issues from."),
30376
30377
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -30378,6 +30379,7 @@ var dispatchToolSpecs = [
30378
30379
  label: z2.string().describe("Optional label filter.").optional(),
30379
30380
  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(),
30380
30381
  updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
30382
+ 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(),
30381
30383
  limit: z2.number({ int: true, min: 1, max: 250 }).describe("Maximum rows, 1-250; default 50.").optional()
30382
30384
  })
30383
30385
  },
@@ -32383,7 +32385,14 @@ class DispatchClient {
32383
32385
  return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
32384
32386
  }
32385
32387
  async getArchitectureSource(project) {
32386
- return this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
32388
+ try {
32389
+ return await this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
32390
+ } catch (error48) {
32391
+ if (error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "SOURCE_NOT_FOUND") {
32392
+ return null;
32393
+ }
32394
+ throw error48;
32395
+ }
32387
32396
  }
32388
32397
  async resolveAsk(id, input) {
32389
32398
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
@@ -33391,6 +33400,20 @@ async function liveSessionTitles(client, needed) {
33391
33400
  function holdsSession(claim) {
33392
33401
  return claim?.actor.kind === "session";
33393
33402
  }
33403
+ function routeHeldBySession(issue2) {
33404
+ return issue2.route_status === "live" && issue2.route?.startsWith("role:") === true;
33405
+ }
33406
+ function routeText(issue2, titles) {
33407
+ if (issue2.route === null)
33408
+ return "none";
33409
+ const holder = issue2.route_holder ?? null;
33410
+ const reach = {
33411
+ live: routeHeldBySession(issue2) && holder !== null ? ` (held by ${titles?.get(holder) ?? holder})` : "",
33412
+ no_holder: issue2.route.startsWith("role:") ? " (nobody holds it right now)" : " (that session is not running right now)",
33413
+ unknown: " (the Envoy listener did not answer, so whether it reaches anyone is unknown)"
33414
+ };
33415
+ return issue2.route + (issue2.route_status == null ? "" : reach[issue2.route_status]);
33416
+ }
33394
33417
  function issueSummary(issue2, events, references, graph, titles) {
33395
33418
  const asks = issue2.open_asks;
33396
33419
  const spec = issue2.artifacts?.find((artifact) => artifact.primary);
@@ -33412,7 +33435,7 @@ function issueSummary(issue2, events, references, graph, titles) {
33412
33435
  ...issue2.priority === null ? [] : [`Priority: P${issue2.priority}`],
33413
33436
  `Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
33414
33437
  componentsLine(issue2.components),
33415
- `Route: ${issue2.route ?? "none"}`,
33438
+ `Route: ${routeText(issue2, titles)}`,
33416
33439
  ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
33417
33440
  "Open asks:",
33418
33441
  ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
@@ -33924,6 +33947,7 @@ async function executeDispatchTool(input) {
33924
33947
  const label = optionalString(args, "label");
33925
33948
  const priority = optionalPriorityFilter(args, "priority");
33926
33949
  const updatedSince = optionalString(args, "updated_since");
33950
+ const routeStatus = optionalString(args, "route_status");
33927
33951
  const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
33928
33952
  const issues = await client.listIssues({
33929
33953
  project,
@@ -33931,7 +33955,8 @@ async function executeDispatchTool(input) {
33931
33955
  ...parent === undefined ? {} : { parent },
33932
33956
  ...label === undefined ? {} : { label },
33933
33957
  ...priority === undefined ? {} : { priority },
33934
- ...updatedSince === undefined ? {} : { updated_since: updatedSince }
33958
+ ...updatedSince === undefined ? {} : { updated_since: updatedSince },
33959
+ ...routeStatus === undefined ? {} : { route_status: routeStatus }
33935
33960
  });
33936
33961
  const rows = issues.slice(0, limit).map((row) => ({
33937
33962
  key: row.key,
@@ -33942,13 +33967,16 @@ async function executeDispatchTool(input) {
33942
33967
  labels: row.labels ?? [],
33943
33968
  open_asks: row.open_asks,
33944
33969
  claim: row.claim ?? null,
33970
+ route: row.route ?? null,
33971
+ route_status: row.route_status ?? null,
33972
+ route_holder: row.route_holder ?? null,
33945
33973
  updated_at: row.updated_at
33946
33974
  }));
33947
33975
  const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
33948
33976
  return {
33949
33977
  text: rows.length === 0 ? `No issues in ${project}.` : [
33950
33978
  `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (issues.length > rows.length ? ` (showing ${rows.length} of ${issues.length})` : ""),
33951
- ...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)}`))
33979
+ ...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)}`))
33952
33980
  ].join(`
33953
33981
  `),
33954
33982
  details: { issues: rows }
@@ -34384,7 +34412,7 @@ ${trailer.join(`
34384
34412
  const [references, graph, titles] = await Promise.all([
34385
34413
  referencesPromise,
34386
34414
  graphSections(client, dispatchIssueRef(read.issue.key)),
34387
- liveSessionTitles(client, holdsSession(read.issue.claim))
34415
+ liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
34388
34416
  ]);
34389
34417
  return {
34390
34418
  text: issueSummary(read.issue, read.events, references, graph, titles),
package/dist/legion.js CHANGED
@@ -30232,6 +30232,7 @@ var ISSUE_STATUSES = [
30232
30232
  function isIssueStatus(value) {
30233
30233
  return ISSUE_STATUSES.includes(value);
30234
30234
  }
30235
+ var ISSUE_ROUTE_STATUSES = ["live", "no_holder", "unknown"];
30235
30236
  var DOC_EDIT_OPS = [
30236
30237
  "replace",
30237
30238
  "delete",
@@ -30538,8 +30539,8 @@ var dispatchToolSpecs = [
30538
30539
  },
30539
30540
  {
30540
30541
  name: "dispatch_issues",
30541
- example: { project: "AGENTC", priority: [0, 1] },
30542
- 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.",
30542
+ example: { project: "AGENTC", route_status: "no_holder" },
30543
+ 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.",
30543
30544
  arguments: (z2) => ({
30544
30545
  project: z2.string().describe("Project key to list issues from."),
30545
30546
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -30547,6 +30548,7 @@ var dispatchToolSpecs = [
30547
30548
  label: z2.string().describe("Optional label filter.").optional(),
30548
30549
  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(),
30549
30550
  updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
30551
+ 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(),
30550
30552
  limit: z2.number({ int: true, min: 1, max: 250 }).describe("Maximum rows, 1-250; default 50.").optional()
30551
30553
  })
30552
30554
  },
@@ -31299,7 +31301,14 @@ class DispatchClient {
31299
31301
  return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
31300
31302
  }
31301
31303
  async getArchitectureSource(project) {
31302
- return this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
31304
+ try {
31305
+ return await this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
31306
+ } catch (error48) {
31307
+ if (error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "SOURCE_NOT_FOUND") {
31308
+ return null;
31309
+ }
31310
+ throw error48;
31311
+ }
31303
31312
  }
31304
31313
  async resolveAsk(id, input) {
31305
31314
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
@@ -32330,6 +32339,20 @@ async function liveSessionTitles(client, needed) {
32330
32339
  function holdsSession(claim) {
32331
32340
  return claim?.actor.kind === "session";
32332
32341
  }
32342
+ function routeHeldBySession(issue2) {
32343
+ return issue2.route_status === "live" && issue2.route?.startsWith("role:") === true;
32344
+ }
32345
+ function routeText(issue2, titles) {
32346
+ if (issue2.route === null)
32347
+ return "none";
32348
+ const holder = issue2.route_holder ?? null;
32349
+ const reach = {
32350
+ live: routeHeldBySession(issue2) && holder !== null ? ` (held by ${titles?.get(holder) ?? holder})` : "",
32351
+ no_holder: issue2.route.startsWith("role:") ? " (nobody holds it right now)" : " (that session is not running right now)",
32352
+ unknown: " (the Envoy listener did not answer, so whether it reaches anyone is unknown)"
32353
+ };
32354
+ return issue2.route + (issue2.route_status == null ? "" : reach[issue2.route_status]);
32355
+ }
32333
32356
  function issueSummary(issue2, events, references, graph, titles) {
32334
32357
  const asks = issue2.open_asks;
32335
32358
  const spec = issue2.artifacts?.find((artifact) => artifact.primary);
@@ -32351,7 +32374,7 @@ function issueSummary(issue2, events, references, graph, titles) {
32351
32374
  ...issue2.priority === null ? [] : [`Priority: P${issue2.priority}`],
32352
32375
  `Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
32353
32376
  componentsLine(issue2.components),
32354
- `Route: ${issue2.route ?? "none"}`,
32377
+ `Route: ${routeText(issue2, titles)}`,
32355
32378
  ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
32356
32379
  "Open asks:",
32357
32380
  ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
@@ -32863,6 +32886,7 @@ async function executeDispatchTool(input) {
32863
32886
  const label = optionalString(args, "label");
32864
32887
  const priority = optionalPriorityFilter(args, "priority");
32865
32888
  const updatedSince = optionalString(args, "updated_since");
32889
+ const routeStatus = optionalString(args, "route_status");
32866
32890
  const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
32867
32891
  const issues = await client.listIssues({
32868
32892
  project,
@@ -32870,7 +32894,8 @@ async function executeDispatchTool(input) {
32870
32894
  ...parent === undefined ? {} : { parent },
32871
32895
  ...label === undefined ? {} : { label },
32872
32896
  ...priority === undefined ? {} : { priority },
32873
- ...updatedSince === undefined ? {} : { updated_since: updatedSince }
32897
+ ...updatedSince === undefined ? {} : { updated_since: updatedSince },
32898
+ ...routeStatus === undefined ? {} : { route_status: routeStatus }
32874
32899
  });
32875
32900
  const rows = issues.slice(0, limit).map((row) => ({
32876
32901
  key: row.key,
@@ -32881,13 +32906,16 @@ async function executeDispatchTool(input) {
32881
32906
  labels: row.labels ?? [],
32882
32907
  open_asks: row.open_asks,
32883
32908
  claim: row.claim ?? null,
32909
+ route: row.route ?? null,
32910
+ route_status: row.route_status ?? null,
32911
+ route_holder: row.route_holder ?? null,
32884
32912
  updated_at: row.updated_at
32885
32913
  }));
32886
32914
  const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
32887
32915
  return {
32888
32916
  text: rows.length === 0 ? `No issues in ${project}.` : [
32889
32917
  `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (issues.length > rows.length ? ` (showing ${rows.length} of ${issues.length})` : ""),
32890
- ...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)}`))
32918
+ ...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)}`))
32891
32919
  ].join(`
32892
32920
  `),
32893
32921
  details: { issues: rows }
@@ -33323,7 +33351,7 @@ ${trailer.join(`
33323
33351
  const [references, graph, titles] = await Promise.all([
33324
33352
  referencesPromise,
33325
33353
  graphSections(client, dispatchIssueRef(read.issue.key)),
33326
- liveSessionTitles(client, holdsSession(read.issue.claim))
33354
+ liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
33327
33355
  ]);
33328
33356
  return {
33329
33357
  text: issueSummary(read.issue, read.events, references, graph, titles),
@@ -33387,7 +33415,7 @@ import { logger } from "@oh-my-pi/pi-utils";
33387
33415
  // package.json
33388
33416
  var package_default = {
33389
33417
  name: "@sjawhar/pi-legion-envoy",
33390
- version: "5.15.1",
33418
+ version: "5.16.0",
33391
33419
  type: "module",
33392
33420
  omp: {
33393
33421
  extensions: [
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.15.1",
3
+ "version": "5.16.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [