@sjawhar/pi-legion-envoy 7.5.2 → 7.6.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
@@ -30073,6 +30073,7 @@ var ASK_URGENCIES = ["low", "med", "high", "blocking"];
30073
30073
  var ASK_QUESTION_MAX = 800;
30074
30074
  var SEARCH_QUERY_MAX = 1000;
30075
30075
  var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passage";
30076
+ var SEARCH_KIND_DEPTH = 100;
30076
30077
  var PROJECT_KEY_PATTERN = /^[A-Z][A-Z0-9]{1,9}$/;
30077
30078
  var ISSUE_STATUSES = [
30078
30079
  "triage",
@@ -30408,7 +30409,7 @@ var dispatchToolSpecs = [
30408
30409
  {
30409
30410
  name: "dispatch_search",
30410
30411
  example: { query: "astrolabe" },
30411
- description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR.',
30412
+ description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR. ' + "Each kind of content is ranked on its own and the lists are merged, so the top holds the best " + "issue, document, ask, comment and message; an issue key searched alone lists that issue first. " + `The answer names how many results match; offset pages through them, and each kind lists at most its best ${SEARCH_KIND_DEPTH}, ` + "so narrow the query or name a project to reach the rest. Paging is exact only while the " + "corpus holds still: content added, changed, or removed between two offsets can shift rows " + "across a page boundary, so one hit can come back twice and another never.",
30412
30413
  arguments: (z2) => ({
30413
30414
  query: z2.string({
30414
30415
  min: 2,
@@ -30416,7 +30417,8 @@ var dispatchToolSpecs = [
30416
30417
  maxHint: SEARCH_QUERY_HINT
30417
30418
  }).describe(`Keyword, phrase, or websearch expression; 2 to ${SEARCH_QUERY_MAX} characters.`),
30418
30419
  project: z2.string().describe("Optional project key to search within.").optional(),
30419
- limit: z2.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
30420
+ limit: z2.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional(),
30421
+ offset: z2.number({ int: true, min: 0 }).describe("Results to skip before the page; nonnegative integer; default 0.").optional()
30420
30422
  }),
30421
30423
  validation: {
30422
30424
  check: (value) => {
@@ -32513,6 +32515,71 @@ class DispatchClient {
32513
32515
  }
32514
32516
  }
32515
32517
 
32518
+ // ../envoy-client/src/search-answer.ts
32519
+ function pageSummaryText(offset, count, total) {
32520
+ return `showing ${offset + 1}-${offset + count} of ${total}`;
32521
+ }
32522
+ function searchResultLine(result, baseUrl) {
32523
+ const href = new URL(result.href, baseUrl).toString();
32524
+ const { owner } = result;
32525
+ if (owner.kind === "document") {
32526
+ const reference = dispatchDocumentRef(owner.project, owner.slug);
32527
+ return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
32528
+ }
32529
+ const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
32530
+ const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
32531
+ return `${label}: ${snippetText(result.snippet)} -> ${href}`;
32532
+ }
32533
+ function searchAnswer(search, query, offset, configUrl) {
32534
+ const results = search.results;
32535
+ const count = results.length;
32536
+ const lines = results.map((result) => searchResultLine(result, configUrl));
32537
+ const noun = count === 1 ? "result" : "results";
32538
+ const noResults = `No results for "${query}".`;
32539
+ if (typeof search.total !== "number") {
32540
+ if (offset !== undefined && offset > 0) {
32541
+ throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
32542
+ }
32543
+ return {
32544
+ text: count === 0 ? noResults : [`${count} ${noun} for "${query}" (${search.took_ms} ms)`, ...lines].join(`
32545
+ `),
32546
+ details: { query, results }
32547
+ };
32548
+ }
32549
+ const { total, reachable, offset: pageOffset } = search;
32550
+ const end = pageOffset + count;
32551
+ const cut = reachable < total ? `Each kind lists only its best ${SEARCH_KIND_DEPTH} matches, so ${reachable} of the ${total} can be paged to; narrow the query or name a project to reach the rest.` : undefined;
32552
+ const details = {
32553
+ query,
32554
+ results,
32555
+ total,
32556
+ reachable,
32557
+ offset: pageOffset,
32558
+ limit: search.limit
32559
+ };
32560
+ if (count === 0) {
32561
+ return {
32562
+ text: total === 0 ? noResults : [
32563
+ `No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
32564
+ ...cut === undefined ? [] : [cut]
32565
+ ].join(`
32566
+ `),
32567
+ details
32568
+ };
32569
+ }
32570
+ const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
32571
+ return {
32572
+ text: [
32573
+ `${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
32574
+ ...cut === undefined ? [] : [cut],
32575
+ ...lines,
32576
+ ...end < reachable ? [`Next page: offset ${end}.`] : []
32577
+ ].join(`
32578
+ `),
32579
+ details
32580
+ };
32581
+ }
32582
+
32516
32583
  // ../envoy-client/src/tool-input-errors.ts
32517
32584
  class ToolInputError extends Error {
32518
32585
  tool;
@@ -32822,17 +32889,6 @@ function duplicateCandidates(error48) {
32822
32889
  throw error48;
32823
32890
  return error48.candidates;
32824
32891
  }
32825
- function searchResultLine(result, baseUrl) {
32826
- const href = new URL(result.href, baseUrl).toString();
32827
- const { owner } = result;
32828
- if (owner.kind === "document") {
32829
- const reference = dispatchDocumentRef(owner.project, owner.slug);
32830
- return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
32831
- }
32832
- const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
32833
- const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
32834
- return `${label}: ${snippetText(result.snippet)} -> ${href}`;
32835
- }
32836
32892
  function askUrgency(args) {
32837
32893
  const value = args.urgency;
32838
32894
  return ASK_URGENCIES.find((urgency) => urgency === value);
@@ -34031,20 +34087,13 @@ async function executeDispatchTool(input) {
34031
34087
  const query = stringArg(args, "query");
34032
34088
  const project = optionalString(args, "project");
34033
34089
  const limit = optionalNumber(args, "limit");
34090
+ const offset = optionalNumber(args, "offset");
34034
34091
  const search = await client.search(query, {
34035
34092
  ...project === undefined ? {} : { project },
34036
- ...limit === undefined ? {} : { limit }
34093
+ ...limit === undefined ? {} : { limit },
34094
+ ...offset === undefined ? {} : { offset }
34037
34095
  });
34038
- const results = search.results;
34039
- const count = results.length;
34040
- return {
34041
- text: count === 0 ? `No results for "${query}".` : [
34042
- `${count} ${count === 1 ? "result" : "results"} for "${query}" (${search.took_ms} ms)`,
34043
- ...results.map((result) => searchResultLine(result, configUrl))
34044
- ].join(`
34045
- `),
34046
- details: { query, results }
34047
- };
34096
+ return searchAnswer(search, query, offset, configUrl);
34048
34097
  }
34049
34098
  case "dispatch_issues": {
34050
34099
  const project = stringArg(args, "project");
@@ -34084,7 +34133,7 @@ async function executeDispatchTool(input) {
34084
34133
  }));
34085
34134
  const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
34086
34135
  const isPartial = offset !== 0 || rows.length !== total;
34087
- const showing = !isPartial ? "" : rows.length === 0 ? `showing 0-0 of ${total}` : `showing ${offset + 1}-${offset + rows.length} of ${total}`;
34136
+ const showing = !isPartial ? "" : rows.length === 0 ? `showing 0-0 of ${total}` : pageSummaryText(offset, rows.length, total);
34088
34137
  return {
34089
34138
  text: rows.length === 0 ? `No issues in ${project}.${isPartial ? ` (${showing})` : ""}` : [
34090
34139
  `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (isPartial ? ` (${showing})` : ""),
@@ -35588,10 +35637,10 @@ function btwPrompt(question) {
35588
35637
  ].join(`
35589
35638
  `);
35590
35639
  }
35591
- function sideTurn(pi, context) {
35640
+ function sideTurn(context) {
35592
35641
  const runEphemeralTurn = context?.runEphemeralTurn;
35593
35642
  if (runEphemeralTurn === undefined)
35594
- return pi.askEphemeral;
35643
+ return;
35595
35644
  return ({ prompt, signal }) => runEphemeralTurn({ promptText: btwPrompt(prompt), signal });
35596
35645
  }
35597
35646
 
@@ -36002,7 +36051,7 @@ function envoyExtension(pi) {
36002
36051
  } else if (rendered.malformedDelivery === true) {
36003
36052
  console.warn("[envoy] dropping malformed Dispatch targeted delivery without a reply address");
36004
36053
  } else if (rendered.delivery?.mode === "btw") {
36005
- const answer = sideTurn(pi, activeSessionContext);
36054
+ const answer = sideTurn(activeSessionContext);
36006
36055
  if (shuttingDown) {
36007
36056
  await refuse(rendered.delivery, "This OMP session is shutting down");
36008
36057
  } else if (answer === undefined) {
@@ -36199,7 +36248,7 @@ function envoyExtension(pi) {
36199
36248
  ],
36200
36249
  port: 0,
36201
36250
  title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
36202
- capabilities: sideTurn(pi, activeSessionContext) === undefined ? CAPABILITIES_WITHOUT_BTW : DELIVERY_CAPABILITIES,
36251
+ capabilities: sideTurn(activeSessionContext) === undefined ? CAPABILITIES_WITHOUT_BTW : DELIVERY_CAPABILITIES,
36203
36252
  driving: false,
36204
36253
  selfSubscribed: true
36205
36254
  });
@@ -36596,7 +36645,7 @@ function envoyExtension(pi) {
36596
36645
  registerEnvoyWhoamiCommand(pi, replyAddress);
36597
36646
  pi.on("before_agent_start", async (event, context) => {
36598
36647
  const id = context.sessionManager.getSessionId();
36599
- if (id === "" || event.prompt.trim() === "" || !context.hasUI || sideTurn(pi, context) === undefined || legionManaged(id)) {
36648
+ if (id === "" || event.prompt.trim() === "" || !context.hasUI || sideTurn(context) === undefined || legionManaged(id)) {
36600
36649
  return;
36601
36650
  }
36602
36651
  if (await isSubagent(context))
@@ -36628,7 +36677,7 @@ function envoyExtension(pi) {
36628
36677
  const id = context.sessionManager.getSessionId();
36629
36678
  endInjectedUserTurns(sessionID);
36630
36679
  const lastReply = event.messages?.findLast((message) => message.role === "assistant");
36631
- const ask = sideTurn(pi, context);
36680
+ const ask = sideTurn(context);
36632
36681
  if (event.willContinue === true || lastReply?.stopReason !== "stop" || !context.hasUI || id === "" || ask === undefined || !checkOwed(id)) {
36633
36682
  return;
36634
36683
  }
@@ -36676,20 +36725,14 @@ function envoyExtension(pi) {
36676
36725
  return;
36677
36726
  const abort = new AbortController;
36678
36727
  askCheckAbort = abort;
36679
- let answered;
36680
- try {
36681
- answered = ask({
36682
- prompt: ASK_SELF_CHECK_PROMPT(open.snapshot.asks),
36683
- signal: abort.signal
36684
- }).then((reply) => reply.replyText, (error48) => {
36685
- if (!abort.signal.aborted)
36686
- logSelfCheckFailure(id, error48);
36687
- return;
36688
- });
36689
- } catch (error48) {
36690
- logSelfCheckFailure(id, error48);
36691
- answered = Promise.resolve(undefined);
36692
- }
36728
+ const answered = ask({
36729
+ prompt: ASK_SELF_CHECK_PROMPT(open.snapshot.asks),
36730
+ signal: abort.signal
36731
+ }).then((reply) => reply.replyText, (error48) => {
36732
+ if (!abort.signal.aborted)
36733
+ logSelfCheckFailure(id, error48);
36734
+ return;
36735
+ });
36693
36736
  const expiry = Promise.withResolvers();
36694
36737
  const expire = setTimeout(() => {
36695
36738
  const timedOut = new Error(`self-check timed out after ${selfCheckTimeoutMs} ms`);
@@ -36816,7 +36859,7 @@ Warnings: ${warnings.join("; ")}`, {
36816
36859
  const undelivered = source !== "" && result.holder === source && inSubagent;
36817
36860
  const published = result.holder === undefined ? `published ${result.envelope.event_id}` : `published ${result.envelope.event_id}; holder ${result.holder}`;
36818
36861
  return toolSuccess(undelivered ? `${published}
36819
- Not delivered: that holder is the session this subagent sends as, and the listener drops a message whose source session is its recipient, so the agent that spawned you did not receive it. Reach it over hub instead.` : published, {
36862
+ Not delivered: that holder is the session this subagent sends as, and the listener drops a message whose source session is its recipient, so the agent that spawned you did not receive it. Reach it with a write to its agent:// address instead.` : published, {
36820
36863
  event_id: result.envelope.event_id,
36821
36864
  topic,
36822
36865
  ...result.holder === undefined ? {} : { holder: result.holder },
@@ -36837,7 +36880,7 @@ Not delivered: that holder is the session this subagent sends as, and the listen
36837
36880
  const address = await replyAddress(context, "", inSubagent);
36838
36881
  const subagent = inSubagent ? {
36839
36882
  session_id: context.sessionManager.getSessionId(),
36840
- note: address === "" ? "This session is a task subagent and registers no Envoy session of its own, and this process records no top-level session its transcript traces back to, so it has no reply address at all: say who you are in the message body, and reach the agent that spawned you over hub." : "This session is a task subagent and registers no Envoy session of its own; a reply to session_id reaches the agent that spawned it, which relays over hub. Your own envoy_publish never reaches that agent \u2014 use hub for that hop."
36883
+ note: address === "" ? "This session is a task subagent and registers no Envoy session of its own, and this process records no top-level session its transcript traces back to, so it has no reply address at all: say who you are in the message body, and reach the agent that spawned you with a write to its agent:// address." : "This session is a task subagent and registers no Envoy session of its own; a reply to session_id reaches the agent that spawned it, which relays it with a write to your agent:// address. Your own envoy_publish never reaches that agent \u2014 use a write to its agent:// address for that hop."
36841
36884
  } : undefined;
36842
36885
  return toolSuccess(JSON.stringify({
36843
36886
  session_id: address,
package/dist/legion.js CHANGED
@@ -14160,6 +14160,7 @@ var ASK_URGENCIES = ["low", "med", "high", "blocking"];
14160
14160
  var ASK_QUESTION_MAX = 800;
14161
14161
  var SEARCH_QUERY_MAX = 1000;
14162
14162
  var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passage";
14163
+ var SEARCH_KIND_DEPTH = 100;
14163
14164
  var PROJECT_KEY_PATTERN = /^[A-Z][A-Z0-9]{1,9}$/;
14164
14165
  var ISSUE_STATUSES = [
14165
14166
  "triage",
@@ -14495,7 +14496,7 @@ var dispatchToolSpecs = [
14495
14496
  {
14496
14497
  name: "dispatch_search",
14497
14498
  example: { query: "astrolabe" },
14498
- description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR.',
14499
+ description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR. ' + "Each kind of content is ranked on its own and the lists are merged, so the top holds the best " + "issue, document, ask, comment and message; an issue key searched alone lists that issue first. " + `The answer names how many results match; offset pages through them, and each kind lists at most its best ${SEARCH_KIND_DEPTH}, ` + "so narrow the query or name a project to reach the rest. Paging is exact only while the " + "corpus holds still: content added, changed, or removed between two offsets can shift rows " + "across a page boundary, so one hit can come back twice and another never.",
14499
14500
  arguments: (z2) => ({
14500
14501
  query: z2.string({
14501
14502
  min: 2,
@@ -14503,7 +14504,8 @@ var dispatchToolSpecs = [
14503
14504
  maxHint: SEARCH_QUERY_HINT
14504
14505
  }).describe(`Keyword, phrase, or websearch expression; 2 to ${SEARCH_QUERY_MAX} characters.`),
14505
14506
  project: z2.string().describe("Optional project key to search within.").optional(),
14506
- limit: z2.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
14507
+ limit: z2.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional(),
14508
+ offset: z2.number({ int: true, min: 0 }).describe("Results to skip before the page; nonnegative integer; default 0.").optional()
14507
14509
  }),
14508
14510
  validation: {
14509
14511
  check: (value) => {
@@ -15298,6 +15300,71 @@ function dispatchChildRef(ownerRef, kind, id) {
15298
15300
  return `${ownerRef}/${kind}/${id}`;
15299
15301
  }
15300
15302
 
15303
+ // ../envoy-client/src/search-answer.ts
15304
+ function pageSummaryText(offset, count, total) {
15305
+ return `showing ${offset + 1}-${offset + count} of ${total}`;
15306
+ }
15307
+ function searchResultLine(result, baseUrl) {
15308
+ const href = new URL(result.href, baseUrl).toString();
15309
+ const { owner } = result;
15310
+ if (owner.kind === "document") {
15311
+ const reference = dispatchDocumentRef(owner.project, owner.slug);
15312
+ return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
15313
+ }
15314
+ const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
15315
+ const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
15316
+ return `${label}: ${snippetText(result.snippet)} -> ${href}`;
15317
+ }
15318
+ function searchAnswer(search, query, offset, configUrl) {
15319
+ const results = search.results;
15320
+ const count = results.length;
15321
+ const lines = results.map((result) => searchResultLine(result, configUrl));
15322
+ const noun = count === 1 ? "result" : "results";
15323
+ const noResults = `No results for "${query}".`;
15324
+ if (typeof search.total !== "number") {
15325
+ if (offset !== undefined && offset > 0) {
15326
+ throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
15327
+ }
15328
+ return {
15329
+ text: count === 0 ? noResults : [`${count} ${noun} for "${query}" (${search.took_ms} ms)`, ...lines].join(`
15330
+ `),
15331
+ details: { query, results }
15332
+ };
15333
+ }
15334
+ const { total, reachable, offset: pageOffset } = search;
15335
+ const end = pageOffset + count;
15336
+ const cut = reachable < total ? `Each kind lists only its best ${SEARCH_KIND_DEPTH} matches, so ${reachable} of the ${total} can be paged to; narrow the query or name a project to reach the rest.` : undefined;
15337
+ const details = {
15338
+ query,
15339
+ results,
15340
+ total,
15341
+ reachable,
15342
+ offset: pageOffset,
15343
+ limit: search.limit
15344
+ };
15345
+ if (count === 0) {
15346
+ return {
15347
+ text: total === 0 ? noResults : [
15348
+ `No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
15349
+ ...cut === undefined ? [] : [cut]
15350
+ ].join(`
15351
+ `),
15352
+ details
15353
+ };
15354
+ }
15355
+ const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
15356
+ return {
15357
+ text: [
15358
+ `${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
15359
+ ...cut === undefined ? [] : [cut],
15360
+ ...lines,
15361
+ ...end < reachable ? [`Next page: offset ${end}.`] : []
15362
+ ].join(`
15363
+ `),
15364
+ details
15365
+ };
15366
+ }
15367
+
15301
15368
  // ../envoy-client/src/tool-input-errors.ts
15302
15369
  class ToolInputError extends Error {
15303
15370
  tool;
@@ -15607,17 +15674,6 @@ function duplicateCandidates(error48) {
15607
15674
  throw error48;
15608
15675
  return error48.candidates;
15609
15676
  }
15610
- function searchResultLine(result, baseUrl) {
15611
- const href = new URL(result.href, baseUrl).toString();
15612
- const { owner } = result;
15613
- if (owner.kind === "document") {
15614
- const reference = dispatchDocumentRef(owner.project, owner.slug);
15615
- return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
15616
- }
15617
- const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
15618
- const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
15619
- return `${label}: ${snippetText(result.snippet)} -> ${href}`;
15620
- }
15621
15677
  function askUrgency(args) {
15622
15678
  const value = args.urgency;
15623
15679
  return ASK_URGENCIES.find((urgency) => urgency === value);
@@ -16816,20 +16872,13 @@ async function executeDispatchTool(input) {
16816
16872
  const query = stringArg(args, "query");
16817
16873
  const project = optionalString(args, "project");
16818
16874
  const limit = optionalNumber(args, "limit");
16875
+ const offset = optionalNumber(args, "offset");
16819
16876
  const search = await client.search(query, {
16820
16877
  ...project === undefined ? {} : { project },
16821
- ...limit === undefined ? {} : { limit }
16878
+ ...limit === undefined ? {} : { limit },
16879
+ ...offset === undefined ? {} : { offset }
16822
16880
  });
16823
- const results = search.results;
16824
- const count = results.length;
16825
- return {
16826
- text: count === 0 ? `No results for "${query}".` : [
16827
- `${count} ${count === 1 ? "result" : "results"} for "${query}" (${search.took_ms} ms)`,
16828
- ...results.map((result) => searchResultLine(result, configUrl))
16829
- ].join(`
16830
- `),
16831
- details: { query, results }
16832
- };
16881
+ return searchAnswer(search, query, offset, configUrl);
16833
16882
  }
16834
16883
  case "dispatch_issues": {
16835
16884
  const project = stringArg(args, "project");
@@ -16869,7 +16918,7 @@ async function executeDispatchTool(input) {
16869
16918
  }));
16870
16919
  const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
16871
16920
  const isPartial = offset !== 0 || rows.length !== total;
16872
- const showing = !isPartial ? "" : rows.length === 0 ? `showing 0-0 of ${total}` : `showing ${offset + 1}-${offset + rows.length} of ${total}`;
16921
+ const showing = !isPartial ? "" : rows.length === 0 ? `showing 0-0 of ${total}` : pageSummaryText(offset, rows.length, total);
16873
16922
  return {
16874
16923
  text: rows.length === 0 ? `No issues in ${project}.${isPartial ? ` (${showing})` : ""}` : [
16875
16924
  `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (isPartial ? ` (${showing})` : ""),
@@ -17475,7 +17524,7 @@ function endInjectedUserTurns(sessionID) {
17475
17524
  // package.json
17476
17525
  var package_default = {
17477
17526
  name: "@sjawhar/pi-legion-envoy",
17478
- version: "7.5.2",
17527
+ version: "7.6.1",
17479
17528
  license: "Apache-2.0",
17480
17529
  type: "module",
17481
17530
  omp: {
@@ -18887,6 +18936,29 @@ function refusedCommand(command, commands, rule) {
18887
18936
  }
18888
18937
  return;
18889
18938
  }
18939
+ var NON_FILE_WRITE_URL = /^(xd|agent|proc):\/\//iu;
18940
+ var READ_HEADER_TAG = /#[0-9A-Fa-f]{4}$/u;
18941
+ var PREFIXED_CONFLICT_URL = /^(.+):(conflict:\/\/.+)$/u;
18942
+ function unwrapReadHeader(path6) {
18943
+ const trimmed = path6.trimEnd();
18944
+ if (trimmed.length < 2 || !trimmed.startsWith("[") || !trimmed.endsWith("]"))
18945
+ return path6;
18946
+ const inner = trimmed.slice(1, -1);
18947
+ const tag = READ_HEADER_TAG.exec(inner);
18948
+ const target = tag === null ? inner : inner.slice(0, tag.index);
18949
+ if (target.length === 0 || tag === null && target.includes("#"))
18950
+ return path6;
18951
+ return target;
18952
+ }
18953
+ function writeTarget(path6) {
18954
+ const unwrapped = unwrapReadHeader(path6);
18955
+ return PREFIXED_CONFLICT_URL.exec(unwrapped)?.[2] ?? unwrapped;
18956
+ }
18957
+ function nonFileWriteScheme(toolCall) {
18958
+ if (toolCall.toolName !== "write" || typeof toolCall.input.path !== "string")
18959
+ return;
18960
+ return NON_FILE_WRITE_URL.exec(writeTarget(toolCall.input.path))?.[1]?.toLowerCase();
18961
+ }
18890
18962
  function paneRuleRefusal(toolCall, rules) {
18891
18963
  if (rules.length === 0)
18892
18964
  return;
@@ -18905,8 +18977,8 @@ function paneRuleRefusal(toolCall, rules) {
18905
18977
  let text;
18906
18978
  if (toolName === "eval" && typeof input.code === "string")
18907
18979
  text = input.code;
18908
- else if (toolName === "hub") {
18909
- text = [input.application, ...Array.isArray(input.args) ? input.args : [], input.text].filter((part) => typeof part === "string").join(" ");
18980
+ else if (nonFileWriteScheme(toolCall) === "proc" && typeof input.content === "string") {
18981
+ text = input.content;
18910
18982
  } else
18911
18983
  return;
18912
18984
  for (const rule of rules) {
@@ -18923,10 +18995,6 @@ var CODE_TOOL_REFUSAL = {
18923
18995
  reviewer: "the reviewer edits nothing except the final .legion/ cleanup commit via bash",
18924
18996
  merger: "the merger only verifies and reports"
18925
18997
  };
18926
- var TOOL_DEVICE_URL = /^xd:\/\//iu;
18927
- function isToolDeviceInvocation(toolCall) {
18928
- return toolCall.toolName === "write" && typeof toolCall.input.path === "string" && TOOL_DEVICE_URL.test(toolCall.input.path);
18929
- }
18930
18998
  function legionExtension(pi) {
18931
18999
  const instance = randomUUID2().slice(0, 8);
18932
19000
  logger2.debug("extension instance loaded", { extension: import.meta.url, instance });
@@ -19007,7 +19075,7 @@ function legionExtension(pi) {
19007
19075
  const active = claimSession.capability(sessionID);
19008
19076
  const codeToolRefusal = active === undefined ? undefined : CODE_TOOL_REFUSAL[active.role];
19009
19077
  const mutatesCode = CODE_MUTATION_TOOLS.includes(toolCall.toolName) || active?.role === "architect" && toolCall.toolName === "bash" && !isSingleLegionCommand(toolCall.input.command);
19010
- if (codeToolRefusal !== undefined && mutatesCode && !isToolDeviceInvocation(toolCall)) {
19078
+ if (codeToolRefusal !== undefined && mutatesCode && nonFileWriteScheme(toolCall) === undefined) {
19011
19079
  return { block: true, reason: codeToolRefusal };
19012
19080
  }
19013
19081
  if (!needsGrant(toolCall))
@@ -160,15 +160,14 @@ follow [Writing a spec](#writing-a-spec).
160
160
  you plan, start a design document, file an issue, ask, post a finding or start work, and what to
161
161
  do with each hit. What it leaves out:
162
162
  ```ts
163
- dispatch_search({ query, project?, limit? })
163
+ dispatch_search({ query, project?, limit?, offset? })
164
164
  ```
165
- Websearch syntax applies: `"merge queue"`, `-daemon`, `OR`. It returns the best `limit` hits (20
166
- by default, 50 at most) across issues, documents, comments, asks, and messages. A query over
167
- 1,000 characters is refused before it is sent: search with the few words `skill://dispatch-first`
168
- describes, never a pasted passage. Issue-owned hit lines start with the issue key; standalone
169
- project-document hit lines start with `dispatch://PROJECT/artifact/<slug>`, followed by the
170
- absolute link. Cite the hit you build on (`dispatch://KEY` or the document reference), or state
171
- "no prior issue" in the spec.
165
+ Websearch syntax applies: `"merge queue"`, `-daemon`, `OR`. Issues, documents, asks, comments, and messages rank separately; the
166
+ merged page orders each kind's best in turn — issue, document, ask, comment, message — and a bare issue key ranks first. A page
167
+ holds `limit` hits (20 default, 50 max); the first line gives the total (`showing 1-20 of 312`); see
168
+ [Search paging](skill://dispatch/references/issues.md#search-paging) for more. Queries over 1,000 characters are refused: use the
169
+ few words `skill://dispatch-first` names, never a pasted passage. Issue hits start with the issue key; document hits start with
170
+ `dispatch://PROJECT/artifact/<slug>`, then the link. Cite the hit (`dispatch://KEY` or the doc ref) or say "no prior issue".
172
171
 
173
172
  `dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns the candidates (`POSSIBLE_DUPLICATE`).
174
173
  Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
@@ -193,6 +193,16 @@ The audit finds four shapes:
193
193
  Run the audit as a step of a coordinator's loop, at each checkpoint, not as a habit: these shapes
194
194
  are found by running the check, not by noticing them.
195
195
 
196
+ ## Search paging
197
+
198
+ `skill://dispatch` sends you here when a `dispatch_search` page is not enough: more hits remain,
199
+ or a kind runs out before the page does.
200
+
201
+ A page holds `limit` hits (20 by default, 50 at most); the first line names how many match
202
+ (`showing 1-20 of 312`), and, while more can be reached, the last line names the next `offset`.
203
+ Each kind lists at most its best 100, so when the result says the rest cannot be paged to, narrow
204
+ the query or name a `project`.
205
+
196
206
  ## Syncing a project's architecture model
197
207
 
198
208
  Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
@@ -25,11 +25,11 @@ reply. A human may reply to your message in turn — the follow-up arrives as a
25
25
  `in_reply_to` names your message and whose `reply_body` quotes it; answer it the same way,
26
26
  `dispatch_message({ issue, in_reply_to: "<their reply id>", body })`, so the exchange reads as one
27
27
  thread. `dispatch_message` itself never carries `target` or `delivery`: agent-to-agent traffic goes
28
- through Envoy or the hub. A bearer that targets over HTTP names its own session in `actor`
29
- (`{kind: "session", id}`), and the card shows that session as the author. `GET /api/v1/agents`
30
- (any authenticated caller) lists live sessions with their capabilities (`aside`, `btw`, `steer`);
31
- target only a session that advertises the mode you want. Sending to a session with no issue
32
- (`POST /api/v1/agents/{session_id}/messages`) stays human-only.
28
+ through Envoy or Oh My Pi's `agent://` messages. A bearer that targets over HTTP names its own
29
+ session in `actor` (`{kind: "session", id}`), and the card shows that session as the author.
30
+ `GET /api/v1/agents` (any authenticated caller) lists live sessions with their capabilities
31
+ (`aside`, `btw`, `steer`); target only a session that advertises the mode you want. Sending to a
32
+ session with no issue (`POST /api/v1/agents/{session_id}/messages`) stays human-only.
33
33
 
34
34
  ## Answering a direct message
35
35
 
@@ -122,8 +122,9 @@ envoy_send(
122
122
 
123
123
  `envoy_whoami`'s `session_id` is the address a reply to you reaches. Inside a `task` subagent it
124
124
  is the session that spawned you: a subagent registers no Envoy session of its own, so a peer
125
- answering it reaches that session, which relays to you over hub. The `subagent` field in that
126
- output carries your own host session id — it is not an address, so never hand it to a peer.
125
+ answering it reaches that session, which relays to you with a `write` to your `agent://` address.
126
+ The `subagent` field in that output carries your own host session id — it is not an address, so
127
+ never hand it to a peer.
127
128
 
128
129
  `session_id` is empty when nothing can be reached: a process that took no Envoy identity, or a
129
130
  subagent whose spawning session this process can no longer place (it forked away, and other
@@ -134,8 +135,8 @@ would send your peers to an agent that never spawned you.
134
135
  Your own `envoy_publish` never reaches the agent that spawned you. The listener delivers nothing
135
136
  to the session a message names as its source, and inside a subagent that source is your parent,
136
137
  so a publish to a role it holds — or to any topic it subscribes to — is accepted and delivered to
137
- nobody. Use hub for that one hop. `envoy_send` to any other session, including a reply, is
138
- unaffected.
138
+ nobody. Use a `write` to its `agent://` address for that one hop. `envoy_send` to any other
139
+ session, including a reply, is unaffected.
139
140
 
140
141
  ### Delivery capabilities
141
142
 
@@ -62,11 +62,11 @@ workflow table. You may still use ordinary `task` subagents for your own phase w
62
62
  is a Legion role.
63
63
  Escalate a product, scope, design, cross-phase, or lifecycle decision to the owning architect with
64
64
  `envoy_publish` to its role topic (`notifications.role.` followed by its encoded token, see
65
- above), carrying the verified facts and the decision needed. `hub` only reaches subagents
66
- inside your own process, not the architect's separate one. Never write a decision block into a
67
- spec yourself: the architect decides whether the human must answer it and writes the block, since
68
- a new version of an approved root spec closes the tree's design gate. A standalone to-do only a
69
- human can do is a `dispatch_ask`, and its replies return to your own session.
65
+ above), carrying the verified facts and the decision needed. A `write` to `agent://` only reaches
66
+ agents inside your own process, not the architect's separate one. Never write a decision block
67
+ into a spec yourself: the architect decides whether the human must answer it and writes the block,
68
+ since a new version of an approved root spec closes the tree's design gate. A standalone to-do
69
+ only a human can do is a `dispatch_ask`, and its replies return to your own session.
70
70
 
71
71
  Because the same agent is always resumed for its phase, you may receive more than one
72
72
  assignment across your lifetime: once the daemon ends your phase it suspends you, and when a later
@@ -152,9 +152,10 @@ new work.
152
152
  clone, so they all share one operation log: `jj undo`, `jj abandon`, and
153
153
  `jj op restore|revert|abandon|undo` rewrite it for every tree at once. The extension refuses them in every
154
154
  phase-worker pane before they run — a `bash` command in any position of a pipeline or `&&`
155
- chain, with or without `-R`, judged on the whole argument list; `eval` code; and a `hub`
156
- process start — from your own tool calls and from any `task` subagent you spawn (it runs in
157
- your pane, against the same log), and a `bash` command whose quoted text merely mentions `jj`
155
+ chain, with or without `-R`, judged on the whole argument list (a supervised service's start
156
+ included); `eval` code; and stdin written to a service (a `write` to `proc://<id>`) — from your
157
+ own tool calls and from any `task` subagent you spawn (it runs in your pane, against the same
158
+ log), and a `bash` command whose quoted text merely mentions `jj`
158
159
  with one of those words (a heredoc, an echo, a commit message) is refused too: write such text
159
160
  with the `write` tool or say "operation-log rollback" instead. `jj restore <paths>`,
160
161
  `jj op log`, and `jj op show` stay allowed. Recover forward only: a new commit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "7.5.2",
3
+ "version": "7.6.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {