@sjawhar/opencode-legion-envoy 5.8.2 → 5.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13886,6 +13886,21 @@ function snippetSegments(snippet) {
13886
13886
  function snippetText(snippet) {
13887
13887
  return snippetSegments(snippet).map(({ text, mark }) => mark ? `**${text}**` : text).join("");
13888
13888
  }
13889
+ // ../contracts/src/dispatch-text-rules.ts
13890
+ var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
13891
+ var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
13892
+ function pictureCaption(name) {
13893
+ return name.replace(/[\\[\]`]/g, "\\$&").replace(/\s+/g, " ");
13894
+ }
13895
+ var PICTURE_TYPES = {
13896
+ "image/png": true,
13897
+ "image/jpeg": true,
13898
+ "image/gif": true,
13899
+ "image/webp": true
13900
+ };
13901
+ function isPictureType(mime) {
13902
+ return PICTURE_TYPES[mime] === true;
13903
+ }
13889
13904
  // ../contracts/src/dispatch-tools.ts
13890
13905
  function dispatchToolSchema(spec, z2, opts) {
13891
13906
  const shape = spec.arguments(z2);
@@ -13959,8 +13974,12 @@ function componentsArgument(z2) {
13959
13974
  }).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
13960
13975
  }
13961
13976
  var SPEC_WRITING_POINTER = 'Write a spec as the "Writing a spec" section of skill://dispatch says.';
13977
+ function imagesArgument(z2, owner, cap) {
13978
+ return z2.array(z2.string({ min: 1 }), { min: 1 }).describe(`Optional local picture files to show inline: PNG, JPEG, GIF or WebP (by their bytes, not their name), each at most 25 MiB. Each is uploaded to ${owner} and appended after a blank line as one ![<file name>](dispatch://\u2026@vN) line, in this order; those lines count toward ${cap}.`).optional();
13979
+ }
13962
13980
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
13963
13981
  var ASK_QUESTION_MAX = 800;
13982
+ var DISPATCH_BODY_MAX = 2000;
13964
13983
  var SEARCH_QUERY_MAX = 1000;
13965
13984
  var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passage";
13966
13985
  var SEARCH_KIND_DEPTH = 100;
@@ -14060,7 +14079,7 @@ var dispatchToolSpecs = [
14060
14079
  }
14061
14080
  ]
14062
14081
  },
14063
- 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 ` + `and has at most 8 options. ${OWNER_REFERENCE}`,
14082
+ 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}`,
14064
14083
  arguments: (z2) => ({
14065
14084
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14066
14085
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14077,7 +14096,8 @@ var dispatchToolSpecs = [
14077
14096
  artifact: z2.string().describe("Artifact slug or id containing the quoted text."),
14078
14097
  quote: z2.string().describe("Exact text the decision concerns."),
14079
14098
  occurrence: z2.number({ int: true, min: 0 }).describe("Zero-based occurrence of the quote.").optional()
14080
- }).describe("Optional document location for the question.").optional()
14099
+ }).describe("Optional document location for the question.").optional(),
14100
+ images: imagesArgument(z2, "the issue (or the project document's project)", `the ${ASK_QUESTION_MAX}-character question limit`)
14081
14101
  }),
14082
14102
  validation: documentOwnerValidation(true)
14083
14103
  },
@@ -14152,7 +14172,7 @@ var dispatchToolSpecs = [
14152
14172
  {
14153
14173
  name: "dispatch_comment",
14154
14174
  example: { issue: "DSP-1", body: "Looks good." },
14155
- 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. ${OWNER_REFERENCE}`,
14175
+ 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}`,
14156
14176
  arguments: (z2) => ({
14157
14177
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14158
14178
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14160,10 +14180,11 @@ var dispatchToolSpecs = [
14160
14180
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
14161
14181
  quote: z2.string().describe("Optional exact quoted document text.").optional(),
14162
14182
  occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
14163
- body: z2.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
14183
+ body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Review comment, at most 2,000 characters."),
14164
14184
  reply_to: z2.string().describe("A comment id (uuid); replying to any comment in a thread continues that thread (an " + "ask's clarification thread included). To reply to an ask, use reply_to_ask with the " + "ask id instead.").optional(),
14165
14185
  reply_to_ask: z2.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
14166
- turn: z2.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
14186
+ turn: z2.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional(),
14187
+ images: imagesArgument(z2, "the issue (or the project document's project)", "the 2,000-character body limit")
14167
14188
  }),
14168
14189
  validation: commentValidation
14169
14190
  },
@@ -14191,11 +14212,12 @@ var dispatchToolSpecs = [
14191
14212
  {
14192
14213
  name: "dispatch_message",
14193
14214
  example: { issue: "DSP-1", body: "Implementation started." },
14194
- 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. ${ISSUE_REFERENCE}`,
14215
+ 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}`,
14195
14216
  arguments: (z2) => ({
14196
14217
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
14197
- body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
14198
- in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional()
14218
+ body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Update text, at most 2,000 characters."),
14219
+ in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional(),
14220
+ images: imagesArgument(z2, "the issue (a reply to a direct message: your own conversation, dispatch://agent/<your session id>)", "the 2,000-character body limit")
14199
14221
  }),
14200
14222
  validation: messageValidation
14201
14223
  },
@@ -14240,13 +14262,13 @@ var dispatchToolSpecs = [
14240
14262
  {
14241
14263
  name: "dispatch_doc_read",
14242
14264
  example: { issue: "DSP-1" },
14243
- 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 file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14265
+ 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,
14244
14266
  arguments: (z2) => ({
14245
14267
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14246
14268
  project: z2.string().describe("Project key owning the document.").optional(),
14247
14269
  artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
14248
14270
  version: z2.number({ int: true, min: 1 }).describe("Optional version number.").optional(),
14249
- ref: z2.string().describe("Optional dispatch:// document reference.").optional()
14271
+ ref: z2.string().describe("Optional dispatch:// document reference; a picture in a conversation on the Agents page is dispatch://agent/<session id>/artifact/<slug>@vN.").optional()
14250
14272
  }),
14251
14273
  validation: documentOwnerValidation(true)
14252
14274
  },
@@ -14286,7 +14308,7 @@ var dispatchToolSpecs = [
14286
14308
  {
14287
14309
  name: "dispatch_read",
14288
14310
  example: { issue: "DSP-1" },
14289
- 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. " + OWNER_REFERENCE,
14311
+ 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,
14290
14312
  arguments: (z2) => ({
14291
14313
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14292
14314
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14630,7 +14652,7 @@ function resolveDispatchConfig(env, options = {}) {
14630
14652
  }
14631
14653
 
14632
14654
  // ../envoy-client/src/dispatch-execute.ts
14633
- import { resolve as resolvePath } from "path";
14655
+ import { resolve as resolvePath2 } from "path";
14634
14656
 
14635
14657
  // ../envoy-client/src/ask-answer.ts
14636
14658
  var HEAD_LENGTH = 120;
@@ -14876,21 +14898,27 @@ class DispatchClient {
14876
14898
  return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
14877
14899
  }
14878
14900
  async projectArtifact(project, input) {
14879
- const artifactPath = ["api", "v1", "projects", project, "artifacts"];
14880
- if ("content" in input)
14881
- return this.#json("POST", artifactPath, input);
14882
- const form = new FormData;
14883
- form.set("name", input.name);
14884
- if (input.summary !== undefined)
14885
- form.set("summary", input.summary);
14886
- if (input.actor !== undefined)
14887
- form.set("actor", JSON.stringify(input.actor));
14888
- form.set("file", input.file, input.name);
14889
- return this.#form("POST", artifactPath, form);
14901
+ return this.#upload(["api", "v1", "projects", project, "artifacts"], input);
14890
14902
  }
14891
14903
  async getProjectArtifact(project, slug) {
14892
14904
  return this.#json("GET", ["api", "v1", "projects", project, "artifacts", slug]);
14893
14905
  }
14906
+ async agentArtifact(sessionId, input) {
14907
+ return this.#upload(["api", "v1", "agents", sessionId, "artifacts"], input);
14908
+ }
14909
+ async getAgentArtifact(sessionId, slug) {
14910
+ return this.#json("GET", ["api", "v1", "agents", sessionId, "artifacts", slug]);
14911
+ }
14912
+ async getIssueArtifact(issue2, slug) {
14913
+ return this.#json("GET", [
14914
+ "api",
14915
+ "v1",
14916
+ "issues",
14917
+ await this.#resolveIssue(issue2),
14918
+ "artifacts",
14919
+ slug
14920
+ ]);
14921
+ }
14894
14922
  async search(query, options = {}) {
14895
14923
  return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
14896
14924
  }
@@ -15011,17 +15039,7 @@ class DispatchClient {
15011
15039
  return this.#json("POST", ["api", "v1", "messages", id, "deliveries", String(attempt), "accept"], input);
15012
15040
  }
15013
15041
  async artifact(issue2, input) {
15014
- const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
15015
- if ("content" in input)
15016
- return this.#json("POST", artifactPath, input);
15017
- const form = new FormData;
15018
- form.set("name", input.name);
15019
- if (input.summary !== undefined)
15020
- form.set("summary", input.summary);
15021
- if (input.actor !== undefined)
15022
- form.set("actor", JSON.stringify(input.actor));
15023
- form.set("file", input.file, input.name);
15024
- return this.#form("POST", artifactPath, form);
15042
+ return this.#upload(["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"], input);
15025
15043
  }
15026
15044
  async getArtifact(id) {
15027
15045
  return this.#json("GET", ["api", "v1", "artifacts", id]);
@@ -15128,6 +15146,18 @@ class DispatchClient {
15128
15146
  });
15129
15147
  return { status: response.status, payload: await this.#response(method, url2, response) };
15130
15148
  }
15149
+ async#upload(path2, input) {
15150
+ if ("content" in input)
15151
+ return this.#json("POST", path2, input);
15152
+ const form = new FormData;
15153
+ form.set("name", input.name);
15154
+ if (input.summary !== undefined)
15155
+ form.set("summary", input.summary);
15156
+ if (input.actor !== undefined)
15157
+ form.set("actor", JSON.stringify(input.actor));
15158
+ form.set("file", input.file, input.name);
15159
+ return this.#form("POST", path2, form);
15160
+ }
15131
15161
  async#form(method, path2, body) {
15132
15162
  const url2 = this.#url(path2);
15133
15163
  const response = await this.fetchImpl(url2, {
@@ -15208,70 +15238,115 @@ function dispatchDocumentRef(owner, slug) {
15208
15238
  function dispatchChildRef(ownerRef, kind, id) {
15209
15239
  return `${ownerRef}/${kind}/${id}`;
15210
15240
  }
15211
-
15212
- // ../envoy-client/src/search-answer.ts
15213
- function pageSummaryText(offset, count, total) {
15214
- return `showing ${offset + 1}-${offset + count} of ${total}`;
15215
- }
15216
- function searchResultLine(result, baseUrl) {
15217
- const href = new URL(result.href, baseUrl).toString();
15218
- const { owner } = result;
15219
- if (owner.kind === "document") {
15220
- const reference = dispatchDocumentRef(owner.project, owner.slug);
15221
- return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
15222
- }
15223
- const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
15224
- const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
15225
- return `${label}: ${snippetText(result.snippet)} -> ${href}`;
15226
- }
15227
- function searchAnswer(search, query, offset, configUrl) {
15228
- const results = search.results;
15229
- const count = results.length;
15230
- const lines = results.map((result) => searchResultLine(result, configUrl));
15231
- const noun = count === 1 ? "result" : "results";
15232
- const noResults = `No results for "${query}".`;
15233
- if (typeof search.total !== "number") {
15234
- if (offset !== undefined && offset > 0) {
15235
- throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
15241
+ function parseDispatchRef(ref) {
15242
+ const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
15243
+ if (projectDocument) {
15244
+ const [, project, artifact2, version3, targetKind, targetID] = projectDocument;
15245
+ if (project === undefined || artifact2 === undefined || version3 !== undefined && Number(version3) < 1) {
15246
+ return null;
15247
+ }
15248
+ if (targetKind === undefined) {
15249
+ return {
15250
+ owner: { kind: "project", project },
15251
+ kind: "artifact",
15252
+ id: artifact2,
15253
+ ...version3 === undefined ? {} : { version: Number(version3) }
15254
+ };
15236
15255
  }
15256
+ if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
15257
+ return null;
15237
15258
  return {
15238
- text: count === 0 ? noResults : [`${count} ${noun} for "${query}" (${search.took_ms} ms)`, ...lines].join(`
15239
- `),
15240
- details: { query, results }
15259
+ owner: { kind: "project", project },
15260
+ kind: targetKind,
15261
+ id: targetID,
15262
+ artifact: artifact2
15241
15263
  };
15242
15264
  }
15243
- const { total, reachable, offset: pageOffset } = search;
15244
- const end = pageOffset + count;
15245
- 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;
15246
- const details = {
15247
- query,
15248
- results,
15249
- total,
15250
- reachable,
15251
- offset: pageOffset,
15252
- limit: search.limit
15253
- };
15254
- if (count === 0) {
15265
+ const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
15266
+ if (!issueReference)
15267
+ return null;
15268
+ const [, issue2, spec, log, children, artifact, version2, ask, comment, message] = issueReference;
15269
+ if (!issue2 || version2 !== undefined && Number(version2) < 1)
15270
+ return null;
15271
+ const owner = { kind: "issue", issue: issue2 };
15272
+ if (spec)
15273
+ return { owner, kind: "spec", id: spec };
15274
+ if (log)
15275
+ return { owner, kind: "log", id: log };
15276
+ if (children)
15277
+ return { owner, kind: "children", id: children };
15278
+ if (artifact) {
15255
15279
  return {
15256
- text: total === 0 ? noResults : [
15257
- `No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
15258
- ...cut === undefined ? [] : [cut]
15259
- ].join(`
15260
- `),
15261
- details
15280
+ owner,
15281
+ kind: "artifact",
15282
+ id: artifact,
15283
+ ...version2 === undefined ? {} : { version: Number(version2) }
15262
15284
  };
15263
15285
  }
15264
- const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
15265
- return {
15266
- text: [
15267
- `${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
15268
- ...cut === undefined ? [] : [cut],
15269
- ...lines,
15270
- ...end < reachable ? [`Next page: offset ${end}.`] : []
15271
- ].join(`
15272
- `),
15273
- details
15274
- };
15286
+ if (ask)
15287
+ return { owner, kind: "ask", id: ask };
15288
+ if (comment)
15289
+ return { owner, kind: "comment", id: comment };
15290
+ if (message)
15291
+ return { owner, kind: "message", id: message };
15292
+ return { owner, kind: "issue", id: issue2 };
15293
+ }
15294
+
15295
+ // ../envoy-client/src/dispatch-picture-tools.ts
15296
+ import { basename, resolve as resolvePath } from "path";
15297
+
15298
+ // ../envoy-client/src/dispatch-pictures.ts
15299
+ var PICTURE_UPLOAD_MAX_BYTES = 25 * 1024 * 1024;
15300
+ var PICTURE_SHOWN_MAX_BYTES = 3750000;
15301
+ var pictureShownMaxText = `${PICTURE_SHOWN_MAX_BYTES.toLocaleString("en-US")} bytes (5 MB of base64)`;
15302
+ function oversizePictureText(name, bytes) {
15303
+ return `${name} is ${bytes.toLocaleString("en-US")} bytes, over the ${pictureShownMaxText} a model is shown`;
15304
+ }
15305
+ var PICTURES_SHOWN_MAX = 8;
15306
+ var PICTURES_SHOWN_MAX_BYTES = 10 * 1024 * 1024;
15307
+ var PICTURE_SNIFF_BYTES = 12;
15308
+ function bytesAt(bytes, offset, expected) {
15309
+ return expected.every((byte, index) => bytes[offset + index] === byte);
15310
+ }
15311
+ function sniffPictureType(bytes) {
15312
+ if (bytesAt(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10]))
15313
+ return "image/png";
15314
+ if (bytesAt(bytes, 0, [255, 216, 255]))
15315
+ return "image/jpeg";
15316
+ if (bytesAt(bytes, 0, [71, 73, 70, 56]) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97) {
15317
+ return "image/gif";
15318
+ }
15319
+ if (bytesAt(bytes, 0, [82, 73, 70, 70]) && bytesAt(bytes, 8, [87, 69, 66, 80])) {
15320
+ return "image/webp";
15321
+ }
15322
+ return;
15323
+ }
15324
+ function toolImage(bytes) {
15325
+ const mimeType = sniffPictureType(bytes);
15326
+ if (mimeType === undefined || bytes.length > PICTURE_SHOWN_MAX_BYTES)
15327
+ return;
15328
+ return { data: Buffer.from(bytes).toString("base64"), mimeType };
15329
+ }
15330
+ function pictureLine(name, address) {
15331
+ return `![${pictureCaption(name)}](${address})`;
15332
+ }
15333
+ function withPictureLines(text, lines) {
15334
+ if (lines.length === 0)
15335
+ return text;
15336
+ const head = text.trimEnd();
15337
+ return head === "" ? lines.join(`
15338
+ `) : `${head}
15339
+
15340
+ ${lines.join(`
15341
+ `)}`;
15342
+ }
15343
+ var PICTURE_SYNTAX = /!\[(?:\\.|[^\\\]])*\]\((dispatch:\/\/[^\s()]+@v[1-9][0-9]*)(?:\s+"(?:\\.|[^\\"])*")?\)/g;
15344
+ function pictureAddresses(text) {
15345
+ return [...text.matchAll(PICTURE_SYNTAX)].map((match) => match[1]);
15346
+ }
15347
+ function picturesNewestFirst(texts) {
15348
+ 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);
15349
+ return [...new Set(ordered.flatMap(({ item }) => pictureAddresses(item.text)))];
15275
15350
  }
15276
15351
 
15277
15352
  // ../envoy-client/src/tool-input-errors.ts
@@ -15397,6 +15472,314 @@ function formatZodIssues(issues, schema) {
15397
15472
  });
15398
15473
  }
15399
15474
 
15475
+ // ../envoy-client/src/dispatch-picture-tools.ts
15476
+ var PICTURE_TYPE_NAMES = "a PNG, JPEG, GIF or WebP";
15477
+ async function localPictures(images, cwd, problems) {
15478
+ if (!Array.isArray(images))
15479
+ return [];
15480
+ const checked = await Promise.all(images.map((path2) => localPicture(path2, cwd)));
15481
+ const pictures = [];
15482
+ for (const outcome of checked) {
15483
+ if (typeof outcome === "string")
15484
+ problems.push(outcome);
15485
+ else if (outcome !== undefined)
15486
+ pictures.push(outcome);
15487
+ }
15488
+ return pictures;
15489
+ }
15490
+ async function localPicture(path2, cwd) {
15491
+ if (typeof path2 !== "string" || path2 === "")
15492
+ return;
15493
+ const absolute = resolvePath(cwd, path2);
15494
+ const file2 = Bun.file(absolute);
15495
+ let size;
15496
+ let head;
15497
+ try {
15498
+ const stats = await file2.stat();
15499
+ if (!stats.isFile())
15500
+ return `images: ${path2} is not a file`;
15501
+ size = stats.size;
15502
+ head = new Uint8Array(await file2.slice(0, PICTURE_SNIFF_BYTES).arrayBuffer());
15503
+ } catch (error48) {
15504
+ return `images: ${path2} cannot be read: ${messageFor(error48)}`;
15505
+ }
15506
+ if (size > PICTURE_UPLOAD_MAX_BYTES) {
15507
+ return `images: ${path2} is ${size.toLocaleString("en-US")} bytes, over the ${PICTURE_UPLOAD_MAX_BYTES / 1024 / 1024} MiB one Dispatch upload takes`;
15508
+ }
15509
+ const type = sniffPictureType(head);
15510
+ if (type === undefined) {
15511
+ return `images: ${path2} is not ${PICTURE_TYPE_NAMES} picture (judged by its bytes, not its name)`;
15512
+ }
15513
+ return { path: path2, name: basename(absolute), file: Bun.file(absolute, { type }) };
15514
+ }
15515
+ function uploadSlug(name) {
15516
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
15517
+ return slug === "" ? "artifact" : slug;
15518
+ }
15519
+ async function textWithPictures(tool, text, pictures, limit, owner, upload, actor) {
15520
+ if (pictures.length === 0)
15521
+ return text;
15522
+ const counted = `${limit.text} plus ${pictures.length} ${pictures.length === 1 ? "picture" : "pictures"}`;
15523
+ const fix = `${limit.shorten} or send fewer pictures`;
15524
+ const predicted = withPictureLines(text, pictures.map((picture) => pictureLine(picture.name, `dispatch://${owner}/artifact/${uploadSlug(picture.name)}@v1`)));
15525
+ if (predicted.length > limit.cap) {
15526
+ throw new ToolInputError(tool, [
15527
+ `${counted} ${overCapMessage(predicted.length, limit.cap)}; ${fix}`
15528
+ ]);
15529
+ }
15530
+ const lines = [];
15531
+ const addresses = [];
15532
+ for (const picture of pictures) {
15533
+ let uploaded;
15534
+ try {
15535
+ uploaded = await upload({ name: picture.name, file: picture.file, actor });
15536
+ } catch (error48) {
15537
+ const earlier = addresses.length === 0 ? "" : `; the pictures before it are uploaded (${addresses.join(", ")})`;
15538
+ throw new Error(`images: ${picture.path} was not uploaded: ${messageFor(error48)}. Nothing was posted${earlier}.`);
15539
+ }
15540
+ const address = `dispatch://${owner}/artifact/${uploaded.artifact.slug}@v${uploaded.version.number}`;
15541
+ addresses.push(address);
15542
+ lines.push(pictureLine(picture.name, address));
15543
+ }
15544
+ const posted = withPictureLines(text, lines);
15545
+ if (posted.length > limit.cap) {
15546
+ throw new Error(`${counted} ${overCapMessage(posted.length, limit.cap)} with the addresses Dispatch gave ` + `the uploads (${addresses.join(", ")}), so nothing was posted; ${fix}.`);
15547
+ }
15548
+ return posted;
15549
+ }
15550
+ var AGENT_ARTIFACT_REF = new RegExp(`^dispatch://agent/(${SESSION_ID_PATTERN})/artifact/([^/@]+)(?:@v(\\d+))?$`, "u");
15551
+ function parseAgentArtifactRef(ref) {
15552
+ const match = ref.match(AGENT_ARTIFACT_REF);
15553
+ if (match === null)
15554
+ return null;
15555
+ const [, session, slug, version2] = match;
15556
+ if (session === undefined || slug === undefined || version2 !== undefined && Number(version2) < 1) {
15557
+ return null;
15558
+ }
15559
+ return { session, slug, ...version2 === undefined ? {} : { version: Number(version2) } };
15560
+ }
15561
+ function datedBodies(records) {
15562
+ return records.map((record2) => ({ text: record2.body, at: record2.created_at }));
15563
+ }
15564
+ function askTexts({ ask, replies }) {
15565
+ return [
15566
+ { text: ask.question, at: ask.created_at },
15567
+ ...ask.answer?.text ? [{ text: ask.answer.text, at: ask.answer.at }] : [],
15568
+ ...ask.resolution === undefined ? [] : [{ text: ask.resolution.reason, at: ask.resolution.at }],
15569
+ ...datedBodies(replies)
15570
+ ];
15571
+ }
15572
+ function eventTexts(event) {
15573
+ let texts;
15574
+ switch (event.type) {
15575
+ case "ask.opened":
15576
+ case "ask.anchor_refreshed":
15577
+ case "ask.edited":
15578
+ case "ask.handed_back":
15579
+ case "ask.answered":
15580
+ case "ask.resolved":
15581
+ texts = [
15582
+ event.payload.question,
15583
+ event.payload.answer?.text,
15584
+ event.payload.resolution?.reason
15585
+ ];
15586
+ break;
15587
+ case "comment.created":
15588
+ case "comment.answered":
15589
+ case "comment.anchor_refreshed":
15590
+ case "comment.edited":
15591
+ case "comment.resolved":
15592
+ case "comment.reopened":
15593
+ case "suggestion.accepted":
15594
+ case "suggestion.rejected":
15595
+ case "message.created":
15596
+ case "message.answered":
15597
+ texts = [event.payload.body];
15598
+ break;
15599
+ default:
15600
+ texts = [];
15601
+ }
15602
+ return texts.filter((text) => typeof text === "string" && text !== "").map((text) => ({ text, at: event.created_at }));
15603
+ }
15604
+ function shownPictureLine(index, address, name, mimeType, bytes) {
15605
+ return `- image ${index}: ${address} (${name}, ${mimeType}, ${bytes.toLocaleString("en-US")} bytes)`;
15606
+ }
15607
+ function pictureTarget(address) {
15608
+ const agent = parseAgentArtifactRef(address);
15609
+ if (agent !== null) {
15610
+ if (agent.version === undefined)
15611
+ return;
15612
+ return {
15613
+ key: `agent/${agent.session}/${agent.slug}`,
15614
+ read: (client) => client.getAgentArtifact(agent.session, agent.slug),
15615
+ version: agent.version
15616
+ };
15617
+ }
15618
+ const ref = parseDispatchRef(address);
15619
+ if (ref?.kind !== "artifact" || ref.version === undefined)
15620
+ return;
15621
+ const { owner, id: slug } = ref;
15622
+ return owner.kind === "issue" ? {
15623
+ key: `${owner.issue}/${slug}`,
15624
+ read: (client) => client.getIssueArtifact(owner.issue, slug),
15625
+ version: ref.version
15626
+ } : {
15627
+ key: `${owner.project}/${slug}`,
15628
+ read: (client) => client.getProjectArtifact(owner.project, slug),
15629
+ version: ref.version
15630
+ };
15631
+ }
15632
+ var SESSIONS_SHOWN_MAX = 64;
15633
+ var picturesShown = new Map;
15634
+ function shownPictures(sessionId) {
15635
+ const shown = picturesShown.get(sessionId) ?? new Set;
15636
+ picturesShown.delete(sessionId);
15637
+ picturesShown.set(sessionId, shown);
15638
+ const oldest = picturesShown.keys().next();
15639
+ if (picturesShown.size > SESSIONS_SHOWN_MAX && oldest.done !== true) {
15640
+ picturesShown.delete(oldest.value);
15641
+ }
15642
+ return shown;
15643
+ }
15644
+ async function readPictures(client, addresses, shown) {
15645
+ if (addresses.length === 0)
15646
+ return { lines: [], images: [], shown: [] };
15647
+ const artifacts = new Map;
15648
+ const images = [];
15649
+ const showing = [];
15650
+ const lines = ["Pictures:"];
15651
+ let shownBytes = 0;
15652
+ const overBudget = `past this read's ${PICTURES_SHOWN_MAX_BYTES / 1024 / 1024} MiB of pictures; dispatch_doc_read shows it`;
15653
+ for (const address of addresses) {
15654
+ const skip = (reason) => lines.push(`- not shown: ${address} (${reason})`);
15655
+ if (shown?.has(address)) {
15656
+ skip("shown earlier this session; dispatch_doc_read shows it again");
15657
+ continue;
15658
+ }
15659
+ if (images.length === PICTURES_SHOWN_MAX) {
15660
+ skip(`past this read's ${PICTURES_SHOWN_MAX} pictures; dispatch_doc_read shows it`);
15661
+ continue;
15662
+ }
15663
+ const target = pictureTarget(address);
15664
+ if (target === undefined) {
15665
+ skip("names no versioned Dispatch artifact");
15666
+ continue;
15667
+ }
15668
+ try {
15669
+ let read = artifacts.get(target.key);
15670
+ if (read === undefined) {
15671
+ read = target.read(client);
15672
+ artifacts.set(target.key, read);
15673
+ }
15674
+ const artifact = await read;
15675
+ const version2 = artifact.versions.find((candidate) => candidate.number === target.version);
15676
+ if (version2 === undefined) {
15677
+ skip(`${artifact.name} has no version ${target.version}`);
15678
+ continue;
15679
+ }
15680
+ if (artifact.kind !== "image") {
15681
+ skip(`${artifact.name} is a ${artifact.kind}, not a picture`);
15682
+ continue;
15683
+ }
15684
+ const mime = version2.mime?.split(";")[0]?.trim();
15685
+ if (mime !== undefined && !isPictureType(mime)) {
15686
+ skip(`${artifact.name} is ${mime}, not ${PICTURE_TYPE_NAMES} a model is shown`);
15687
+ continue;
15688
+ }
15689
+ if (version2.size !== undefined && version2.size > PICTURE_SHOWN_MAX_BYTES) {
15690
+ skip(oversizePictureText(artifact.name, version2.size));
15691
+ continue;
15692
+ }
15693
+ if (version2.size !== undefined && shownBytes + version2.size > PICTURES_SHOWN_MAX_BYTES) {
15694
+ skip(overBudget);
15695
+ continue;
15696
+ }
15697
+ const file2 = await client.fileVersion(artifact.id, target.version);
15698
+ const image = toolImage(file2.bytes);
15699
+ if (image === undefined) {
15700
+ skip(sniffPictureType(file2.bytes) === undefined ? `${artifact.name} is not ${PICTURE_TYPE_NAMES} a model is shown` : oversizePictureText(artifact.name, file2.bytes.length));
15701
+ continue;
15702
+ }
15703
+ if (shownBytes + file2.bytes.length > PICTURES_SHOWN_MAX_BYTES) {
15704
+ skip(overBudget);
15705
+ continue;
15706
+ }
15707
+ shownBytes += file2.bytes.length;
15708
+ images.push(image);
15709
+ showing.push(address);
15710
+ lines.push(shownPictureLine(images.length, address, artifact.name, image.mimeType, file2.bytes.length));
15711
+ } catch (error48) {
15712
+ skip(`unavailable: ${messageFor(error48)}`);
15713
+ }
15714
+ }
15715
+ return { lines, images, shown: showing };
15716
+ }
15717
+
15718
+ // ../envoy-client/src/search-answer.ts
15719
+ function pageSummaryText(offset, count, total) {
15720
+ return `showing ${offset + 1}-${offset + count} of ${total}`;
15721
+ }
15722
+ function searchResultLine(result, baseUrl) {
15723
+ const href = new URL(result.href, baseUrl).toString();
15724
+ const { owner } = result;
15725
+ if (owner.kind === "document") {
15726
+ const reference = dispatchDocumentRef(owner.project, owner.slug);
15727
+ return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
15728
+ }
15729
+ const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
15730
+ const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
15731
+ return `${label}: ${snippetText(result.snippet)} -> ${href}`;
15732
+ }
15733
+ function searchAnswer(search, query, offset, configUrl) {
15734
+ const results = search.results;
15735
+ const count = results.length;
15736
+ const lines = results.map((result) => searchResultLine(result, configUrl));
15737
+ const noun = count === 1 ? "result" : "results";
15738
+ const noResults = `No results for "${query}".`;
15739
+ if (typeof search.total !== "number") {
15740
+ if (offset !== undefined && offset > 0) {
15741
+ throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
15742
+ }
15743
+ return {
15744
+ text: count === 0 ? noResults : [`${count} ${noun} for "${query}" (${search.took_ms} ms)`, ...lines].join(`
15745
+ `),
15746
+ details: { query, results }
15747
+ };
15748
+ }
15749
+ const { total, reachable, offset: pageOffset } = search;
15750
+ const end = pageOffset + count;
15751
+ 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;
15752
+ const details = {
15753
+ query,
15754
+ results,
15755
+ total,
15756
+ reachable,
15757
+ offset: pageOffset,
15758
+ limit: search.limit
15759
+ };
15760
+ if (count === 0) {
15761
+ return {
15762
+ text: total === 0 ? noResults : [
15763
+ `No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
15764
+ ...cut === undefined ? [] : [cut]
15765
+ ].join(`
15766
+ `),
15767
+ details
15768
+ };
15769
+ }
15770
+ const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
15771
+ return {
15772
+ text: [
15773
+ `${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
15774
+ ...cut === undefined ? [] : [cut],
15775
+ ...lines,
15776
+ ...end < reachable ? [`Next page: offset ${end}.`] : []
15777
+ ].join(`
15778
+ `),
15779
+ details
15780
+ };
15781
+ }
15782
+
15400
15783
  // ../envoy-client/src/dispatch-execute.ts
15401
15784
  function documentTopic(artifact) {
15402
15785
  return documentTopicOf(artifact.project, artifact.slug);
@@ -15415,6 +15798,7 @@ function followsAsk(owner) {
15415
15798
  return `You follow this ask: its answer and replies reach you directly. For every event on ${owner.label}: envoy_subscribe ${owner.topic}`;
15416
15799
  }
15417
15800
  var triageAdviceShown = new Set;
15801
+ 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 (`plan-gap-analyst` on Oh My Pi) check it now and fix what it finds. See the `dispatch-first` skill, "Design in the spec".';
15418
15802
  function renderAdvice(tool, key, advice, opts) {
15419
15803
  if (advice === undefined)
15420
15804
  return [];
@@ -15624,59 +16008,11 @@ function askQuestionWithRef(args) {
15624
16008
  function askQuestionProblem(withRef) {
15625
16009
  return withRef.length > ASK_QUESTION_MAX ? `question plus ref ${overCapMessage(withRef.length, ASK_QUESTION_MAX)}; shorten the question or drop the ref` : undefined;
15626
16010
  }
15627
- function parseDispatchRef(ref) {
15628
- const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
15629
- if (projectDocument) {
15630
- const [, project, artifact2, version3, targetKind, targetID] = projectDocument;
15631
- if (project === undefined || artifact2 === undefined || version3 !== undefined && Number(version3) < 1) {
15632
- return null;
15633
- }
15634
- if (targetKind === undefined) {
15635
- return {
15636
- owner: { kind: "project", project },
15637
- kind: "artifact",
15638
- id: artifact2,
15639
- ...version3 === undefined ? {} : { version: Number(version3) }
15640
- };
15641
- }
15642
- if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
15643
- return null;
15644
- return {
15645
- owner: { kind: "project", project },
15646
- kind: targetKind,
15647
- id: targetID,
15648
- artifact: artifact2
15649
- };
15650
- }
15651
- const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
15652
- if (!issueReference)
15653
- return null;
15654
- const [, issue2, spec, log, children, artifact, version2, ask, comment, message] = issueReference;
15655
- if (!issue2 || version2 !== undefined && Number(version2) < 1)
15656
- return null;
15657
- const owner = { kind: "issue", issue: issue2 };
15658
- if (spec)
15659
- return { owner, kind: "spec", id: spec };
15660
- if (log)
15661
- return { owner, kind: "log", id: log };
15662
- if (children)
15663
- return { owner, kind: "children", id: children };
15664
- if (artifact) {
15665
- return {
15666
- owner,
15667
- kind: "artifact",
15668
- id: artifact,
15669
- ...version2 === undefined ? {} : { version: Number(version2) }
15670
- };
15671
- }
15672
- if (ask)
15673
- return { owner, kind: "ask", id: ask };
15674
- if (comment)
15675
- return { owner, kind: "comment", id: comment };
15676
- if (message)
15677
- return { owner, kind: "message", id: message };
15678
- return { owner, kind: "issue", id: issue2 };
15679
- }
16011
+ var pictureSendingTools = {
16012
+ dispatch_ask: true,
16013
+ dispatch_comment: true,
16014
+ dispatch_message: true
16015
+ };
15680
16016
  function refTarget(ref, kind, id) {
15681
16017
  const ownerRef = ref.owner.kind === "issue" ? dispatchIssueRef(ref.owner.issue) : dispatchDocumentRef(ref.owner.project, `${ref.artifact}`);
15682
16018
  return dispatchChildRef(ownerRef, kind, id);
@@ -15856,6 +16192,15 @@ function dispatchRefFromUrl(value, serverUrl) {
15856
16192
  return `dispatch://${key}/message/${message}`;
15857
16193
  return `dispatch://${key}`;
15858
16194
  }
16195
+ const agentArtifactPage = url2.pathname.match(/^\/agents\/([^/]+)\/artifacts\/([^/]+)\/?$/);
16196
+ if (agentArtifactPage) {
16197
+ const [, session, slug2] = agentArtifactPage;
16198
+ try {
16199
+ return `dispatch://agent/${decodeURIComponent(session ?? "")}/artifact/${decodeURIComponent(slug2 ?? "")}${versionOf("v")}`;
16200
+ } catch {
16201
+ return;
16202
+ }
16203
+ }
15859
16204
  const documentPage = url2.pathname.match(/^\/projects\/([A-Z][A-Z0-9]{1,9})\/documents\/([^/]+)\/?$/);
15860
16205
  if (!documentPage)
15861
16206
  return;
@@ -15866,7 +16211,7 @@ function dispatchRefFromUrl(value, serverUrl) {
15866
16211
  return;
15867
16212
  return item === undefined ? document : `${document}/${item.kind}/${item.id}`;
15868
16213
  }
15869
- 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>, or " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename); " + "a dashboard URL on this Dispatch server is accepted too";
16214
+ 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";
15870
16215
  var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
15871
16216
  var toolSchemas = new Map;
15872
16217
  function toolSchema(tool) {
@@ -15890,6 +16235,16 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
15890
16235
  const refText = dispatchRefFromUrl(refArgument, serverUrl) ?? refArgument;
15891
16236
  if (refText !== refArgument)
15892
16237
  args = { ...input, ref: refText };
16238
+ const agentArtifact = parseAgentArtifactRef(refText);
16239
+ if (agentArtifact !== null) {
16240
+ if (tool !== "dispatch_doc_read") {
16241
+ problems.push(`ref ${refText} names a picture in a conversation on the Agents page; only dispatch_doc_read reads one`);
16242
+ }
16243
+ if (args.issue !== undefined || args.project !== undefined || args.artifact !== undefined) {
16244
+ problems.push("a dispatch://agent/... ref names its upload alone: name no issue, project, or artifact with it");
16245
+ }
16246
+ return { args, ref: null, owner: null, agentArtifact };
16247
+ }
15893
16248
  ref = parseDispatchRef(refText);
15894
16249
  if (ref === null) {
15895
16250
  problems.push(tool === "dispatch_ask" && !refText.startsWith("dispatch://") ? "ref must be a dispatch:// reference" : refGrammarProblem);
@@ -16232,6 +16587,7 @@ function eventHead(event) {
16232
16587
  case "ask.answered":
16233
16588
  return `${textHead(event.payload.question)} -> ${textHead(askAnswerText(event.payload.answer))}`;
16234
16589
  case "comment.created":
16590
+ case "comment.answered":
16235
16591
  case "comment.anchor_refreshed":
16236
16592
  case "comment.edited":
16237
16593
  case "comment.resolved":
@@ -16417,20 +16773,42 @@ async function blockAsks(client, resolved, state) {
16417
16773
  const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
16418
16774
  return asks.filter((ask) => typeof ask.block_id === "string" && ask.block_artifact?.id === resolved.artifact.id);
16419
16775
  }
16420
- async function readUploadedFile(client, resolved, requested) {
16421
- const { artifact } = resolved;
16776
+ async function readUploadedFile(client, artifact, details, requested, owner, shown) {
16422
16777
  const latest = Math.max(0, ...artifact.versions.map((version2) => version2.number));
16423
16778
  const number4 = requested ?? latest;
16424
- const file2 = await client.fileVersion(artifact.id, number4);
16425
16779
  const of = number4 === latest ? "" : ` of ${latest}`;
16780
+ const bytesRoute = `GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`;
16781
+ const tooLarge = (mime, bytes) => ({
16782
+ text: `${oversizePictureText(artifact.name, bytes)}, so dispatch_doc_read cannot show this ` + `uploaded ${mime} picture (version ${number4}${of}). ${bytesRoute}`,
16783
+ details
16784
+ });
16785
+ const stated = artifact.versions.find((version2) => version2.number === number4);
16786
+ if (artifact.kind === "image" && stated?.size !== undefined && stated.size > PICTURE_SHOWN_MAX_BYTES) {
16787
+ return tooLarge(stated.mime ?? "image", stated.size);
16788
+ }
16789
+ const file2 = await client.fileVersion(artifact.id, number4);
16426
16790
  const size = `${file2.bytes.length.toLocaleString("en-US")} bytes`;
16427
- const details = resolved.owner.kind === "project" ? { project: artifact.project, document: documentLabel(artifact.project, artifact.slug) } : { issue: resolved.issue?.key };
16791
+ if (artifact.kind === "image") {
16792
+ const image = toolImage(file2.bytes);
16793
+ if (image !== undefined) {
16794
+ const address = `dispatch://${owner}/artifact/${artifact.slug}@v${number4}`;
16795
+ shown?.add(address);
16796
+ return {
16797
+ text: `Picture ${artifact.name}: ${image.mimeType}, version ${number4}${of}, ${size}.
16798
+ ` + shownPictureLine(1, address, artifact.name, image.mimeType, file2.bytes.length),
16799
+ details,
16800
+ images: [image]
16801
+ };
16802
+ }
16803
+ if (sniffPictureType(file2.bytes) !== undefined)
16804
+ return tooLarge(file2.mime, file2.bytes.length);
16805
+ }
16428
16806
  let text;
16429
16807
  try {
16430
16808
  text = new TextDecoder("utf-8", { fatal: true }).decode(file2.bytes);
16431
16809
  } catch {
16432
16810
  return {
16433
- 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. GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`,
16811
+ 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}`,
16434
16812
  details
16435
16813
  };
16436
16814
  }
@@ -16571,6 +16949,7 @@ async function executeDispatchTool(input) {
16571
16949
  problems.push(...formatZodIssues(issues, schema));
16572
16950
  }
16573
16951
  problems.push(...argumentProblems(input.tool, ownerArguments.args));
16952
+ const pictures = pictureSendingTools[input.tool] === true ? await localPictures(ownerArguments.args.images, input.cwd, problems) : [];
16574
16953
  if (problems.length > 0)
16575
16954
  throw new ToolInputError(input.tool, problems);
16576
16955
  const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
@@ -16622,6 +17001,17 @@ async function executeDispatchTool(input) {
16622
17001
  throw new Error("issue or project is required");
16623
17002
  return owner;
16624
17003
  };
17004
+ const postWithPictures = (text, limit, owner2, upload) => textWithPictures(input.tool, text, pictures, limit, owner2, upload, actor);
17005
+ const writePictures = (resolved, text, limit) => {
17006
+ if (resolved?.owner.kind === "project") {
17007
+ const { project } = resolved.owner;
17008
+ return postWithPictures(text, limit, project, (upload) => client.projectArtifact(project, upload));
17009
+ }
17010
+ const issueKey = issue2();
17011
+ return postWithPictures(text, limit, issueKey, (upload) => client.artifact(issueKey, upload));
17012
+ };
17013
+ const hostSession = input.sessionId?.trim();
17014
+ const shown = hostSession ? shownPictures(hostSession) : undefined;
16625
17015
  switch (input.tool) {
16626
17016
  case "dispatch_issue": {
16627
17017
  const project = stringArg(args, "project");
@@ -16659,6 +17049,7 @@ async function executeDispatchTool(input) {
16659
17049
  return {
16660
17050
  text: [
16661
17051
  `Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
17052
+ ...spec === undefined ? [] : [SPEC_CHECK_REMINDER],
16662
17053
  ...adviceLines
16663
17054
  ].join(`
16664
17055
  `),
@@ -16924,8 +17315,15 @@ async function executeDispatchTool(input) {
16924
17315
  const multiple = optionalBoolean(args, "multiple");
16925
17316
  const urgency = askUrgency(args);
16926
17317
  const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
17318
+ const withRef = askQuestionWithRef(args);
17319
+ const refAppended = withRef !== stringArg(args, "question");
17320
+ const question = await writePictures(resolved, withRef, {
17321
+ cap: ASK_QUESTION_MAX,
17322
+ text: refAppended ? "question plus ref" : "question",
17323
+ shorten: refAppended ? "shorten the question, drop the ref" : "shorten the question"
17324
+ });
16927
17325
  const askInput = {
16928
- question: askQuestionWithRef(args),
17326
+ question,
16929
17327
  ...Array.isArray(options) ? { options } : {},
16930
17328
  ...multiple === undefined ? {} : { multiple },
16931
17329
  ...urgency === undefined ? {} : { urgency },
@@ -16983,8 +17381,13 @@ ${followsAsk(askOwner)}`,
16983
17381
  ]);
16984
17382
  }
16985
17383
  const requestedTurn = optionalString(args, "turn");
17384
+ const body = await writePictures(resolved, stringArg(args, "body"), {
17385
+ cap: DISPATCH_BODY_MAX,
17386
+ text: "body",
17387
+ shorten: "shorten the body"
17388
+ });
16986
17389
  const commentInput = {
16987
- body: stringArg(args, "body"),
17390
+ body,
16988
17391
  ...anchored === undefined ? {} : { anchor: anchored },
16989
17392
  ...replyTo === undefined ? {} : { reply_to: replyTo },
16990
17393
  ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
@@ -17044,16 +17447,29 @@ ${followsAsk(askOwner)}`,
17044
17447
  }
17045
17448
  case "dispatch_message": {
17046
17449
  const inReplyTo = messageInReplyTo(args);
17047
- const body = stringArg(args, "body");
17450
+ const written = stringArg(args, "body");
17451
+ const bodyLimit = {
17452
+ cap: DISPATCH_BODY_MAX,
17453
+ text: "body",
17454
+ shorten: "shorten the body"
17455
+ };
17048
17456
  if (owner === null && inReplyTo !== undefined) {
17049
- const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor }, { followUp: true });
17050
- if (reply.duplicate === true && reply.body === body) {
17457
+ let body2 = written;
17458
+ if (pictures.length > 0) {
17459
+ const sessionId = input.sessionId?.trim();
17460
+ if (!sessionId) {
17461
+ throw new Error("host session id is required to send pictures in a direct-message reply");
17462
+ }
17463
+ body2 = await postWithPictures(written, bodyLimit, `agent/${sessionId}`, (upload) => client.agentArtifact(sessionId, upload));
17464
+ }
17465
+ const reply = await client.messageReply(inReplyTo, { body: body2, attempt: 1, actor }, { followUp: true });
17466
+ if (reply.duplicate === true && reply.body === body2) {
17051
17467
  return {
17052
17468
  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.",
17053
17469
  details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
17054
17470
  };
17055
17471
  }
17056
- if (reply.body !== body) {
17472
+ if (reply.body !== body2) {
17057
17473
  return {
17058
17474
  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.",
17059
17475
  details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
@@ -17073,6 +17489,7 @@ ${followsAsk(askOwner)}`,
17073
17489
  };
17074
17490
  }
17075
17491
  const issueKey = issue2();
17492
+ const body = await postWithPictures(written, bodyLimit, issueKey, (upload) => client.artifact(issueKey, upload));
17076
17493
  const message = await client.message(issueKey, {
17077
17494
  body,
17078
17495
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
@@ -17136,11 +17553,20 @@ ${followsAsk(askOwner)}`,
17136
17553
  };
17137
17554
  }
17138
17555
  case "dispatch_doc_read": {
17556
+ const { agentArtifact } = ownerArguments;
17557
+ if (agentArtifact !== undefined) {
17558
+ const artifact = await client.getAgentArtifact(agentArtifact.session, agentArtifact.slug);
17559
+ return readUploadedFile(client, artifact, { session: agentArtifact.session, artifact: artifact.slug }, optionalNumber(args, "version") ?? agentArtifact.version, `agent/${agentArtifact.session}`, shown);
17560
+ }
17139
17561
  const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
17140
17562
  const resolved = await resolveDocument(documentOwner(), artifactReference);
17141
17563
  const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
17142
17564
  if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
17143
- return readUploadedFile(client, resolved, version2);
17565
+ const { artifact } = resolved;
17566
+ return readUploadedFile(client, artifact, resolved.owner.kind === "project" ? {
17567
+ project: artifact.project,
17568
+ document: documentLabel(artifact.project, artifact.slug)
17569
+ } : { issue: resolved.issue?.key }, version2, resolved.owner.kind === "project" ? resolved.owner.project : resolved.owner.issue, shown);
17144
17570
  }
17145
17571
  const documentPromise = client.docRead(resolved.artifact.id, version2);
17146
17572
  const marksPromise = openArtifactMarks(client, resolved);
@@ -17188,7 +17614,8 @@ ${trailer.join(`
17188
17614
  const details = await followedAskDetails(client, result.ask, resolved.artifact);
17189
17615
  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.`;
17190
17616
  return {
17191
- 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.`,
17617
+ 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 ? `
17618
+ ${SPEC_CHECK_REMINDER}` : ""}`,
17192
17619
  details: { ...details, artifact: resolved.artifact.id, version: result.version }
17193
17620
  };
17194
17621
  }
@@ -17198,7 +17625,7 @@ ${trailer.join(`
17198
17625
  const content = optionalString(args, "content");
17199
17626
  const artifactInput = content === undefined ? {
17200
17627
  name,
17201
- file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
17628
+ file: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))),
17202
17629
  ...summary === undefined ? {} : { summary },
17203
17630
  actor
17204
17631
  } : {
@@ -17251,18 +17678,29 @@ ${trailer.join(`
17251
17678
  }
17252
17679
  case "dispatch_read": {
17253
17680
  const message = optionalString(args, "message");
17681
+ const picturesOf = (texts) => readPictures(client, picturesNewestFirst(texts), shown);
17682
+ const showImages = (pictures3) => {
17683
+ for (const address of pictures3.shown)
17684
+ shown?.add(address);
17685
+ return pictures3.images.length === 0 ? {} : { images: pictures3.images };
17686
+ };
17254
17687
  if (message !== undefined) {
17255
17688
  const sessionId = input.sessionId?.trim();
17256
17689
  if (!sessionId)
17257
17690
  throw new Error("host session id is required for dispatch_read({message})");
17258
17691
  const thread = await client.getMessageThread(messageIdOf(message), sessionId);
17259
17692
  const issueKey2 = thread.message.issue_key;
17693
+ const [pictures3, graph2] = await Promise.all([
17694
+ picturesOf(datedBodies([thread.message, ...thread.replies])),
17695
+ issueKey2 === null ? [] : graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))
17696
+ ]);
17260
17697
  return {
17261
- text: messageSummary(thread, issueKey2 === null ? [] : await graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))),
17698
+ text: messageSummary(thread, [...pictures3.lines, ...graph2]),
17262
17699
  details: {
17263
17700
  message: thread.message.id,
17264
17701
  ...issueKey2 === null ? {} : { issue: issueKey2 }
17265
- }
17702
+ },
17703
+ ...showImages(pictures3)
17266
17704
  };
17267
17705
  }
17268
17706
  if (ownerArguments.ref?.kind === "ask") {
@@ -17270,9 +17708,14 @@ ${trailer.join(`
17270
17708
  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"));
17271
17709
  const askRead = await client.getAsk(id);
17272
17710
  const askRef = refTarget(ref, "ask", id);
17711
+ const [pictures3, graph2] = await Promise.all([
17712
+ picturesOf(askTexts(askRead)),
17713
+ graphSections(client, askRef)
17714
+ ]);
17273
17715
  return {
17274
- text: askSummary(askRead, await graphSections(client, askRef)),
17275
- details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue }
17716
+ text: askSummary(askRead, [...pictures3.lines, ...graph2]),
17717
+ details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue },
17718
+ ...showImages(pictures3)
17276
17719
  };
17277
17720
  }
17278
17721
  if (ownerArguments.ref?.kind === "comment") {
@@ -17280,9 +17723,14 @@ ${trailer.join(`
17280
17723
  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));
17281
17724
  const comment = await client.getComment(id);
17282
17725
  const commentRef = refTarget(ref, "comment", id);
17726
+ const [pictures3, graph2] = await Promise.all([
17727
+ picturesOf(datedBodies([comment.comment, ...comment.replies])),
17728
+ graphSections(client, commentRef)
17729
+ ]);
17283
17730
  return {
17284
- text: commentSummary(comment, await graphSections(client, commentRef)),
17285
- details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key }
17731
+ text: commentSummary(comment, [...pictures3.lines, ...graph2]),
17732
+ details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key },
17733
+ ...showImages(pictures3)
17286
17734
  };
17287
17735
  }
17288
17736
  if (ownerArguments.ref?.kind === "message") {
@@ -17291,9 +17739,14 @@ ${trailer.join(`
17291
17739
  }
17292
17740
  const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
17293
17741
  const messageRef = dispatchChildRef(dispatchIssueRef(ownerArguments.ref.owner.issue), "message", ownerArguments.ref.id);
17742
+ const [pictures3, graph2] = await Promise.all([
17743
+ picturesOf(datedBodies([messageRead.message, ...messageRead.replies])),
17744
+ graphSections(client, messageRef)
17745
+ ]);
17294
17746
  return {
17295
- text: messageSummary(messageRead, await graphSections(client, messageRef)),
17296
- details: { issue: messageRead.message.issue_key }
17747
+ text: messageSummary(messageRead, [...pictures3.lines, ...graph2]),
17748
+ details: { issue: messageRead.message.issue_key },
17749
+ ...showImages(pictures3)
17297
17750
  };
17298
17751
  }
17299
17752
  if (documentOwner().kind === "project") {
@@ -17317,9 +17770,12 @@ ${trailer.join(`
17317
17770
  const issueKey = issue2();
17318
17771
  if (ownerArguments.ref?.kind === "log") {
17319
17772
  const read2 = await client.read(issueKey);
17773
+ const pictures3 = await picturesOf(read2.events.flatMap(eventTexts));
17320
17774
  return {
17321
- text: logSummary(read2.issue, read2.events),
17322
- details: { issue: read2.issue.key }
17775
+ text: [logSummary(read2.issue, read2.events), ...pictures3.lines].join(`
17776
+ `),
17777
+ details: { issue: read2.issue.key },
17778
+ ...showImages(pictures3)
17323
17779
  };
17324
17780
  }
17325
17781
  if (ownerArguments.ref?.kind === "children") {
@@ -17332,14 +17788,19 @@ ${trailer.join(`
17332
17788
  const readPromise = client.read(issueKey);
17333
17789
  const referencesPromise = issueReferencesOrUnavailable(client, issueKey);
17334
17790
  const read = await readPromise;
17335
- const [references, graph, titles] = await Promise.all([
17791
+ const [references, graph, titles, pictures2] = await Promise.all([
17336
17792
  referencesPromise,
17337
17793
  graphSections(client, dispatchIssueRef(read.issue.key)),
17338
- liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
17794
+ liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue)),
17795
+ picturesOf([
17796
+ ...read.issue.open_asks.map((ask) => ({ text: ask.question, at: ask.created_at })),
17797
+ ...read.events.flatMap(eventTexts)
17798
+ ])
17339
17799
  ]);
17340
17800
  return {
17341
- text: issueSummary(read.issue, read.events, references, graph, titles),
17342
- details: { issue: read.issue.key }
17801
+ text: issueSummary(read.issue, read.events, references, [...pictures2.lines, ...graph], titles),
17802
+ details: { issue: read.issue.key },
17803
+ ...showImages(pictures2)
17343
17804
  };
17344
17805
  }
17345
17806
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "5.8.2",
3
+ "version": "5.9.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
package/skills/AGENTS.md CHANGED
@@ -9,7 +9,7 @@ event intake, process lifecycle, credentials, and role delivery.
9
9
  | `ce-simplify-code/` | the implementer, once per pull request | the behaviour-preserving simplify pass, in the implementer's first implementing round (Legion's copy of the MIT-licensed Compound Engineering skill; `LICENSE` beside it) |
10
10
  | `dispatch/` | every role, and any session writing to Dispatch | specs, asks, comments, artifacts, and messages on native Dispatch |
11
11
  | `dispatch-brainstorming/` | any session with Dispatch that starts a design conversation or writes a plan, as `dispatch-first` directs | the design conversation in the issue's spec, turn by turn to one approval, and the plan as the issue's `plan.md`; in a session with Dispatch it replaces superpowers' `brainstorming` and `writing-plans` |
12
- | `dispatch-first/` | every session with Dispatch configured, injected by each host plugin on every request (Oh My Pi), on every `SessionStart` (startup, resume, clear, compact, fork) and `SubagentStart` (Claude Code), or as an instruction file (OpenCode) | searching Dispatch before acting, extending the existing issue, citing decisions, closing duplicates; kept under 60 lines and 6,000 characters |
12
+ | `dispatch-first/` | every session with Dispatch configured, injected by each host plugin on every request (Oh My Pi), on every `SessionStart` (startup, resume, clear, compact, fork) and `SubagentStart` (Claude Code), or as an instruction file (OpenCode) | searching Dispatch before acting, extending the existing issue, citing decisions, closing duplicates, the fresh-reader check before a spec reaches a human; kept under 60 lines and 6,000 characters |
13
13
  | `envoy/` | every role | subscriptions, agent-to-agent messages, and topic formats |
14
14
  | `legion-architect/` | root and sub-architects | tree ownership, decomposition, waves, gates, integration, sign-off |
15
15
  | `legion-controller/` | the controller root process | wake routing, keeping the admission slots full from `todo`, the daily report, escalation |
@@ -416,6 +416,9 @@ It returns live or versioned markdown with open marks. A live read ends with a d
416
416
  `artifact` reads the issue specification; a project needs `artifact`; and a `dispatch://PROJECT/artifact/<document-ref>`
417
417
  ref supplies both, where `document-ref` is the slug (an id or a filename resolves when no document has that slug). A file
418
418
  `dispatch_artifact` uploaded reads its text at the latest or named version, or a description when it is not UTF-8 text.
419
+ A picture (PNG, JPEG, GIF or WebP under 3,750,000 bytes, 5 MB of base64) comes back as an image you see, with its name, type, size and version;
420
+ that includes `dispatch://agent/<session id>/artifact/<slug>@vN`, one a person sent you on the Agents page. A session is shown each picture once,
421
+ since the provider refuses a request over 32 MB: later reads and deliveries name it, and `dispatch_doc_read` shows it again (after compaction too).
419
422
 
420
423
  Editing one is [Editing a document](skill://dispatch/references/document-edits.md): the shape of `dispatch_doc_edit`,
421
424
  how to quote the text you mean, one `replace` per paragraph, preconditions against a stale edit, and
@@ -474,6 +477,9 @@ dispatch_message({ issue, body })
474
477
  It returns `details` `{ issue, message }`. `body` is capped at 2,000 characters. A message is not a decision
475
478
  (a decision block, or `dispatch_ask` for a human to-do) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
476
479
 
480
+ **Pictures.** To show a screenshot inline, pass its path in `images` (`dispatch_message`, `dispatch_comment`, `dispatch_ask`);
481
+ `dispatch_read` hands you the pictures what it shows embeds. Both are in [Documents](skill://dispatch/references/documents.md), "Artifacts".
482
+
477
483
  A BTW, Aside or Steer frame, or a message from the Agents page, is answered as
478
484
  [Targeted and direct messages](skill://dispatch/references/messages.md) says.
479
485
 
@@ -146,6 +146,29 @@ slug also arrives on `artifact.created` events. Dispatch suffixes a slug two doc
146
146
  filename can be another's slug (`plan v2` takes `plan-v2`, then a document named `plan-v2` takes `plan-v2-2`): a bare
147
147
  `artifact` that names both is refused with each one's id, while a `dispatch://` reference's document part is always the slug.
148
148
 
149
+ A picture meant to be seen inline in a message, comment or ask is not uploaded with `dispatch_artifact`: pass its local
150
+ path in that tool's `images` (PNG, JPEG, GIF or WebP by its bytes, at most 25 MiB each). Each is uploaded to the issue (a
151
+ project document's project; for a reply to a direct message, your own conversation) and appended after a blank line as
152
+ one `![<file name>](dispatch://<owner>/artifact/<slug>@vN)` line, about 60 characters that count toward the body or
153
+ question cap; a call the lines would carry over the cap is refused with the count before anything uploads, and again,
154
+ with nothing posted, in the rare case where Dispatch gives an upload a longer address than that (a later version of a
155
+ name the owner already holds, or a slug another file took first). The `@vN` pins the version the upload made (the same
156
+ file name again makes the next version). A plain `dispatch://KEY/artifact/<slug>` reference to an image stays a link
157
+ with a thumbnail.
158
+
159
+ `dispatch_read` hands you the pictures the messages, asks and comments it shows embed, newest first, at most 8 and
160
+ 10 MiB per read, and its `Pictures:` section names each one shown, in order, and the rest by reference for
161
+ `dispatch_doc_read`, which shows one picture (at most 3,750,000 bytes, the 5 MB of base64 a model provider takes; a
162
+ larger one or an SVG is described). A picture in a
163
+ conversation on the Agents page belongs to that conversation, `dispatch://agent/<session id>/artifact/<slug>@vN`, and
164
+ `dispatch_doc_read` reads it by that reference alone. On Oh My Pi an Inbox delivery carries its pictures the same way; in
165
+ Claude Code it stays text with the reference.
166
+
167
+ A session is shown each picture once. A later read or delivery that embeds a picture you were already shown names it in
168
+ `Pictures:` as `shown earlier this session` and sends no bytes, since every request carries the session's whole history
169
+ and a model provider refuses one over 32 MB; `dispatch_doc_read` always shows the picture, so ask it for one you need
170
+ again, after compaction too.
171
+
149
172
  Documents are CommonMark. A bare `<https://example.com|text>` is a CommonMark autolink and is normalised: the angle brackets are
150
173
  dropped and the URL keeps `|text`. A backslash-escaped `\<https://example.com|text>` displays as `<https://example.com|text>` in the
151
174
  document but comes back re-escaped (`\<`) from `dispatch_doc_read`. A Slack mrkdwn draft, or any other payload that is not Markdown,
@@ -50,7 +50,7 @@ found yourself.
50
50
  A design conversation with a person, or the implementation plan that follows one, uses
51
51
  `skill://dispatch-brainstorming`: read it before your first design question on a change, the spec,
52
52
  or that plan. It replaces superpowers' `brainstorming` and `writing-plans` here, even when the user
53
- invokes one by name. A Legion architect's own issue spec follows `skill://legion-architect`.
53
+ invokes one by name. A Legion architect's own issue spec follows `skill://legion-architect`. Before a spec's first version and each approval request, have a fresh read-only subagent (on Oh My Pi, `task(agent="plan-gap-analyst")`) check it against the code, the human's words and any command output a claim rests on, and resolve what it flags.
54
54
 
55
55
  ## Load the full skill before you write
56
56