@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 +35 -7
- package/dist/legion.js +36 -8
- package/dist/skills/dispatch/SKILL.md +24 -4
- package/package.json +1 -1
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",
|
|
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,
|
|
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
|
-
|
|
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
|
|
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",
|
|
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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
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),
|
|
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.
|