@sjawhar/pi-envoy 10.25.1 → 10.26.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/dispatch.js CHANGED
@@ -14026,6 +14026,7 @@ var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passag
14026
14026
  var SEARCH_KIND_DEPTH = 100;
14027
14027
  var PROJECT_KEY_PATTERN = /^[A-Z][A-Z0-9]{1,9}$/;
14028
14028
  var PROJECT_KEY_RULE = "an uppercase letter, then 1 to 9 uppercase letters or digits, such as CORE";
14029
+ var ARCHITECTURE_SOURCE_REPO_PATTERN = /^[A-Za-z0-9-]+\/(?!\.\.?$)[A-Za-z0-9._-]+$/;
14029
14030
  var ISSUE_STATUSES = [
14030
14031
  "triage",
14031
14032
  "icebox",
@@ -14391,7 +14392,7 @@ var dispatchToolSpecs = [
14391
14392
  {
14392
14393
  name: "dispatch_issues",
14393
14394
  example: { project: "PROJ", limit: 250, offset: 250 },
14394
- 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, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), 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. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
14395
+ 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, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), 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. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never. " + "all takes the whole filtered list in one read instead, for a caller that must not miss or " + "repeat a row while other writers change the list, and especially for one whose own writes " + "move issues out of what it filtered on.",
14395
14396
  arguments: (z2) => ({
14396
14397
  project: z2.string().describe("Project key to list issues from."),
14397
14398
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -14401,7 +14402,8 @@ var dispatchToolSpecs = [
14401
14402
  updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
14402
14403
  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(),
14403
14404
  limit: z2.number({ int: true, min: 1, max: MAX_ISSUE_PAGE_LIMIT }).describe(`Maximum rows, 1-${MAX_ISSUE_PAGE_LIMIT}; default ${DEFAULT_ISSUE_PAGE_LIMIT}.`).optional(),
14404
- offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional()
14405
+ offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional(),
14406
+ all: z2.boolean().describe("Take every matching issue in one unpaged read, so no row is missed or repeated while " + "the list changes. Refused with limit or offset.").optional()
14405
14407
  })
14406
14408
  },
14407
14409
  {
@@ -14423,11 +14425,19 @@ var dispatchToolSpecs = [
14423
14425
  },
14424
14426
  {
14425
14427
  name: "dispatch_architecture_sync",
14426
- example: { project: "CORE" },
14427
- description: "Import a project's architecture model from its configured source repository now, instead of " + "waiting for the server's five-minute schedule. Returns the imported commit and component " + "count, or the recorded error when the model was rejected (the previous model stays up). " + "The source itself is configured by a human in Settings; 404 SOURCE_NOT_FOUND without one.",
14428
+ example: { project: "CORE", repo: "acme/infra" },
14429
+ description: "Import a project's architecture model from its source repositories now, instead of waiting " + "for the server's five-minute schedule. A project may have several sources, one per " + "repository, and its model is every source's components together. Without repo every " + "source imports and each answers on its own line; with repo only that source does. Each " + "line names the imported commit, or the recorded error when that source's model was " + "rejected (its previous components stay up, and every other source is untouched). Sources " + "are saved in Settings or with PUT /api/v1/projects/{key}/architecture-sources/{owner}/{repo}; " + "404 SOURCE_NOT_FOUND when the project has none, or none for that repository.",
14428
14430
  arguments: (z2) => ({
14429
- project: z2.string().describe("Project key whose architecture source to sync, such as CORE.")
14431
+ project: z2.string().describe("Project key whose architecture sources to sync, such as CORE."),
14432
+ repo: z2.string().describe("One source repository of the project to sync alone, owner/name; omit it to sync every source.").optional()
14430
14433
  }),
14434
+ validation: {
14435
+ check: (value) => {
14436
+ const { repo } = value;
14437
+ return typeof repo !== "string" || ARCHITECTURE_SOURCE_REPO_PATTERN.test(repo);
14438
+ },
14439
+ message: "repo is not a repository: give it as owner/name, such as acme/infra."
14440
+ },
14431
14441
  strict: true
14432
14442
  },
14433
14443
  {
@@ -14805,9 +14815,8 @@ function parseCommand(argv, io) {
14805
14815
  const token = rest[index];
14806
14816
  if (!token.startsWith("--")) {
14807
14817
  if (ISSUE_KEY.test(token) && surface.flags.has("--issue") && !setBy.has("issue")) {
14808
- const corrected = [command, ...rest.slice(0, index), "--issue", token];
14809
- corrected.push(...rest.slice(index + 1));
14810
- problems.push(`unexpected argument ${token}: use --issue <KEY>, as in: dispatch ${corrected.map(quoteWord).join(" ")}`);
14818
+ if (claim("issue", "--issue", false))
14819
+ args.issue = token;
14811
14820
  continue;
14812
14821
  }
14813
14822
  problems.push(`unexpected argument ${quoteWord(token)}: every value follows its flag`);
@@ -15164,6 +15173,9 @@ function asErrorShape(value) {
15164
15173
  function isJson(response) {
15165
15174
  return response.headers.get("content-type")?.includes("application/json") ?? false;
15166
15175
  }
15176
+ function isNoRoute(error48) {
15177
+ return error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "NOT_FOUND" && error48.message.startsWith("no route for ");
15178
+ }
15167
15179
  var ISSUE_EVENTS_PAGE_MAX = 200;
15168
15180
  var DISPATCH_TOOL_DEADLINE_MS = 60000;
15169
15181
  function requestSignal(signal) {
@@ -15191,6 +15203,18 @@ function whyNotAPage(answer) {
15191
15203
  }
15192
15204
  return `${what}. Retrying will not help: the same request gets the same answer until that ` + "Dispatch is upgraded or fixed.";
15193
15205
  }
15206
+ function whyNotAnArray(answer) {
15207
+ if (typeof answer === "string")
15208
+ return `text that is not JSON, ${GATEWAY_PAGE}, so a retry may succeed`;
15209
+ if (answer === null)
15210
+ return "null";
15211
+ if (typeof answer !== "object")
15212
+ return `a ${typeof answer}`;
15213
+ if ("issues" in answer && Array.isArray(answer.issues)) {
15214
+ return "a page, so this Dispatch pages a request that carries no limit and no offset";
15215
+ }
15216
+ return "an object that is not the listing";
15217
+ }
15194
15218
  var GATEWAY_PAGE = "which looks like a proxy or gateway page rather than Dispatch's own answer";
15195
15219
  function gatewayAdvice(method, status) {
15196
15220
  if (!transientStatus(status)) {
@@ -15266,6 +15290,14 @@ class DispatchClient {
15266
15290
  return answer;
15267
15291
  throw new Error(`GET ${this.#url(path, query)} asked for a page ({issues, total, limit, offset}) and got ` + refusal);
15268
15292
  }
15293
+ async listAllIssues(options) {
15294
+ const path = ["api", "v1", "issues"];
15295
+ const query = { ...options };
15296
+ const answer = await this.#json("GET", path, undefined, query);
15297
+ if (Array.isArray(answer))
15298
+ return answer;
15299
+ throw new Error(`GET ${this.#url(path, query)} asked for the unpaged listing and got ${whyNotAnArray(answer)}.`);
15300
+ }
15269
15301
  async listProjectArtifacts(project, unlinked = false) {
15270
15302
  return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
15271
15303
  }
@@ -15354,19 +15386,69 @@ class DispatchClient {
15354
15386
  async listProjects() {
15355
15387
  return this.#json("GET", ["api", "v1", "projects"]);
15356
15388
  }
15357
- async syncArchitectureSource(project) {
15358
- return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
15359
- }
15360
- async getArchitectureSource(project) {
15389
+ async listArchitectureSources(project) {
15361
15390
  try {
15362
- return await this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
15391
+ return await this.#json("GET", ["api", "v1", "projects", project, "architecture-sources"]);
15392
+ } catch (error48) {
15393
+ if (!isNoRoute(error48))
15394
+ throw error48;
15395
+ }
15396
+ try {
15397
+ const source = await this.#json("GET", [
15398
+ "api",
15399
+ "v1",
15400
+ "projects",
15401
+ project,
15402
+ "architecture-source"
15403
+ ]);
15404
+ return source === null ? [] : [source];
15363
15405
  } catch (error48) {
15364
15406
  if (error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "SOURCE_NOT_FOUND") {
15365
- return null;
15407
+ return [];
15366
15408
  }
15367
15409
  throw error48;
15368
15410
  }
15369
15411
  }
15412
+ async syncArchitectureSources(project) {
15413
+ try {
15414
+ return await this.#json("POST", [
15415
+ "api",
15416
+ "v1",
15417
+ "projects",
15418
+ project,
15419
+ "architecture-sources",
15420
+ "sync"
15421
+ ]);
15422
+ } catch (error48) {
15423
+ if (!isNoRoute(error48))
15424
+ throw error48;
15425
+ }
15426
+ const source = await this.#json("POST", [
15427
+ "api",
15428
+ "v1",
15429
+ "projects",
15430
+ project,
15431
+ "architecture-source",
15432
+ "sync"
15433
+ ]);
15434
+ return [source];
15435
+ }
15436
+ async syncArchitectureSource(project, repo2) {
15437
+ const [owner, name, ...rest] = repo2.split("/");
15438
+ if (!owner || !name || rest.length > 0) {
15439
+ throw new Error(`repository must be owner/name, got ${JSON.stringify(repo2)}`);
15440
+ }
15441
+ return this.#json("POST", [
15442
+ "api",
15443
+ "v1",
15444
+ "projects",
15445
+ project,
15446
+ "architecture-sources",
15447
+ owner,
15448
+ name,
15449
+ "sync"
15450
+ ]);
15451
+ }
15370
15452
  async resolveAsk(id, input) {
15371
15453
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
15372
15454
  }
@@ -16445,14 +16527,31 @@ async function architectureGuidance(client, project, components) {
16445
16527
  if (components.mode === "none" || components.ids.length > 0)
16446
16528
  return;
16447
16529
  try {
16448
- if (await client.getArchitectureSource(project) === null) {
16530
+ if ((await client.listArchitectureSources(project)).length === 0)
16449
16531
  return;
16450
- }
16451
16532
  return `Project ${project} has an architecture model, but this issue is not linked to any of its current components. ${architectureComponentsAction}`;
16452
16533
  } catch (error48) {
16453
16534
  return `Could not check whether project ${project} has an architecture model: ${messageFor(error48)}. ${architectureComponentsAction}`;
16454
16535
  }
16455
16536
  }
16537
+ function architectureSyncLines(project, source) {
16538
+ if (source.last_error === null) {
16539
+ return `Synced ${project} architecture from ${source.repo}@${source.branch}: commit ${source.last_commit ?? "unknown"} (${source.last_sync_at ?? "unknown time"}).`;
16540
+ }
16541
+ let kept;
16542
+ if (source.last_commit !== null) {
16543
+ kept = `The previous model of ${source.repo} stays up (commit ${source.last_commit}).`;
16544
+ } else if (source.components > 0) {
16545
+ kept = `The previous model of ${source.repo} stays up (${source.components} ${source.components === 1 ? "component" : "components"}).`;
16546
+ } else {
16547
+ kept = `No model has ever imported from ${source.repo}.`;
16548
+ }
16549
+ return [
16550
+ `Sync failed for ${project} (${source.repo}@${source.branch}): ${source.last_error}`,
16551
+ kept
16552
+ ].join(`
16553
+ `);
16554
+ }
16456
16555
  function isDuplicateCandidate(value) {
16457
16556
  if (typeof value !== "object" || value === null)
16458
16557
  return false;
@@ -16696,6 +16795,18 @@ function argumentProblems(tool, args) {
16696
16795
  }
16697
16796
  break;
16698
16797
  }
16798
+ case "dispatch_issues": {
16799
+ if (optionalBoolean(args, "all") === true) {
16800
+ const paging = [
16801
+ ...optionalNumber(args, "limit") === undefined ? [] : ["--limit"],
16802
+ ...optionalNumber(args, "offset") === undefined ? [] : ["--offset"]
16803
+ ];
16804
+ if (paging.length > 0) {
16805
+ problems.push(`--all takes every matching issue in one unpaged read, so it cannot be combined with ${paging.join(" or ")}: name --all on its own, or page with --limit and --offset`);
16806
+ }
16807
+ }
16808
+ break;
16809
+ }
16699
16810
  }
16700
16811
  return problems;
16701
16812
  }
@@ -18013,7 +18124,7 @@ async function executeDispatchTool(input) {
18013
18124
  const priority = optionalPriorityFilter(args, "priority");
18014
18125
  const updatedSince = optionalString(args, "updated_since");
18015
18126
  const routeStatus = optionalString(args, "route_status");
18016
- const page = await client.listIssuePage({
18127
+ const filters = {
18017
18128
  project,
18018
18129
  ...status === undefined ? {} : { status },
18019
18130
  ...parent === undefined ? {} : { parent },
@@ -18021,7 +18132,13 @@ async function executeDispatchTool(input) {
18021
18132
  ...priority === undefined ? {} : { priority },
18022
18133
  ...updatedSince === undefined ? {} : { updated_since: updatedSince },
18023
18134
  ...routeStatus === undefined ? {} : { route_status: routeStatus }
18024
- }, {
18135
+ };
18136
+ const page = optionalBoolean(args, "all") === true ? await client.listAllIssues(filters).then((issues) => ({
18137
+ issues,
18138
+ total: issues.length,
18139
+ limit: issues.length,
18140
+ offset: 0
18141
+ })) : await client.listIssuePage(filters, {
18025
18142
  limit: optionalNumber(args, "limit") ?? DEFAULT_ISSUE_PAGE_LIMIT,
18026
18143
  offset: optionalNumber(args, "offset") ?? 0
18027
18144
  });
@@ -18083,20 +18200,19 @@ async function executeDispatchTool(input) {
18083
18200
  }
18084
18201
  case "dispatch_architecture_sync": {
18085
18202
  const project = stringArg(args, "project");
18086
- const source = await client.syncArchitectureSource(project);
18087
- const at = source.last_sync_at ?? "unknown time";
18203
+ const repo2 = optionalString(args, "repo");
18204
+ const sources = repo2 === undefined ? await client.syncArchitectureSources(project) : [await client.syncArchitectureSource(project, repo2)];
18088
18205
  return {
18089
- text: source.last_error === null ? `Synced ${project} architecture from ${source.repo}@${source.branch}: commit ${source.last_commit ?? "unknown"} (${at}).` : [
18090
- `Sync failed for ${project} (${source.repo}@${source.branch}): ${source.last_error}`,
18091
- source.last_commit === null ? "No model has ever imported for this project." : `The previous model stays up (commit ${source.last_commit}).`
18092
- ].join(`
18206
+ text: sources.map((source) => architectureSyncLines(project, source)).join(`
18093
18207
  `),
18094
18208
  details: {
18095
18209
  project,
18096
- repo: source.repo,
18097
- branch: source.branch,
18098
- commit: source.last_commit,
18099
- error: source.last_error
18210
+ sources: sources.map((source) => ({
18211
+ repo: source.repo,
18212
+ branch: source.branch,
18213
+ commit: source.last_commit,
18214
+ error: source.last_error
18215
+ }))
18100
18216
  }
18101
18217
  };
18102
18218
  }
package/dist/envoy.js CHANGED
@@ -30013,6 +30013,7 @@ var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passag
30013
30013
  var SEARCH_KIND_DEPTH = 100;
30014
30014
  var PROJECT_KEY_PATTERN = /^[A-Z][A-Z0-9]{1,9}$/;
30015
30015
  var PROJECT_KEY_RULE = "an uppercase letter, then 1 to 9 uppercase letters or digits, such as CORE";
30016
+ var ARCHITECTURE_SOURCE_REPO_PATTERN = /^[A-Za-z0-9-]+\/(?!\.\.?$)[A-Za-z0-9._-]+$/;
30016
30017
  var ISSUE_STATUSES = [
30017
30018
  "triage",
30018
30019
  "icebox",
@@ -30378,7 +30379,7 @@ var dispatchToolSpecs = [
30378
30379
  {
30379
30380
  name: "dispatch_issues",
30380
30381
  example: { project: "PROJ", limit: 250, offset: 250 },
30381
- 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, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), 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. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
30382
+ 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, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), 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. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never. " + "all takes the whole filtered list in one read instead, for a caller that must not miss or " + "repeat a row while other writers change the list, and especially for one whose own writes " + "move issues out of what it filtered on.",
30382
30383
  arguments: (z2) => ({
30383
30384
  project: z2.string().describe("Project key to list issues from."),
30384
30385
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -30388,7 +30389,8 @@ var dispatchToolSpecs = [
30388
30389
  updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
30389
30390
  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(),
30390
30391
  limit: z2.number({ int: true, min: 1, max: MAX_ISSUE_PAGE_LIMIT }).describe(`Maximum rows, 1-${MAX_ISSUE_PAGE_LIMIT}; default ${DEFAULT_ISSUE_PAGE_LIMIT}.`).optional(),
30391
- offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional()
30392
+ offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional(),
30393
+ all: z2.boolean().describe("Take every matching issue in one unpaged read, so no row is missed or repeated while " + "the list changes. Refused with limit or offset.").optional()
30392
30394
  })
30393
30395
  },
30394
30396
  {
@@ -30410,11 +30412,19 @@ var dispatchToolSpecs = [
30410
30412
  },
30411
30413
  {
30412
30414
  name: "dispatch_architecture_sync",
30413
- example: { project: "CORE" },
30414
- description: "Import a project's architecture model from its configured source repository now, instead of " + "waiting for the server's five-minute schedule. Returns the imported commit and component " + "count, or the recorded error when the model was rejected (the previous model stays up). " + "The source itself is configured by a human in Settings; 404 SOURCE_NOT_FOUND without one.",
30415
+ example: { project: "CORE", repo: "acme/infra" },
30416
+ description: "Import a project's architecture model from its source repositories now, instead of waiting " + "for the server's five-minute schedule. A project may have several sources, one per " + "repository, and its model is every source's components together. Without repo every " + "source imports and each answers on its own line; with repo only that source does. Each " + "line names the imported commit, or the recorded error when that source's model was " + "rejected (its previous components stay up, and every other source is untouched). Sources " + "are saved in Settings or with PUT /api/v1/projects/{key}/architecture-sources/{owner}/{repo}; " + "404 SOURCE_NOT_FOUND when the project has none, or none for that repository.",
30415
30417
  arguments: (z2) => ({
30416
- project: z2.string().describe("Project key whose architecture source to sync, such as CORE.")
30418
+ project: z2.string().describe("Project key whose architecture sources to sync, such as CORE."),
30419
+ repo: z2.string().describe("One source repository of the project to sync alone, owner/name; omit it to sync every source.").optional()
30417
30420
  }),
30421
+ validation: {
30422
+ check: (value) => {
30423
+ const { repo } = value;
30424
+ return typeof repo !== "string" || ARCHITECTURE_SOURCE_REPO_PATTERN.test(repo);
30425
+ },
30426
+ message: "repo is not a repository: give it as owner/name, such as acme/infra."
30427
+ },
30418
30428
  strict: true
30419
30429
  },
30420
30430
  {
@@ -32708,6 +32718,9 @@ function asErrorShape(value) {
32708
32718
  function isJson(response) {
32709
32719
  return response.headers.get("content-type")?.includes("application/json") ?? false;
32710
32720
  }
32721
+ function isNoRoute(error48) {
32722
+ return error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "NOT_FOUND" && error48.message.startsWith("no route for ");
32723
+ }
32711
32724
  var ISSUE_EVENTS_PAGE_MAX = 200;
32712
32725
  var DISPATCH_TOOL_DEADLINE_MS = 60000;
32713
32726
  function requestSignal(signal) {
@@ -32735,6 +32748,18 @@ function whyNotAPage(answer) {
32735
32748
  }
32736
32749
  return `${what}. Retrying will not help: the same request gets the same answer until that ` + "Dispatch is upgraded or fixed.";
32737
32750
  }
32751
+ function whyNotAnArray(answer) {
32752
+ if (typeof answer === "string")
32753
+ return `text that is not JSON, ${GATEWAY_PAGE}, so a retry may succeed`;
32754
+ if (answer === null)
32755
+ return "null";
32756
+ if (typeof answer !== "object")
32757
+ return `a ${typeof answer}`;
32758
+ if ("issues" in answer && Array.isArray(answer.issues)) {
32759
+ return "a page, so this Dispatch pages a request that carries no limit and no offset";
32760
+ }
32761
+ return "an object that is not the listing";
32762
+ }
32738
32763
  var GATEWAY_PAGE = "which looks like a proxy or gateway page rather than Dispatch's own answer";
32739
32764
  function gatewayAdvice(method, status) {
32740
32765
  if (!transientStatus(status)) {
@@ -32810,6 +32835,14 @@ class DispatchClient {
32810
32835
  return answer;
32811
32836
  throw new Error(`GET ${this.#url(path2, query)} asked for a page ({issues, total, limit, offset}) and got ` + refusal);
32812
32837
  }
32838
+ async listAllIssues(options) {
32839
+ const path2 = ["api", "v1", "issues"];
32840
+ const query = { ...options };
32841
+ const answer = await this.#json("GET", path2, undefined, query);
32842
+ if (Array.isArray(answer))
32843
+ return answer;
32844
+ throw new Error(`GET ${this.#url(path2, query)} asked for the unpaged listing and got ${whyNotAnArray(answer)}.`);
32845
+ }
32813
32846
  async listProjectArtifacts(project, unlinked = false) {
32814
32847
  return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
32815
32848
  }
@@ -32898,19 +32931,69 @@ class DispatchClient {
32898
32931
  async listProjects() {
32899
32932
  return this.#json("GET", ["api", "v1", "projects"]);
32900
32933
  }
32901
- async syncArchitectureSource(project) {
32902
- return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
32903
- }
32904
- async getArchitectureSource(project) {
32934
+ async listArchitectureSources(project) {
32905
32935
  try {
32906
- return await this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
32936
+ return await this.#json("GET", ["api", "v1", "projects", project, "architecture-sources"]);
32937
+ } catch (error48) {
32938
+ if (!isNoRoute(error48))
32939
+ throw error48;
32940
+ }
32941
+ try {
32942
+ const source = await this.#json("GET", [
32943
+ "api",
32944
+ "v1",
32945
+ "projects",
32946
+ project,
32947
+ "architecture-source"
32948
+ ]);
32949
+ return source === null ? [] : [source];
32907
32950
  } catch (error48) {
32908
32951
  if (error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "SOURCE_NOT_FOUND") {
32909
- return null;
32952
+ return [];
32910
32953
  }
32911
32954
  throw error48;
32912
32955
  }
32913
32956
  }
32957
+ async syncArchitectureSources(project) {
32958
+ try {
32959
+ return await this.#json("POST", [
32960
+ "api",
32961
+ "v1",
32962
+ "projects",
32963
+ project,
32964
+ "architecture-sources",
32965
+ "sync"
32966
+ ]);
32967
+ } catch (error48) {
32968
+ if (!isNoRoute(error48))
32969
+ throw error48;
32970
+ }
32971
+ const source = await this.#json("POST", [
32972
+ "api",
32973
+ "v1",
32974
+ "projects",
32975
+ project,
32976
+ "architecture-source",
32977
+ "sync"
32978
+ ]);
32979
+ return [source];
32980
+ }
32981
+ async syncArchitectureSource(project, repo2) {
32982
+ const [owner, name, ...rest] = repo2.split("/");
32983
+ if (!owner || !name || rest.length > 0) {
32984
+ throw new Error(`repository must be owner/name, got ${JSON.stringify(repo2)}`);
32985
+ }
32986
+ return this.#json("POST", [
32987
+ "api",
32988
+ "v1",
32989
+ "projects",
32990
+ project,
32991
+ "architecture-sources",
32992
+ owner,
32993
+ name,
32994
+ "sync"
32995
+ ]);
32996
+ }
32914
32997
  async resolveAsk(id, input) {
32915
32998
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
32916
32999
  }
@@ -34151,7 +34234,7 @@ var envoyToolSpecs = [
34151
34234
  },
34152
34235
  {
34153
34236
  name: "envoy_publish",
34154
- description: `Publish an Envoy message to a notifications. topic. A role or built-in topic is delivered as its guide says; any other notifications. topic you name (a team topic such as notifications.team.deploy-squad.all) is live only: it reaches every subscribed session that holds its own NATS connection, yours included, and is not retained. A session the listener delivers to over a serve port, such as an OpenCode agent, receives nothing on such a topic and is told nothing (dispatch://INFRA-122). ${DELIVERY_CONTRACT}`,
34237
+ description: `Publish an Envoy message to a notifications. topic. A role or built-in topic is delivered as its guide says; any other notifications. topic you name (a team topic such as notifications.team.deploy-squad.all) is live only: it reaches every session subscribed to it when you publish, and is not retained. A session that holds its own NATS connection receives it directly, yours included; a session the listener delivers to over a serve port, such as an OpenCode agent, receives it through its machine's listener when that listener subscribes to every topic (ENVOY_SUBSCRIBE_ALL_TOPICS=1; the shared hub's listener does not). ${DELIVERY_CONTRACT}`,
34155
34238
  arguments: (schema) => ({ topic: schema.string(), ...messageArguments(schema) }),
34156
34239
  operation: EnvoyToolOperation.publish
34157
34240
  },
@@ -34724,12 +34807,17 @@ var REPLAY_ARRAY_BYTES = 2;
34724
34807
  function jsonBytes(value) {
34725
34808
  return encoder.encode(JSON.stringify(value)).length;
34726
34809
  }
34727
- function readModel(message) {
34728
- if (typeof message.model !== "string" || message.model === "")
34810
+ var SEQ_PER_MS = 1000;
34811
+ function modelReference(provider, id) {
34812
+ if (typeof id !== "string" || id === "")
34729
34813
  return;
34730
- if (typeof message.provider !== "string" || message.provider === "")
34814
+ if (typeof provider !== "string" || provider === "")
34731
34815
  return;
34732
- return capAgentStreamText(`${message.provider}/${message.model}`, AGENT_STREAM_LIMITS.modelChars);
34816
+ return `${provider}/${id}`;
34817
+ }
34818
+ function readModel(message) {
34819
+ const model = modelReference(message.provider, message.model);
34820
+ return model === undefined ? undefined : capAgentStreamText(model, AGENT_STREAM_LIMITS.modelChars);
34733
34821
  }
34734
34822
  function readMessage(raw) {
34735
34823
  if (typeof raw !== "object" || raw === null)
@@ -34854,16 +34942,31 @@ class AgentStreamPublisher {
34854
34942
  #history = new Map;
34855
34943
  #settled = new Set;
34856
34944
  #publishedAt = new Map;
34857
- #seq = 0;
34945
+ #seq;
34858
34946
  #watchedUntil = 0;
34947
+ #listedCommands;
34859
34948
  #commands;
34860
34949
  #commandsSeq = 0;
34950
+ #settings;
34951
+ #settingsSeq = 0;
34952
+ #settingsJson;
34861
34953
  constructor(deps) {
34862
34954
  this.#deps = deps;
34955
+ this.#seq = Math.trunc(deps.now()) * SEQ_PER_MS;
34863
34956
  }
34864
34957
  noteViewer() {
34865
34958
  this.#watchedUntil = this.#deps.now() + AGENT_STREAM_WATCH_TTL_MS;
34866
34959
  }
34960
+ publishState(subject2) {
34961
+ if (!this.watched)
34962
+ return;
34963
+ if (this.#settings !== undefined) {
34964
+ this.#deps.publish(subject2, JSON.stringify(this.#settingsFrame(this.#settings)));
34965
+ }
34966
+ if (this.#commands !== undefined) {
34967
+ this.#deps.publish(subject2, JSON.stringify(this.#commandsFrame(this.#commands)));
34968
+ }
34969
+ }
34867
34970
  get watched() {
34868
34971
  return this.#deps.now() < this.#watchedUntil;
34869
34972
  }
@@ -34872,35 +34975,67 @@ class AgentStreamPublisher {
34872
34975
  this.#settled.clear();
34873
34976
  this.#publishedAt.clear();
34874
34977
  this.#watchedUntil = 0;
34978
+ this.#listedCommands = undefined;
34875
34979
  this.#commands = undefined;
34980
+ this.#settings = undefined;
34981
+ this.#settingsJson = undefined;
34876
34982
  }
34877
34983
  setCommands(subject2, commands) {
34878
- const limit = AGENT_STREAM_LIMITS.historyBytes - REPLAY_ARRAY_BYTES;
34879
- let bytes = jsonBytes(this.#commandsFrame([], this.#seq + 1));
34880
34984
  const seen = new Set;
34881
- const kept = [];
34985
+ const listed = [];
34882
34986
  for (const command of commands) {
34883
- if (kept.length >= AGENT_STREAM_LIMITS.commands)
34987
+ if (listed.length >= AGENT_STREAM_LIMITS.commands)
34884
34988
  break;
34885
34989
  const name = command.name;
34886
34990
  if (name.length > AGENT_STREAM_LIMITS.commandNameChars)
34887
34991
  continue;
34888
34992
  if (!isTypableCommandName(name) || seen.has(name))
34889
34993
  continue;
34890
- const listed = {
34994
+ seen.add(name);
34995
+ listed.push({
34891
34996
  name,
34892
34997
  source: command.source,
34893
34998
  ...command.description !== undefined && {
34894
34999
  description: capAgentStreamText(command.description, AGENT_STREAM_LIMITS.commandDescriptionChars)
34895
35000
  },
34896
35001
  ...command.terminalOnly === true && { terminalOnly: true }
34897
- };
34898
- const cost = jsonBytes(listed) + (kept.length === 0 ? 0 : 1);
35002
+ });
35003
+ }
35004
+ this.#listedCommands = listed;
35005
+ this.#fitCommands(subject2);
35006
+ }
35007
+ setSettings(subject2, settings) {
35008
+ const kept = settings.reported ? {
35009
+ ...settings.model !== undefined && { model: settings.model },
35010
+ models: [...settings.models],
35011
+ reported: true,
35012
+ thinking: settings.thinking === null ? null : { level: settings.thinking.level, levels: [...settings.thinking.levels] }
35013
+ } : { reported: false };
35014
+ const json2 = JSON.stringify(kept);
35015
+ if (json2 === this.#settingsJson)
35016
+ return;
35017
+ this.#settings = kept;
35018
+ this.#settingsJson = json2;
35019
+ this.#seq += 1;
35020
+ this.#settingsSeq = this.#seq;
35021
+ if (this.watched)
35022
+ this.#deps.publish(subject2, JSON.stringify(this.#settingsFrame(kept)));
35023
+ this.#fitCommands(subject2);
35024
+ }
35025
+ #fitCommands(subject2) {
35026
+ const listed = this.#listedCommands;
35027
+ if (listed === undefined)
35028
+ return;
35029
+ const settings = this.#settings === undefined ? 0 : jsonBytes(this.#settingsFrame(this.#settings)) + 1;
35030
+ const limit = AGENT_STREAM_LIMITS.historyBytes - REPLAY_ARRAY_BYTES - settings;
35031
+ let bytes = jsonBytes(this.#commandsFrame([], this.#seq + 1));
35032
+ const kept = [];
35033
+ for (const command of listed) {
35034
+ const cost = jsonBytes(command) + (kept.length === 0 ? 0 : 1);
34899
35035
  if (bytes + cost > limit)
34900
35036
  break;
34901
35037
  bytes += cost;
34902
- seen.add(name);
34903
- kept.push(listed);
35038
+ kept.push(command);
34904
35039
  }
34905
35040
  if (JSON.stringify(kept) === JSON.stringify(this.#commands))
34906
35041
  return;
@@ -34913,6 +35048,9 @@ class AgentStreamPublisher {
34913
35048
  #commandsFrame(commands, seq = this.#commandsSeq) {
34914
35049
  return { commands, kind: "commands", seq, v: AGENT_STREAM_PROTOCOL };
34915
35050
  }
35051
+ #settingsFrame(settings) {
35052
+ return { ...settings, kind: "settings", seq: this.#settingsSeq, v: AGENT_STREAM_PROTOCOL };
35053
+ }
34916
35054
  record(subject2, raw, streaming, dispatchMessageId) {
34917
35055
  const message = readMessage(raw);
34918
35056
  if (message === null)
@@ -34958,23 +35096,26 @@ class AgentStreamPublisher {
34958
35096
  if (!this.watched)
34959
35097
  return { frames: [], session_id: sessionID, v: AGENT_STREAM_PROTOCOL };
34960
35098
  const ordered = [...this.#history.values()].sort((left, right) => left.at === right.at ? left.seq - right.seq : left.at - right.at);
34961
- const commands = this.#commands === undefined ? undefined : this.#commandsFrame(this.#commands);
34962
- let used = REPLAY_ARRAY_BYTES + (commands === undefined ? 0 : jsonBytes(commands));
35099
+ const tail = [];
35100
+ if (this.#settings !== undefined)
35101
+ tail.push(this.#settingsFrame(this.#settings));
35102
+ if (this.#commands !== undefined)
35103
+ tail.push(this.#commandsFrame(this.#commands));
35104
+ let used = REPLAY_ARRAY_BYTES + tail.reduce((bytes, frame, index) => bytes + jsonBytes(frame) + (index === 0 ? 0 : 1), 0);
34963
35105
  const kept = [];
34964
35106
  for (let index = ordered.length - 1;index >= 0; index -= 1) {
34965
35107
  const entry = ordered[index];
34966
35108
  if (entry === undefined)
34967
35109
  continue;
34968
35110
  const frame = frameFor(entry);
34969
- const separated = kept.length > 0 || commands !== undefined;
35111
+ const separated = kept.length > 0 || tail.length > 0;
34970
35112
  const cost = jsonBytes(frame) + (separated ? 1 : 0);
34971
35113
  if (used + cost > AGENT_STREAM_LIMITS.historyBytes)
34972
35114
  break;
34973
35115
  used += cost;
34974
35116
  kept.unshift(frame);
34975
35117
  }
34976
- if (commands !== undefined)
34977
- kept.push(commands);
35118
+ kept.push(...tail);
34978
35119
  return { frames: kept, session_id: sessionID, v: AGENT_STREAM_PROTOCOL };
34979
35120
  }
34980
35121
  }
@@ -35334,6 +35475,25 @@ import("@oh-my-pi/pi-coding-agent/extensibility/extensions/send-user-input-handl
35334
35475
  }, () => {
35335
35476
  return;
35336
35477
  });
35478
+ function liveAgentSession(session) {
35479
+ return host2.AgentRegistry?.global().list().find((ref) => ref.session?.sessionManager.getSessionId() === session)?.session ?? undefined;
35480
+ }
35481
+ function liveSessionSettings(live) {
35482
+ if (live === undefined || !("model" in live) || typeof live.configuredThinkingLevel !== "function" || typeof live.getAvailableModels !== "function" || typeof live.getAvailableEffortSelectors !== "function") {
35483
+ return { reported: false };
35484
+ }
35485
+ const model = modelReference(live.model?.provider, live.model?.id);
35486
+ const levels = live.getAvailableEffortSelectors();
35487
+ return {
35488
+ ...model !== undefined && { model },
35489
+ models: live.getAvailableModels().flatMap((available) => {
35490
+ const listed = modelReference(available.provider, available.id);
35491
+ return listed === undefined ? [] : [listed];
35492
+ }),
35493
+ reported: true,
35494
+ thinking: levels.length === 0 ? null : { level: live.configuredThinkingLevel() ?? null, levels }
35495
+ };
35496
+ }
35337
35497
  var codec2 = import_nats2.StringCodec();
35338
35498
  var NATS_RETRY_INTERVAL_MS = 15000;
35339
35499
  var DELIVERY_PICTURES_TIMEOUT_MS = 20000;
@@ -35739,6 +35899,7 @@ function envoyExtension(pi) {
35739
35899
  typedPictures.withdraw(turn.messageId);
35740
35900
  return typedInputReply(turn.body, { error: messageFor(error48) });
35741
35901
  }).then(async (reply) => {
35902
+ refreshAgentStreamSettings(sessionID);
35742
35903
  if (reply !== undefined)
35743
35904
  await postDispatchReply(delivery, { body: reply }, replySessionID);
35744
35905
  }).catch((error48) => {
@@ -35960,7 +36121,7 @@ function envoyExtension(pi) {
35960
36121
  return;
35961
36122
  const builtins = hostBuiltinCommands?.() ?? [];
35962
36123
  const reserved = new Set(builtins.flatMap((builtin) => [builtin.name, ...builtin.aliases]));
35963
- const live = host2.AgentRegistry?.global().list().find((ref) => ref.session?.sessionManager.getSessionId() === session)?.session;
36124
+ const live = liveAgentSession(session);
35964
36125
  const commands = builtins.map((builtin) => ({
35965
36126
  description: builtin.description,
35966
36127
  name: builtin.name,
@@ -35986,6 +36147,18 @@ function envoyExtension(pi) {
35986
36147
  }
35987
36148
  agentStream.setCommands(agentStreamFramesSubject(session), commands);
35988
36149
  };
36150
+ const refreshAgentStreamSettings = (session) => {
36151
+ if (session === "")
36152
+ return;
36153
+ try {
36154
+ agentStream.setSettings(agentStreamFramesSubject(session), liveSessionSettings(liveAgentSession(session)));
36155
+ } catch (error48) {
36156
+ logger3.warn("envoy: could not refresh agent stream settings", {
36157
+ error: messageFor(error48),
36158
+ sessionID: session
36159
+ });
36160
+ }
36161
+ };
35989
36162
  const pumpAgentStreamControl = async (subscription, session) => {
35990
36163
  try {
35991
36164
  for await (const message of subscription) {
@@ -35996,6 +36169,7 @@ function envoyExtension(pi) {
35996
36169
  const request = parsed;
35997
36170
  if (request.type !== "replay" && request.type !== "watch")
35998
36171
  continue;
36172
+ refreshAgentStreamSettings(session);
35999
36173
  const attached = agentStreamViewerAttached;
36000
36174
  if (request.type === "replay" || !attached || Date.now() - agentStreamCommandsRefreshedOnWatchAt >= AGENT_STREAM_WATCH_INTERVAL_MS) {
36001
36175
  try {
@@ -36012,8 +36186,11 @@ function envoyExtension(pi) {
36012
36186
  }
36013
36187
  const reply = message.reply;
36014
36188
  const answer = request.type === "replay" && reply !== undefined ? JSON.stringify(agentStream.replay(session)) : undefined;
36189
+ const arming = !agentStream.watched;
36015
36190
  agentStream.noteViewer();
36016
36191
  agentStreamViewerAttached = true;
36192
+ if (arming)
36193
+ agentStream.publishState(agentStreamFramesSubject(session));
36017
36194
  if (answer === undefined || reply === undefined)
36018
36195
  continue;
36019
36196
  connection?.publish(reply, codec2.encode(answer));
@@ -36072,6 +36249,12 @@ function envoyExtension(pi) {
36072
36249
  pi.on("message_end", async (event) => {
36073
36250
  recordAgentStreamMessage(event.message, false);
36074
36251
  });
36252
+ pi.on("turn_start", async () => {
36253
+ refreshAgentStreamSettings(sessionID);
36254
+ });
36255
+ pi.on("retry_fallback_applied", async () => {
36256
+ refreshAgentStreamSettings(sessionID);
36257
+ });
36075
36258
  const endRole = () => {
36076
36259
  claimedRoleTopic = undefined;
36077
36260
  claimedRoleSessionID = undefined;
@@ -136,7 +136,7 @@ EOF
136
136
 
137
137
  - Attach every architectural issue to the components it changes. Attach the root before you decompose it: children inherit.
138
138
  - `--components-json '{"mode":"explicit","ids":["web"]}'`, `'{"mode":"none","reason":"hiring, not code"}'` (only for non-architectural work, with a concrete reason), or `'{"mode":"inherit"}'`.
139
- - Component ids: each non-external `components[].id` from `GET /api/v1/projects/{key}/architecture`, or the file names under `.dispatch/architecture/` in its source repository. Others are refused `COMPONENTS_INPUT`. A closed issue can be classified without reopening.
139
+ - Component ids: each non-external `components[].id` from `GET /api/v1/projects/{key}/architecture`, or the file names under `.dispatch/architecture/` in any of the project's source repositories. A project may import from several; ids are project-wide, so one issue can name components of different sources. Others are refused `COMPONENTS_INPUT`. A closed issue can be classified without reopening.
140
140
  - Change code and `.dispatch/architecture/<id>.md` in the same review. A stale model: `dispatch architecture-sync` ([issues.md](skill://dispatch/references/issues.md#syncing-a-projects-architecture-model)).
141
141
 
142
142
  ## Close what you opened
@@ -63,4 +63,4 @@ As a surface's owner, run `dispatch issues --project <KEY> --priority 0 --priori
63
63
 
64
64
  ## Syncing a project's architecture model
65
65
 
66
- `dispatch architecture-sync --project CORE` imports the model now. It prints the commit, or the error, and keeps the old model. No source configured answers `SOURCE_NOT_FOUND`.
66
+ A project imports its model from one or more source repositories, each on its own branch. `dispatch architecture-sync --project CORE` imports every source now and prints a line for each: `Synced CORE architecture from <repo>@<branch>: commit <sha> (<time>).`, or `Sync failed for CORE (<repo>@<branch>): <error>` followed by what of that source still shows: its commit, its component count when the source was re-saved on another branch and has not imported from it yet, or that none ever imported. A failed source keeps its previous components; every other source is untouched. `--repo <owner/name>` imports that one source alone. No source configured, or no source from that repository, answers `SOURCE_NOT_FOUND`. A credential or branch problem is that source's failed line, or with `--repo` an error, `SOURCE_ACCESS`.
@@ -66,6 +66,6 @@ Every topic starts with `notifications.`.
66
66
 
67
67
  - Send to one session with `envoy_send`, to the one session that holds a named duty with a role, and to a group with a team topic.
68
68
  - Publish to a role with `envoy_publish(topic="notifications.role.<role>", ...)`; never subscribe to a role topic, because the listener forwards a role message to its holder only. A publish returns the live `holder`; an unheld role returns an error. `envoy_role_get(role=...)` finds the holder first.
69
- - For a team, the group subscribes to `notifications.team.<name>.>`, and any member publishes to `notifications.team.<name>.all`. Envoy treats any other `notifications.` topic that no source in the table uses the same way. It reaches only the sessions subscribed when it is published (yours too, when you subscribe), and is not kept for a session that subscribes later. A topic outside `notifications.` is refused.
69
+ - For a team, the group subscribes to `notifications.team.<name>.>`, and any member publishes to `notifications.team.<name>.all`. Envoy treats any other `notifications.` topic that no source in the table uses the same way. It reaches only the sessions subscribed when it is published (yours too, when you subscribe, unless you are a serve-port session, which receives it only through a listener run with `ENVOY_SUBSCRIBE_ALL_TOPICS=1` and never its own publish), and is not kept for a session that subscribes later. A topic outside `notifications.` is refused.
70
70
  - How a role claim survives a resume, a fork or a dead process: `skill://envoy/references/roles.md`.
71
71
  - In a Legion pane, the plugin claims your role at session start; read `skill://envoy/references/legion.md` before you claim, release or publish to a role, or answer a role delivery failure there.
@@ -54,7 +54,7 @@ envoy:
54
54
  It depends on where a session takes its deliveries, not on whether the topic is built in or your own.
55
55
 
56
56
  - A session that holds its own NATS connection (every Oh My Pi session, and a Claude Code one) receives every message published on a subject it subscribes to, whoever sent it: its own publish included, and a subagent's publish to a topic the parent subscribes to. This holds for a topic the stream keeps and for a live team topic alike.
57
- - A session the listener delivers to by push, over a serve port (such as the OpenCode plugin), does not receive its own publish, and receives no team topic at all, since it holds no NATS subscription of its own.
57
+ - A session the listener delivers to by push, over a serve port (such as the OpenCode plugin), does not receive its own publish, and receives a team topic only through the listener that owns it when that listener runs with `ENVOY_SUBSCRIBE_ALL_TOPICS=1` (the shared hub's listener does not).
58
58
  - A role is not a subject its holder subscribes to: the listener finds the live holder and forwards the message to it, and drops the message when that holder is the message's source session.
59
59
 
60
60
  ## Listener API errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-envoy",
3
- "version": "10.25.1",
3
+ "version": "10.26.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {