@sjawhar/pi-envoy 8.4.0 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/envoy.js CHANGED
@@ -16111,7 +16111,7 @@ var require_mod4 = __commonJS((exports) => {
16111
16111
 
16112
16112
  // extensions/envoy.ts
16113
16113
  import { existsSync } from "fs";
16114
- import { dirname, resolve } from "path";
16114
+ import { delimiter, dirname, resolve } from "path";
16115
16115
  import { fileURLToPath } from "url";
16116
16116
 
16117
16117
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
@@ -29647,19 +29647,11 @@ function date4(params) {
29647
29647
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
29648
29648
  config(en_default());
29649
29649
  // ../contracts/src/dispatch-api.ts
29650
- function serviceSubjectLabel(subject) {
29651
- const parts = subject.split(":");
29652
- if (parts.length === 4 && parts[0] === "system" && parts[1] === "serviceaccount" && parts[2] !== "" && parts[3] !== "") {
29653
- return `${parts[2]}/${parts[3]}`;
29654
- }
29655
- return subject;
29656
- }
29657
29650
  var MAX_ISSUE_PAGE_LIMIT = 250;
29658
29651
  var DEFAULT_ISSUE_PAGE_LIMIT = 50;
29659
29652
  var ASK_TURNS = ["human", "agent"];
29660
29653
  var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
29661
29654
  var DELIVERY_DUPLICATE_WINDOW_MS = 72 * 60 * 60 * 1000;
29662
- var SEARCH_DEGRADED_EMBEDDER_UNAVAILABLE = "embedder_unavailable";
29663
29655
  var DispatchEventSchema = exports_external.object({
29664
29656
  issue_key: exports_external.string().nullable(),
29665
29657
  artifact_id: exports_external.string().nullish(),
@@ -29860,36 +29852,6 @@ var AskFollowerEventPayloadSchema = exports_external.object({
29860
29852
  session_id: exports_external.string().optional(),
29861
29853
  by: exports_external.object({ kind: exports_external.string(), id: exports_external.string().optional() }).passthrough().optional()
29862
29854
  });
29863
-
29864
- // ../contracts/src/actor-label.ts
29865
- function shortSessionId(id) {
29866
- return `session:${id.slice(0, 8)}\u2026`;
29867
- }
29868
- function sessionLabel(sessionId, title) {
29869
- const trimmed = title?.trim() ?? "";
29870
- return trimmed === "" ? shortSessionId(sessionId) : trimmed;
29871
- }
29872
- function actorName(actor, titles) {
29873
- if (actor.kind !== "session") {
29874
- return actor.id;
29875
- }
29876
- const live = titles?.get(actor.id)?.trim();
29877
- return sessionLabel(actor.id, live === undefined || live === "" ? actor.origin?.session_title : live);
29878
- }
29879
- function actorLabel(actor, titles) {
29880
- const name = actorName(actor, titles);
29881
- if (actor.kind !== "session") {
29882
- return name;
29883
- }
29884
- if (actor.owner !== undefined) {
29885
- return `${name} (for ${actor.owner})`;
29886
- }
29887
- const service = actor.service;
29888
- if (service === undefined) {
29889
- return name;
29890
- }
29891
- return `${name} (as ${serviceSubjectLabel(service)})`;
29892
- }
29893
29855
  // ../contracts/src/agent-stream.ts
29894
29856
  var AGENT_STREAM_SUBJECT_PREFIX = "agentstream.";
29895
29857
  var AGENT_STREAM_PROTOCOL = 1;
@@ -29915,101 +29877,9 @@ function capAgentStreamText(text, limit) {
29915
29877
  return text;
29916
29878
  return text.slice(0, limit) + AGENT_STREAM_TRUNCATION_SUFFIX;
29917
29879
  }
29918
- // ../contracts/src/claim-holds.ts
29919
- function claimHolds(claim, titles) {
29920
- if (claim.actor.kind !== "session") {
29921
- return "holds";
29922
- }
29923
- if (titles === undefined) {
29924
- return "unknown";
29925
- }
29926
- return titles.has(claim.actor.id) ? "holds" : "lapsed";
29927
- }
29928
- // ../contracts/src/dispatch-href.ts
29929
- function hasControlCharacter(value) {
29930
- return /\p{Cc}/u.test(value);
29931
- }
29932
- function itemFromSearch(search) {
29933
- const params = new URLSearchParams(search);
29934
- const ask = params.get("ask");
29935
- const comment = params.get("comment");
29936
- if (ask === null && comment === null) {
29937
- return;
29938
- }
29939
- if (ask !== null && comment !== null) {
29940
- return null;
29941
- }
29942
- const raw = ask ?? comment ?? "";
29943
- if (raw === "" || raw.includes("/")) {
29944
- return null;
29945
- }
29946
- let id;
29947
- try {
29948
- id = decodeURIComponent(raw);
29949
- } catch {
29950
- return null;
29951
- }
29952
- if (hasControlCharacter(id)) {
29953
- return null;
29954
- }
29955
- return ask === null ? { id, kind: "comment" } : { id, kind: "ask" };
29956
- }
29957
- // ../contracts/src/dispatch-snippet.ts
29958
- var HTML_ENTITIES = [
29959
- ["&lt;", "<"],
29960
- ["&gt;", ">"],
29961
- ["&#39;", "'"],
29962
- ["&#34;", '"'],
29963
- ["&amp;", "&"]
29964
- ];
29965
- function decodeEntities(text) {
29966
- let decoded = text;
29967
- for (const [entity, character] of HTML_ENTITIES) {
29968
- decoded = decoded.replaceAll(entity, character);
29969
- }
29970
- return decoded;
29971
- }
29972
- function hasOnlyBalancedMarkers(snippet) {
29973
- let marked = false;
29974
- for (const marker of snippet.matchAll(/<mark>|<\/mark>/gu)) {
29975
- if (marker[0] === "<mark>") {
29976
- if (marked)
29977
- return false;
29978
- marked = true;
29979
- } else {
29980
- if (!marked)
29981
- return false;
29982
- marked = false;
29983
- }
29984
- }
29985
- return !marked;
29986
- }
29987
- function snippetSegments(snippet) {
29988
- if (!hasOnlyBalancedMarkers(snippet)) {
29989
- return snippet === "" ? [] : [{ text: decodeEntities(snippet), mark: false }];
29990
- }
29991
- let mark = false;
29992
- const segments = [];
29993
- for (const part of snippet.split(/(<mark>|<\/mark>)/u)) {
29994
- if (part === "<mark>") {
29995
- mark = true;
29996
- } else if (part === "</mark>") {
29997
- mark = false;
29998
- } else if (part !== "") {
29999
- segments.push({ text: decodeEntities(part), mark });
30000
- }
30001
- }
30002
- return segments;
30003
- }
30004
- function snippetText(snippet) {
30005
- return snippetSegments(snippet).map(({ text, mark }) => mark ? `**${text}**` : text).join("");
30006
- }
30007
29880
  // ../contracts/src/dispatch-text-rules.ts
30008
29881
  var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
30009
29882
  var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
30010
- function pictureCaption(name) {
30011
- return name.replace(/[\\[\]`]/g, "\\$&").replace(/\s+/g, " ");
30012
- }
30013
29883
  var PICTURE_TYPES = {
30014
29884
  "image/png": true,
30015
29885
  "image/jpeg": true,
@@ -30026,7 +29896,7 @@ function dispatchToolSchema(spec, z2, opts) {
30026
29896
  const schemaOptions = strict === undefined ? undefined : { strict };
30027
29897
  return spec.validation === undefined ? z2.object(shape, schemaOptions) : z2.refineObject(shape, spec.validation.check, spec.validation.message, schemaOptions);
30028
29898
  }
30029
- var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only dispatch_issue with external creates a native issue.";
29899
+ var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only dispatch issue with --external creates a native issue.";
30030
29900
  var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
30031
29901
  var ASK_QUESTION_CONTRACT = "The question carries the problem the reader recognises and why it matters now, what constrains " + "the answer, and the recommendation with its reason. It asks how to solve the problem or which " + "outcome is wanted; never enumerate choices in the question.";
30032
29902
  var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
@@ -30127,7 +29997,7 @@ var dispatchToolSpecs = [
30127
29997
  {
30128
29998
  name: "dispatch_issue",
30129
29999
  example: { project: "DSP", title: "Native workspace" },
30130
- description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
30000
+ description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
30131
30001
  arguments: (z2) => ({
30132
30002
  project: z2.string().describe("Project key for the new issue."),
30133
30003
  title: z2.string().describe("Concise issue title."),
@@ -30174,7 +30044,7 @@ var dispatchToolSpecs = [
30174
30044
  {
30175
30045
  name: "dispatch_claim",
30176
30046
  example: { issue: "DSP-1" },
30177
- description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or release: true to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch_read, and dispatch_issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, so also move the issue to in_progress with " + "dispatch_issue_update when you start. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
30047
+ description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, so also move the issue to in_progress with " + "dispatch issue-update when you start. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
30178
30048
  arguments: (z2) => ({
30179
30049
  issue: z2.string().describe(ISSUE_REFERENCE),
30180
30050
  release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
@@ -30197,7 +30067,7 @@ var dispatchToolSpecs = [
30197
30067
  }
30198
30068
  ]
30199
30069
  },
30200
- description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch_doc_edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch_request_approval. Do not use this tool for a status " + "update or discussion; use dispatch_message instead. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. " + "Anchor a to-do about a document passage, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". ` + `A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} characters, ` + `the lines images appends included, and has at most 8 options. ${OWNER_REFERENCE}`,
30070
+ description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch doc-edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch request-approval. Do not use this tool for a status " + "update or discussion; use dispatch message instead. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. " + "Anchor a to-do about a document passage, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". ` + `A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} characters, ` + `the lines images appends included, and has at most 8 options. ${OWNER_REFERENCE}`,
30201
30071
  arguments: (z2) => ({
30202
30072
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30203
30073
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30271,7 +30141,7 @@ var dispatchToolSpecs = [
30271
30141
  {
30272
30142
  name: "dispatch_resolve_comment",
30273
30143
  example: { comment: "01234567-0000-4000-8000-000000000001" },
30274
- description: "Resolve a review comment thread once it has been addressed - typically your own comment " + "after the document was fixed. Any session or human may resolve any open comment on an " + "open issue or project document; reopening a resolved comment is human-only (the dashboard). " + "Not for asks: use dispatch_resolve_ask.",
30144
+ description: "Resolve a review comment thread once it has been addressed - typically your own comment " + "after the document was fixed. Any session or human may resolve any open comment on an " + "open issue or project document; reopening a resolved comment is human-only (the dashboard). " + "Not for asks: use dispatch resolve-ask.",
30275
30145
  arguments: (z2) => ({
30276
30146
  comment: z2.string().describe("Comment id (uuid), or a dispatch://KEY/comment/<id> or " + "dispatch://PROJECT/artifact/<slug>/comment/<id> reference; a reference accepts an " + "8+ character id prefix that is unique on its owner.")
30277
30147
  }),
@@ -30290,7 +30160,7 @@ var dispatchToolSpecs = [
30290
30160
  {
30291
30161
  name: "dispatch_comment",
30292
30162
  example: { issue: "DSP-1", body: "Looks good." },
30293
- description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters, the lines images appends included. ${OWNER_REFERENCE}`,
30163
+ description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch ask. " + "Do not use it for an exact replacement; use " + `dispatch suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters, the lines images appends included. ${OWNER_REFERENCE}`,
30294
30164
  arguments: (z2) => ({
30295
30165
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30296
30166
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30314,7 +30184,7 @@ var dispatchToolSpecs = [
30314
30184
  quote: "old wording",
30315
30185
  replace_with: "new wording"
30316
30186
  },
30317
- description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `dispatch_comment instead. Optional explanation is at most 2,000 characters. ${OWNER_REFERENCE}`,
30187
+ description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `dispatch comment instead. Optional explanation is at most 2,000 characters. ${OWNER_REFERENCE}`,
30318
30188
  arguments: (z2) => ({
30319
30189
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30320
30190
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30330,7 +30200,7 @@ var dispatchToolSpecs = [
30330
30200
  {
30331
30201
  name: "dispatch_message",
30332
30202
  example: { issue: "DSP-1", body: "Implementation started." },
30333
- description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch_ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) or document feedback (dispatch_comment). " + "To answer a human's direct message to this session - one sent from the Agents page, which names no issue - pass that message's bare id as " + "in_reply_to and no issue; the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters, the lines images appends included. ${ISSUE_REFERENCE}`,
30203
+ description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) or document feedback (dispatch comment). " + "To answer a human's direct message to this session - one sent from the Agents page, which names no issue - pass that message's bare id as " + "in_reply_to and no issue; the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch read --message <id> reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters, the lines images appends included. ${ISSUE_REFERENCE}`,
30334
30204
  arguments: (z2) => ({
30335
30205
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
30336
30206
  body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Update text, at most 2,000 characters."),
@@ -30347,7 +30217,7 @@ var dispatchToolSpecs = [
30347
30217
  ops: [{ op: "delete_column", block: "table-123", index: 1 }],
30348
30218
  precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
30349
30219
  },
30350
- description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch_doc_read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
30220
+ description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch comment, dispatch suggest, or dispatch doc-read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch doc-read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
30351
30221
  arguments: (z2) => ({
30352
30222
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30353
30223
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30367,7 +30237,7 @@ var dispatchToolSpecs = [
30367
30237
  attributes: z2.unknown().describe("Typed block attributes for retype; a CR LF or a lone carriage return in a string value is written as a line feed.").optional()
30368
30238
  }, { strict: true })).describe("Flat tagged edits; an operation takes only the keys below, a misspelled one is refused rather than ignored, and the server validates the fields its op requires."),
30369
30239
  precondition: z2.object({
30370
- document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch_doc_read.").optional(),
30240
+ document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch doc-read.").optional(),
30371
30241
  blocks: z2.array(z2.object({
30372
30242
  id: z2.string({ min: 1 }).describe("Stable block id from GET /api/v1/artifacts/{id}/blocks."),
30373
30243
  token: z2.string({ min: 1 }).describe("That block's full-state token, including inline marks.")
@@ -30380,7 +30250,7 @@ var dispatchToolSpecs = [
30380
30250
  {
30381
30251
  name: "dispatch_doc_read",
30382
30252
  example: { issue: "DSP-1" },
30383
- description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
30253
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
30384
30254
  arguments: (z2) => ({
30385
30255
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30386
30256
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30393,7 +30263,7 @@ var dispatchToolSpecs = [
30393
30263
  {
30394
30264
  name: "dispatch_request_approval",
30395
30265
  example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
30396
- description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch_doc_read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
30266
+ description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
30397
30267
  arguments: (z2) => ({
30398
30268
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30399
30269
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30406,7 +30276,7 @@ var dispatchToolSpecs = [
30406
30276
  name: "dispatch_artifact",
30407
30277
  example: { issue: "DSP-1", name: "design.md", content: `# Design
30408
30278
  ` },
30409
- description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch_doc_edit instead. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
30279
+ description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch doc-edit instead. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
30410
30280
  arguments: (z2) => ({
30411
30281
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30412
30282
  project: z2.string().describe("Project key for an unlinked document.").optional(),
@@ -30426,7 +30296,7 @@ var dispatchToolSpecs = [
30426
30296
  {
30427
30297
  name: "dispatch_read",
30428
30298
  example: { issue: "DSP-1" },
30429
- description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch_doc_read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch_doc_read shows it again. " + OWNER_REFERENCE,
30299
+ description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch doc-read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch doc-read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch doc-read shows it again. " + OWNER_REFERENCE,
30430
30300
  arguments: (z2) => ({
30431
30301
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30432
30302
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30461,7 +30331,7 @@ var dispatchToolSpecs = [
30461
30331
  {
30462
30332
  name: "dispatch_issues",
30463
30333
  example: { project: "PROJ", limit: 250, offset: 250 },
30464
- 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.",
30334
+ 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.",
30465
30335
  arguments: (z2) => ({
30466
30336
  project: z2.string().describe("Project key to list issues from."),
30467
30337
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -30559,19 +30429,9 @@ var ROLE_TOPIC_PREFIX = "notifications.role.";
30559
30429
  var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
30560
30430
  var DISPATCH_ISSUE_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}issue.`;
30561
30431
  var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
30562
- function dispatchIssueSubject(issueKey, type) {
30563
- return `${DISPATCH_ISSUE_TOPIC_PREFIX}${issueKey}.${type}`;
30564
- }
30565
- function dispatchDocumentSubject(project, slug, type) {
30566
- return `${DISPATCH_DOCUMENT_TOPIC_PREFIX}${project}.${slug}.${type}`;
30567
- }
30568
30432
  function agentSubject(session) {
30569
30433
  return `${AGENT_TOPIC_PREFIX}${session}`;
30570
30434
  }
30571
- // ../contracts/src/repo.ts
30572
- function canonicalRepo(owner, repo) {
30573
- return `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase().replace(/\.git$/, "")}`;
30574
- }
30575
30435
  // ../contracts/src/tool-schema.ts
30576
30436
  function overCapMessage(length, max) {
30577
30437
  return `is ${length - max} characters over the ${max}-character limit (${length}/${max})`;
@@ -31186,19 +31046,240 @@ function askAnswerText(answer) {
31186
31046
  return `${selected} - ${text}`;
31187
31047
  }
31188
31048
 
31189
- // ../envoy-client/src/dispatch-owner.ts
31190
- function documentLabel(project, slug) {
31191
- return `${project}/${slug}`;
31049
+ // ../envoy-client/src/errors.ts
31050
+ function messageFor(error48) {
31051
+ return error48 instanceof Error ? error48.message : String(error48);
31192
31052
  }
31193
- function issueTopic(key) {
31194
- return { label: key, topic: dispatchIssueSubject(key, ">") };
31053
+ function hasErrnoCode(error48, code) {
31054
+ return error48 instanceof Error && "code" in error48 && error48.code === code;
31195
31055
  }
31196
- function documentTopicOf(project, slug) {
31197
- return {
31198
- label: documentLabel(project, slug),
31199
- topic: dispatchDocumentSubject(project, slug, ">")
31200
- };
31056
+
31057
+ // ../envoy-client/src/dispatch-command.ts
31058
+ var BARE_WORD = /^[A-Za-z0-9_./:@%+=,-]+$/;
31059
+ var OPTION_SEPARATOR = ": ";
31060
+ function commandName(tool) {
31061
+ return tool.replace(/^dispatch_/, "").replaceAll("_", "-");
31062
+ }
31063
+ function flagName(field) {
31064
+ return field.replaceAll("_", "-");
31065
+ }
31066
+ var toolsByCommand = new Map(dispatchToolSpecs.map((spec) => [commandName(spec.name), spec.name]));
31067
+ function commandFlags(tool) {
31068
+ return [...surfaceFor(tool).flags.keys()];
31069
+ }
31070
+ function fieldFlag(tool, field) {
31071
+ for (const [flag, entry] of surfaceFor(tool).flags) {
31072
+ if (entry.field === field)
31073
+ return flag;
31074
+ }
31075
+ return `--${flagName(field)}`;
31076
+ }
31077
+ var surfaces = new Map;
31078
+ function surfaceFor(tool) {
31079
+ const cached2 = surfaces.get(tool);
31080
+ if (cached2 !== undefined)
31081
+ return cached2;
31082
+ const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
31083
+ if (spec === undefined)
31084
+ throw new Error(`Unknown Dispatch tool: ${tool}`);
31085
+ const fields = fieldKinds(spec);
31086
+ const surface = {
31087
+ spec,
31088
+ command: commandName(spec.name),
31089
+ fields,
31090
+ flags: flagTable(spec.name, fields)
31091
+ };
31092
+ surfaces.set(tool, surface);
31093
+ return surface;
31094
+ }
31095
+ function fieldKinds(spec) {
31096
+ const schema = exports_external.toJSONSchema(dispatchToolSchema(spec, zodSchemaApi(exports_external)));
31097
+ const required2 = new Set(schema.required ?? []);
31098
+ const fields = new Map;
31099
+ for (const [field, node] of Object.entries(schema.properties ?? {})) {
31100
+ const nonNull = (node.anyOf ?? [node]).filter((member) => member.type !== "null");
31101
+ const nullable2 = node.anyOf !== undefined && nonNull.length < node.anyOf.length;
31102
+ const inner = nonNull.length === 1 ? nonNull[0] : undefined;
31103
+ const kind = inner === undefined ? "json" : kindOf(field, inner);
31104
+ const values = inner?.enum?.map(String);
31105
+ const description = node.description ?? inner?.description;
31106
+ fields.set(field, {
31107
+ kind,
31108
+ nullable: nullable2,
31109
+ required: required2.has(field),
31110
+ ...description === undefined ? {} : { description },
31111
+ ...values === undefined ? {} : { values }
31112
+ });
31113
+ }
31114
+ return fields;
31115
+ }
31116
+ function kindOf(field, node) {
31117
+ switch (node.type) {
31118
+ case "string":
31119
+ return "string";
31120
+ case "integer":
31121
+ case "number":
31122
+ return "number";
31123
+ case "boolean":
31124
+ return "boolean";
31125
+ case "array": {
31126
+ const item = node.items;
31127
+ if (item === undefined)
31128
+ return "json";
31129
+ if (field === "options" && item.type === "object")
31130
+ return "options";
31131
+ if (item.type === "string")
31132
+ return { array: "string" };
31133
+ if (item.type === "integer" || item.type === "number")
31134
+ return { array: "number" };
31135
+ const members = item.anyOf ?? [];
31136
+ const numeric = members.filter((m) => m.type === "integer" || m.type === "number");
31137
+ if (members.length === 2 && numeric.length === 1 && members.some((member) => member.type === "null")) {
31138
+ return { array: "number-or-null" };
31139
+ }
31140
+ return "json";
31141
+ }
31142
+ default:
31143
+ return "json";
31144
+ }
31145
+ }
31146
+ function singular(field) {
31147
+ return field.endsWith("s") ? field.slice(0, -1) : field;
31148
+ }
31149
+ function placeholder(info) {
31150
+ if (info.values !== undefined)
31151
+ return `<${info.values.join("|")}>`;
31152
+ if (info.kind === "number")
31153
+ return "<n>";
31154
+ if (typeof info.kind === "object")
31155
+ return info.kind.array === "string" ? "<text>" : "<n>";
31156
+ return "<text>";
31157
+ }
31158
+ function flagTable(tool, fields) {
31159
+ const flags = new Map;
31160
+ const add = (flag, entry) => {
31161
+ if (flags.has(flag))
31162
+ throw new Error(`${tool}: two fields claim the flag ${flag}`);
31163
+ flags.set(flag, entry);
31164
+ };
31165
+ for (const [field, info] of fields) {
31166
+ const name = flagName(field);
31167
+ const about = [info.description, info.required ? "Required." : undefined].filter((part) => part !== undefined).join(" ");
31168
+ const { kind } = info;
31169
+ if (kind === "string") {
31170
+ add(`--${name}`, { field, action: "set", value: placeholder(info), text: about });
31171
+ add(`--${name}-file`, {
31172
+ field,
31173
+ action: "file",
31174
+ value: "<path>",
31175
+ text: `--${name} read from a file; - reads stdin.`
31176
+ });
31177
+ } else if (kind === "number") {
31178
+ add(`--${name}`, { field, action: "set", value: "<n>", text: about });
31179
+ } else if (kind === "boolean") {
31180
+ add(`--${name}`, { field, action: "true", text: about });
31181
+ add(`--no-${name}`, { field, action: "false", text: `Sends ${field} as false.` });
31182
+ } else if (kind === "options") {
31183
+ add(`--${singular(name)}`, {
31184
+ field,
31185
+ action: "append",
31186
+ value: '"<label>: <description>"',
31187
+ text: `${about} One option per flag, repeated; split at the first ": " (without one, the whole value is the label). A label that holds ": " goes in --${name}-json.`.trim()
31188
+ });
31189
+ add(`--${name}-json`, {
31190
+ field,
31191
+ action: "json",
31192
+ value: "<json>",
31193
+ text: `Every option as a JSON list of {label, description?}, in place of --${singular(name)}.`
31194
+ });
31195
+ } else if (kind === "json") {
31196
+ add(`--${name}-json`, { field, action: "json", value: "<json>", text: about });
31197
+ add(`--${name}-json-file`, {
31198
+ field,
31199
+ action: "json-file",
31200
+ value: "<path>",
31201
+ text: `--${name}-json read from a file; - reads stdin.`
31202
+ });
31203
+ } else {
31204
+ const none = kind.array === "number-or-null" ? " none is null." : "";
31205
+ add(`--${singular(name)}`, {
31206
+ field,
31207
+ action: "append",
31208
+ value: placeholder(info),
31209
+ text: `${about} One item per flag, repeated.${none}`.trim()
31210
+ });
31211
+ }
31212
+ if (info.nullable || typeof kind === "object" || kind === "options") {
31213
+ add(`--clear-${name}`, {
31214
+ field,
31215
+ action: "clear",
31216
+ text: info.nullable ? `Sends ${field} as null.` : `Sends ${field} as an empty list.`
31217
+ });
31218
+ }
31219
+ }
31220
+ add("--help", { action: "help", text: "Prints this help." });
31221
+ add("--dry-run", { action: "dry-run", text: "Prints the arguments as JSON and sends nothing." });
31222
+ return flags;
31223
+ }
31224
+ function quoteWord(value) {
31225
+ return BARE_WORD.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
31226
+ }
31227
+ function flagWithValue(flag, value) {
31228
+ return value.startsWith("--") ? [`${flag}=${quoteWord(value)}`] : [flag, quoteWord(value)];
31229
+ }
31230
+ function scalarText(value) {
31231
+ return typeof value === "string" ? value : JSON.stringify(value);
31232
+ }
31233
+ function fieldWords(field, info, value) {
31234
+ const name = flagName(field);
31235
+ const { kind } = info;
31236
+ if (value === null && info.nullable)
31237
+ return [`--clear-${name}`];
31238
+ if (kind === "boolean" && typeof value === "boolean") {
31239
+ return [value ? `--${name}` : `--no-${name}`];
31240
+ }
31241
+ if (kind === "json")
31242
+ return flagWithValue(`--${name}-json`, JSON.stringify(value));
31243
+ if (Array.isArray(value) && (kind === "options" || typeof kind === "object")) {
31244
+ if (value.length === 0)
31245
+ return [`--clear-${name}`];
31246
+ if (kind === "options") {
31247
+ const texts = value.map(optionText);
31248
+ if (texts.includes(undefined))
31249
+ return flagWithValue(`--${name}-json`, JSON.stringify(value));
31250
+ return texts.flatMap((text) => flagWithValue(`--${singular(name)}`, text ?? ""));
31251
+ }
31252
+ const flag = `--${singular(name)}`;
31253
+ return value.flatMap((item) => flagWithValue(flag, item === null ? "none" : scalarText(item)));
31254
+ }
31255
+ return flagWithValue(`--${name}`, scalarText(value));
31256
+ }
31257
+ function optionText(option) {
31258
+ if (typeof option !== "object" || option === null || !("label" in option)) {
31259
+ return scalarText(option);
31260
+ }
31261
+ const label = scalarText(option.label);
31262
+ if (label.includes(OPTION_SEPARATOR))
31263
+ return;
31264
+ return "description" in option && option.description !== undefined ? `${label}${OPTION_SEPARATOR}${scalarText(option.description)}` : label;
31265
+ }
31266
+ function commandLine(tool, args) {
31267
+ const surface = surfaceFor(tool);
31268
+ const words = ["dispatch", surface.command];
31269
+ for (const [field, info] of surface.fields) {
31270
+ const value = args[field];
31271
+ if (value !== undefined)
31272
+ words.push(...fieldWords(field, info, value));
31273
+ }
31274
+ for (const [field, value] of Object.entries(args)) {
31275
+ if (value !== undefined && !surface.fields.has(field)) {
31276
+ words.push(...flagWithValue(`--${flagName(field)}`, scalarText(value)));
31277
+ }
31278
+ }
31279
+ return words.join(" ");
31201
31280
  }
31281
+
31282
+ // ../envoy-client/src/dispatch-owner.ts
31202
31283
  function dispatchIssueRef(key) {
31203
31284
  return `dispatch://${key}`;
31204
31285
  }
@@ -31271,7 +31352,6 @@ function oversizePictureText(name, bytes) {
31271
31352
  }
31272
31353
  var PICTURES_SHOWN_MAX = 8;
31273
31354
  var PICTURES_SHOWN_MAX_BYTES = 10 * 1024 * 1024;
31274
- var PICTURE_SNIFF_BYTES = 12;
31275
31355
  function bytesAt(bytes, offset, expected) {
31276
31356
  return expected.every((byte, index) => bytes[offset + index] === byte);
31277
31357
  }
@@ -31297,27 +31377,10 @@ function toolImage(bytes) {
31297
31377
  function imageBlocks(images) {
31298
31378
  return images.map(({ data, mimeType }) => ({ type: "image", data, mimeType }));
31299
31379
  }
31300
- function pictureLine(name, address) {
31301
- return `![${pictureCaption(name)}](${address})`;
31302
- }
31303
- function withPictureLines(text, lines) {
31304
- if (lines.length === 0)
31305
- return text;
31306
- const head = text.trimEnd();
31307
- return head === "" ? lines.join(`
31308
- `) : `${head}
31309
-
31310
- ${lines.join(`
31311
- `)}`;
31312
- }
31313
31380
  var PICTURE_SYNTAX = /!\[(?:\\.|[^\\\]])*\]\((dispatch:\/\/[^\s()]+@v[1-9][0-9]*)(?:\s+"(?:\\.|[^\\"])*")?\)/g;
31314
31381
  function pictureAddresses(text) {
31315
31382
  return [...text.matchAll(PICTURE_SYNTAX)].map((match) => match[1]);
31316
31383
  }
31317
- function picturesNewestFirst(texts) {
31318
- const ordered = texts.map((item, index) => ({ item, index, at: Date.parse(item.at) })).sort((left, right) => (Number.isNaN(right.at) ? 0 : right.at) - (Number.isNaN(left.at) ? 0 : left.at) || left.index - right.index);
31319
- return [...new Set(ordered.flatMap(({ item }) => pictureAddresses(item.text)))];
31320
- }
31321
31384
 
31322
31385
  // ../envoy-client/src/delivery.ts
31323
31386
  var KNOWN_SOURCES = EnvelopeSchema.shape.source.enum;
@@ -31561,8 +31624,11 @@ function dispatchCommentReplyWith(event, topic, comment) {
31561
31624
  const threadID = thread === "reply_to" ? comment.id : String(reply);
31562
31625
  if (event.issue_key !== null) {
31563
31626
  return {
31564
- tool: "dispatch_comment",
31565
- args: { issue: event.issue_key, [thread]: threadID, body: "..." }
31627
+ command: commandLine("dispatch_comment", {
31628
+ issue: event.issue_key,
31629
+ [thread]: threadID,
31630
+ body: "..."
31631
+ })
31566
31632
  };
31567
31633
  }
31568
31634
  let project = comment.project_key;
@@ -31573,8 +31639,12 @@ function dispatchCommentReplyWith(event, topic, comment) {
31573
31639
  if (project === undefined || project === "" || artifact === undefined || artifact === "")
31574
31640
  return;
31575
31641
  return {
31576
- tool: "dispatch_comment",
31577
- args: { project, artifact, [thread]: threadID, body: "..." }
31642
+ command: commandLine("dispatch_comment", {
31643
+ project,
31644
+ artifact,
31645
+ [thread]: threadID,
31646
+ body: "..."
31647
+ })
31578
31648
  };
31579
31649
  }
31580
31650
  function askResolutionText(resolution) {
@@ -31820,7 +31890,7 @@ function renderInbound(raw, sessionID, subject2) {
31820
31890
  const ask = follower.data.ask_id ?? "?";
31821
31891
  return {
31822
31892
  skip: false,
31823
- content: frame.event.type === "ask.follower_added" ? `Now following ask ${ask} on ${owner} (added by ${who}): its answer and replies reach you directly; dispatch_follow unfollow to stop.` : `No longer following ask ${ask} on ${owner} (removed by ${who}).`,
31893
+ content: frame.event.type === "ask.follower_added" ? `Now following ask ${ask} on ${owner} (added by ${who}): its answer and replies reach you directly; ${commandLine("dispatch_follow", { ask, action: "unfollow" })} to stop.` : `No longer following ask ${ask} on ${owner} (removed by ${who}).`,
31824
31894
  envelope: envelope2
31825
31895
  };
31826
31896
  }
@@ -31853,12 +31923,11 @@ function renderInbound(raw, sessionID, subject2) {
31853
31923
  ...typeof message2.data.broadcast_id === "string" ? { broadcastId: message2.data.broadcast_id } : {}
31854
31924
  };
31855
31925
  dispatchReply = {
31856
- tool: "dispatch_message",
31857
- args: {
31926
+ command: commandLine("dispatch_message", {
31858
31927
  ...frame.event.issue_key === null ? {} : { issue: frame.event.issue_key },
31859
31928
  in_reply_to: message2.data.id,
31860
31929
  body: "..."
31861
- }
31930
+ })
31862
31931
  };
31863
31932
  }
31864
31933
  } else if (frame.event.type === "comment.created") {
@@ -31969,11 +32038,6 @@ import { readFileSync as readFileSync2 } from "fs";
31969
32038
  import { homedir } from "os";
31970
32039
  import * as path from "path";
31971
32040
 
31972
- // ../envoy-client/src/errors.ts
31973
- function messageFor(error48) {
31974
- return error48 instanceof Error ? error48.message : String(error48);
31975
- }
31976
-
31977
32041
  // ../envoy-client/src/secret-file.ts
31978
32042
  import { readFileSync } from "fs";
31979
32043
  function readSecretFile(variable, filePath) {
@@ -32113,89 +32177,29 @@ function activeDispatchConfig(env, options = {}) {
32113
32177
  return { ...config2, url: url2, token };
32114
32178
  }
32115
32179
 
32116
- // ../envoy-client/src/dispatch-execute.ts
32117
- import { resolve as resolvePath2 } from "path";
32118
-
32119
- // ../envoy-client/src/dispatch-cwd.ts
32120
- import { execFile } from "child_process";
32121
- import { promisify } from "util";
32122
-
32123
- // ../envoy-client/src/machine.ts
32124
- import { hostname as hostname3 } from "os";
32125
- function machineID() {
32126
- return process.env["ENVOY_MACHINE_ID"] || hostname3();
32180
+ // ../envoy-client/src/dispatch-first.ts
32181
+ import { readFileSync as readFileSync3 } from "fs";
32182
+ import { join as join2 } from "path";
32183
+ var DISPATCH_FIRST_MARKER = "<dispatch-first-skill>";
32184
+ function dispatchFirstSkillFile(skillsDirectory) {
32185
+ return join2(skillsDirectory, "dispatch-first", "SKILL.md");
32127
32186
  }
32128
-
32129
- // ../envoy-client/src/dispatch-cwd.ts
32130
- var execFileAsync = promisify(execFile);
32131
- var defaultExec = (file2, args, options) => execFileAsync(file2, args, { cwd: options.cwd, timeout: 5000 });
32132
- async function tryExec(exec, file2, args, cwd) {
32187
+ function readDispatchFirstContext(skillFile) {
32188
+ let skill;
32133
32189
  try {
32134
- const { stdout } = await exec(file2, args, { cwd });
32135
- return stdout;
32136
- } catch {
32137
- return null;
32138
- }
32139
- }
32140
- function parseRemoteList(stdout) {
32141
- const remotes = new Map;
32142
- for (const line of stdout.split(`
32143
- `)) {
32144
- const trimmed = line.trim();
32145
- if (!trimmed)
32146
- continue;
32147
- const match = trimmed.match(/^(\S+)\s+(\S+)/);
32148
- const [, name, url2] = match ?? [];
32149
- if (name && url2)
32150
- remotes.set(name, url2);
32151
- }
32152
- return remotes;
32153
- }
32154
- function selectRemoteUrl(remotes) {
32155
- const origin = remotes.get("origin");
32156
- if (origin)
32157
- return origin;
32158
- const candidates = [...remotes.entries()].filter(([name]) => name !== "upstream");
32159
- return candidates.length === 1 ? candidates[0]?.[1] ?? null : null;
32160
- }
32161
- var GITHUB_REMOTE_PATTERNS = [
32162
- /^https:\/\/(?:[^@/\s]+@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i,
32163
- /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?\/?$/i,
32164
- /^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i
32165
- ];
32166
- function parseGitHubRemoteUrl(url2) {
32167
- const trimmed = url2.trim();
32168
- for (const pattern of GITHUB_REMOTE_PATTERNS) {
32169
- const match = trimmed.match(pattern);
32170
- if (match)
32171
- return canonicalRepo(match[1] ?? "", match[2] ?? "");
32172
- }
32173
- return null;
32174
- }
32175
- async function resolveCwdRepo(cwd, exec) {
32176
- const jjOutput = await tryExec(exec, "jj", ["git", "remote", "list"], cwd);
32177
- if (jjOutput !== null) {
32178
- const url2 = selectRemoteUrl(parseRemoteList(jjOutput));
32179
- return url2 ? parseGitHubRemoteUrl(url2) : null;
32180
- }
32181
- const originUrl = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd);
32182
- return originUrl ? parseGitHubRemoteUrl(originUrl) : null;
32183
- }
32184
- async function resolveOrigin(env, exec, cwd) {
32185
- const origin = {
32186
- cwd,
32187
- machine: machineID()
32188
- };
32189
- const pane = env.TMUX_PANE;
32190
- if (pane) {
32191
- const output = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd);
32192
- const [target, paneId] = output?.trim().split(" ") ?? [];
32193
- if (target)
32194
- origin.tmux = target;
32195
- if (paneId)
32196
- origin.pane = paneId;
32190
+ skill = readFileSync3(skillFile, "utf8");
32191
+ } catch (error48) {
32192
+ throw new Error(`the dispatch-first skill ${skillFile} could not be read: ${messageFor(error48)}`);
32197
32193
  }
32198
- return origin;
32194
+ const body = skill.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
32195
+ return [
32196
+ DISPATCH_FIRST_MARKER,
32197
+ "This session has Dispatch, so the dispatch-first skill is loaded for it. Follow it.",
32198
+ "",
32199
+ body,
32200
+ "</dispatch-first-skill>"
32201
+ ].join(`
32202
+ `);
32199
32203
  }
32200
32204
 
32201
32205
  // ../envoy-client/src/dispatch-http.ts
@@ -32660,20 +32664,21 @@ class DispatchClient {
32660
32664
  }
32661
32665
  }
32662
32666
 
32663
- // ../envoy-client/src/dispatch-picture-tools.ts
32664
- import { basename, resolve as resolvePath } from "path";
32665
-
32666
32667
  // ../envoy-client/src/tool-input-errors.ts
32667
32668
  class ToolInputError extends Error {
32668
32669
  tool;
32669
32670
  problems;
32670
32671
  constructor(tool, problems) {
32671
32672
  const count = problems.length;
32672
- const help = dispatchInputHelp(tool);
32673
+ const cli = isDispatchTool(tool);
32674
+ const help = cli ? [
32675
+ `Allowed flags: ${commandFlags(tool).join(", ")}`,
32676
+ `Example: ${commandLine(tool, exampleFor(tool))}`
32677
+ ] : [];
32673
32678
  super([
32674
- `${tool} was not called: ${count} problem${count === 1 ? "" : "s"}`,
32679
+ `${cli ? `dispatch ${commandName(tool)}` : tool} was not called: ${count} problem${count === 1 ? "" : "s"}`,
32675
32680
  ...problems.map((problem) => `- ${problem}`),
32676
- ...help === undefined ? [] : help.map((line) => `- ${line}`)
32681
+ ...help.map((line) => `- ${line}`)
32677
32682
  ].join(`
32678
32683
  `));
32679
32684
  this.name = "ToolInputError";
@@ -32681,13 +32686,12 @@ class ToolInputError extends Error {
32681
32686
  this.problems = problems;
32682
32687
  }
32683
32688
  }
32684
- function dispatchInputHelp(tool) {
32689
+ function isDispatchTool(tool) {
32690
+ return dispatchToolSpecs.some((spec) => spec.name === tool);
32691
+ }
32692
+ function exampleFor(tool) {
32685
32693
  const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
32686
- if (spec === undefined)
32687
- return;
32688
- const schema = dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
32689
- const allowed = Object.keys(shapeOf(schema) ?? {}).join(", ") || "none";
32690
- return [`Allowed keys: ${allowed}`, `Example: ${tool}(${JSON.stringify(spec.example)})`];
32694
+ return { ...spec?.example };
32691
32695
  }
32692
32696
  function unwrap(schema) {
32693
32697
  let current = schema;
@@ -32755,16 +32759,32 @@ function describeExpected(expected, schema) {
32755
32759
  return `a ${expected}`;
32756
32760
  }
32757
32761
  }
32758
- function formatZodIssues(issues, schema) {
32762
+ function pathText(path2, cliTool) {
32763
+ const [head, ...rest] = path2.map(String);
32764
+ if (cliTool === undefined || head === undefined)
32765
+ return path2.map(String).join(".");
32766
+ let flag = fieldFlag(cliTool, head);
32767
+ let index = 0;
32768
+ for (;index < rest.length && /^\d+$/.test(rest[index]); index++) {
32769
+ flag += `[${rest[index]}]`;
32770
+ }
32771
+ const tail = rest.slice(index).map((segment) => /^\d+$/.test(segment) ? `[${segment}]` : `.${segment}`).join("").replace(/^\./, "");
32772
+ return tail === "" ? flag : `${flag} ${tail}`;
32773
+ }
32774
+ function formatZodIssues(issues, schema, tool) {
32775
+ const cliTool = isDispatchTool(tool) ? tool : undefined;
32759
32776
  const allowed = Object.keys(shapeOf(schema) ?? {}).join(", ");
32760
32777
  return issues.flatMap((issue2) => {
32761
- const path2 = issue2.path.map(String).join(".");
32778
+ const path2 = pathText(issue2.path, cliTool);
32762
32779
  switch (issue2.code) {
32763
32780
  case "invalid_type":
32764
32781
  return issue2.input === undefined ? [`${path2} is required (${issue2.expected})`] : [
32765
32782
  `${path2} must be ${describeExpected(issue2.expected, schemaAt(schema, issue2.path))}, not ${describeInput(issue2.input)}`
32766
32783
  ];
32767
32784
  case "unrecognized_keys": {
32785
+ if (cliTool !== undefined && issue2.path.length === 0) {
32786
+ return issue2.keys.map((key) => `unknown flag ${fieldFlag(cliTool, key)}`);
32787
+ }
32768
32788
  const here = Object.keys(shapeOf(schemaAt(schema, issue2.path)) ?? {}).join(", ") || allowed;
32769
32789
  const where = path2 === "" ? "" : ` in ${path2}`;
32770
32790
  return issue2.keys.map((key) => `unknown field "${key}"${where}; allowed: ${here}`);
@@ -32788,79 +32808,6 @@ function formatZodIssues(issues, schema) {
32788
32808
 
32789
32809
  // ../envoy-client/src/dispatch-picture-tools.ts
32790
32810
  var PICTURE_TYPE_NAMES = "a PNG, JPEG, GIF or WebP";
32791
- async function localPictures(images, cwd, problems) {
32792
- if (!Array.isArray(images))
32793
- return [];
32794
- const checked = await Promise.all(images.map((path2) => localPicture(path2, cwd)));
32795
- const pictures = [];
32796
- for (const outcome of checked) {
32797
- if (typeof outcome === "string")
32798
- problems.push(outcome);
32799
- else if (outcome !== undefined)
32800
- pictures.push(outcome);
32801
- }
32802
- return pictures;
32803
- }
32804
- async function localPicture(path2, cwd) {
32805
- if (typeof path2 !== "string" || path2 === "")
32806
- return;
32807
- const absolute = resolvePath(cwd, path2);
32808
- const file2 = Bun.file(absolute);
32809
- let size;
32810
- let head;
32811
- try {
32812
- const stats = await file2.stat();
32813
- if (!stats.isFile())
32814
- return `images: ${path2} is not a file`;
32815
- size = stats.size;
32816
- head = new Uint8Array(await file2.slice(0, PICTURE_SNIFF_BYTES).arrayBuffer());
32817
- } catch (error48) {
32818
- return `images: ${path2} cannot be read: ${messageFor(error48)}`;
32819
- }
32820
- if (size > PICTURE_UPLOAD_MAX_BYTES) {
32821
- return `images: ${path2} is ${size.toLocaleString("en-US")} bytes, over the ${PICTURE_UPLOAD_MAX_BYTES / 1024 / 1024} MiB one Dispatch upload takes`;
32822
- }
32823
- const type = sniffPictureType(head);
32824
- if (type === undefined) {
32825
- return `images: ${path2} is not ${PICTURE_TYPE_NAMES} picture (judged by its bytes, not its name)`;
32826
- }
32827
- return { path: path2, name: basename(absolute), file: Bun.file(absolute, { type }) };
32828
- }
32829
- function uploadSlug(name) {
32830
- const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
32831
- return slug === "" ? "artifact" : slug;
32832
- }
32833
- async function textWithPictures(tool, text, pictures, limit, owner, upload, actor) {
32834
- if (pictures.length === 0)
32835
- return text;
32836
- const counted = `${limit.text} plus ${pictures.length} ${pictures.length === 1 ? "picture" : "pictures"}`;
32837
- const fix = `${limit.shorten} or send fewer pictures`;
32838
- const predicted = withPictureLines(text, pictures.map((picture) => pictureLine(picture.name, `dispatch://${owner}/artifact/${uploadSlug(picture.name)}@v1`)));
32839
- if (predicted.length > limit.cap) {
32840
- throw new ToolInputError(tool, [
32841
- `${counted} ${overCapMessage(predicted.length, limit.cap)}; ${fix}`
32842
- ]);
32843
- }
32844
- const lines = [];
32845
- const addresses = [];
32846
- for (const picture of pictures) {
32847
- let uploaded;
32848
- try {
32849
- uploaded = await upload({ name: picture.name, file: picture.file, actor });
32850
- } catch (error48) {
32851
- const earlier = addresses.length === 0 ? "" : `; the pictures before it are uploaded (${addresses.join(", ")})`;
32852
- throw new Error(`images: ${picture.path} was not uploaded: ${messageFor(error48)}. Nothing was posted${earlier}.`);
32853
- }
32854
- const address = `dispatch://${owner}/artifact/${uploaded.artifact.slug}@v${uploaded.version.number}`;
32855
- addresses.push(address);
32856
- lines.push(pictureLine(picture.name, address));
32857
- }
32858
- const posted = withPictureLines(text, lines);
32859
- if (posted.length > limit.cap) {
32860
- throw new Error(`${counted} ${overCapMessage(posted.length, limit.cap)} with the addresses Dispatch gave ` + `the uploads (${addresses.join(", ")}), so nothing was posted; ${fix}.`);
32861
- }
32862
- return posted;
32863
- }
32864
32811
  var AGENT_ARTIFACT_REF = new RegExp(`^dispatch://agent/(${SESSION_ID_PATTERN})/artifact/([^/@]+)(?:@v(\\d+))?$`, "u");
32865
32812
  function parseAgentArtifactRef(ref) {
32866
32813
  const match = ref.match(AGENT_ARTIFACT_REF);
@@ -32872,49 +32819,6 @@ function parseAgentArtifactRef(ref) {
32872
32819
  }
32873
32820
  return { session, slug, ...version2 === undefined ? {} : { version: Number(version2) } };
32874
32821
  }
32875
- function datedBodies(records) {
32876
- return records.map((record2) => ({ text: record2.body, at: record2.created_at }));
32877
- }
32878
- function askTexts({ ask, replies }) {
32879
- return [
32880
- { text: ask.question, at: ask.created_at },
32881
- ...ask.answer?.text ? [{ text: ask.answer.text, at: ask.answer.at }] : [],
32882
- ...ask.resolution === undefined ? [] : [{ text: ask.resolution.reason, at: ask.resolution.at }],
32883
- ...datedBodies(replies)
32884
- ];
32885
- }
32886
- function eventTexts(event) {
32887
- let texts;
32888
- switch (event.type) {
32889
- case "ask.opened":
32890
- case "ask.anchor_refreshed":
32891
- case "ask.edited":
32892
- case "ask.handed_back":
32893
- case "ask.answered":
32894
- case "ask.resolved":
32895
- texts = [
32896
- event.payload.question,
32897
- event.payload.answer?.text,
32898
- event.payload.resolution?.reason
32899
- ];
32900
- break;
32901
- case "comment.created":
32902
- case "comment.answered":
32903
- case "comment.anchor_refreshed":
32904
- case "comment.edited":
32905
- case "comment.resolved":
32906
- case "comment.reopened":
32907
- case "suggestion.accepted":
32908
- case "suggestion.rejected":
32909
- case "message.created":
32910
- case "message.answered":
32911
- texts = [event.payload.body];
32912
- break;
32913
- default:
32914
- texts = [];
32915
- }
32916
- return texts.filter((text) => typeof text === "string" && text !== "").map((text) => ({ text, at: event.created_at }));
32917
- }
32918
32822
  function shownPictureLine(index, address, name, mimeType, bytes) {
32919
32823
  return `- image ${index}: ${address} (${name}, ${mimeType}, ${bytes.toLocaleString("en-US")} bytes)`;
32920
32824
  }
@@ -32975,15 +32879,15 @@ async function readPictures(client, addresses, shown) {
32975
32879
  const showing = [];
32976
32880
  const lines = ["Pictures:"];
32977
32881
  let shownBytes = 0;
32978
- const overBudget = `past this read's ${PICTURES_SHOWN_MAX_BYTES / 1024 / 1024} MiB of pictures; dispatch_doc_read shows it`;
32882
+ const overBudget = `past this read's ${PICTURES_SHOWN_MAX_BYTES / 1024 / 1024} MiB of pictures; dispatch doc-read shows it`;
32979
32883
  for (const address of addresses) {
32980
32884
  const skip = (reason) => lines.push(`- not shown: ${address} (${reason})`);
32981
32885
  if (shown?.has(address)) {
32982
- skip("shown earlier this session; dispatch_doc_read shows it again");
32886
+ skip("shown earlier this session; dispatch doc-read shows it again");
32983
32887
  continue;
32984
32888
  }
32985
32889
  if (images.length === PICTURES_SHOWN_MAX) {
32986
- skip(`past this read's ${PICTURES_SHOWN_MAX} pictures; dispatch_doc_read shows it`);
32890
+ skip(`past this read's ${PICTURES_SHOWN_MAX} pictures; dispatch doc-read shows it`);
32987
32891
  continue;
32988
32892
  }
32989
32893
  const target = pictureTarget(address);
@@ -33041,2190 +32945,130 @@ async function readPictures(client, addresses, shown) {
33041
32945
  return { lines, images, shown: showing };
33042
32946
  }
33043
32947
 
33044
- // ../envoy-client/src/search-answer.ts
33045
- function pageSummaryText(offset, count, total) {
33046
- return `showing ${offset + 1}-${offset + count} of ${total}`;
33047
- }
33048
- function searchResultLine(result, baseUrl) {
33049
- const href = new URL(result.href, baseUrl).toString();
33050
- const { owner } = result;
33051
- if (owner.kind === "document") {
33052
- const reference = dispatchDocumentRef(owner.project, owner.slug);
33053
- return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
33054
- }
33055
- const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
33056
- const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
33057
- return `${label}: ${snippetText(result.snippet)} -> ${href}`;
33058
- }
33059
- function searchAnswer(search, query, offset, configUrl) {
33060
- const results = search.results;
33061
- const count = results.length;
33062
- const lines = results.map((result) => searchResultLine(result, configUrl));
33063
- const noun = count === 1 ? "result" : "results";
33064
- const noResults = `No results for "${query}".`;
33065
- const degraded = search.degraded === SEARCH_DEGRADED_EMBEDDER_UNAVAILABLE ? "Searched by keyword only: meaning search was unavailable for this request." : undefined;
33066
- if (typeof search.total !== "number") {
33067
- if (offset !== undefined && offset > 0) {
33068
- throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
33069
- }
33070
- return {
33071
- text: count === 0 ? [noResults, ...degraded === undefined ? [] : [degraded]].join(`
33072
- `) : [
33073
- `${count} ${noun} for "${query}" (${search.took_ms} ms)`,
33074
- ...degraded === undefined ? [] : [degraded],
33075
- ...lines
33076
- ].join(`
33077
- `),
33078
- details: { query, results, ...degraded === undefined ? {} : { degraded: search.degraded } }
33079
- };
33080
- }
33081
- const { total, reachable, offset: pageOffset } = search;
33082
- const end = pageOffset + count;
33083
- 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;
33084
- const details = {
33085
- query,
33086
- results,
33087
- total,
33088
- reachable,
33089
- offset: pageOffset,
33090
- limit: search.limit,
33091
- ...degraded === undefined ? {} : { degraded: search.degraded }
33092
- };
33093
- if (count === 0) {
33094
- return {
33095
- text: total === 0 ? [noResults, ...degraded === undefined ? [] : [degraded]].join(`
33096
- `) : [
33097
- `No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
33098
- ...degraded === undefined ? [] : [degraded],
33099
- ...cut === undefined ? [] : [cut]
33100
- ].join(`
33101
- `),
33102
- details
33103
- };
33104
- }
33105
- const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
33106
- return {
33107
- text: [
33108
- `${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
33109
- ...degraded === undefined ? [] : [degraded],
33110
- ...cut === undefined ? [] : [cut],
33111
- ...lines,
33112
- ...end < reachable ? [`Next page: offset ${end}.`] : []
33113
- ].join(`
33114
- `),
33115
- details
33116
- };
32948
+ // ../envoy-client/src/dispatch-session-state.ts
32949
+ import {
32950
+ appendFileSync,
32951
+ closeSync,
32952
+ fstatSync,
32953
+ mkdirSync,
32954
+ openSync,
32955
+ readdirSync,
32956
+ readFileSync as readFileSync4,
32957
+ readSync,
32958
+ renameSync,
32959
+ rmSync,
32960
+ statSync,
32961
+ writeFileSync
32962
+ } from "fs";
32963
+ import { homedir as homedir2 } from "os";
32964
+ import { join as join3 } from "path";
32965
+
32966
+ // ../envoy-client/src/dispatch-cwd.ts
32967
+ import { execFile } from "child_process";
32968
+ import { promisify } from "util";
32969
+
32970
+ // ../envoy-client/src/machine.ts
32971
+ import { hostname as hostname3 } from "os";
32972
+ function machineID() {
32973
+ return process.env["ENVOY_MACHINE_ID"] || hostname3();
33117
32974
  }
33118
32975
 
32976
+ // ../envoy-client/src/dispatch-cwd.ts
32977
+ var execFileAsync = promisify(execFile);
32978
+
33119
32979
  // ../envoy-client/src/dispatch-execute.ts
33120
- function documentTopic(artifact) {
33121
- return documentTopicOf(artifact.project, artifact.slug);
33122
- }
33123
- function resolvedTopic(resolved) {
33124
- if (resolved.owner.kind === "project")
33125
- return documentTopic(resolved.artifact);
33126
- if (resolved.issue === undefined)
33127
- throw new Error("issue document is missing its issue");
33128
- return issueTopic(resolved.issue.key);
33129
- }
33130
- function notSubscribed(owner) {
33131
- return `(not subscribed to ${owner.label}; envoy_subscribe ${owner.topic} for every event on it)`;
33132
- }
33133
- function followsAsk(owner) {
33134
- return `You follow this ask: its answer and replies reach you directly. For every event on ${owner.label}: envoy_subscribe ${owner.topic}`;
33135
- }
33136
32980
  var triageAdviceShown = new Set;
33137
- var SPEC_CHECK_REMINDER = 'Has a fresh reader checked this spec? Each claim about how a system works today should trace to code read or a command run, and each requirement to the human\'s words or a cited fact. If not, have a fresh read-only subagent check it now and fix what it finds: on Oh My Pi, `task(agent="scout")`, or whatever read-only agent your host bundles; a Legion architect\'s own spec uses the Legion plugin\'s `plan-gap-analyst` agent instead, which only a Legion session has. See the `dispatch-first` skill, "Design in the spec".';
33138
- function renderAdvice(tool, key, advice, opts) {
33139
- if (advice === undefined)
33140
- return [];
33141
- const hasIssueAdvice = advice.issue_status !== undefined;
33142
- const openAsks = advice.your_open_asks;
33143
- const writesSinceHuman = advice.session_writes_since_human;
33144
- const lines = [];
33145
- const unparsed = advice.unparsed_openers;
33146
- if (unparsed !== undefined && unparsed.count > 0 && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
33147
- const quoted = unparsed.examples.map((example) => JSON.stringify(example)).join(", ");
33148
- const subject2 = unparsed.count === 1 ? "1 typed-block opening in this document is text, not a block" : `${unparsed.count} typed-block openings in this document are text, not blocks`;
33149
- lines.push(`${subject2}: ${quoted}. An opening like \`:::ask{\u2026}\` makes a block only as a line of its own, so as text it asks nobody. Mentioning the syntax on purpose? Put it in code. See the \`dispatch\` skill, "Decision blocks".`);
33150
- }
33151
- if (advice.decision_blocks === 0 && opts.isPrimarySpec === true && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
33152
- lines.push('This spec holds no ask blocks, so nothing here reaches a human\'s inbox. Want human feedback? See the `dispatch` skill, "Decision blocks".');
33153
- }
33154
- if (hasIssueAdvice && writesSinceHuman !== undefined && writesSinceHuman >= 3 && (tool === "dispatch_message" || tool === "dispatch_ask" || tool === "dispatch_comment" && opts.isAskReply !== true)) {
33155
- const middle = writesSinceHuman >= 6 ? "Stop posting here until a human replies." : "Progress ledger or scratchpad? If so, stop.";
33156
- lines.push(`You've sent ${writesSinceHuman} messages on ${key} with no human response. ${middle} See the \`dispatch\` skill, "Structure over stream".`);
33157
- }
33158
- if (advice.issue_status === "triage" && tool !== "dispatch_issue" && !(tool === "dispatch_issue_update" && opts.setsStatus === true) && !triageAdviceShown.has(key)) {
33159
- triageAdviceShown.add(key);
33160
- lines.push(`${key} is still in triage \u2014 nobody can see its development status. See the \`dispatch\` skill, "Issue status is yours to move".`);
33161
- }
33162
- if (hasIssueAdvice && openAsks !== undefined && openAsks.length > 0 && (tool === "dispatch_message" || tool === "dispatch_doc_edit" || tool === "dispatch_issue_update" || tool === "dispatch_comment" && opts.replyToOwnAsk !== true)) {
33163
- const askCount = Math.min(openAsks.length, 2);
33164
- for (let index = 0;index < askCount; index += 1) {
33165
- const ask = openAsks[index];
33166
- if (ask === undefined)
33167
- break;
33168
- lines.push(`You still have an open ask on ${key}: "${ask.question.slice(0, 80)}" (${ask.id}). Still needed? See the \`dispatch\` skill, "Close what you opened".`);
33169
- }
33170
- }
33171
- return lines;
32981
+ var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch doc-read); a dashboard URL on this Dispatch server is accepted too";
32982
+ var toolSchemas = new Map;
32983
+ function asObject(value) {
32984
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
33172
32985
  }
33173
- function renderSuggestions(suggestions, configUrl) {
33174
- if (suggestions === undefined)
33175
- return [];
33176
- if (suggestions.missing !== undefined) {
33177
- return [`Related-item search was skipped: ${suggestions.missing}.`];
33178
- }
33179
- const lines = [];
33180
- if (suggestions.related.length > 0) {
33181
- lines.push("Possibly related, found by search:");
33182
- for (const item of suggestions.related) {
33183
- lines.push(`- ${suggestionLabel(item)} \u2192 ${new URL(item.href, configUrl).toString()}`);
33184
- }
33185
- }
33186
- if (suggestions.decision !== undefined) {
33187
- const item = suggestions.decision;
33188
- const when = item.answered_at === undefined ? "" : ` on ${item.answered_at}`;
33189
- lines.push(`A past decision may already answer this: ${suggestionLabel(item)}, answered by ${item.answered_by}${when} \u2192 ${new URL(item.href, configUrl).toString()}`);
32986
+
32987
+ // ../envoy-client/src/dispatch-session-state.ts
32988
+ var SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
32989
+ var PRIVATE_DIR = 448;
32990
+ var PRIVATE_FILE = 384;
32991
+ var IDLE_SESSION_MS = 14 * 86400000;
32992
+ function sessionStateRoot(env) {
32993
+ if (env.DISPATCH_STATE_DIR)
32994
+ return env.DISPATCH_STATE_DIR;
32995
+ return join3(env.XDG_STATE_HOME || join3(homedir2(), ".local", "state"), "dispatch");
32996
+ }
32997
+ function sessionDirectory(env, sessionId) {
32998
+ if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
32999
+ throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
33000
+ }
33001
+ return join3(sessionStateRoot(env), "sessions", sessionId);
33002
+ }
33003
+ function ensureDirectory(dir) {
33004
+ mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR });
33005
+ }
33006
+ function writeAtomically(path2, text) {
33007
+ const temporary = `${path2}.${process.pid}.${Date.now()}.tmp`;
33008
+ writeFileSync(temporary, text, { mode: PRIVATE_FILE });
33009
+ renameSync(temporary, path2);
33010
+ }
33011
+ function readResultsSince(dir, offset, onMalformed) {
33012
+ const path2 = join3(dir, "results.jsonl");
33013
+ let fd;
33014
+ try {
33015
+ fd = openSync(path2, "r");
33016
+ } catch (error48) {
33017
+ if (hasErrnoCode(error48, "ENOENT"))
33018
+ return { entries: [], offset };
33019
+ throw error48;
33190
33020
  }
33191
- return lines;
33192
- }
33193
- function suggestionLabel(item) {
33194
- if (item.owner.kind === "issue") {
33195
- return `${item.owner.key} [${item.owner.status}] ${item.owner.title}`;
33021
+ const entries = [];
33022
+ let complete;
33023
+ try {
33024
+ const size = fstatSync(fd).size;
33025
+ if (size <= offset)
33026
+ return { entries: [], offset: size };
33027
+ const buffer = Buffer.alloc(size - offset);
33028
+ readSync(fd, buffer, 0, buffer.length, offset);
33029
+ complete = buffer.lastIndexOf(10) + 1;
33030
+ const lines = buffer.subarray(0, complete).toString("utf-8").split(`
33031
+ `);
33032
+ let firstLine;
33033
+ lines.forEach((line, index) => {
33034
+ if (line === "")
33035
+ return;
33036
+ try {
33037
+ entries.push(JSON.parse(line));
33038
+ } catch (error48) {
33039
+ firstLine ??= linesBefore(fd, offset) + 1;
33040
+ onMalformed(`${path2}:${firstLine + index}: skipped a ledger line that is not JSON (${messageFor(error48)})`);
33041
+ }
33042
+ });
33043
+ } finally {
33044
+ closeSync(fd);
33196
33045
  }
33197
- return `${item.owner.name} (${item.owner.project}/${item.owner.slug})`;
33046
+ return { entries, offset: offset + complete };
33198
33047
  }
33199
- function documentResultDetails(artifact) {
33200
- return {
33201
- project: artifact.project,
33202
- artifact: artifact.id,
33203
- document: documentLabel(artifact.project, artifact.slug)
33204
- };
33048
+ function linesBefore(fd, offset) {
33049
+ const prefix = Buffer.alloc(offset);
33050
+ readSync(fd, prefix, 0, offset, 0);
33051
+ let count = 0;
33052
+ for (const byte of prefix)
33053
+ if (byte === 10)
33054
+ count += 1;
33055
+ return count;
33205
33056
  }
33206
- function writeResultDetails(resolved, fields) {
33207
- if (resolved.owner.kind === "project") {
33208
- return { ...documentResultDetails(resolved.artifact), ...fields };
33057
+ function resultsEnd(dir) {
33058
+ try {
33059
+ return statSync(join3(dir, "results.jsonl")).size;
33060
+ } catch (error48) {
33061
+ if (hasErrnoCode(error48, "ENOENT"))
33062
+ return 0;
33063
+ throw error48;
33209
33064
  }
33210
- if (resolved.issue === undefined)
33211
- throw new Error("issue document is missing its issue");
33212
- return { issue: resolved.issue.key, ...fields };
33213
33065
  }
33214
- async function askOwnerDetails(client, ask, artifact) {
33215
- if (ask.issue_key !== null) {
33216
- return { issue: ask.issue_key, ask: ask.id };
33217
- }
33218
- if (ask.artifact_id === undefined || ask.artifact_id === null) {
33219
- throw new Error("document ask is missing its artifact ID");
33220
- }
33221
- const owner = artifact ?? await client.getArtifact(ask.artifact_id);
33222
- return { ...documentResultDetails(owner), ask: ask.id };
33223
- }
33224
- async function commentOwnerResult(client, comment, artifact) {
33225
- if (comment.issue_key !== null) {
33226
- return { label: comment.issue_key, details: { issue: comment.issue_key, comment: comment.id } };
33227
- }
33228
- if (comment.artifact_id === undefined || comment.artifact_id === null) {
33229
- throw new Error("document comment is missing its artifact ID");
33230
- }
33231
- const owner = artifact ?? await client.getArtifact(comment.artifact_id);
33232
- return {
33233
- label: documentLabel(owner.project, owner.slug),
33234
- details: { ...documentResultDetails(owner), comment: comment.id }
33235
- };
33236
- }
33237
- async function followedAskDetails(client, ask, artifact) {
33238
- return { ...await askOwnerDetails(client, ask, artifact), follows: { ask: ask.id } };
33239
- }
33240
- var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
33241
- var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
33242
- var bareIssueNumberPattern = /^[1-9][0-9]*$/;
33243
- var issueFreeTools = {
33244
- dispatch_issue: true,
33245
- dispatch_edit_ask: true,
33246
- dispatch_resolve_ask: true,
33247
- dispatch_resolve_comment: true,
33248
- dispatch_follow: true,
33249
- dispatch_search: true,
33250
- dispatch_issues: true,
33251
- dispatch_open_asks: true,
33252
- dispatch_whoami: true,
33253
- dispatch_architecture_sync: true
33254
- };
33255
- function canonicalExternalIssueRef(value) {
33256
- const match = value.trim().match(externalIssueRefPattern);
33257
- return match ? `${canonicalRepo(match[1] ?? "", match[2] ?? "")}#${match[3]}` : value;
33258
- }
33259
- function stringArg(args, name) {
33260
- const value = args[name];
33261
- if (typeof value !== "string")
33262
- throw new Error(`${name} is required`);
33263
- return value;
33264
- }
33265
- function optionalString(args, name) {
33266
- const value = args[name];
33267
- return typeof value === "string" ? value : undefined;
33268
- }
33269
- function optionalBoolean(args, name) {
33270
- const value = args[name];
33271
- return typeof value === "boolean" ? value : undefined;
33272
- }
33273
- function optionalNumber(args, name) {
33274
- const value = args[name];
33275
- return typeof value === "number" ? value : undefined;
33276
- }
33277
- function optionalPriority(args, name) {
33278
- const value = args[name];
33279
- if (value === null)
33280
- return null;
33281
- return typeof value === "number" ? value : undefined;
33282
- }
33283
- function optionalPriorityFilter(args, name) {
33284
- const value = args[name];
33285
- return Array.isArray(value) ? value.map((item) => item ?? "none") : undefined;
33286
- }
33287
- function optionalComponents(args, name) {
33288
- const value = args[name];
33289
- if (typeof value !== "object" || value === null)
33290
- return;
33291
- const input = value;
33292
- return {
33293
- mode: input.mode,
33294
- ...input.ids === undefined ? {} : { ids: input.ids },
33295
- ...input.reason === undefined ? {} : { reason: input.reason }
33296
- };
33297
- }
33298
- function componentsChange(input, after) {
33299
- switch (input.mode) {
33300
- case "explicit":
33301
- return `components -> explicit [${after.ids.join(", ")}]`;
33302
- case "none":
33303
- return `components -> none (${after.reason ?? input.reason ?? ""})`;
33304
- case "inherit":
33305
- return "components -> inherit";
33306
- }
33307
- }
33308
- var architectureComponentsAction = 'Review the `dispatch` skill, "Architecture components", to attach it to the parts it changes or mark it as non-architectural with a reason.';
33309
- async function architectureGuidance(client, project, components) {
33310
- if (components.mode === "none" || components.ids.length > 0)
33311
- return;
33312
- try {
33313
- if (await client.getArchitectureSource(project) === null) {
33314
- return;
33315
- }
33316
- return `Project ${project} has an architecture model, but this issue is not linked to any of its current components. ${architectureComponentsAction}`;
33317
- } catch (error48) {
33318
- return `Could not check whether project ${project} has an architecture model: ${messageFor(error48)}. ${architectureComponentsAction}`;
33319
- }
33320
- }
33321
- function isDuplicateCandidate(value) {
33322
- if (typeof value !== "object" || value === null)
33323
- return false;
33324
- const candidate = value;
33325
- return typeof candidate.key === "string" && typeof candidate.title === "string" && typeof candidate.status === "string" && typeof candidate.snippet === "string" && typeof candidate.shared_terms === "number" && typeof candidate.href === "string";
33326
- }
33327
- function duplicateCandidates(error48) {
33328
- if (error48.candidates === undefined || !error48.candidates.every(isDuplicateCandidate))
33329
- throw error48;
33330
- return error48.candidates;
33331
- }
33332
- function askUrgency(args) {
33333
- const value = args.urgency;
33334
- return ASK_URGENCIES.find((urgency) => urgency === value);
33335
- }
33336
- function questionWithRef(question, ref) {
33337
- return ref === undefined || question.includes(ref) ? question : `${question}
33338
-
33339
- Ref: ${ref}`;
33340
- }
33341
- function askQuestionWithRef(args) {
33342
- return questionWithRef(stringArg(args, "question"), optionalString(args, "ref"));
33343
- }
33344
- function askQuestionProblem(withRef) {
33345
- return withRef.length > ASK_QUESTION_MAX ? `question plus ref ${overCapMessage(withRef.length, ASK_QUESTION_MAX)}; shorten the question or drop the ref` : undefined;
33346
- }
33347
- var pictureSendingTools = {
33348
- dispatch_ask: true,
33349
- dispatch_comment: true,
33350
- dispatch_message: true
33351
- };
33352
- function refTarget(ref, kind, id) {
33353
- const ownerRef = ref.owner.kind === "issue" ? dispatchIssueRef(ref.owner.issue) : dispatchDocumentRef(ref.owner.project, `${ref.artifact}`);
33354
- return dispatchChildRef(ownerRef, kind, id);
33355
- }
33356
- var canonicalUUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
33357
- var compactUUIDPattern = /^[0-9a-f]{32}$/i;
33358
- function normalizeUUID(value) {
33359
- let compact = value;
33360
- if (value.slice(0, 9).toLowerCase() === "urn:uuid:") {
33361
- compact = value.slice(9);
33362
- } else if (value.length === 38 && value.startsWith("{") && value.endsWith("}")) {
33363
- compact = value.slice(1, -1);
33364
- }
33365
- if (canonicalUUIDPattern.test(compact))
33366
- compact = compact.replaceAll("-", "");
33367
- if (!compactUUIDPattern.test(compact))
33368
- return;
33369
- return `${compact.slice(0, 8)}-${compact.slice(8, 12)}-${compact.slice(12, 16)}-${compact.slice(16, 20)}-${compact.slice(20)}`.toLowerCase();
33370
- }
33371
- var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
33372
- var commentIdProblem = "comment must be a bare comment id or a dispatch://.../comment/<id> reference";
33373
- var messageIdProblem = "in_reply_to must be a full message id (uuid) or a dispatch://KEY/message/<id> reference";
33374
- function askId(args) {
33375
- const ask = stringArg(args, "ask");
33376
- return ask.startsWith("dispatch://") ? parseDispatchRef(ask)?.id ?? ask : ask;
33377
- }
33378
- function messageIdOf(value) {
33379
- const id = value.startsWith("dispatch://") ? (() => {
33380
- const reference = parseDispatchRef(value);
33381
- return reference?.kind === "message" ? reference.id : undefined;
33382
- })() : value;
33383
- return id === undefined ? undefined : normalizeUUID(id);
33384
- }
33385
- var idPrefixPattern = /^[0-9a-f][0-9a-f-]{7,}$/i;
33386
- function refOwnerName(ref) {
33387
- return ref.owner.kind === "issue" ? ref.owner.issue : `${ref.owner.project}/${ref.artifact}`;
33388
- }
33389
- async function resolveIdPrefix(tool, kind, id, ownerName, list) {
33390
- const fullID = normalizeUUID(id);
33391
- if (fullID !== undefined)
33392
- return fullID;
33393
- if (!idPrefixPattern.test(id)) {
33394
- throw new ToolInputError(tool, [
33395
- `${kind} id ${id} must be a full uuid or a prefix of at least 8 hex characters`
33396
- ]);
33397
- }
33398
- const prefix = id.toLowerCase();
33399
- const matches = (await list()).filter((item) => item.id.toLowerCase().startsWith(prefix));
33400
- if (matches.length === 1 && matches[0] !== undefined)
33401
- return matches[0].id;
33402
- throw new ToolInputError(tool, [
33403
- matches.length === 0 ? `${kind} id ${id} matches none of the ${kind}s on ${ownerName}; use the full id` : `${kind} id ${id} matches ${matches.length} ${kind}s on ${ownerName}; use the full id`
33404
- ]);
33405
- }
33406
- var askIdShapeProblem = "ask ids are uuids (a prefix of at least 8 hex characters works)";
33407
- async function sessionOpenAsks(client, sessionId) {
33408
- const session = sessionId?.trim();
33409
- if (!session)
33410
- throw new Error("host session id is required to read this session's open asks");
33411
- return (await client.openAsks(session)).asks;
33412
- }
33413
- function openAskHints(asks) {
33414
- if (asks.length === 0)
33415
- return "you have no open asks";
33416
- const hints = asks.slice(0, askHintLimit).map((ask) => `${ask.id.slice(0, 8)} \u2014 ${textHead(ask.question)}`);
33417
- return `your open asks: ${hints.join("; ")}`;
33418
- }
33419
- async function resolveAskArgument(tool, args, client, sessionId) {
33420
- const id = askId(args);
33421
- if (normalizeUUID(id) === undefined && !idPrefixPattern.test(id)) {
33422
- const asks = await sessionOpenAsks(client, sessionId).catch(() => {
33423
- return;
33424
- });
33425
- throw new ToolInputError(tool, [
33426
- asks === undefined ? askIdShapeProblem : `${askIdShapeProblem}; ${openAskHints(asks)}`
33427
- ]);
33428
- }
33429
- return resolveIdPrefix(tool, "ask", id, "this session", () => sessionOpenAsks(client, sessionId));
33430
- }
33431
- function messageInReplyTo(args) {
33432
- const inReplyTo = optionalString(args, "in_reply_to");
33433
- return inReplyTo === undefined ? undefined : messageIdOf(inReplyTo);
33434
- }
33435
- function argumentProblems(tool, args) {
33436
- const problems = [];
33437
- switch (tool) {
33438
- case "dispatch_ask": {
33439
- const question = optionalString(args, "question");
33440
- const ref = optionalString(args, "ref");
33441
- if (question !== undefined && ref !== undefined && question.length <= ASK_QUESTION_MAX) {
33442
- const problem = askQuestionProblem(questionWithRef(question, ref));
33443
- if (problem !== undefined)
33444
- problems.push(problem);
33445
- }
33446
- break;
33447
- }
33448
- case "dispatch_edit_ask":
33449
- case "dispatch_resolve_ask":
33450
- case "dispatch_follow": {
33451
- const ask = optionalString(args, "ask");
33452
- if (ask?.startsWith("dispatch://") && parseDispatchRef(ask)?.kind !== "ask") {
33453
- problems.push(askIdProblem);
33454
- }
33455
- break;
33456
- }
33457
- case "dispatch_resolve_comment": {
33458
- const comment = optionalString(args, "comment");
33459
- if (comment?.startsWith("dispatch://") && parseDispatchRef(comment)?.kind !== "comment") {
33460
- problems.push(commentIdProblem);
33461
- }
33462
- break;
33463
- }
33464
- case "dispatch_comment": {
33465
- if (optionalString(args, "quote") !== undefined && optionalString(args, "artifact") === undefined) {
33466
- problems.push("artifact is required when quote is supplied");
33467
- }
33468
- if (optionalString(args, "reply_to") !== undefined && optionalString(args, "reply_to_ask") !== undefined) {
33469
- problems.push("reply_to and reply_to_ask cannot both be set");
33470
- }
33471
- break;
33472
- }
33473
- case "dispatch_message": {
33474
- const inReplyTo = optionalString(args, "in_reply_to");
33475
- if (inReplyTo !== undefined && messageIdOf(inReplyTo) === undefined) {
33476
- problems.push(messageIdProblem);
33477
- }
33478
- break;
33479
- }
33480
- case "dispatch_read": {
33481
- const message = optionalString(args, "message");
33482
- if (message !== undefined && messageIdOf(message) === undefined) {
33483
- problems.push("message must be a full message id (uuid) or a dispatch://KEY/message/<id> reference");
33484
- }
33485
- break;
33486
- }
33487
- }
33488
- return problems;
33489
- }
33490
- function dispatchRefFromUrl(value, serverUrl) {
33491
- let url2;
33492
- let origin;
33493
- try {
33494
- url2 = new URL(value);
33495
- origin = new URL(serverUrl).origin;
33496
- } catch {
33497
- return;
33498
- }
33499
- if (url2.origin !== origin)
33500
- return;
33501
- const versionOf = (name) => {
33502
- const value2 = url2.searchParams.get(name);
33503
- return value2 !== null && /^[1-9][0-9]*$/.test(value2) ? `@v${value2}` : "";
33504
- };
33505
- const issuePage = url2.pathname.match(/^\/issues\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec|log|conversation|children)|\/artifacts\/([^/]+)|\/asks\/([^/]+)|\/comments\/([^/]+)|\/messages\/([^/]+))?\/?$/);
33506
- if (issuePage) {
33507
- const [, key, page, artifact, ask, comment, message] = issuePage;
33508
- const version2 = versionOf("v");
33509
- if (page === "spec" || artifact !== undefined) {
33510
- const item2 = itemFromSearch(url2.search);
33511
- if (item2 === null)
33512
- return;
33513
- if (item2 !== undefined)
33514
- return `dispatch://${key}/${item2.kind}/${item2.id}`;
33515
- }
33516
- if (page === "spec" && version2 !== "")
33517
- return `dispatch://${key}/artifact/spec${version2}`;
33518
- if (page !== undefined)
33519
- return `dispatch://${key}/${page === "conversation" ? "log" : page}`;
33520
- if (artifact !== undefined) {
33521
- return `dispatch://${key}/artifact/${decodeURIComponent(artifact)}${version2}`;
33522
- }
33523
- if (ask !== undefined)
33524
- return `dispatch://${key}/ask/${ask}`;
33525
- if (comment !== undefined)
33526
- return `dispatch://${key}/comment/${comment}`;
33527
- if (message !== undefined)
33528
- return `dispatch://${key}/message/${message}`;
33529
- return `dispatch://${key}`;
33530
- }
33531
- const agentArtifactPage = url2.pathname.match(/^\/agents\/([^/]+)\/artifacts\/([^/]+)\/?$/);
33532
- if (agentArtifactPage) {
33533
- const [, session, slug2] = agentArtifactPage;
33534
- try {
33535
- return `dispatch://agent/${decodeURIComponent(session ?? "")}/artifact/${decodeURIComponent(slug2 ?? "")}${versionOf("v")}`;
33536
- } catch {
33537
- return;
33538
- }
33539
- }
33540
- const documentPage = url2.pathname.match(/^\/projects\/([A-Z][A-Z0-9]{1,9})\/documents\/([^/]+)\/?$/);
33541
- if (!documentPage)
33542
- return;
33543
- const [, project, slug] = documentPage;
33544
- const document = `dispatch://${project}/artifact/${decodeURIComponent(slug ?? "")}${versionOf("version")}`;
33545
- const item = itemFromSearch(url2.search);
33546
- if (item === null)
33547
- return;
33548
- return item === undefined ? document : `${document}/${item.kind}/${item.id}`;
33549
- }
33550
- var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch_doc_read); a dashboard URL on this Dispatch server is accepted too";
33551
- var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
33552
- var toolSchemas = new Map;
33553
- function toolSchema(tool) {
33554
- const cached2 = toolSchemas.get(tool);
33555
- if (cached2 !== undefined)
33556
- return cached2;
33557
- const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
33558
- if (!spec)
33559
- throw new Error(`Unknown Dispatch tool: ${tool}`);
33560
- const schema = dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
33561
- toolSchemas.set(tool, schema);
33562
- return schema;
33563
- }
33564
- async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, problems) {
33565
- if (issueFreeTools[tool] === true)
33566
- return { args: input, ref: null, owner: null };
33567
- const refArgument = input.ref;
33568
- let ref = null;
33569
- let args = input;
33570
- if (typeof refArgument === "string") {
33571
- const refText = dispatchRefFromUrl(refArgument, serverUrl) ?? refArgument;
33572
- if (refText !== refArgument)
33573
- args = { ...input, ref: refText };
33574
- const agentArtifact = parseAgentArtifactRef(refText);
33575
- if (agentArtifact !== null) {
33576
- if (tool !== "dispatch_doc_read") {
33577
- problems.push(`ref ${refText} names a picture in a conversation on the Agents page; only dispatch_doc_read reads one`);
33578
- }
33579
- if (args.issue !== undefined || args.project !== undefined || args.artifact !== undefined) {
33580
- problems.push("a dispatch://agent/... ref names its upload alone: name no issue, project, or artifact with it");
33581
- }
33582
- return { args, ref: null, owner: null, agentArtifact };
33583
- }
33584
- ref = parseDispatchRef(refText);
33585
- if (ref === null) {
33586
- problems.push(tool === "dispatch_ask" && !refText.startsWith("dispatch://") ? "ref must be a dispatch:// reference" : refGrammarProblem);
33587
- }
33588
- }
33589
- const issueArgument = args.issue;
33590
- const projectArgument = args.project;
33591
- const artifactArgument = args.artifact;
33592
- const versionArgument = args.version;
33593
- if (issueArgument !== undefined && projectArgument !== undefined) {
33594
- problems.push("exactly one of issue and project is required");
33595
- }
33596
- if (typeof projectArgument === "string") {
33597
- if (!PROJECT_KEY_PATTERN.test(projectArgument)) {
33598
- problems.push("project must be a project key such as CORE");
33599
- }
33600
- const refDocument = ref?.owner.kind === "project" ? ref.artifact ?? ref.id : undefined;
33601
- if (ref?.owner.kind === "project" && (ref.owner.project !== projectArgument || artifactArgument !== undefined && artifactArgument !== refDocument)) {
33602
- problems.push("project and ref must name the same document");
33603
- }
33604
- return {
33605
- args: {
33606
- ...args,
33607
- ...artifactArgument === undefined && refDocument !== undefined ? { artifact: refDocument } : {},
33608
- ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
33609
- },
33610
- ref,
33611
- owner: { kind: "project", project: projectArgument }
33612
- };
33613
- }
33614
- if (ref?.owner.kind === "project") {
33615
- return {
33616
- args: {
33617
- ...args,
33618
- project: ref.owner.project,
33619
- ...artifactArgument === undefined ? { artifact: ref.artifact ?? ref.id } : {},
33620
- ...versionArgument === undefined && ref.version !== undefined ? { version: ref.version } : {}
33621
- },
33622
- ref,
33623
- owner: ref.owner
33624
- };
33625
- }
33626
- if (issueArgument !== undefined || ref !== null) {
33627
- const issue3 = typeof issueArgument === "string" ? canonicalExternalIssueRef(issueArgument) : ref?.owner.kind === "issue" ? ref.owner.issue : undefined;
33628
- return {
33629
- args: {
33630
- ...args,
33631
- ...issue3 === undefined ? {} : { issue: issue3 },
33632
- ...artifactArgument === undefined && (ref?.kind === "spec" || ref?.kind === "artifact") ? { artifact: ref.id } : {},
33633
- ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
33634
- },
33635
- ref,
33636
- owner: issue3 === undefined ? null : { kind: "issue", issue: issue3 }
33637
- };
33638
- }
33639
- const replyTarget = args.in_reply_to;
33640
- if (tool === "dispatch_message" && typeof replyTarget === "string" && !replyTarget.startsWith("dispatch://")) {
33641
- return { args, ref, owner: null };
33642
- }
33643
- if (tool === "dispatch_read" && typeof args.message === "string") {
33644
- return { args, ref, owner: null };
33645
- }
33646
- const legionIssue = env.LEGION_ISSUE;
33647
- if (!legionIssue) {
33648
- problems.push(ownerRequiredProblem);
33649
- return { args, ref, owner: null };
33650
- }
33651
- if (nativeIssueKeyPattern.test(legionIssue) || externalIssueRefPattern.test(legionIssue)) {
33652
- const issue3 = canonicalExternalIssueRef(legionIssue);
33653
- return { args: { ...args, issue: issue3 }, ref, owner: { kind: "issue", issue: issue3 } };
33654
- }
33655
- if (!bareIssueNumberPattern.test(legionIssue)) {
33656
- problems.push("LEGION_ISSUE must be a native issue key (e.g. LEGION-3), an external owner/repo#n reference, or a bare positive issue number");
33657
- return { args, ref, owner: null };
33658
- }
33659
- const repo2 = await resolveCwdRepo(cwd, exec);
33660
- if (!repo2) {
33661
- problems.push("issue is required; LEGION_ISSUE needs a GitHub repository in cwd");
33662
- return { args, ref, owner: null };
33663
- }
33664
- const issue2 = `${repo2}#${legionIssue}`;
33665
- return { args: { ...args, issue: issue2 }, ref, owner: { kind: "issue", issue: issue2 } };
33666
- }
33667
- function artifactByReference(artifacts, artifactReference, owner, canonical) {
33668
- const bySlug = artifacts.find((candidate) => candidate.slug === artifactReference);
33669
- if (canonical && bySlug !== undefined)
33670
- return bySlug;
33671
- const byId = artifacts.find((candidate) => candidate.id === artifactReference);
33672
- if (byId !== undefined)
33673
- return byId;
33674
- const byName = artifacts.filter((candidate) => candidate.name === artifactReference);
33675
- if (bySlug !== undefined) {
33676
- const named = byName.filter((candidate) => candidate.id !== bySlug.id && candidate.kind === "doc");
33677
- if (named.length > 0) {
33678
- throw new Error(documentReferenceProblem(artifactReference, [bySlug, ...named], owner, "id"));
33679
- }
33680
- return bySlug;
33681
- }
33682
- if (byName.length > 1) {
33683
- throw new Error(documentReferenceProblem(artifactReference, byName, owner, "slug"));
33684
- }
33685
- const [artifact] = byName;
33686
- if (artifact === undefined) {
33687
- throw new Error(documentReferenceProblem(artifactReference, artifacts, owner));
33688
- }
33689
- return artifact;
33690
- }
33691
- async function resolveArtifact(client, owner, artifactReference, { canonical = false } = {}) {
33692
- if (owner.kind === "project") {
33693
- if (artifactReference === undefined) {
33694
- throw new Error("artifact is required for a project document");
33695
- }
33696
- const routed = await client.getProjectArtifact(owner.project, artifactReference).catch((error48) => {
33697
- if (!dispatchAnswered(error48, 404))
33698
- throw error48;
33699
- return;
33700
- });
33701
- if (canonical && routed !== undefined)
33702
- return { owner, artifact: routed };
33703
- const unlinked = await client.listProjectArtifacts(owner.project, true);
33704
- const artifacts = routed === undefined ? unlinked : [routed, ...unlinked.filter((candidate) => candidate.id !== routed.id)];
33705
- return {
33706
- owner,
33707
- artifact: artifactByReference(artifacts, artifactReference, "project", canonical)
33708
- };
33709
- }
33710
- const issue2 = await client.getIssue(owner.issue);
33711
- let artifact;
33712
- if (artifactReference === undefined || artifactReference === "spec") {
33713
- artifact = issue2.artifacts.find((candidate) => candidate.primary || candidate.id === issue2.primary_artifact_id);
33714
- } else {
33715
- artifact = artifactByReference(issue2.artifacts, artifactReference, "issue", canonical);
33716
- }
33717
- if (!artifact) {
33718
- throw new Error(documentReferenceProblem(artifactReference ?? "spec", issue2.artifacts, "issue"));
33719
- }
33720
- return { owner, issue: issue2, artifact };
33721
- }
33722
- var documentHintLimit = 8;
33723
- function documentReferenceProblem(reference, documents, owner, use) {
33724
- const hints = documents.slice(0, documentHintLimit).map((document) => use === "id" ? `${document.id} (${document.slug}, ${document.name})` : `${document.slug} (${document.name})`);
33725
- const list = hints.length === 0 ? "none" : hints.join(", ");
33726
- return use === undefined ? `document "${reference}" not found by slug; this ${owner}'s documents: ${list}` : `"${reference}" names ${documents.length} documents on this ${owner}; use ${use === "id" ? "the id" : "a slug"}: ${list}`;
33727
- }
33728
- var askHintLimit = 8;
33729
- function askIDInputProblem(asks, scope) {
33730
- const hints = asks.slice(0, askHintLimit).map((ask) => `${ask.id.slice(0, 8)}\u2026 ${textHead(ask.question)}`);
33731
- return `ask IDs are UUIDs; use the full ask ID; ${scope} open asks: ${hints.length === 0 ? "none" : hints.join(", ")}`;
33732
- }
33733
- async function invalidReplyToAskProblem(client, owner, resolved) {
33734
- if (owner.kind === "issue") {
33735
- const issue2 = resolved?.issue ?? await client.getIssue(owner.issue);
33736
- return askIDInputProblem(issue2.open_asks, "this issue's");
33737
- }
33738
- if (resolved === undefined)
33739
- throw new Error("project document is missing its resolved artifact");
33740
- return askIDInputProblem(await client.getArtifactAsks(resolved.artifact.id, "open"), "this document's");
33741
- }
33742
- function anchor(artifact, args) {
33743
- const quote = optionalString(args, "quote");
33744
- if (quote === undefined)
33745
- return;
33746
- const occurrence = optionalNumber(args, "occurrence");
33747
- return { artifact: artifact.id, quote, ...occurrence === undefined ? {} : { occurrence } };
33748
- }
33749
- function toolActor(origin, input) {
33750
- return {
33751
- kind: "session",
33752
- id: input.sessionId ?? "unknown",
33753
- origin: {
33754
- ...origin,
33755
- host: input.host,
33756
- ...input.sessionTitle === undefined ? {} : { session_title: input.sessionTitle }
33757
- }
33758
- };
33759
- }
33760
- function approvalLine(artifact) {
33761
- const approval = artifact.approval;
33762
- if (approval === undefined || approval.state === "draft")
33763
- return;
33764
- switch (approval.state) {
33765
- case "awaiting": {
33766
- const turn = approval.waiting_on === undefined ? "" : `, waiting on ${approval.waiting_on}`;
33767
- return `Approval: awaiting${turn} (requested by ${approval.requested_by?.id ?? "unknown"}, ask ${approval.ask_id ?? "?"})`;
33768
- }
33769
- case "approved":
33770
- return `Approval: approved v${approval.version} by ${approval.by?.id ?? "unknown"}`;
33771
- case "stale":
33772
- return `Approval: approved v${approval.version} by ${approval.by?.id ?? "unknown"}, edited since (now v${approval.latest_version}) - request approval again once the human has agreed to every point in this version`;
33773
- case "changes_requested":
33774
- return `Approval: changes requested on v${approval.version} by ${approval.by?.id ?? "unknown"}: ${approval.reason ?? ""}`;
33775
- }
33776
- }
33777
- function componentsLine(components) {
33778
- const inherited = components.inherited_from === null ? "" : ` (inherited from ${components.inherited_from})`;
33779
- const retired = components.unknown.length === 0 ? "" : ` (retired: ${components.unknown.join(", ")})`;
33780
- switch (components.mode) {
33781
- case "explicit":
33782
- return `Components: ${components.ids.length === 0 ? "none live" : components.ids.join(", ")}${inherited}${retired}`;
33783
- case "none":
33784
- return `Components: none \u2014 ${components.reason ?? ""}${inherited}`;
33785
- case "inherit":
33786
- return "Components: unassigned";
33787
- }
33788
- }
33789
- function claimText(claim, titles) {
33790
- const holding = claimHolds(claim, titles);
33791
- const marker = holding === "unknown" ? " \xB7 liveness unknown" : holding === "lapsed" ? " \xB7 not running" : "";
33792
- return `${actorLabel(claim.actor, titles)} since ${claim.at}${marker}`;
33793
- }
33794
- async function liveSessionTitles(client, needed) {
33795
- if (!needed) {
33796
- return;
33797
- }
33798
- try {
33799
- const agents = await client.listAgents();
33800
- return new Map(agents.map((agent) => [agent.session_id, agent.title]));
33801
- } catch {
33802
- return;
33803
- }
33804
- }
33805
- function holdsSession(claim) {
33806
- return claim?.actor.kind === "session";
33807
- }
33808
- function routeHeldBySession(issue2) {
33809
- return issue2.route_status === "live" && issue2.route?.startsWith("role:") === true;
33810
- }
33811
- function routeText(issue2, titles) {
33812
- if (issue2.route === null)
33813
- return "none";
33814
- const holder = issue2.route_holder ?? null;
33815
- const reach = {
33816
- live: routeHeldBySession(issue2) && holder !== null ? ` (held by ${titles?.get(holder) ?? holder})` : "",
33817
- no_holder: issue2.route.startsWith("role:") ? " (nobody holds it right now)" : " (that session is not running right now)",
33818
- unknown: " (the Envoy listener did not answer, so whether it reaches anyone is unknown)"
33819
- };
33820
- return issue2.route + (issue2.route_status == null ? "" : reach[issue2.route_status]);
33821
- }
33822
- function progressText(progress, sep) {
33823
- if (progress === undefined)
33824
- return "";
33825
- const parts = [];
33826
- if (progress.tasks !== null)
33827
- parts.push(`tasks ${progress.tasks.done}/${progress.tasks.total}`);
33828
- if (progress.children !== null) {
33829
- parts.push(`children ${progress.children.done}/${progress.children.total}`);
33830
- }
33831
- return parts.join(sep);
33832
- }
33833
- function issueSummary(issue2, events, references, graph, titles) {
33834
- const asks = issue2.open_asks;
33835
- const spec = issue2.artifacts?.find((artifact) => artifact.primary);
33836
- const specApproval = spec === undefined ? undefined : approvalLine(spec);
33837
- if (issue2.priority === undefined)
33838
- throw new Error("Dispatch issue is missing priority");
33839
- if (issue2.assignee === undefined)
33840
- throw new Error("Dispatch issue is missing assignee");
33841
- if (issue2.components === undefined)
33842
- throw new Error("Dispatch issue is missing components");
33843
- if (issue2.claim === undefined)
33844
- throw new Error("Dispatch issue is missing claim");
33845
- if (issue2.external_links === undefined) {
33846
- throw new Error("Dispatch issue is missing external_links");
33847
- }
33848
- return [
33849
- `Title: ${issue2.title}`,
33850
- `Key: ${issue2.key}`,
33851
- `Status: ${issue2.status}`,
33852
- `Assignee: ${issue2.assignee ?? "unassigned"}`,
33853
- `Claimed by: ${issue2.claim === null ? "nobody" : claimText(issue2.claim, titles)}`,
33854
- ...issue2.priority === null ? [] : [`Priority: P${issue2.priority}`],
33855
- `Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
33856
- componentsLine(issue2.components),
33857
- `Route: ${routeText(issue2, titles)}`,
33858
- `Progress: ${progressText(issue2.progress, ", ") || "none"}`,
33859
- ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
33860
- "External links:",
33861
- ...issue2.external_links.length === 0 ? ["- none"] : issue2.external_links.map((link) => `- ${link.url}${link.kind === undefined ? "" : ` (${link.kind})`}`),
33862
- "Open asks:",
33863
- ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
33864
- "References:",
33865
- ...typeof references === "string" ? [`- ${references}`] : references.members.length === 0 ? ["- none"] : references.members.map(({ artifact, depth, via }) => `- ${artifact.project}/${artifact.slug} \xB7 depth ${depth} via ${via.kind} ${via.id}`),
33866
- ...typeof references === "string" || !references.truncated ? [] : ["- more references beyond 8 hops"],
33867
- "Events:",
33868
- ...events.length === 0 ? ["- none"] : events.map(eventLine),
33869
- ...graph
33870
- ].join(`
33871
- `);
33872
- }
33873
- function referenceLines(edges) {
33874
- if (typeof edges === "string")
33875
- return [`- ${edges}`];
33876
- if (edges.length === 0)
33877
- return ["- none"];
33878
- return edges.map((edge) => {
33879
- const excerpt2 = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
33880
- return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt2}${edge.created_at})`;
33881
- });
33882
- }
33883
- function unavailableReason(error48) {
33884
- return dispatchAnswered(error48, 404) ? "unavailable" : `unavailable: ${messageFor(error48)}`;
33885
- }
33886
- async function graphEdges(client, query) {
33887
- try {
33888
- return (await client.getReferences(query)).edges;
33889
- } catch (error48) {
33890
- return unavailableReason(error48);
33891
- }
33892
- }
33893
- async function issueReferencesOrUnavailable(client, key) {
33894
- try {
33895
- return await client.getIssueReferences(key);
33896
- } catch (error48) {
33897
- return unavailableReason(error48);
33898
- }
33899
- }
33900
- async function graphSections(client, ref) {
33901
- const [incoming, outgoing] = await Promise.all([
33902
- graphEdges(client, { to: ref }),
33903
- graphEdges(client, { from: ref })
33904
- ]);
33905
- return ["Referenced by:", ...referenceLines(incoming), "Links:", ...referenceLines(outgoing)];
33906
- }
33907
- function logSummary(issue2, events) {
33908
- return [
33909
- `Key: ${issue2.key}`,
33910
- "Events:",
33911
- ...events.length === 0 ? ["- none"] : events.map(eventLine)
33912
- ].join(`
33913
- `);
33914
- }
33915
- function eventHead(event) {
33916
- switch (event.type) {
33917
- case "ask.opened":
33918
- case "ask.anchor_refreshed":
33919
- case "ask.edited":
33920
- case "ask.handed_back":
33921
- case "ask.resolved":
33922
- return textHead(event.payload.question);
33923
- case "ask.answered": {
33924
- const previousAnswer = event.payload.previous_answer;
33925
- return `${textHead(event.payload.question)} -> ${textHead(askAnswerText(event.payload.answer))}${previousAnswer === undefined ? "" : ` (was: ${textHead(askAnswerText(previousAnswer))})`}`;
33926
- }
33927
- case "comment.created":
33928
- case "comment.answered":
33929
- case "comment.anchor_refreshed":
33930
- case "comment.edited":
33931
- case "comment.resolved":
33932
- case "comment.reopened":
33933
- case "suggestion.accepted":
33934
- case "suggestion.rejected":
33935
- case "message.created":
33936
- case "message.answered":
33937
- return textHead(event.payload.body);
33938
- case "artifact.version":
33939
- return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
33940
- case "issue.created":
33941
- case "issue.updated":
33942
- case "issue.closed":
33943
- return `status ${event.payload.status}`;
33944
- default:
33945
- return;
33946
- }
33947
- }
33948
- function actorText(actor) {
33949
- const service = actor.kind === "session" ? actor.service : undefined;
33950
- if (service === undefined) {
33951
- return `${actor.kind} ${actor.id}`;
33952
- }
33953
- return `${actor.kind} ${actor.id} (as ${serviceSubjectLabel(service)})`;
33954
- }
33955
- function eventLine(event) {
33956
- const head = eventHead(event);
33957
- return `- #${event.seq} ${event.type} \xB7 ${actorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
33958
- }
33959
- function childrenSummary(issue2) {
33960
- return [
33961
- `Key: ${issue2.key}`,
33962
- "Children:",
33963
- ...issue2.children.length === 0 ? ["- none"] : issue2.children.map((child) => `- ${child.key}: ${child.title} (${child.status})`)
33964
- ].join(`
33965
- `);
33966
- }
33967
- function askSummary({ ask, replies, answers }, graph) {
33968
- const answer = ask.answer;
33969
- const earlierAnswers = answers.slice(0, -1);
33970
- const chain = replies.flatMap((reply) => [
33971
- `${reply.id} \xB7 ${actorText(reply.author)}`,
33972
- `Body: ${reply.body}`
33973
- ]);
33974
- return [
33975
- `Question: ${ask.question}`,
33976
- ...anchorLines(ask),
33977
- "Options:",
33978
- ...ask.options.length === 0 ? ["- none"] : ask.options.map((option) => `- ${option.label}${option.description ? ` \u2014 ${option.description}` : ""}`),
33979
- `State: ${ask.state}`,
33980
- "Answer:",
33981
- ...answer === null ? ["- none"] : [
33982
- `- By: ${answer.user}`,
33983
- `- Selected: ${answer.selected.length === 0 ? "none" : answer.selected.join(", ")}`,
33984
- ...answer.text === null ? [] : [`- Text: ${answer.text}`]
33985
- ],
33986
- ...earlierAnswers.length === 0 ? [] : [
33987
- "Earlier answers:",
33988
- ...earlierAnswers.map((earlier) => `- ${earlier.at} \xB7 ${earlier.user} \xB7 ${askAnswerText(earlier)}`)
33989
- ],
33990
- ...ask.resolution === undefined ? [] : [
33991
- "Resolution:",
33992
- `- By: ${ask.resolution.actor.id}`,
33993
- `- Kind: ${ask.resolution.kind}`,
33994
- `- Reason: ${ask.resolution.reason}`
33995
- ],
33996
- "Replies:",
33997
- ...chain.length === 0 ? ["- none"] : chain,
33998
- ...graph
33999
- ].join(`
34000
- `);
34001
- }
34002
- function openAskAge(ageSeconds) {
34003
- const seconds = Math.max(0, Math.floor(ageSeconds));
34004
- if (seconds < 60)
34005
- return `${seconds}s`;
34006
- const minutes = Math.floor(seconds / 60);
34007
- if (minutes < 60)
34008
- return `${minutes}m${seconds % 60 === 0 ? "" : ` ${seconds % 60}s`}`;
34009
- const hours = Math.floor(minutes / 60);
34010
- if (hours < 24)
34011
- return `${hours}h${minutes % 60 === 0 ? "" : ` ${minutes % 60}m`}`;
34012
- const days = Math.floor(hours / 24);
34013
- return `${days}d${hours % 24 === 0 ? "" : ` ${hours % 24}h`}`;
34014
- }
34015
- function openAskOwner(ask) {
34016
- return "issue" in ask.owner ? `${ask.owner.issue.key}: ${ask.owner.issue.title}` : `${ask.owner.document.project} / ${ask.owner.document.name}`;
34017
- }
34018
- function openAskLine(ask, baseUrl) {
34019
- const priority = ask.priority === null ? "" : `P${ask.priority} \xB7 `;
34020
- return `- ${openAskAge(ask.age_seconds)} \xB7 ${priority}${openAskOwner(ask)} \xB7 ${ask.question} \xB7 ${new URL(ask.ref, baseUrl).toString()}`;
34021
- }
34022
- function formatOpenAsksSummary(response, baseUrl) {
34023
- const scope = "active open asks you authored on open issues and project documents";
34024
- if (response.count === 0) {
34025
- return ["There are no unanswered asks for this session.", `Scope: ${scope}.`].join(`
34026
- `);
34027
- }
34028
- const waitingOnHuman = response.asks.filter((ask) => ask.waiting_on === "human");
34029
- const waitingOnAgent = response.asks.filter((ask) => ask.waiting_on === "agent");
34030
- return [
34031
- `${response.count} unanswered ${response.count === 1 ? "ask" : "asks"} you authored on active issues and project documents.`,
34032
- "",
34033
- `Waiting on human (${waitingOnHuman.length}):`,
34034
- ...waitingOnHuman.map((ask) => openAskLine(ask, baseUrl)),
34035
- "",
34036
- `Waiting on agent (${waitingOnAgent.length}):`,
34037
- ...waitingOnAgent.map((ask) => openAskLine(ask, baseUrl))
34038
- ].join(`
34039
- `);
34040
- }
34041
- function commentSummary({ comment, replies }, graph) {
34042
- const root = [
34043
- `${comment.id} \xB7 ${actorText(comment.author)}`,
34044
- ...anchorLines(comment),
34045
- `Body: ${comment.body}`
34046
- ];
34047
- const chain = replies.flatMap((reply) => [
34048
- `${reply.id} \xB7 ${actorText(reply.author)}`,
34049
- ...anchorLines(reply),
34050
- `Body: ${reply.body}`
34051
- ]);
34052
- return [
34053
- "Comment:",
34054
- ...root,
34055
- "Reply chain:",
34056
- ...chain.length === 0 ? ["- none"] : chain,
34057
- ...graph
34058
- ].join(`
34059
- `);
34060
- }
34061
- function anchorLines(record2) {
34062
- return [
34063
- ...record2.anchor?.quote === undefined ? [] : [`> ${record2.anchor.quote}`],
34064
- ...record2.anchor_block === undefined ? [] : [`Position: ${positionText(record2.anchor_block)}`],
34065
- ...record2.anchor_block_error === undefined ? [] : [`Position: unavailable (${record2.anchor_block_error})`]
34066
- ];
34067
- }
34068
- function positionText(block) {
34069
- const { table, path: path2 } = block;
34070
- const segments = path2.map((entry) => `${entry.type}[${entry.index}]`);
34071
- if (table === undefined || table.row === null)
34072
- return segments.join(" \u203A ");
34073
- const tableAt = path2.findIndex((entry) => entry.type === "table");
34074
- const cells = table.cells ?? [];
34075
- const label = (table.column === null ? cells : cells.slice(0, table.column)).map((cell) => cell.trim()).filter((cell) => cell !== "" && !/^\d+$/.test(cell)).join(" \xB7 ");
34076
- const row = label === "" ? `row ${table.row}` : `row ${table.row} (${label})`;
34077
- const header = table.header === null || table.header === "" ? String(table.column) : table.header;
34078
- return [
34079
- ...segments.slice(0, tableAt + 1),
34080
- table.column === null ? row : `${row}, column ${header}`
34081
- ].join(" \u203A ");
34082
- }
34083
- function messageSummary({ message, replies }, graph) {
34084
- const root = [`${message.id} \xB7 ${actorText(message.author)}`, `Body: ${message.body}`];
34085
- const chain = replies.flatMap((reply) => [
34086
- `${reply.id} \xB7 ${actorText(reply.author)}`,
34087
- `Body: ${reply.body}`
34088
- ]);
34089
- return [
34090
- "Message:",
34091
- ...root,
34092
- "Reply chain:",
34093
- ...chain.length === 0 ? ["- none"] : chain,
34094
- ...graph
34095
- ].join(`
34096
- `);
34097
- }
34098
- async function openArtifactMarks(client, resolved) {
34099
- const asksPromise = resolved.owner.kind === "project" ? client.getArtifactAsks(resolved.artifact.id) : Promise.resolve(resolved.issue?.open_asks ?? []);
34100
- const commentsPromise = resolved.owner.kind === "project" ? client.getArtifactComments(resolved.artifact.id) : client.getComments(resolved.issue?.key ?? "", resolved.artifact.id);
34101
- const commentsResultPromise = commentsPromise.then((value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }));
34102
- const asks = await asksPromise;
34103
- const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
34104
- const commentsResult = await commentsResultPromise;
34105
- if (commentsResult.status === "rejected") {
34106
- if (dispatchAnswered(commentsResult.reason, 404))
34107
- return marks;
34108
- throw commentsResult.reason;
34109
- }
34110
- return [
34111
- ...marks,
34112
- ...commentsResult.value.filter((comment) => !comment.resolved && comment.anchor?.artifact_id === resolved.artifact.id).map((comment) => `comment ${comment.id}`)
34113
- ];
34114
- }
34115
- async function blockAsks(client, resolved, state) {
34116
- const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
34117
- return asks.filter((ask) => typeof ask.block_id === "string" && ask.block_artifact?.id === resolved.artifact.id);
34118
- }
34119
- async function readUploadedFile(client, artifact, details, requested, owner, shown) {
34120
- const latest = Math.max(0, ...artifact.versions.map((version2) => version2.number));
34121
- const number4 = requested ?? latest;
34122
- const of = number4 === latest ? "" : ` of ${latest}`;
34123
- const bytesRoute = `GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`;
34124
- const tooLarge = (mime, bytes) => ({
34125
- text: `${oversizePictureText(artifact.name, bytes)}, so dispatch_doc_read cannot show this ` + `uploaded ${mime} picture (version ${number4}${of}). ${bytesRoute}`,
34126
- details
34127
- });
34128
- const stated = artifact.versions.find((version2) => version2.number === number4);
34129
- if (artifact.kind === "image" && stated?.size !== undefined && stated.size > PICTURE_SHOWN_MAX_BYTES) {
34130
- return tooLarge(stated.mime ?? "image", stated.size);
34131
- }
34132
- const file2 = await client.fileVersion(artifact.id, number4);
34133
- const size = `${file2.bytes.length.toLocaleString("en-US")} bytes`;
34134
- if (artifact.kind === "image") {
34135
- const image = toolImage(file2.bytes);
34136
- if (image !== undefined) {
34137
- const address = `dispatch://${owner}/artifact/${artifact.slug}@v${number4}`;
34138
- shown?.add(address);
34139
- return {
34140
- text: `Picture ${artifact.name}: ${image.mimeType}, version ${number4}${of}, ${size}.
34141
- ` + shownPictureLine(1, address, artifact.name, image.mimeType, file2.bytes.length),
34142
- details,
34143
- images: [image]
34144
- };
34145
- }
34146
- if (sniffPictureType(file2.bytes) !== undefined)
34147
- return tooLarge(file2.mime, file2.bytes.length);
34148
- }
34149
- let text;
34150
- try {
34151
- text = new TextDecoder("utf-8", { fatal: true }).decode(file2.bytes);
34152
- } catch {
34153
- return {
34154
- text: `${artifact.name} is an uploaded ${file2.mime} file (version ${number4}${of}, ${size}) that is not ` + `UTF-8 text, so dispatch_doc_read cannot show it. ${bytesRoute}`,
34155
- details
34156
- };
34157
- }
34158
- return {
34159
- text: `File ${artifact.name}: ${file2.mime}, version ${number4}${of}, ${size}.
34160
-
34161
- ${text}`,
34162
- details
34163
- };
34164
- }
34165
- async function refuseOpenDecisionBlocks(client, tool, resolved) {
34166
- const artifact = resolved.artifact;
34167
- const latest = artifact.approval?.latest_version;
34168
- if (latest === undefined || latest < 1 || artifact.approval?.state === "approved")
34169
- return;
34170
- const blocks = (await client.artifactBlocks(artifact.id)).filter((block) => block.type === "ask");
34171
- if (blocks.length === 0)
34172
- return;
34173
- const [documentAsks, version2] = await Promise.all([
34174
- blockAsks(client, resolved),
34175
- client.docRead(artifact.id, latest)
34176
- ]);
34177
- const asks = new Map(documentAsks.map((ask) => [ask.block_id, ask]));
34178
- const lines = version2.markdown.split(`
34179
- `);
34180
- const open = blocks.flatMap((block) => {
34181
- const ask = asks.get(block.id);
34182
- const named = ask === undefined ? `block ${block.id}` : `${JSON.stringify(ask.question)} (block ${block.id}, ask ${ask.id})`;
34183
- const states = lines.filter((line) => line.includes(`ask{#${block.id} `) || line.includes(`ask{#${block.id}}`)).map((line) => /\bstate="(\w+)"/.exec(line)?.[1]);
34184
- if (states.length === 0)
34185
- return [`${named}, which version ${latest} does not hold yet`];
34186
- if (!states.includes("open") && states.some((state) => state !== undefined))
34187
- return [];
34188
- if (ask === undefined)
34189
- return [`${named}, whose ask Dispatch has not opened yet`];
34190
- if (ask.state === "open")
34191
- return [named];
34192
- const next = ask.state === "answered" ? "fold the answer into the text" : "write the decision into the text";
34193
- return [
34194
- `${named}, ${ask.state} but still open in version ${latest}: ${next} with dispatch_doc_edit, which writes a version that carries it`
34195
- ];
34196
- });
34197
- if (open.length === 0)
34198
- return;
34199
- const count = open.length === 1 ? "1 open decision block" : `${open.length} open decision blocks`;
34200
- throw new Error([
34201
- `${tool} was not called: ${artifact.name} (version ${latest}) has ${count}. Answering one writes a new version, which would move this request to that version and leave it waiting on you.`,
34202
- ...open.map((line) => `- ${line}`),
34203
- "Do not request approval over an open block, even when a human asked for it. Tell the human which block is open and ask them to answer it or to waive it. Once it is answered, fold the answer into the text with dispatch_doc_edit. If they waive it, close the block with dispatch_resolve_ask (kind resolved, their words as the reason) and write their decision into the text with dispatch_doc_edit. Then request approval again once the human has agreed to every point in the new version: the call opens the request, or hands an open one back to the human."
34204
- ].join(`
34205
- `));
34206
- }
34207
- async function refuseRemovingOpenDecisionBlocks(client, tool, resolved, ops) {
34208
- const removing = ops.filter((operation) => operation.block !== undefined && (operation.op === "delete" || operation.op === "retype" && operation.type !== "ask"));
34209
- if (removing.length === 0)
34210
- return;
34211
- const artifact = resolved.artifact;
34212
- const blocks = await client.artifactBlocks(artifact.id);
34213
- const askBlocks = blocks.filter((block) => block.type === "ask");
34214
- const removed = new Set;
34215
- for (const operation of removing) {
34216
- const target = blocks.find((block) => block.id === operation.block);
34217
- if (target === undefined)
34218
- continue;
34219
- for (const block of askBlocks) {
34220
- if (block.id === target.id || operation.op === "delete" && block.from >= target.from && block.to <= target.to) {
34221
- removed.add(block.id);
34222
- }
34223
- }
34224
- }
34225
- if (removed.size === 0)
34226
- return;
34227
- const asks = await blockAsks(client, resolved, "open");
34228
- const open = asks.filter((ask) => removed.has(ask.block_id));
34229
- if (open.length === 0)
34230
- return;
34231
- const [what, question] = open.length === 1 ? ["a decision block whose ask is", "question"] : [`${open.length} decision blocks whose asks are`, "questions"];
34232
- throw new Error([
34233
- `${tool} was not called: it would remove ${what} still open, and the human's ${question} would leave their Inbox unanswered.`,
34234
- ...open.map((ask) => `- ${JSON.stringify(ask.question)} (block ${ask.block_id}, ask ${ask.id})`),
34235
- "A decision block leaves the document once its ask is answered or resolved. Until then, reword it with replace, relocate it with move, or change its question, options, urgency or multiple with dispatch_edit_ask if you asked it; each keeps it."
34236
- ].join(`
34237
- `));
34238
- }
34239
- function refusalWithCode(error48, ...clauses) {
34240
- const suffix = clauses.filter((clause) => clause !== "").join("; ");
34241
- const joined = suffix === "" ? "" : `; ${suffix}`;
34242
- if (error48 instanceof DispatchGatewayError) {
34243
- let told = error48.message;
34244
- if (joined !== "") {
34245
- told = error48.mayHaveReachedDispatch ? `${error48.answer}${joined}` : `${error48.answer}, so ${error48.advice}${joined}`;
34246
- }
34247
- return new DispatchGatewayError(error48.status, error48.answer, error48.advice, `${error48.code}: ${told}`);
34248
- }
34249
- if (!(error48 instanceof DispatchServiceError))
34250
- return error48;
34251
- return new DispatchServiceError(error48.code, error48.status, `${error48.code}: ${error48.message}${joined}`, error48.candidates, error48.current, error48.mismatches);
34252
- }
34253
- function dispatchAnswered(error48, status) {
34254
- return error48 instanceof DispatchServiceError && error48.fromDispatch && error48.status === status;
34255
- }
34256
- function writeMayHaveLanded(error48) {
34257
- if (error48 instanceof DispatchGatewayError)
34258
- return error48.mayHaveReachedDispatch;
34259
- return !(error48 instanceof DispatchServiceError) || error48.status >= 500;
34260
- }
34261
- function withAccount(error48, account) {
34262
- const message = messageFor(error48);
34263
- const told = /[.!?]$/.test(message) ? `${message} ${account.charAt(0).toUpperCase()}${account.slice(1)}` : `${message}; ${account}`;
34264
- return new Error(told, { cause: error48 });
34265
- }
34266
- async function executeDispatchTool(input) {
34267
- const configUrl = input.config.url;
34268
- const configToken = input.config.token;
34269
- if (!input.config.enabled || !configUrl || !configToken) {
34270
- throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
34271
- }
34272
- const baseFetch = input.fetchImpl ?? fetch;
34273
- const fetchImpl = Object.assign(async (...args2) => {
34274
- try {
34275
- return await baseFetch(...args2);
34276
- } catch (error48) {
34277
- if (error48 instanceof TypeError) {
34278
- throw new Error(`Dispatch at ${configUrl} is unreachable: ${error48.message}. If the Dispatch URL changed, restart this agent process so it picks up the new configuration.`);
34279
- }
34280
- throw error48;
34281
- }
34282
- }, { preconnect: baseFetch.preconnect });
34283
- const env = input.env ?? process.env;
34284
- const exec = input.exec ?? defaultExec;
34285
- const problems = [];
34286
- const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec, configUrl, problems);
34287
- const ownerMissing = issueFreeTools[input.tool] !== true && ownerArguments.owner === null;
34288
- const schema = toolSchema(input.tool);
34289
- const parsed = schema.safeParse(ownerArguments.args, { reportInput: true });
34290
- if (!parsed.success) {
34291
- const issues = parsed.error.issues.filter((issue3) => !ownerMissing || !(issue3.code === "invalid_type" && issue3.path.length === 1 && issue3.path[0] === "issue" && issue3.input === undefined || issue3.code === "custom" && issue3.path.length === 0 && (issue3.message.startsWith("Exactly one of issue and project is required") || issue3.message.startsWith("issue is required unless in_reply_to"))));
34292
- problems.push(...formatZodIssues(issues, schema));
34293
- }
34294
- problems.push(...argumentProblems(input.tool, ownerArguments.args));
34295
- const pictures = pictureSendingTools[input.tool] === true ? await localPictures(ownerArguments.args.images, input.cwd, problems) : [];
34296
- if (problems.length > 0)
34297
- throw new ToolInputError(input.tool, problems);
34298
- const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
34299
- if (input.tool === "dispatch_open_asks") {
34300
- const client2 = dispatchClient();
34301
- const project = optionalString(ownerArguments.args, "project");
34302
- if (project !== undefined) {
34303
- const response2 = await client2.openAsksForProject(project);
34304
- return { text: formatOpenAsksSummary(response2, configUrl), details: { ...response2 } };
34305
- }
34306
- const sessionId = input.sessionId?.trim();
34307
- if (!sessionId)
34308
- throw new Error("host session id is required for dispatch_open_asks");
34309
- const response = await client2.openAsks(sessionId);
34310
- return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
34311
- }
34312
- if (input.tool === "dispatch_whoami") {
34313
- const sessionId = input.sessionId?.trim();
34314
- if (!sessionId)
34315
- throw new Error("host session id is required for dispatch_whoami");
34316
- const client2 = dispatchClient();
34317
- const identity = await client2.whoami();
34318
- const owner2 = identity.kind === "agent" ? identity.owner : identity.login.toLowerCase();
34319
- const service = identity.kind === "agent" ? identity.service ?? null : null;
34320
- const unowned = "no owner, so issues you create without an assignee are unassigned (or inherit their parent's).";
34321
- return {
34322
- text: owner2 !== null ? `Session ${sessionId} acts for ${owner2}: issues you create without an assignee are assigned to ${owner2}.` : service !== null ? `Session ${sessionId} runs as service ${serviceSubjectLabel(service)}: ${unowned}` : `Session ${sessionId} runs under the shared token: ${unowned}`,
34323
- details: { session: sessionId, owner: owner2, service }
34324
- };
34325
- }
34326
- const args = parsed.success ? parsed.data : ownerArguments.args;
34327
- const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
34328
- const client = dispatchClient();
34329
- const refDocument = ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : ownerArguments.ref?.artifact;
34330
- const resolveDocument = (documentOwner2, reference) => resolveArtifact(client, documentOwner2, reference, {
34331
- canonical: reference !== undefined && reference === refDocument
34332
- });
34333
- const owner = ownerArguments.owner?.kind === "issue" ? {
34334
- kind: "issue",
34335
- issue: await resolveExistingIssue(client, ownerArguments.owner.issue)
34336
- } : ownerArguments.owner;
34337
- const issue2 = () => {
34338
- if (owner?.kind !== "issue")
34339
- throw new Error("issue is required");
34340
- return owner.issue;
34341
- };
34342
- const documentOwner = () => {
34343
- if (owner === null)
34344
- throw new Error("issue or project is required");
34345
- return owner;
34346
- };
34347
- const postWithPictures = (text, limit, owner2, upload) => textWithPictures(input.tool, text, pictures, limit, owner2, upload, actor);
34348
- const writePictures = (resolved, text, limit) => {
34349
- if (resolved?.owner.kind === "project") {
34350
- const { project } = resolved.owner;
34351
- return postWithPictures(text, limit, project, (upload) => client.projectArtifact(project, upload));
34352
- }
34353
- const issueKey = issue2();
34354
- return postWithPictures(text, limit, issueKey, (upload) => client.artifact(issueKey, upload));
34355
- };
34356
- const hostSession = input.sessionId?.trim();
34357
- const shown = hostSession ? shownPictures(hostSession) : undefined;
34358
- switch (input.tool) {
34359
- case "dispatch_issue": {
34360
- const project = stringArg(args, "project");
34361
- const title = stringArg(args, "title");
34362
- const parent = optionalString(args, "parent");
34363
- const external2 = optionalString(args, "external");
34364
- const force = optionalBoolean(args, "force");
34365
- const spec = optionalString(args, "spec");
34366
- const priority = optionalPriority(args, "priority");
34367
- const assignee = optionalString(args, "assignee");
34368
- const components = optionalComponents(args, "components");
34369
- const labels = args.labels;
34370
- try {
34371
- const created = await client.issue({
34372
- project,
34373
- title,
34374
- ...parent === undefined ? {} : { parent },
34375
- ...external2 === undefined ? {} : { external: external2 },
34376
- ...force === undefined ? {} : { force },
34377
- ...spec === undefined ? {} : { spec },
34378
- ...priority === undefined ? {} : { priority },
34379
- ...assignee === undefined ? {} : { assignee },
34380
- ...components === undefined ? {} : { components },
34381
- ...Array.isArray(labels) ? { labels } : {},
34382
- actor
34383
- });
34384
- const componentGuidance = await architectureGuidance(client, created.project, created.components);
34385
- const adviceLines = [
34386
- ...renderAdvice(input.tool, created.key, created.advice, {
34387
- isPrimarySpec: spec !== undefined
34388
- }),
34389
- ...componentGuidance === undefined ? [] : [componentGuidance],
34390
- ...renderSuggestions(created.advice?.suggestions, configUrl)
34391
- ];
34392
- return {
34393
- text: [
34394
- `Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
34395
- ...spec === undefined ? [] : [SPEC_CHECK_REMINDER],
34396
- ...adviceLines
34397
- ].join(`
34398
- `),
34399
- details: {
34400
- issue: created.key,
34401
- ...created.advice === undefined ? {} : { advice: created.advice }
34402
- }
34403
- };
34404
- } catch (error48) {
34405
- if (!(error48 instanceof DispatchServiceError) || error48.code !== "POSSIBLE_DUPLICATE") {
34406
- throw error48;
34407
- }
34408
- const candidates = duplicateCandidates(error48);
34409
- return {
34410
- text: [
34411
- `Not created: "${title}" looks like a duplicate.`,
34412
- ...candidates.map((candidate) => {
34413
- const href = new URL(candidate.href, configUrl).toString();
34414
- return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
34415
- }),
34416
- "Reference the existing issue, or call dispatch_issue again with force: true after reading it."
34417
- ].join(`
34418
- `),
34419
- details: { duplicates: candidates }
34420
- };
34421
- }
34422
- }
34423
- case "dispatch_issue_update": {
34424
- const issueKey = issue2();
34425
- const status = optionalString(args, "status");
34426
- const reason = optionalString(args, "reason");
34427
- const title = optionalString(args, "title");
34428
- const route = optionalString(args, "route");
34429
- const parent = optionalString(args, "parent");
34430
- const components = optionalComponents(args, "components");
34431
- const priority = optionalPriority(args, "priority");
34432
- const labels = Array.isArray(args.labels) ? args.labels : undefined;
34433
- const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
34434
- let before;
34435
- try {
34436
- before = await client.getIssue(issueKey);
34437
- } catch (error48) {
34438
- throw refusalWithCode(error48);
34439
- }
34440
- let closingNote;
34441
- if (reason !== undefined) {
34442
- try {
34443
- const message = await client.message(issueKey, { body: reason, actor });
34444
- closingNote = {
34445
- id: message.id,
34446
- ref: dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id)
34447
- };
34448
- } catch (error48) {
34449
- const told = writeMayHaveLanded(error48) ? "the reason may or may not have been posted, and the close was not sent: read the issue's messages before retrying, since retrying this call posts its reason again" : "the reason was not posted, so the close was not sent";
34450
- if (error48 instanceof DispatchServiceError)
34451
- throw refusalWithCode(error48, told);
34452
- throw withAccount(error48, told);
34453
- }
34454
- }
34455
- const linked = before.external_links.map((link) => link.url);
34456
- const newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
34457
- let after;
34458
- try {
34459
- after = await client.updateIssue(issueKey, {
34460
- ...status === undefined ? {} : { status },
34461
- ...title === undefined ? {} : { title },
34462
- ...labels === undefined ? {} : { labels },
34463
- ...priority === undefined ? {} : { priority },
34464
- ...route === undefined ? {} : { route },
34465
- ...parent === undefined ? {} : { parent: parent === "" ? null : parent },
34466
- ...components === undefined ? {} : { components },
34467
- ...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url2) => ({ url: url2 }))] },
34468
- actor
34469
- });
34470
- } catch (error48) {
34471
- const taken = dispatchAnswered(error48, 500) && newLinks.length > 0 ? `one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
34472
- if (closingNote === undefined)
34473
- throw refusalWithCode(error48, taken);
34474
- const posted = `the reason already landed as message ${closingNote.id} (${closingNote.ref})`;
34475
- const fix = error48 instanceof DispatchGatewayError && error48.transient ? "" : "fix what refused the close, then ";
34476
- const landed = writeMayHaveLanded(error48) ? `${posted}, and the close may or may not have taken effect. Read the issue's status before retrying: done means it closed; otherwise retry with a reason that points at message ${closingNote.id}, since retrying this call posts its reason again` : `${posted} but the issue did not close. Retrying this call posts its reason again, so ${fix}retry with a reason that points at message ${closingNote.id}`;
34477
- if (error48 instanceof DispatchServiceError)
34478
- throw refusalWithCode(error48, taken, landed);
34479
- throw withAccount(error48, landed);
34480
- }
34481
- const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
34482
- const changes = [
34483
- ...closingNote === undefined ? [] : [`reason posted as message ${closingNote.id} (${closingNote.ref})`],
34484
- ...status === undefined ? [] : [`status ${before.status} -> ${after.status}`],
34485
- ...title === undefined ? [] : [`title "${after.title}"`],
34486
- ...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
34487
- ...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
34488
- ...requestedLinks === undefined ? [] : [
34489
- newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
34490
- ],
34491
- ...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
34492
- ...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
34493
- ...components === undefined ? [] : [componentsChange(components, after.components)]
34494
- ];
34495
- const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
34496
- setsStatus: status !== undefined
34497
- });
34498
- return {
34499
- text: [
34500
- `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
34501
- ...adviceLines
34502
- ].join(`
34503
- `),
34504
- details: {
34505
- issue: after.key,
34506
- status: after.status,
34507
- external_links: after.external_links.map((link) => link.url),
34508
- ...closingNote === undefined ? {} : { message: closingNote.id },
34509
- ...after.advice === undefined ? {} : { advice: after.advice }
34510
- }
34511
- };
34512
- }
34513
- case "dispatch_claim": {
34514
- const issueKey = issue2();
34515
- const release = optionalBoolean(args, "release") ?? false;
34516
- let after;
34517
- try {
34518
- after = release ? await client.releaseIssueClaim(issueKey, { actor }) : await client.claimIssue(issueKey, { actor });
34519
- } catch (error48) {
34520
- throw refusalWithCode(error48);
34521
- }
34522
- const held = after.claim;
34523
- let text;
34524
- if (release) {
34525
- text = `${issueKey}: claim released; nobody is working it now. Its status is still ${after.status} \u2014 move it yourself if that is no longer where the work is.`;
34526
- } else {
34527
- if (held === null) {
34528
- throw new Error(`Dispatch claimed ${issueKey} but answered with no claim`);
34529
- }
34530
- text = `${issueKey}: claimed by you since ${held.at}. Its status is ${after.status}; a claim moves nothing, so move it to in_progress with dispatch_issue_update when you start, and release the claim when you stop.`;
34531
- }
34532
- return {
34533
- text: [text, notSubscribed(issueTopic(issueKey))].join(`
34534
- `),
34535
- details: { issue: issueKey, status: after.status, claim: held }
34536
- };
34537
- }
34538
- case "dispatch_search": {
34539
- const query = stringArg(args, "query");
34540
- const project = optionalString(args, "project");
34541
- const limit = optionalNumber(args, "limit");
34542
- const offset = optionalNumber(args, "offset");
34543
- const search = await client.search(query, {
34544
- ...project === undefined ? {} : { project },
34545
- ...limit === undefined ? {} : { limit },
34546
- ...offset === undefined ? {} : { offset }
34547
- });
34548
- return searchAnswer(search, query, offset, configUrl);
34549
- }
34550
- case "dispatch_issues": {
34551
- const project = stringArg(args, "project");
34552
- const status = optionalString(args, "status");
34553
- const parent = optionalString(args, "parent");
34554
- const label = optionalString(args, "label");
34555
- const priority = optionalPriorityFilter(args, "priority");
34556
- const updatedSince = optionalString(args, "updated_since");
34557
- const routeStatus = optionalString(args, "route_status");
34558
- const page = await client.listIssuePage({
34559
- project,
34560
- ...status === undefined ? {} : { status },
34561
- ...parent === undefined ? {} : { parent },
34562
- ...label === undefined ? {} : { label },
34563
- ...priority === undefined ? {} : { priority },
34564
- ...updatedSince === undefined ? {} : { updated_since: updatedSince },
34565
- ...routeStatus === undefined ? {} : { route_status: routeStatus }
34566
- }, {
34567
- limit: optionalNumber(args, "limit") ?? DEFAULT_ISSUE_PAGE_LIMIT,
34568
- offset: optionalNumber(args, "offset") ?? 0
34569
- });
34570
- const { total, limit, offset } = page;
34571
- const rows = page.issues.map((row) => ({
34572
- key: row.key,
34573
- title: row.title,
34574
- status: row.status,
34575
- priority: row.priority,
34576
- parent: row.parent,
34577
- labels: row.labels ?? [],
34578
- open_asks: row.open_asks,
34579
- claim: row.claim ?? null,
34580
- route: row.route ?? null,
34581
- route_status: row.route_status ?? null,
34582
- route_holder: row.route_holder ?? null,
34583
- updated_at: row.updated_at,
34584
- progress: row.progress
34585
- }));
34586
- const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
34587
- const isPartial = offset !== 0 || rows.length !== total;
34588
- const showing = !isPartial ? "" : rows.length === 0 ? `showing 0-0 of ${total}` : pageSummaryText(offset, rows.length, total);
34589
- return {
34590
- text: rows.length === 0 ? `No issues in ${project}.${isPartial ? ` (${showing})` : ""}` : [
34591
- `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (isPartial ? ` (${showing})` : ""),
34592
- ...rows.map((row) => {
34593
- const progress = progressText(row.progress, " \xB7 ");
34594
- return `${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)}`) + (progress === "" ? "" : ` \xB7 ${progress}`) + (row.route === null || row.route_status === "live" || row.route_status === null ? "" : ` \xB7 route ${routeText(row)}`);
34595
- })
34596
- ].join(`
34597
- `),
34598
- details: { issues: rows, total, offset, limit }
34599
- };
34600
- }
34601
- case "dispatch_architecture_sync": {
34602
- const project = stringArg(args, "project");
34603
- const source = await client.syncArchitectureSource(project);
34604
- const at = source.last_sync_at ?? "unknown time";
34605
- return {
34606
- text: source.last_error === null ? `Synced ${project} architecture from ${source.repo}@${source.branch}: commit ${source.last_commit ?? "unknown"} (${at}).` : [
34607
- `Sync failed for ${project} (${source.repo}@${source.branch}): ${source.last_error}`,
34608
- source.last_commit === null ? "No model has ever imported for this project." : `The previous model stays up (commit ${source.last_commit}).`
34609
- ].join(`
34610
- `),
34611
- details: {
34612
- project,
34613
- repo: source.repo,
34614
- branch: source.branch,
34615
- commit: source.last_commit,
34616
- error: source.last_error
34617
- }
34618
- };
34619
- }
34620
- case "dispatch_resolve_ask": {
34621
- const kind = stringArg(args, "kind");
34622
- const id = await resolveAskArgument(input.tool, args, client, input.sessionId);
34623
- const ask = await client.resolveAsk(id, {
34624
- kind,
34625
- reason: stringArg(args, "reason"),
34626
- actor
34627
- });
34628
- if (ask.resolution === undefined)
34629
- throw new Error("resolved ask is missing its resolution");
34630
- return {
34631
- text: `${kind === "retracted" ? "Retracted" : "Resolved"} ask ${ask.id}: ${ask.resolution.reason}`,
34632
- details: await askOwnerDetails(client, ask)
34633
- };
34634
- }
34635
- case "dispatch_resolve_comment": {
34636
- const reference = stringArg(args, "comment");
34637
- const ref = reference.startsWith("dispatch://") ? parseDispatchRef(reference) : null;
34638
- let id = reference;
34639
- let document;
34640
- if (ref?.owner.kind === "issue") {
34641
- const issueKey = ref.owner.issue;
34642
- id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), () => client.getComments(issueKey));
34643
- } else if (ref !== null) {
34644
- const artifact = (await resolveArtifact(client, ref.owner, ref.artifact, { canonical: true })).artifact;
34645
- document = artifact;
34646
- id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), () => client.getArtifactComments(artifact.id));
34647
- }
34648
- const comment = await client.resolveComment(id, actor);
34649
- const { label, details } = await commentOwnerResult(client, comment, document);
34650
- return { text: `Resolved comment ${comment.id} on ${label}.`, details };
34651
- }
34652
- case "dispatch_ask": {
34653
- const anchorArgs = asObject(args.anchor);
34654
- const owner2 = documentOwner();
34655
- const artifactReference = optionalString(args, "artifact") ?? (anchorArgs === null ? undefined : optionalString(anchorArgs, "artifact"));
34656
- const resolved = owner2.kind === "project" || artifactReference === undefined ? owner2.kind === "project" ? await resolveDocument(owner2, artifactReference) : undefined : await resolveDocument(owner2, artifactReference);
34657
- const options = args.options;
34658
- const multiple = optionalBoolean(args, "multiple");
34659
- const urgency = askUrgency(args);
34660
- const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
34661
- const withRef = askQuestionWithRef(args);
34662
- const refAppended = withRef !== stringArg(args, "question");
34663
- const question = await writePictures(resolved, withRef, {
34664
- cap: ASK_QUESTION_MAX,
34665
- text: refAppended ? "question plus ref" : "question",
34666
- shorten: refAppended ? "shorten the question, drop the ref" : "shorten the question"
34667
- });
34668
- const askInput = {
34669
- question,
34670
- ...Array.isArray(options) ? { options } : {},
34671
- ...multiple === undefined ? {} : { multiple },
34672
- ...urgency === undefined ? {} : { urgency },
34673
- ...anchored === undefined ? {} : { anchor: anchored },
34674
- actor
34675
- };
34676
- const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue2(), askInput);
34677
- const askOwner = ask.issue_key !== null ? issueTopic(ask.issue_key) : resolved === undefined ? issueTopic(issue2()) : documentTopic(resolved.artifact);
34678
- const adviceLines = [
34679
- ...renderAdvice(input.tool, askOwner.label, ask.advice, {}),
34680
- ...renderSuggestions(ask.advice?.suggestions, configUrl)
34681
- ];
34682
- return {
34683
- text: [
34684
- `Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
34685
- ${followsAsk(askOwner)}`,
34686
- ...adviceLines
34687
- ].join(`
34688
- `),
34689
- details: {
34690
- ...await followedAskDetails(client, ask, resolved?.artifact),
34691
- ...ask.advice === undefined ? {} : { advice: ask.advice }
34692
- }
34693
- };
34694
- }
34695
- case "dispatch_edit_ask": {
34696
- const question = optionalString(args, "question");
34697
- const options = args.options;
34698
- const multiple = optionalBoolean(args, "multiple");
34699
- const urgency = askUrgency(args);
34700
- const id = await resolveAskArgument(input.tool, args, client, input.sessionId);
34701
- const ask = await client.editAsk(id, {
34702
- ...question === undefined ? {} : { question },
34703
- ...Array.isArray(options) ? { options } : {},
34704
- ...multiple === undefined ? {} : { multiple },
34705
- ...urgency === undefined ? {} : { urgency },
34706
- actor
34707
- });
34708
- return {
34709
- text: `Ask edited: ${ask.question}`,
34710
- details: await askOwnerDetails(client, ask)
34711
- };
34712
- }
34713
- case "dispatch_comment": {
34714
- const artifactReference = optionalString(args, "artifact");
34715
- const owner2 = documentOwner();
34716
- const resolved = owner2.kind === "project" || artifactReference === undefined ? owner2.kind === "project" ? await resolveDocument(owner2, artifactReference) : undefined : await resolveDocument(owner2, artifactReference);
34717
- const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
34718
- const replyTo = optionalString(args, "reply_to");
34719
- const replyToAskReference = optionalString(args, "reply_to_ask");
34720
- const replyToAsk = replyToAskReference === undefined ? undefined : normalizeUUID(replyToAskReference);
34721
- if (replyToAskReference !== undefined && replyToAsk === undefined) {
34722
- throw new ToolInputError(input.tool, [
34723
- await invalidReplyToAskProblem(client, owner2, resolved)
34724
- ]);
34725
- }
34726
- const requestedTurn = optionalString(args, "turn");
34727
- const body = await writePictures(resolved, stringArg(args, "body"), {
34728
- cap: DISPATCH_BODY_MAX,
34729
- text: "body",
34730
- shorten: "shorten the body"
34731
- });
34732
- const commentInput = {
34733
- body,
34734
- ...anchored === undefined ? {} : { anchor: anchored },
34735
- ...replyTo === undefined ? {} : { reply_to: replyTo },
34736
- ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
34737
- ...requestedTurn === undefined ? {} : { turn: requestedTurn },
34738
- actor
34739
- };
34740
- const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue2(), commentInput);
34741
- const commentOwner2 = resolved === undefined ? issueTopic(issue2()) : resolvedTopic(resolved);
34742
- const commentDetails = resolved === undefined ? { issue: comment.issue_key, comment: comment.id } : writeResultDetails(resolved, { comment: comment.id });
34743
- const adviceLines = renderAdvice(input.tool, commentOwner2.label, comment.advice, {
34744
- isAskReply: replyToAsk !== undefined,
34745
- replyToOwnAsk: replyToAsk !== undefined && (comment.advice?.your_open_asks?.some((ask) => ask.id === replyToAsk) ?? false)
34746
- });
34747
- if (replyToAsk !== undefined) {
34748
- const askState = comment.ask_waiting_on === undefined ? "" : `; ask now waiting on ${comment.ask_waiting_on}`;
34749
- return {
34750
- text: [
34751
- `Replied on ask ${replyToAsk} (comment ${comment.id}${askState}). ${followsAsk(commentOwner2)}`,
34752
- ...adviceLines
34753
- ].join(`
34754
- `),
34755
- details: {
34756
- ...commentDetails,
34757
- ask: replyToAsk,
34758
- follows: { ask: replyToAsk },
34759
- ...comment.ask_waiting_on === undefined ? {} : { ask_waiting_on: comment.ask_waiting_on },
34760
- ...comment.advice === undefined ? {} : { advice: comment.advice }
34761
- }
34762
- };
34763
- }
34764
- return {
34765
- text: [`Posted comment ${comment.id} ${notSubscribed(commentOwner2)}`, ...adviceLines].join(`
34766
- `),
34767
- details: {
34768
- ...commentDetails,
34769
- ...comment.advice === undefined ? {} : { advice: comment.advice }
34770
- }
34771
- };
34772
- }
34773
- case "dispatch_suggest": {
34774
- const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
34775
- const anchored = anchor(resolved.artifact, args);
34776
- if (anchored === undefined)
34777
- throw new Error("quote is required");
34778
- const body = optionalString(args, "body");
34779
- const suggestionInput = {
34780
- ...body === undefined ? {} : { body },
34781
- anchor: anchored,
34782
- replace_with: stringArg(args, "replace_with"),
34783
- actor
34784
- };
34785
- const comment = resolved.owner.kind === "project" ? await client.artifactSuggest(resolved.artifact.id, suggestionInput) : await client.suggest(issue2(), suggestionInput);
34786
- return {
34787
- text: `Posted suggestion ${comment.id} ${notSubscribed(resolvedTopic(resolved))}`,
34788
- details: writeResultDetails(resolved, { comment: comment.id })
34789
- };
34790
- }
34791
- case "dispatch_message": {
34792
- const inReplyTo = messageInReplyTo(args);
34793
- const written = stringArg(args, "body");
34794
- const bodyLimit = {
34795
- cap: DISPATCH_BODY_MAX,
34796
- text: "body",
34797
- shorten: "shorten the body"
34798
- };
34799
- if (owner === null && inReplyTo !== undefined) {
34800
- let body2 = written;
34801
- if (pictures.length > 0) {
34802
- const sessionId = input.sessionId?.trim();
34803
- if (!sessionId) {
34804
- throw new Error("host session id is required to send pictures in a direct-message reply");
34805
- }
34806
- body2 = await postWithPictures(written, bodyLimit, `agent/${sessionId}`, (upload) => client.agentArtifact(sessionId, upload));
34807
- }
34808
- const reply = await client.messageReply(inReplyTo, { body: body2, attempt: 1, actor }, { followUp: true });
34809
- if (reply.duplicate === true && reply.body === body2) {
34810
- return {
34811
- text: `Dispatch already has this exact text in the conversation (message ${reply.id}); ` + "nothing new was posted. Send different text if you have more to say.",
34812
- details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
34813
- };
34814
- }
34815
- if (reply.body !== body2) {
34816
- return {
34817
- text: `Message ${inReplyTo} was already answered by message ${reply.id}; Dispatch kept ` + "that reply and posted nothing. Wait for their next message rather than answering " + "this one again.",
34818
- details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
34819
- };
34820
- }
34821
- const readBack = `dispatch_read({message: "${inReplyTo}"}) reads the conversation back.`;
34822
- const parent = reply.in_reply_to ?? undefined;
34823
- const follows = parent === inReplyTo ? undefined : parent;
34824
- return {
34825
- text: follows === undefined ? `Replied to message ${inReplyTo} with message ${reply.id}. ${readBack}` : `Replied to message ${inReplyTo} with message ${reply.id}, a follow-up threaded ` + `under your reply ${follows}. ${readBack}`,
34826
- details: {
34827
- message: reply.id,
34828
- in_reply_to: inReplyTo,
34829
- posted: true,
34830
- ...follows === undefined ? {} : { follows }
34831
- }
34832
- };
34833
- }
34834
- const issueKey = issue2();
34835
- const body = await postWithPictures(written, bodyLimit, issueKey, (upload) => client.artifact(issueKey, upload));
34836
- const message = await client.message(issueKey, {
34837
- body,
34838
- ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
34839
- actor
34840
- });
34841
- const messageRef = dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id);
34842
- const adviceLines = renderAdvice(input.tool, issueKey, message.advice, {});
34843
- return {
34844
- text: [
34845
- `Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
34846
- ...adviceLines
34847
- ].join(`
34848
- `),
34849
- details: {
34850
- issue: issueKey,
34851
- message: message.id,
34852
- ...message.advice === undefined ? {} : { advice: message.advice }
34853
- }
34854
- };
34855
- }
34856
- case "dispatch_doc_edit": {
34857
- const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
34858
- const ops = args.ops;
34859
- const summary = optionalString(args, "summary");
34860
- const { precondition: rawPrecondition } = args;
34861
- const precondition = rawPrecondition;
34862
- await refuseRemovingOpenDecisionBlocks(client, input.tool, resolved, ops);
34863
- const edited = await client.docEdit(resolved.artifact.id, {
34864
- ops,
34865
- ...summary === undefined ? {} : { summary },
34866
- ...precondition === undefined ? {} : { precondition },
34867
- actor
34868
- });
34869
- const retyped = ops.filter((operation) => operation.op === "retype").length;
34870
- const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
34871
- const retypedText = retyped === 0 ? "" : `; retyped ${retyped} block${retyped === 1 ? "" : "s"}`;
34872
- const nothingChanged = edited.changed === false;
34873
- const head = nothingChanged ? `Applied ${edited.applied} ops${retypedText}; nothing changed (${versionText})` : `Applied ${edited.applied} ops${retypedText} (${versionText})`;
34874
- const unchangedOps = edited.unchanged_ops ?? [];
34875
- const unchangedText = unchangedOps.length === 0 ? "" : `; ${unchangedOps.length === 1 ? "operation" : "operations"} ${unchangedOps.join(", ")} changed nothing`;
34876
- const lostOps = edited.lost_ops;
34877
- const lostText = lostOps === undefined || lostOps !== null && lostOps.length === 0 ? "" : lostOps === null ? "; could not confirm this edit survived, because the live document is being reloaded or holds a tree too deep to read \u2014 re-read it" : `; ${versionText} carries text the live document no longer has: a concurrent change removed what ${lostOps.length === 1 ? "operation" : "operations"} ${lostOps.join(", ")} wrote \u2014 re-read the document`;
34878
- const applied = `${head}${unchangedText}${lostText}`;
34879
- const adviceLines = renderAdvice(input.tool, resolvedTopic(resolved).label, edited.advice, {});
34880
- const tokenTrailer = edited.token === undefined ? [] : [`Document token: ${edited.token}`];
34881
- return {
34882
- text: [
34883
- `${applied} ${notSubscribed(resolvedTopic(resolved))}`,
34884
- ...tokenTrailer,
34885
- ...adviceLines
34886
- ].join(`
34887
- `),
34888
- details: writeResultDetails(resolved, {
34889
- applied: edited.applied,
34890
- ...edited.version === null ? {} : { version: edited.version.number },
34891
- ...nothingChanged ? { changed: false } : {},
34892
- ...lostOps === undefined ? {} : { lost_ops: lostOps },
34893
- ...edited.token === undefined ? {} : { token: edited.token },
34894
- ...edited.advice === undefined ? {} : { advice: edited.advice }
34895
- })
34896
- };
34897
- }
34898
- case "dispatch_doc_read": {
34899
- const { agentArtifact } = ownerArguments;
34900
- if (agentArtifact !== undefined) {
34901
- const artifact = await client.getAgentArtifact(agentArtifact.session, agentArtifact.slug);
34902
- return readUploadedFile(client, artifact, { session: agentArtifact.session, artifact: artifact.slug }, optionalNumber(args, "version") ?? agentArtifact.version, `agent/${agentArtifact.session}`, shown);
34903
- }
34904
- const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
34905
- const resolved = await resolveDocument(documentOwner(), artifactReference);
34906
- const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
34907
- if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
34908
- const { artifact } = resolved;
34909
- return readUploadedFile(client, artifact, resolved.owner.kind === "project" ? {
34910
- project: artifact.project,
34911
- document: documentLabel(artifact.project, artifact.slug)
34912
- } : { issue: resolved.issue?.key }, version2, resolved.owner.kind === "project" ? resolved.owner.project : resolved.owner.issue, shown);
34913
- }
34914
- const documentPromise = client.docRead(resolved.artifact.id, version2);
34915
- const marksPromise = openArtifactMarks(client, resolved);
34916
- const marksResultPromise = marksPromise.then((value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }));
34917
- const document = await documentPromise;
34918
- const marksResult = await marksResultPromise;
34919
- if (marksResult.status === "rejected")
34920
- throw marksResult.reason;
34921
- const marks = marksResult.value;
34922
- const approval = approvalLine(resolved.artifact);
34923
- const trailer = [
34924
- ..."token" in document && document.token !== undefined ? [`Document token: ${document.token}`] : [],
34925
- ...marks.length === 0 ? [] : [`Open anchored asks/comments: ${marks.join(", ")}`],
34926
- ...approval === undefined ? [] : [approval]
34927
- ];
34928
- return {
34929
- text: trailer.length === 0 ? document.markdown : `${document.markdown}
34930
-
34931
- ${trailer.join(`
34932
- `)}`,
34933
- details: resolved.owner.kind === "project" ? {
34934
- project: resolved.artifact.project,
34935
- document: `${resolved.artifact.project}/${resolved.artifact.slug}`
34936
- } : { issue: resolved.issue?.key }
34937
- };
34938
- }
34939
- case "dispatch_request_approval": {
34940
- const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
34941
- const resolved = await resolveDocument(documentOwner(), artifactReference);
34942
- await refuseOpenDecisionBlocks(client, input.tool, resolved);
34943
- const result = await client.requestApproval(resolved.artifact.id, {
34944
- actor,
34945
- summary: stringArg(args, "summary")
34946
- });
34947
- if (result.ask === null) {
34948
- return {
34949
- text: `${resolved.artifact.name} (document id ${resolved.artifact.id}) is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. An edit after approval makes it stale, so request again only for a new version, once the human has agreed to every point in it.`,
34950
- details: {
34951
- ...resolved.owner.kind === "project" ? documentResultDetails(resolved.artifact) : { issue: resolved.issue?.key },
34952
- artifact: resolved.artifact.id,
34953
- version: result.version
34954
- }
34955
- };
34956
- }
34957
- const details = await followedAskDetails(client, result.ask, resolved.artifact);
34958
- const outcome = result.recorded ? `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}).` : `The approval request for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}) already waits on the human, so this call changed nothing: nothing since it last reached the human (a newer version, a human's reply in its thread, or your progress note) left it waiting on you.`;
34959
- return {
34960
- text: `${outcome} The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested. An edit before the answer moves this request to the new version and leaves it waiting on you, and an edit after approval makes the approval stale: either way, request again for the new version once the human has agreed to every point in it, which hands this request back or opens a new one.${result.recorded ? `
34961
- ${SPEC_CHECK_REMINDER}` : ""}`,
34962
- details: { ...details, artifact: resolved.artifact.id, version: result.version }
34963
- };
34964
- }
34965
- case "dispatch_artifact": {
34966
- const summary = optionalString(args, "summary");
34967
- const name = stringArg(args, "name");
34968
- const content = optionalString(args, "content");
34969
- const artifactInput = content === undefined ? {
34970
- name,
34971
- file: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))),
34972
- ...summary === undefined ? {} : { summary },
34973
- actor
34974
- } : {
34975
- name,
34976
- content,
34977
- ...summary === undefined ? {} : { summary },
34978
- actor
34979
- };
34980
- const artifactOwner = documentOwner();
34981
- const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue2(), artifactInput);
34982
- const artifactRef = dispatchDocumentRef(artifactOwner.kind === "project" ? artifactOwner.project : issue2(), result.artifact.slug);
34983
- const uploadOwner = artifactOwner.kind === "project" ? documentTopic(result.artifact) : issueTopic(issue2());
34984
- const adviceLines = renderAdvice(input.tool, uploadOwner.label, result.advice, {
34985
- isPrimarySpec: result.artifact.primary || result.artifact.name === "spec.md"
34986
- });
34987
- return {
34988
- text: [
34989
- `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef}) ${notSubscribed(uploadOwner)}`,
34990
- ...adviceLines
34991
- ].join(`
34992
- `),
34993
- details: artifactOwner.kind === "project" ? {
34994
- ...documentResultDetails(result.artifact),
34995
- version: result.version.number,
34996
- ...result.advice === undefined ? {} : { advice: result.advice }
34997
- } : {
34998
- issue: issue2(),
34999
- artifact: result.artifact.id,
35000
- version: result.version.number,
35001
- ...result.advice === undefined ? {} : { advice: result.advice }
35002
- }
35003
- };
35004
- }
35005
- case "dispatch_follow": {
35006
- const sessionId = input.sessionId?.trim();
35007
- if (!sessionId)
35008
- throw new Error("host session id is required for dispatch_follow");
35009
- const ask = await resolveAskArgument(input.tool, args, client, sessionId);
35010
- const action = stringArg(args, "action");
35011
- if (action === "unfollow") {
35012
- await client.unfollowAsk(ask, sessionId, actor);
35013
- return { text: `Unfollowed ask ${ask}.`, details: { ask } };
35014
- }
35015
- const read = await client.getAsk(ask);
35016
- await client.followAsk(ask, sessionId, actor);
35017
- return {
35018
- text: `Following ask ${ask}: its answer and replies reach this session directly.`,
35019
- details: await followedAskDetails(client, read.ask)
35020
- };
35021
- }
35022
- case "dispatch_read": {
35023
- const message = optionalString(args, "message");
35024
- const picturesOf = (texts) => readPictures(client, picturesNewestFirst(texts), shown);
35025
- const showImages = (pictures3) => {
35026
- for (const address of pictures3.shown)
35027
- shown?.add(address);
35028
- return pictures3.images.length === 0 ? {} : { images: pictures3.images };
35029
- };
35030
- if (message !== undefined) {
35031
- const sessionId = input.sessionId?.trim();
35032
- if (!sessionId)
35033
- throw new Error("host session id is required for dispatch_read({message})");
35034
- const thread = await client.getMessageThread(messageIdOf(message), sessionId);
35035
- const issueKey2 = thread.message.issue_key;
35036
- const [pictures3, graph2] = await Promise.all([
35037
- picturesOf(datedBodies([thread.message, ...thread.replies])),
35038
- issueKey2 === null ? [] : graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))
35039
- ]);
35040
- return {
35041
- text: messageSummary(thread, [...pictures3.lines, ...graph2]),
35042
- details: {
35043
- message: thread.message.id,
35044
- ...issueKey2 === null ? {} : { issue: issueKey2 }
35045
- },
35046
- ...showImages(pictures3)
35047
- };
35048
- }
35049
- if (ownerArguments.ref?.kind === "ask") {
35050
- const ref = ownerArguments.ref;
35051
- const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveDocument(ref.owner, ref.artifact)).artifact.id, "all"));
35052
- const askRead = await client.getAsk(id);
35053
- const askRef = refTarget(ref, "ask", id);
35054
- const [pictures3, graph2] = await Promise.all([
35055
- picturesOf(askTexts(askRead)),
35056
- graphSections(client, askRef)
35057
- ]);
35058
- return {
35059
- text: askSummary(askRead, [...pictures3.lines, ...graph2]),
35060
- details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue },
35061
- ...showImages(pictures3)
35062
- };
35063
- }
35064
- if (ownerArguments.ref?.kind === "comment") {
35065
- const ref = ownerArguments.ref;
35066
- const id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveDocument(ref.owner, ref.artifact)).artifact.id));
35067
- const comment = await client.getComment(id);
35068
- const commentRef = refTarget(ref, "comment", id);
35069
- const [pictures3, graph2] = await Promise.all([
35070
- picturesOf(datedBodies([comment.comment, ...comment.replies])),
35071
- graphSections(client, commentRef)
35072
- ]);
35073
- return {
35074
- text: commentSummary(comment, [...pictures3.lines, ...graph2]),
35075
- details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key },
35076
- ...showImages(pictures3)
35077
- };
35078
- }
35079
- if (ownerArguments.ref?.kind === "message") {
35080
- if (ownerArguments.ref.owner.kind !== "issue") {
35081
- throw new Error("message references are issue-scoped");
35082
- }
35083
- const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
35084
- const messageRef = dispatchChildRef(dispatchIssueRef(ownerArguments.ref.owner.issue), "message", ownerArguments.ref.id);
35085
- const [pictures3, graph2] = await Promise.all([
35086
- picturesOf(datedBodies([messageRead.message, ...messageRead.replies])),
35087
- graphSections(client, messageRef)
35088
- ]);
35089
- return {
35090
- text: messageSummary(messageRead, [...pictures3.lines, ...graph2]),
35091
- details: { issue: messageRead.message.issue_key },
35092
- ...showImages(pictures3)
35093
- };
35094
- }
35095
- if (documentOwner().kind === "project") {
35096
- const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
35097
- const documentRef = dispatchDocumentRef(resolved.artifact.project, resolved.artifact.slug);
35098
- return {
35099
- text: [
35100
- `Document: ${resolved.artifact.project} / ${resolved.artifact.name}`,
35101
- `Reference: ${documentRef}`,
35102
- `Versions: ${resolved.artifact.versions.length}`,
35103
- ...approvalLine(resolved.artifact) === undefined ? [] : [approvalLine(resolved.artifact)],
35104
- ...await graphSections(client, documentRef)
35105
- ].join(`
35106
- `),
35107
- details: {
35108
- project: resolved.artifact.project,
35109
- document: documentLabel(resolved.artifact.project, resolved.artifact.slug)
35110
- }
35111
- };
35112
- }
35113
- const issueKey = issue2();
35114
- if (ownerArguments.ref?.kind === "log") {
35115
- const read2 = await client.read(issueKey);
35116
- const pictures3 = await picturesOf(read2.events.flatMap(eventTexts));
35117
- return {
35118
- text: [logSummary(read2.issue, read2.events), ...pictures3.lines].join(`
35119
- `),
35120
- details: { issue: read2.issue.key },
35121
- ...showImages(pictures3)
35122
- };
35123
- }
35124
- if (ownerArguments.ref?.kind === "children") {
35125
- const read2 = await client.read(issueKey);
35126
- return {
35127
- text: childrenSummary(read2.issue),
35128
- details: { issue: read2.issue.key }
35129
- };
35130
- }
35131
- const readPromise = client.read(issueKey);
35132
- const referencesPromise = issueReferencesOrUnavailable(client, issueKey);
35133
- const read = await readPromise;
35134
- const [references, graph, titles, pictures2] = await Promise.all([
35135
- referencesPromise,
35136
- graphSections(client, dispatchIssueRef(read.issue.key)),
35137
- liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue)),
35138
- picturesOf([
35139
- ...read.issue.open_asks.map((ask) => ({ text: ask.question, at: ask.created_at })),
35140
- ...read.events.flatMap(eventTexts)
35141
- ])
35142
- ]);
35143
- return {
35144
- text: issueSummary(read.issue, read.events, references, [...pictures2.lines, ...graph], titles),
35145
- details: { issue: read.issue.key },
35146
- ...showImages(pictures2)
35147
- };
35148
- }
35149
- default:
35150
- throw new Error(`Unknown Dispatch tool: ${input.tool}`);
35151
- }
35152
- }
35153
- async function resolveExistingIssue(client, issueReference) {
35154
- try {
35155
- return await client.resolveIssue(issueReference);
35156
- } catch (error48) {
35157
- if (dispatchAnswered(error48, 404)) {
35158
- throw new Error(`no Dispatch issue is linked to ${issueReference}; create it first with ` + `dispatch_issue({ external: "${issueReference}", ... })`);
35159
- }
35160
- throw error48;
35161
- }
35162
- }
35163
- function asObject(value) {
35164
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
35165
- }
35166
-
35167
- // ../envoy-client/src/dispatch-first.ts
35168
- import { readFileSync as readFileSync3 } from "fs";
35169
- import { join as join2 } from "path";
35170
- var DISPATCH_FIRST_MARKER = "<dispatch-first-skill>";
35171
- function dispatchFirstSkillFile(skillsDirectory) {
35172
- return join2(skillsDirectory, "dispatch-first", "SKILL.md");
35173
- }
35174
- function readDispatchFirstContext(skillFile) {
35175
- let skill;
35176
- try {
35177
- skill = readFileSync3(skillFile, "utf8");
35178
- } catch (error48) {
35179
- throw new Error(`the dispatch-first skill ${skillFile} could not be read: ${messageFor(error48)}`);
35180
- }
35181
- const body = skill.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
35182
- return [
35183
- DISPATCH_FIRST_MARKER,
35184
- "This session has Dispatch, so the dispatch-first skill is loaded for it. Follow it.",
35185
- "",
35186
- body,
35187
- "</dispatch-first-skill>"
35188
- ].join(`
35189
- `);
33066
+ function writeSessionTitle(dir, title) {
33067
+ ensureDirectory(dir);
33068
+ writeAtomically(join3(dir, "title"), title);
35190
33069
  }
35191
33070
 
35192
33071
  // ../envoy-client/src/dispatch-subscribe.ts
35193
- function dispatchFollowNotice(details) {
35194
- if (typeof details !== "object" || details === null)
35195
- return null;
35196
- const { follows, issue: issue2, document } = details;
35197
- if (typeof follows !== "object" || follows === null)
35198
- return null;
35199
- const { ask } = follows;
35200
- if (typeof ask !== "string" || ask === "")
35201
- return null;
35202
- let owner;
35203
- if (typeof issue2 === "string" && issue2 !== "") {
35204
- owner = issueTopic(issue2);
35205
- } else if (typeof document === "string") {
35206
- const [project, slug] = document.split("/", 2);
35207
- if (!project || !slug)
35208
- return null;
35209
- owner = { label: document, topic: documentTopicOf(project, slug).topic };
35210
- } else {
35211
- return null;
35212
- }
35213
- return {
35214
- ask,
35215
- text: `Following ask ${ask} on ${owner.label}: its answer and replies reach you directly (dispatch_follow unfollow to stop). For every event on ${owner.label}: envoy_subscribe ${owner.topic}.`
35216
- };
35217
- }
35218
- function createFollowAnnouncer(emit) {
35219
- const announced = new Set;
35220
- return (details) => {
35221
- const notice = dispatchFollowNotice(details);
35222
- if (notice === null || announced.has(notice.ask))
35223
- return;
35224
- announced.add(notice.ask);
35225
- return emit(notice.text);
35226
- };
35227
- }
35228
33072
  function subscriptionRemovedTopics(raw, sessionID) {
35229
33073
  let envelope2;
35230
33074
  try {
@@ -35331,29 +33175,25 @@ var envoyToolSpecs = [
35331
33175
  arguments: (schema) => ({
35332
33176
  topics: schema.array(schema.string()).describe("NATS-style topic patterns to subscribe to.")
35333
33177
  }),
35334
- operation: EnvoyToolOperation.subscribe,
35335
- requiresSubscriptionCapability: true
33178
+ operation: EnvoyToolOperation.subscribe
35336
33179
  },
35337
33180
  {
35338
33181
  name: "envoy_unsubscribe",
35339
33182
  description: "Unsubscribe this session from Envoy topics, or remove all current subscriptions if topics are omitted.",
35340
33183
  arguments: (schema) => ({ topics: schema.array(schema.string()).optional() }),
35341
- operation: EnvoyToolOperation.unsubscribe,
35342
- requiresSubscriptionCapability: true
33184
+ operation: EnvoyToolOperation.unsubscribe
35343
33185
  },
35344
33186
  {
35345
33187
  name: "envoy_list",
35346
33188
  description: "List the current Envoy topic subscriptions for this session so you can confirm the exact topic shapes that are active.",
35347
33189
  arguments: () => ({}),
35348
- operation: EnvoyToolOperation.listInterests,
35349
- requiresSubscriptionCapability: false
33190
+ operation: EnvoyToolOperation.listInterests
35350
33191
  },
35351
33192
  {
35352
33193
  name: "envoy_inbox",
35353
33194
  description: "List this Pi session's 50 most recent rendered Envoy deliveries, newest first.",
35354
33195
  arguments: () => ({}),
35355
- operation: EnvoyToolOperation.inbox,
35356
- requiresSubscriptionCapability: false
33196
+ operation: EnvoyToolOperation.inbox
35357
33197
  },
35358
33198
  {
35359
33199
  name: "envoy_send",
@@ -35362,36 +33202,31 @@ var envoyToolSpecs = [
35362
33202
  session_id: schema.string().describe("Target session ID; find it with envoy_sessions or envoy_whoami."),
35363
33203
  ...messageArguments(schema)
35364
33204
  }),
35365
- operation: EnvoyToolOperation.send,
35366
- requiresSubscriptionCapability: false
33205
+ operation: EnvoyToolOperation.send
35367
33206
  },
35368
33207
  {
35369
33208
  name: "envoy_publish",
35370
33209
  description: `Publish an Envoy message to any topic. ${DELIVERY_CONTRACT}`,
35371
33210
  arguments: (schema) => ({ topic: schema.string(), ...messageArguments(schema) }),
35372
- operation: EnvoyToolOperation.publish,
35373
- requiresSubscriptionCapability: false
33211
+ operation: EnvoyToolOperation.publish
35374
33212
  },
35375
33213
  {
35376
33214
  name: "envoy_role_set",
35377
33215
  description: "Set the current session as the holder of a named role. Messages published to notifications.role.<role> route to this session.",
35378
33216
  arguments: (schema) => ({ role: schema.string() }),
35379
- operation: EnvoyToolOperation.setRole,
35380
- requiresSubscriptionCapability: false
33217
+ operation: EnvoyToolOperation.setRole
35381
33218
  },
35382
33219
  {
35383
33220
  name: "envoy_role_get",
35384
33221
  description: "Get the live holder of a named Envoy role.",
35385
33222
  arguments: (schema) => ({ role: schema.string() }),
35386
- operation: EnvoyToolOperation.getRole,
35387
- requiresSubscriptionCapability: false
33223
+ operation: EnvoyToolOperation.getRole
35388
33224
  },
35389
33225
  {
35390
33226
  name: "envoy_whoami",
35391
33227
  description: "Returns this session's Envoy identity: session ID, machine ID, port, and directory. session_id is the address a reply reaches. Where a host runs a task subagent inside its parent's process, such a subagent registers no Envoy session of its own, so its session_id is the parent session that spawned it and the result says so.",
35392
33228
  arguments: () => ({}),
35393
- operation: EnvoyToolOperation.whoami,
35394
- requiresSubscriptionCapability: false
33229
+ operation: EnvoyToolOperation.whoami
35395
33230
  },
35396
33231
  {
35397
33232
  name: "envoy_sessions",
@@ -35401,8 +33236,7 @@ var envoyToolSpecs = [
35401
33236
  dir: schema.string().optional(),
35402
33237
  title: schema.string().optional()
35403
33238
  }),
35404
- operation: EnvoyToolOperation.listSessions,
35405
- requiresSubscriptionCapability: false
33239
+ operation: EnvoyToolOperation.listSessions
35406
33240
  }
35407
33241
  ];
35408
33242
  function sendConfirmationText(result) {
@@ -35420,7 +33254,7 @@ function parseEnvoyToolArguments(operation, parameters) {
35420
33254
  }
35421
33255
  const parsed = schema.safeParse(parameters, { reportInput: true });
35422
33256
  if (!parsed.success) {
35423
- throw new ToolInputError(spec.name, formatZodIssues(parsed.error.issues, schema));
33257
+ throw new ToolInputError(spec.name, formatZodIssues(parsed.error.issues, schema, spec.name));
35424
33258
  }
35425
33259
  return parsed.data;
35426
33260
  }
@@ -35764,6 +33598,70 @@ function legionRoleClaimBridge() {
35764
33598
  return envoyPluginInterface().roleClaim;
35765
33599
  }
35766
33600
 
33601
+ // ../pi-shared/src/shell-command.ts
33602
+ var HEREDOC_OPENING = /^(.*?)[ \t]*<<(-?)[ \t]*'([A-Za-z_][A-Za-z0-9_]*)'[ \t]*$/;
33603
+ var BLANK_LINE = /^[ \t]*$/;
33604
+ var REFUSED_UNQUOTED = "\n;&|$`<>()#\\";
33605
+ var REFUSED_DOUBLE_QUOTED = "$`\\";
33606
+ function dispatchCommandHead(command) {
33607
+ if (typeof command !== "string" || hasControlCharacter(command))
33608
+ return;
33609
+ const all = command.split(`
33610
+ `);
33611
+ const first = all.findIndex((line) => !BLANK_LINE.test(line));
33612
+ if (first === -1)
33613
+ return;
33614
+ const last = all.findLastIndex((line) => !BLANK_LINE.test(line));
33615
+ const lines = all.slice(first, last + 1);
33616
+ let head = lines[0] ?? "";
33617
+ if (lines.length > 1) {
33618
+ const opening = HEREDOC_OPENING.exec(head);
33619
+ if (opening === null)
33620
+ return;
33621
+ const stripTabs = opening[2] === "-";
33622
+ const delimiter = opening[3];
33623
+ const end = lines.findIndex((line, index) => index > 0 && (stripTabs ? line.replace(/^\t+/, "") : line) === delimiter);
33624
+ if (end !== lines.length - 1)
33625
+ return;
33626
+ head = opening[1] ?? "";
33627
+ }
33628
+ head = head.replace(/^[ \t]+|[ \t]+$/g, "");
33629
+ return firstWord(head) === "dispatch" && headScan(head) ? head : undefined;
33630
+ }
33631
+ function firstWord(head) {
33632
+ return head.split(/[ \t]/, 1)[0] ?? "";
33633
+ }
33634
+ function hasControlCharacter(text) {
33635
+ for (let index = 0;index < text.length; index += 1) {
33636
+ const code = text.charCodeAt(index);
33637
+ if (code < 32 && code !== 10 && code !== 9 || code === 127)
33638
+ return true;
33639
+ }
33640
+ return false;
33641
+ }
33642
+ function headScan(head) {
33643
+ let quote;
33644
+ for (const char of head) {
33645
+ if (quote === "'") {
33646
+ if (char === "'")
33647
+ quote = undefined;
33648
+ continue;
33649
+ }
33650
+ if (quote === '"') {
33651
+ if (char === '"')
33652
+ quote = undefined;
33653
+ else if (REFUSED_DOUBLE_QUOTED.includes(char))
33654
+ return false;
33655
+ continue;
33656
+ }
33657
+ if (char === "'" || char === '"')
33658
+ quote = char;
33659
+ else if (REFUSED_UNQUOTED.includes(char))
33660
+ return false;
33661
+ }
33662
+ return quote === undefined;
33663
+ }
33664
+
35767
33665
  // ../pi-shared/src/subagent-session.ts
35768
33666
  import fs from "fs";
35769
33667
  import path2 from "path";
@@ -35825,7 +33723,7 @@ function toolFailure(error48) {
35825
33723
  }
35826
33724
 
35827
33725
  // extensions/envoy.ts
35828
- import { logger as logger3 } from "@oh-my-pi/pi-utils";
33726
+ import { logger as logger3, procmgr } from "@oh-my-pi/pi-utils";
35829
33727
  var import_nats2 = __toESM(require_mod4(), 1);
35830
33728
 
35831
33729
  // src/agent-stream.ts
@@ -36223,7 +34121,10 @@ function adviceCount(details, key) {
36223
34121
  const count = asObject(asObject(details)?.advice)?.[key];
36224
34122
  return typeof count === "number" ? count : 0;
36225
34123
  }
36226
- function opensAsk({ toolName, details }) {
34124
+ function opensAsk({
34125
+ toolName,
34126
+ details
34127
+ }) {
36227
34128
  switch (toolName) {
36228
34129
  case "dispatch_ask":
36229
34130
  case "dispatch_request_approval":
@@ -36237,12 +34138,6 @@ function opensAsk({ toolName, details }) {
36237
34138
  return false;
36238
34139
  }
36239
34140
  }
36240
- function deviceTool({ toolName, details }) {
36241
- if (toolName !== "write")
36242
- return;
36243
- const tool = asObject(asObject(details)?.xdev)?.tool;
36244
- return typeof tool === "string" ? tool : undefined;
36245
- }
36246
34141
 
36247
34142
  // src/side-turn.ts
36248
34143
  function btwPrompt(question) {
@@ -36377,8 +34272,7 @@ var ECHOED_CHOICE = /\bPROCEEDING\b/;
36377
34272
  var isWaitingVerdict = (reply) => WAITING_VERDICT.test(reply) && !ECHOED_CHOICE.test(reply);
36378
34273
  var ASK_SELF_CHECK_TIMEOUT_MS = 60000;
36379
34274
  var ASK_CHECKS_PER_PERIOD = 5;
36380
- var UNASKED_WAIT_REMINDER = "You just said you are waiting on a human for something no open ask in Dispatch covers. " + "Open it now: a decision block in the document it concerns (dispatch_doc_edit with an ask block), or " + "dispatch_ask for a to-do only a human can do, naming exactly what you need and from whom.";
36381
- var DISPATCH_TOOL_PREFIX = "dispatch_";
34275
+ var UNASKED_WAIT_REMINDER = "You just said you are waiting on a human for something no open ask in Dispatch covers. " + "Open it now: a decision block in the document it concerns (dispatch doc-edit with an ask block), or " + "dispatch ask for a to-do only a human can do, naming exactly what you need and from whom.";
36382
34276
  function isLegionManagedEntry(entry) {
36383
34277
  if (typeof entry !== "object" || entry === null)
36384
34278
  return false;
@@ -36417,6 +34311,11 @@ function resolveSkillsDirectory() {
36417
34311
  }
36418
34312
  var SKILLS_DIRECTORY = resolveSkillsDirectory();
36419
34313
  var DISPATCH_FIRST_CONTEXT = readDispatchFirstContext(dispatchFirstSkillFile(SKILLS_DIRECTORY));
34314
+ var PLUGIN_BIN = resolve(dirname(fileURLToPath(import.meta.url)), "..", "bin");
34315
+ if (!(process.env.PATH ?? "").split(delimiter).includes(PLUGIN_BIN)) {
34316
+ process.env.PATH = `${PLUGIN_BIN}${delimiter}${process.env.PATH ?? ""}`;
34317
+ }
34318
+ process.env.DISPATCH_HOST = "omp";
36420
34319
  function envoyExtension(pi) {
36421
34320
  logger3.debug("extension instance loaded", { extension: import.meta.url });
36422
34321
  const publishedInterface = publishEnvoyPluginInterface(import.meta.url);
@@ -36433,7 +34332,7 @@ function envoyExtension(pi) {
36433
34332
  const subscriptions = new Map;
36434
34333
  const delivered = createDeliveryDedupe();
36435
34334
  let connection;
36436
- let sessionDirectory = "";
34335
+ let sessionDirectory2 = "";
36437
34336
  let sessionID = "";
36438
34337
  let recordedSessionKey;
36439
34338
  let heartbeatRegistered = false;
@@ -36508,7 +34407,7 @@ function envoyExtension(pi) {
36508
34407
  };
36509
34408
  const restoreLocalSessionState = (context) => {
36510
34409
  const previousSessionID = sessionID;
36511
- sessionDirectory = context.cwd;
34410
+ sessionDirectory2 = context.cwd;
36512
34411
  sessionID = context.sessionManager.getSessionId();
36513
34412
  recordedSessionKey = recordEnvoySession({
36514
34413
  previousKey: recordedSessionKey,
@@ -36534,6 +34433,32 @@ function envoyExtension(pi) {
36534
34433
  if (previousSessionID !== sessionID)
36535
34434
  forgetShownPictures(previousSessionID);
36536
34435
  };
34436
+ let dispatchLedger;
34437
+ const bindDispatchSession = (id) => {
34438
+ if (id === "") {
34439
+ delete process.env.DISPATCH_SESSION_ID;
34440
+ dispatchLedger = undefined;
34441
+ return;
34442
+ }
34443
+ process.env.DISPATCH_SESSION_ID = id;
34444
+ dispatchLedger = {
34445
+ sessionID: id,
34446
+ offset: resultsEnd(sessionDirectory(process.env, id))
34447
+ };
34448
+ };
34449
+ const readNewDispatchResults = () => {
34450
+ const id = process.env.DISPATCH_SESSION_ID;
34451
+ if (id === undefined || id === "")
34452
+ return [];
34453
+ const dir = sessionDirectory(process.env, id);
34454
+ if (dispatchLedger?.sessionID !== id) {
34455
+ dispatchLedger = { sessionID: id, offset: resultsEnd(dir) };
34456
+ return [];
34457
+ }
34458
+ const read = readResultsSince(dir, dispatchLedger.offset, (problem) => logger3.warn(`envoy: ${problem}`));
34459
+ dispatchLedger.offset = read.offset;
34460
+ return read.entries;
34461
+ };
36537
34462
  pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
36538
34463
  registerEnvoyMessageRenderer(pi);
36539
34464
  const ensureConnection = async () => {
@@ -36815,7 +34740,7 @@ function envoyExtension(pi) {
36815
34740
  };
36816
34741
  const registerSession = () => client.subscribe({
36817
34742
  sessionID,
36818
- directory: sessionDirectory,
34743
+ directory: sessionDirectory2,
36819
34744
  topics: [
36820
34745
  ...new Set([
36821
34746
  agentSubject(sessionID),
@@ -36889,6 +34814,10 @@ function envoyExtension(pi) {
36889
34814
  if (healing)
36890
34815
  return;
36891
34816
  healing = true;
34817
+ if (drifted) {
34818
+ bindDispatchSession(liveSessionID);
34819
+ procmgr.refreshShellConfigCache();
34820
+ }
36892
34821
  const afterOutage = registrationFailed;
36893
34822
  (drifted ? establishSession(context) : registerSession()).then(() => drifted ? undefined : reassertRole(afterOutage, context), (error48) => {
36894
34823
  registrationFailed = true;
@@ -37071,10 +35000,11 @@ function envoyExtension(pi) {
37071
35000
  pi.on("session_start", async (_event, context) => {
37072
35001
  if (await isSubagent(context))
37073
35002
  return;
35003
+ bindDispatchSession(context.sessionManager.getSessionId());
37074
35004
  const previousSessionID = sessionID;
37075
35005
  restoreLocalSessionState(context);
37076
35006
  if (dispatchConfig.error !== null) {
37077
- context.ui.notify(`envoy: dispatch tool disabled \u2014 ${dispatchConfig.error}`, "warning");
35007
+ context.ui.notify(`envoy: Dispatch disabled \u2014 ${dispatchConfig.error}`, "warning");
37078
35008
  }
37079
35009
  if (defaults.natsUrls.length === 0) {
37080
35010
  context.ui.notify([
@@ -37136,8 +35066,13 @@ function envoyExtension(pi) {
37136
35066
  details: LOCAL_ENVOY_NOTICE
37137
35067
  }, { deliverAs: "steer", triggerTurn: false });
37138
35068
  };
37139
- pi.on("session_switch", (event, context) => rebind(event.reason, context));
37140
- pi.on("session_branch", (_event, context) => rebind(undefined, context));
35069
+ const rebindDispatch = async (reason, context) => {
35070
+ if (!await isSubagent(context))
35071
+ bindDispatchSession(context.sessionManager.getSessionId());
35072
+ await rebind(reason, context);
35073
+ };
35074
+ pi.on("session_switch", (event, context) => rebindDispatch(event.reason, context));
35075
+ pi.on("session_branch", (_event, context) => rebindDispatch(undefined, context));
37141
35076
  pi.on("session_tree", (_event, context) => rebind(undefined, context));
37142
35077
  pi.on("session_shutdown", async (_event, context) => {
37143
35078
  shuttingDown = true;
@@ -37192,33 +35127,6 @@ function envoyExtension(pi) {
37192
35127
  });
37193
35128
  }
37194
35129
  if (dispatchConfig.enabled) {
37195
- for (const spec of dispatchToolSpecs) {
37196
- pi.registerTool({
37197
- name: spec.name,
37198
- label: spec.name,
37199
- description: spec.description,
37200
- parameters: dispatchToolSchema(spec, zodSchemaApi(pi.zod)),
37201
- lenientArgValidation: true,
37202
- execute: async (_id, params, signal, _onUpdate, context) => {
37203
- try {
37204
- const result = await executeDispatchTool({
37205
- tool: spec.name,
37206
- args: params,
37207
- cwd: context.cwd,
37208
- host: "omp",
37209
- sessionId: context.sessionManager.getSessionId(),
37210
- sessionTitle: context.sessionManager.getSessionName?.(),
37211
- config: currentDispatchConfig(),
37212
- signal,
37213
- env: process.env
37214
- });
37215
- return toolSuccess(result.text, result.details, result.images);
37216
- } catch (error48) {
37217
- return toolFailure(error48);
37218
- }
37219
- }
37220
- });
37221
- }
37222
35130
  pi.on("context", async (event) => {
37223
35131
  const messages = withDispatchFirst(event.messages, DISPATCH_FIRST_CONTEXT);
37224
35132
  return messages === undefined ? undefined : { messages };
@@ -37347,22 +35255,38 @@ function envoyExtension(pi) {
37347
35255
  return;
37348
35256
  pi.sendMessage({ customType: ASK_REMINDER_MESSAGE, content: UNASKED_WAIT_REMINDER, display: false }, { deliverAs: "steer", triggerTurn: true });
37349
35257
  }
37350
- const announceFollow = createFollowAnnouncer((text) => {
37351
- pi.sendMessage({ customType: "envoy-message", content: text, display: true, details: LOCAL_ENVOY_NOTICE }, { deliverAs: "steer", triggerTurn: false });
35258
+ let writtenTitle;
35259
+ pi.on("tool_call", async (event, context) => {
35260
+ if (event.toolName !== "bash" && event.toolName !== "eval")
35261
+ return;
35262
+ if (context.agent?.kind !== "main")
35263
+ return;
35264
+ const id = process.env.DISPATCH_SESSION_ID;
35265
+ if (id === undefined || id === "")
35266
+ return;
35267
+ const title = context.sessionManager.getSessionName?.() ?? "";
35268
+ if (writtenTitle?.sessionID === id && writtenTitle.title === title)
35269
+ return;
35270
+ writeSessionTitle(sessionDirectory(process.env, id), title);
35271
+ writtenTitle = { sessionID: id, title };
35272
+ return;
37352
35273
  });
37353
35274
  pi.on("tool_result", async (event, context) => {
35275
+ const entries = readNewDispatchResults();
37354
35276
  if (event.isError)
37355
35277
  return;
37356
- if (askAwareness.session_id === context.sessionManager.getSessionId() && askAwareness.period > 0) {
37357
- const tool = deviceTool(event) ?? event.toolName;
37358
- if (opensAsk(event)) {
37359
- askAwareness = { ...askAwareness, check_due: false };
37360
- abortSelfCheck("the agent opened the ask itself");
37361
- } else if (!tool.startsWith(DISPATCH_TOOL_PREFIX)) {
37362
- askAwareness = { ...askAwareness, check_due: true };
37363
- }
35278
+ if (askAwareness.session_id !== context.sessionManager.getSessionId() || askAwareness.period === 0) {
35279
+ return;
35280
+ }
35281
+ const openedAsk = entries.some((entry) => entry.details !== undefined && opensAsk({ toolName: entry.tool, details: entry.details }));
35282
+ const bashRanDispatchAlone = event.toolName === "bash" && dispatchCommandHead(event.input.command) !== undefined;
35283
+ const owesNoCheck = entries.length > 0 && (bashRanDispatchAlone || event.toolName === "eval");
35284
+ if (openedAsk) {
35285
+ askAwareness = { ...askAwareness, check_due: false };
35286
+ abortSelfCheck("the agent opened the ask itself");
35287
+ } else if (!owesNoCheck) {
35288
+ askAwareness = { ...askAwareness, check_due: true };
37364
35289
  }
37365
- announceFollow(event.details);
37366
35290
  });
37367
35291
  async function execute(operation, rawParameters, context) {
37368
35292
  try {
@@ -37467,7 +35391,7 @@ Not delivered: that holder is the session this subagent sends as, and the listen
37467
35391
  return toolSuccess(JSON.stringify({
37468
35392
  session_id: address,
37469
35393
  machine_id: machineID(),
37470
- dir: sessionDirectory || context.cwd,
35394
+ dir: sessionDirectory2 || context.cwd,
37471
35395
  ...subagent === undefined ? {} : { subagent }
37472
35396
  }, null, 2), {
37473
35397
  sessionID: address,