@sjawhar/pi-legion-envoy 5.15.2 → 5.16.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 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
  },
@@ -33398,6 +33400,20 @@ async function liveSessionTitles(client, needed) {
33398
33400
  function holdsSession(claim) {
33399
33401
  return claim?.actor.kind === "session";
33400
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
+ }
33401
33417
  function issueSummary(issue2, events, references, graph, titles) {
33402
33418
  const asks = issue2.open_asks;
33403
33419
  const spec = issue2.artifacts?.find((artifact) => artifact.primary);
@@ -33419,7 +33435,7 @@ function issueSummary(issue2, events, references, graph, titles) {
33419
33435
  ...issue2.priority === null ? [] : [`Priority: P${issue2.priority}`],
33420
33436
  `Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
33421
33437
  componentsLine(issue2.components),
33422
- `Route: ${issue2.route ?? "none"}`,
33438
+ `Route: ${routeText(issue2, titles)}`,
33423
33439
  ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
33424
33440
  "Open asks:",
33425
33441
  ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
@@ -33931,6 +33947,7 @@ async function executeDispatchTool(input) {
33931
33947
  const label = optionalString(args, "label");
33932
33948
  const priority = optionalPriorityFilter(args, "priority");
33933
33949
  const updatedSince = optionalString(args, "updated_since");
33950
+ const routeStatus = optionalString(args, "route_status");
33934
33951
  const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
33935
33952
  const issues = await client.listIssues({
33936
33953
  project,
@@ -33938,7 +33955,8 @@ async function executeDispatchTool(input) {
33938
33955
  ...parent === undefined ? {} : { parent },
33939
33956
  ...label === undefined ? {} : { label },
33940
33957
  ...priority === undefined ? {} : { priority },
33941
- ...updatedSince === undefined ? {} : { updated_since: updatedSince }
33958
+ ...updatedSince === undefined ? {} : { updated_since: updatedSince },
33959
+ ...routeStatus === undefined ? {} : { route_status: routeStatus }
33942
33960
  });
33943
33961
  const rows = issues.slice(0, limit).map((row) => ({
33944
33962
  key: row.key,
@@ -33949,13 +33967,16 @@ async function executeDispatchTool(input) {
33949
33967
  labels: row.labels ?? [],
33950
33968
  open_asks: row.open_asks,
33951
33969
  claim: row.claim ?? null,
33970
+ route: row.route ?? null,
33971
+ route_status: row.route_status ?? null,
33972
+ route_holder: row.route_holder ?? null,
33952
33973
  updated_at: row.updated_at
33953
33974
  }));
33954
33975
  const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
33955
33976
  return {
33956
33977
  text: rows.length === 0 ? `No issues in ${project}.` : [
33957
33978
  `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (issues.length > rows.length ? ` (showing ${rows.length} of ${issues.length})` : ""),
33958
- ...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)}`))
33959
33980
  ].join(`
33960
33981
  `),
33961
33982
  details: { issues: rows }
@@ -34391,7 +34412,7 @@ ${trailer.join(`
34391
34412
  const [references, graph, titles] = await Promise.all([
34392
34413
  referencesPromise,
34393
34414
  graphSections(client, dispatchIssueRef(read.issue.key)),
34394
- liveSessionTitles(client, holdsSession(read.issue.claim))
34415
+ liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
34395
34416
  ]);
34396
34417
  return {
34397
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
  },
@@ -32337,6 +32339,20 @@ async function liveSessionTitles(client, needed) {
32337
32339
  function holdsSession(claim) {
32338
32340
  return claim?.actor.kind === "session";
32339
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
+ }
32340
32356
  function issueSummary(issue2, events, references, graph, titles) {
32341
32357
  const asks = issue2.open_asks;
32342
32358
  const spec = issue2.artifacts?.find((artifact) => artifact.primary);
@@ -32358,7 +32374,7 @@ function issueSummary(issue2, events, references, graph, titles) {
32358
32374
  ...issue2.priority === null ? [] : [`Priority: P${issue2.priority}`],
32359
32375
  `Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
32360
32376
  componentsLine(issue2.components),
32361
- `Route: ${issue2.route ?? "none"}`,
32377
+ `Route: ${routeText(issue2, titles)}`,
32362
32378
  ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
32363
32379
  "Open asks:",
32364
32380
  ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
@@ -32870,6 +32886,7 @@ async function executeDispatchTool(input) {
32870
32886
  const label = optionalString(args, "label");
32871
32887
  const priority = optionalPriorityFilter(args, "priority");
32872
32888
  const updatedSince = optionalString(args, "updated_since");
32889
+ const routeStatus = optionalString(args, "route_status");
32873
32890
  const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
32874
32891
  const issues = await client.listIssues({
32875
32892
  project,
@@ -32877,7 +32894,8 @@ async function executeDispatchTool(input) {
32877
32894
  ...parent === undefined ? {} : { parent },
32878
32895
  ...label === undefined ? {} : { label },
32879
32896
  ...priority === undefined ? {} : { priority },
32880
- ...updatedSince === undefined ? {} : { updated_since: updatedSince }
32897
+ ...updatedSince === undefined ? {} : { updated_since: updatedSince },
32898
+ ...routeStatus === undefined ? {} : { route_status: routeStatus }
32881
32899
  });
32882
32900
  const rows = issues.slice(0, limit).map((row) => ({
32883
32901
  key: row.key,
@@ -32888,13 +32906,16 @@ async function executeDispatchTool(input) {
32888
32906
  labels: row.labels ?? [],
32889
32907
  open_asks: row.open_asks,
32890
32908
  claim: row.claim ?? null,
32909
+ route: row.route ?? null,
32910
+ route_status: row.route_status ?? null,
32911
+ route_holder: row.route_holder ?? null,
32891
32912
  updated_at: row.updated_at
32892
32913
  }));
32893
32914
  const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
32894
32915
  return {
32895
32916
  text: rows.length === 0 ? `No issues in ${project}.` : [
32896
32917
  `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (issues.length > rows.length ? ` (showing ${rows.length} of ${issues.length})` : ""),
32897
- ...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)}`))
32898
32919
  ].join(`
32899
32920
  `),
32900
32921
  details: { issues: rows }
@@ -33330,7 +33351,7 @@ ${trailer.join(`
33330
33351
  const [references, graph, titles] = await Promise.all([
33331
33352
  referencesPromise,
33332
33353
  graphSections(client, dispatchIssueRef(read.issue.key)),
33333
- liveSessionTitles(client, holdsSession(read.issue.claim))
33354
+ liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
33334
33355
  ]);
33335
33356
  return {
33336
33357
  text: issueSummary(read.issue, read.events, references, graph, titles),
@@ -33394,7 +33415,7 @@ import { logger } from "@oh-my-pi/pi-utils";
33394
33415
  // package.json
33395
33416
  var package_default = {
33396
33417
  name: "@sjawhar/pi-legion-envoy",
33397
- version: "5.15.2",
33418
+ version: "5.16.1",
33398
33419
  type: "module",
33399
33420
  omp: {
33400
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,13 @@ 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
+ A close that says the defect cannot happen cites the code that makes it impossible. An issue
316
+ closed because a rewrite forecloses it names the file and line in the rewrite that does so; a
317
+ close that cannot name one is not foreclosed, it is unread. The cheapest way for a rewrite to reach
318
+ parity is to port the code, defect included: LEGION-211's bare `git worktree prune`, filed against
319
+ the TypeScript daemon, had been ported into the Go coordinator and was live in production.
320
+
321
+ The audit finds four shapes:
315
322
 
316
323
  - **Unstaffed work.** A plan or measurement exists, and no one is building it.
317
324
  - **Unrecorded delivery.** An issue not yet in `testing` or `done`, claimed or not, has a merged PR
@@ -321,6 +328,25 @@ The audit finds three shapes:
321
328
  sits in backlog (OPS-132, done by hand on every migration merge). It leaves no plan and no PR to
322
329
  find; the tell is your own messages. Doing something by hand more than once means an issue is
323
330
  wearing the wrong status.
331
+ - **Unreachable route.** An open issue whose route names a role nobody holds, or a session that is
332
+ not running, reaches nobody, whatever its priority, and the priority filter above never finds
333
+ it. List it on its own:
334
+ ```ts
335
+ dispatch_issues({ project, route_status: "no_holder", limit: 250 })
336
+ ```
337
+ Each row reads `route role:sre (nobody holds it right now)` or `route session:<id> (that session
338
+ is not running right now)`. That is one read of the listener, and one read is a restart gap as
339
+ often as a vacancy: an agent box that restarts or resumes keeps the session id, but the session
340
+ is absent from the listener for minutes, and its role with it. On 2026-09-27, 58 of 63 session
341
+ routes one read showed as unreachable pointed at a single session that was moving between boxes.
342
+ So a route is unowned only when it is `no_holder` on two reads at least ten minutes apart: list
343
+ again after ten minutes and act on the issues both lists name. Confirm with the second
344
+ `dispatch_issues` read, not `envoy_role_get`: a role lookup releases the claim of a holder whose
345
+ session is absent from the registry as it answers. Then staff the role, re-route the
346
+ issue to a live holder, or clear the route and assign it (AGENTC-1065, a P2 production listener
347
+ 503, sat routed to an unheld `role:sre` with no assignee). `route_status: "unknown"` means the
348
+ listener did not answer, so a route could not be judged; a `no_holder` filter refuses rather
349
+ than answer an empty list then.
324
350
 
325
351
  Run the audit as a step of a coordinator's loop, at each checkpoint, not as a habit: these shapes
326
352
  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.2",
3
+ "version": "5.16.1",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [