@sjawhar/opencode-legion-envoy 6.4.1 → 6.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.
@@ -15,7 +15,7 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/server.ts
18
- import { existsSync } from "fs";
18
+ import { existsSync, readFileSync as readFileSync3 } from "fs";
19
19
  import path3 from "path";
20
20
  import { fileURLToPath } from "url";
21
21
 
@@ -13552,19 +13552,11 @@ function date4(params) {
13552
13552
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
13553
13553
  config(en_default());
13554
13554
  // ../contracts/src/dispatch-api.ts
13555
- function serviceSubjectLabel(subject) {
13556
- const parts = subject.split(":");
13557
- if (parts.length === 4 && parts[0] === "system" && parts[1] === "serviceaccount" && parts[2] !== "" && parts[3] !== "") {
13558
- return `${parts[2]}/${parts[3]}`;
13559
- }
13560
- return subject;
13561
- }
13562
13555
  var MAX_ISSUE_PAGE_LIMIT = 250;
13563
13556
  var DEFAULT_ISSUE_PAGE_LIMIT = 50;
13564
13557
  var ASK_TURNS = ["human", "agent"];
13565
13558
  var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
13566
13559
  var DELIVERY_DUPLICATE_WINDOW_MS = 72 * 60 * 60 * 1000;
13567
- var SEARCH_DEGRADED_EMBEDDER_UNAVAILABLE = "embedder_unavailable";
13568
13560
  var DispatchEventSchema = exports_external.object({
13569
13561
  issue_key: exports_external.string().nullable(),
13570
13562
  artifact_id: exports_external.string().nullish(),
@@ -13765,36 +13757,6 @@ var AskFollowerEventPayloadSchema = exports_external.object({
13765
13757
  session_id: exports_external.string().optional(),
13766
13758
  by: exports_external.object({ kind: exports_external.string(), id: exports_external.string().optional() }).passthrough().optional()
13767
13759
  });
13768
-
13769
- // ../contracts/src/actor-label.ts
13770
- function shortSessionId(id) {
13771
- return `session:${id.slice(0, 8)}\u2026`;
13772
- }
13773
- function sessionLabel(sessionId, title) {
13774
- const trimmed = title?.trim() ?? "";
13775
- return trimmed === "" ? shortSessionId(sessionId) : trimmed;
13776
- }
13777
- function actorName(actor, titles) {
13778
- if (actor.kind !== "session") {
13779
- return actor.id;
13780
- }
13781
- const live = titles?.get(actor.id)?.trim();
13782
- return sessionLabel(actor.id, live === undefined || live === "" ? actor.origin?.session_title : live);
13783
- }
13784
- function actorLabel(actor, titles) {
13785
- const name = actorName(actor, titles);
13786
- if (actor.kind !== "session") {
13787
- return name;
13788
- }
13789
- if (actor.owner !== undefined) {
13790
- return `${name} (for ${actor.owner})`;
13791
- }
13792
- const service = actor.service;
13793
- if (service === undefined) {
13794
- return name;
13795
- }
13796
- return `${name} (as ${serviceSubjectLabel(service)})`;
13797
- }
13798
13760
  // ../contracts/src/agent-stream.ts
13799
13761
  var AGENT_STREAM_LIMITS = {
13800
13762
  partChars: 16000,
@@ -13804,118 +13766,11 @@ var AGENT_STREAM_LIMITS = {
13804
13766
  historyBytes: 512 * 1024,
13805
13767
  snapshotIntervalMs: 100
13806
13768
  };
13807
- // ../contracts/src/claim-holds.ts
13808
- function claimHolds(claim, titles) {
13809
- if (claim.actor.kind !== "session") {
13810
- return "holds";
13811
- }
13812
- if (titles === undefined) {
13813
- return "unknown";
13814
- }
13815
- return titles.has(claim.actor.id) ? "holds" : "lapsed";
13816
- }
13817
- // ../contracts/src/dispatch-href.ts
13818
- function hasControlCharacter(value) {
13819
- return /\p{Cc}/u.test(value);
13820
- }
13821
- function itemFromSearch(search) {
13822
- const params = new URLSearchParams(search);
13823
- const ask = params.get("ask");
13824
- const comment = params.get("comment");
13825
- if (ask === null && comment === null) {
13826
- return;
13827
- }
13828
- if (ask !== null && comment !== null) {
13829
- return null;
13830
- }
13831
- const raw = ask ?? comment ?? "";
13832
- if (raw === "" || raw.includes("/")) {
13833
- return null;
13834
- }
13835
- let id;
13836
- try {
13837
- id = decodeURIComponent(raw);
13838
- } catch {
13839
- return null;
13840
- }
13841
- if (hasControlCharacter(id)) {
13842
- return null;
13843
- }
13844
- return ask === null ? { id, kind: "comment" } : { id, kind: "ask" };
13845
- }
13846
- // ../contracts/src/dispatch-snippet.ts
13847
- var HTML_ENTITIES = [
13848
- ["&lt;", "<"],
13849
- ["&gt;", ">"],
13850
- ["&#39;", "'"],
13851
- ["&#34;", '"'],
13852
- ["&amp;", "&"]
13853
- ];
13854
- function decodeEntities(text) {
13855
- let decoded = text;
13856
- for (const [entity, character] of HTML_ENTITIES) {
13857
- decoded = decoded.replaceAll(entity, character);
13858
- }
13859
- return decoded;
13860
- }
13861
- function hasOnlyBalancedMarkers(snippet) {
13862
- let marked = false;
13863
- for (const marker of snippet.matchAll(/<mark>|<\/mark>/gu)) {
13864
- if (marker[0] === "<mark>") {
13865
- if (marked)
13866
- return false;
13867
- marked = true;
13868
- } else {
13869
- if (!marked)
13870
- return false;
13871
- marked = false;
13872
- }
13873
- }
13874
- return !marked;
13875
- }
13876
- function snippetSegments(snippet) {
13877
- if (!hasOnlyBalancedMarkers(snippet)) {
13878
- return snippet === "" ? [] : [{ text: decodeEntities(snippet), mark: false }];
13879
- }
13880
- let mark = false;
13881
- const segments = [];
13882
- for (const part of snippet.split(/(<mark>|<\/mark>)/u)) {
13883
- if (part === "<mark>") {
13884
- mark = true;
13885
- } else if (part === "</mark>") {
13886
- mark = false;
13887
- } else if (part !== "") {
13888
- segments.push({ text: decodeEntities(part), mark });
13889
- }
13890
- }
13891
- return segments;
13892
- }
13893
- function snippetText(snippet) {
13894
- return snippetSegments(snippet).map(({ text, mark }) => mark ? `**${text}**` : text).join("");
13895
- }
13896
13769
  // ../contracts/src/dispatch-text-rules.ts
13897
13770
  var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
13898
13771
  var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
13899
- function pictureCaption(name) {
13900
- return name.replace(/[\\[\]`]/g, "\\$&").replace(/\s+/g, " ");
13901
- }
13902
- var PICTURE_TYPES = {
13903
- "image/png": true,
13904
- "image/jpeg": true,
13905
- "image/gif": true,
13906
- "image/webp": true
13907
- };
13908
- function isPictureType(mime) {
13909
- return PICTURE_TYPES[mime] === true;
13910
- }
13911
13772
  // ../contracts/src/dispatch-tools.ts
13912
- function dispatchToolSchema(spec, z2, opts) {
13913
- const shape = spec.arguments(z2);
13914
- const strict = opts?.strict ?? spec.strict;
13915
- const schemaOptions = strict === undefined ? undefined : { strict };
13916
- return spec.validation === undefined ? z2.object(shape, schemaOptions) : z2.refineObject(shape, spec.validation.check, spec.validation.message, schemaOptions);
13917
- }
13918
- 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.";
13773
+ 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.";
13919
13774
  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.";
13920
13775
  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.";
13921
13776
  var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
@@ -14016,7 +13871,7 @@ var dispatchToolSpecs = [
14016
13871
  {
14017
13872
  name: "dispatch_issue",
14018
13873
  example: { project: "DSP", title: "Native workspace" },
14019
- 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}`,
13874
+ 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}`,
14020
13875
  arguments: (z2) => ({
14021
13876
  project: z2.string().describe("Project key for the new issue."),
14022
13877
  title: z2.string().describe("Concise issue title."),
@@ -14063,7 +13918,7 @@ var dispatchToolSpecs = [
14063
13918
  {
14064
13919
  name: "dispatch_claim",
14065
13920
  example: { issue: "DSP-1" },
14066
- 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,
13921
+ 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,
14067
13922
  arguments: (z2) => ({
14068
13923
  issue: z2.string().describe(ISSUE_REFERENCE),
14069
13924
  release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
@@ -14086,7 +13941,7 @@ var dispatchToolSpecs = [
14086
13941
  }
14087
13942
  ]
14088
13943
  },
14089
- 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}`,
13944
+ 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}`,
14090
13945
  arguments: (z2) => ({
14091
13946
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14092
13947
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14160,7 +14015,7 @@ var dispatchToolSpecs = [
14160
14015
  {
14161
14016
  name: "dispatch_resolve_comment",
14162
14017
  example: { comment: "01234567-0000-4000-8000-000000000001" },
14163
- 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.",
14018
+ 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.",
14164
14019
  arguments: (z2) => ({
14165
14020
  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.")
14166
14021
  }),
@@ -14179,7 +14034,7 @@ var dispatchToolSpecs = [
14179
14034
  {
14180
14035
  name: "dispatch_comment",
14181
14036
  example: { issue: "DSP-1", body: "Looks good." },
14182
- 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}`,
14037
+ 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}`,
14183
14038
  arguments: (z2) => ({
14184
14039
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14185
14040
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14203,7 +14058,7 @@ var dispatchToolSpecs = [
14203
14058
  quote: "old wording",
14204
14059
  replace_with: "new wording"
14205
14060
  },
14206
- 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}`,
14061
+ 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}`,
14207
14062
  arguments: (z2) => ({
14208
14063
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14209
14064
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14219,7 +14074,7 @@ var dispatchToolSpecs = [
14219
14074
  {
14220
14075
  name: "dispatch_message",
14221
14076
  example: { issue: "DSP-1", body: "Implementation started." },
14222
- 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}`,
14077
+ 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}`,
14223
14078
  arguments: (z2) => ({
14224
14079
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
14225
14080
  body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Update text, at most 2,000 characters."),
@@ -14236,7 +14091,7 @@ var dispatchToolSpecs = [
14236
14091
  ops: [{ op: "delete_column", block: "table-123", index: 1 }],
14237
14092
  precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
14238
14093
  },
14239
- 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}`,
14094
+ 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}`,
14240
14095
  arguments: (z2) => ({
14241
14096
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14242
14097
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14256,7 +14111,7 @@ var dispatchToolSpecs = [
14256
14111
  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()
14257
14112
  }, { 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."),
14258
14113
  precondition: z2.object({
14259
- document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch_doc_read.").optional(),
14114
+ document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch doc-read.").optional(),
14260
14115
  blocks: z2.array(z2.object({
14261
14116
  id: z2.string({ min: 1 }).describe("Stable block id from GET /api/v1/artifacts/{id}/blocks."),
14262
14117
  token: z2.string({ min: 1 }).describe("That block's full-state token, including inline marks.")
@@ -14269,7 +14124,7 @@ var dispatchToolSpecs = [
14269
14124
  {
14270
14125
  name: "dispatch_doc_read",
14271
14126
  example: { issue: "DSP-1" },
14272
- 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,
14127
+ 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,
14273
14128
  arguments: (z2) => ({
14274
14129
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14275
14130
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14282,7 +14137,7 @@ var dispatchToolSpecs = [
14282
14137
  {
14283
14138
  name: "dispatch_request_approval",
14284
14139
  example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
14285
- 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,
14140
+ 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,
14286
14141
  arguments: (z2) => ({
14287
14142
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14288
14143
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14295,7 +14150,7 @@ var dispatchToolSpecs = [
14295
14150
  name: "dispatch_artifact",
14296
14151
  example: { issue: "DSP-1", name: "design.md", content: `# Design
14297
14152
  ` },
14298
- 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}`,
14153
+ 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}`,
14299
14154
  arguments: (z2) => ({
14300
14155
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14301
14156
  project: z2.string().describe("Project key for an unlinked document.").optional(),
@@ -14315,7 +14170,7 @@ var dispatchToolSpecs = [
14315
14170
  {
14316
14171
  name: "dispatch_read",
14317
14172
  example: { issue: "DSP-1" },
14318
- 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,
14173
+ 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,
14319
14174
  arguments: (z2) => ({
14320
14175
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14321
14176
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14350,7 +14205,7 @@ var dispatchToolSpecs = [
14350
14205
  {
14351
14206
  name: "dispatch_issues",
14352
14207
  example: { project: "PROJ", limit: 250, offset: 250 },
14353
- 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.",
14208
+ 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.",
14354
14209
  arguments: (z2) => ({
14355
14210
  project: z2.string().describe("Project key to list issues from."),
14356
14211
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -14432,19 +14287,9 @@ var AGENT_TOPIC_PREFIX = "notifications.agent.";
14432
14287
  var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
14433
14288
  var DISPATCH_ISSUE_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}issue.`;
14434
14289
  var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
14435
- function dispatchIssueSubject(issueKey, type) {
14436
- return `${DISPATCH_ISSUE_TOPIC_PREFIX}${issueKey}.${type}`;
14437
- }
14438
- function dispatchDocumentSubject(project, slug, type) {
14439
- return `${DISPATCH_DOCUMENT_TOPIC_PREFIX}${project}.${slug}.${type}`;
14440
- }
14441
14290
  function agentSubject(session) {
14442
14291
  return `${AGENT_TOPIC_PREFIX}${session}`;
14443
14292
  }
14444
- // ../contracts/src/repo.ts
14445
- function canonicalRepo(owner, repo) {
14446
- return `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase().replace(/\.git$/, "")}`;
14447
- }
14448
14293
  // ../contracts/src/tool-schema.ts
14449
14294
  function overCapMessage(length, max) {
14450
14295
  return `is ${length - max} characters over the ${max}-character limit (${length}/${max})`;
@@ -14658,3394 +14503,210 @@ function resolveDispatchConfig(env, options = {}) {
14658
14503
  return { enabled: true, url: url2.url, token, error: null };
14659
14504
  }
14660
14505
 
14661
- // ../envoy-client/src/dispatch-execute.ts
14662
- import { resolve as resolvePath2 } from "path";
14663
-
14664
- // ../envoy-client/src/ask-answer.ts
14665
- var HEAD_LENGTH = 120;
14666
- function textHead(text) {
14667
- const flat = text.replace(/\s+/g, " ").trim();
14668
- return flat.length > HEAD_LENGTH ? `${flat.slice(0, HEAD_LENGTH)}\u2026` : flat;
14669
- }
14670
- function askAnswerText(answer) {
14671
- if (answer === null || answer === undefined)
14672
- return "";
14673
- const selected = (answer.selected ?? []).join(", ");
14674
- const text = answer.text ?? "";
14675
- if (text === "")
14676
- return selected;
14677
- if (selected === "")
14678
- return text;
14679
- return `${selected} - ${text}`;
14506
+ // ../envoy-client/src/dispatch-first.ts
14507
+ import { join as join2 } from "path";
14508
+ function dispatchFirstSkillFile(skillsDirectory) {
14509
+ return join2(skillsDirectory, "dispatch-first", "SKILL.md");
14680
14510
  }
14681
14511
 
14682
- // ../envoy-client/src/dispatch-cwd.ts
14683
- import { execFile } from "child_process";
14684
- import { promisify } from "util";
14685
-
14686
14512
  // ../envoy-client/src/machine.ts
14687
14513
  import { hostname as hostname3 } from "os";
14688
14514
  function machineID() {
14689
14515
  return process.env["ENVOY_MACHINE_ID"] || hostname3();
14690
14516
  }
14691
14517
 
14692
- // ../envoy-client/src/dispatch-cwd.ts
14693
- var execFileAsync = promisify(execFile);
14694
- var defaultExec = (file2, args, options) => execFileAsync(file2, args, { cwd: options.cwd, timeout: 5000 });
14695
- async function tryExec(exec, file2, args, cwd) {
14696
- try {
14697
- const { stdout } = await exec(file2, args, { cwd });
14698
- return stdout;
14699
- } catch {
14700
- return null;
14701
- }
14702
- }
14703
- function parseRemoteList(stdout) {
14704
- const remotes = new Map;
14705
- for (const line of stdout.split(`
14706
- `)) {
14707
- const trimmed = line.trim();
14708
- if (!trimmed)
14709
- continue;
14710
- const match = trimmed.match(/^(\S+)\s+(\S+)/);
14711
- const [, name, url2] = match ?? [];
14712
- if (name && url2)
14713
- remotes.set(name, url2);
14714
- }
14715
- return remotes;
14716
- }
14717
- function selectRemoteUrl(remotes) {
14718
- const origin = remotes.get("origin");
14719
- if (origin)
14720
- return origin;
14721
- const candidates = [...remotes.entries()].filter(([name]) => name !== "upstream");
14722
- return candidates.length === 1 ? candidates[0]?.[1] ?? null : null;
14723
- }
14724
- var GITHUB_REMOTE_PATTERNS = [
14725
- /^https:\/\/(?:[^@/\s]+@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i,
14726
- /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?\/?$/i,
14727
- /^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i
14728
- ];
14729
- function parseGitHubRemoteUrl(url2) {
14730
- const trimmed = url2.trim();
14731
- for (const pattern of GITHUB_REMOTE_PATTERNS) {
14732
- const match = trimmed.match(pattern);
14733
- if (match)
14734
- return canonicalRepo(match[1] ?? "", match[2] ?? "");
14735
- }
14736
- return null;
14737
- }
14738
- async function resolveCwdRepo(cwd, exec) {
14739
- const jjOutput = await tryExec(exec, "jj", ["git", "remote", "list"], cwd);
14740
- if (jjOutput !== null) {
14741
- const url2 = selectRemoteUrl(parseRemoteList(jjOutput));
14742
- return url2 ? parseGitHubRemoteUrl(url2) : null;
14743
- }
14744
- const originUrl = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd);
14745
- return originUrl ? parseGitHubRemoteUrl(originUrl) : null;
14746
- }
14747
- async function resolveOrigin(env, exec, cwd) {
14748
- const origin = {
14749
- cwd,
14750
- machine: machineID()
14751
- };
14752
- const pane = env.TMUX_PANE;
14753
- if (pane) {
14754
- const output = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd);
14755
- const [target, paneId] = output?.trim().split(" ") ?? [];
14756
- if (target)
14757
- origin.tmux = target;
14758
- if (paneId)
14759
- origin.pane = paneId;
14760
- }
14761
- return origin;
14762
- }
14763
-
14764
- // ../envoy-client/src/dispatch-http.ts
14765
- class DispatchServiceError extends Error {
14766
- code;
14767
- status;
14768
- candidates;
14769
- current;
14770
- mismatches;
14771
- name = "DispatchServiceError";
14772
- fromDispatch = true;
14773
- constructor(code, status, message, candidates, current, mismatches) {
14774
- super(message);
14775
- this.code = code;
14776
- this.status = status;
14777
- this.candidates = candidates;
14778
- this.current = current;
14779
- this.mismatches = mismatches;
14780
- }
14518
+ // ../envoy-client/src/dispatch-command.ts
14519
+ function commandName(tool) {
14520
+ return tool.replace(/^dispatch_/, "").replaceAll("_", "-");
14781
14521
  }
14522
+ var toolsByCommand = new Map(dispatchToolSpecs.map((spec) => [commandName(spec.name), spec.name]));
14523
+ var surfaces = new Map;
14782
14524
 
14783
- class DispatchGatewayError extends DispatchServiceError {
14784
- answer;
14785
- advice;
14786
- fromDispatch = false;
14787
- transient;
14788
- mayHaveReachedDispatch;
14789
- constructor(status, answer, advice, message = `${answer}, so ${advice}.`) {
14790
- super(`HTTP_${status}`, status, message);
14791
- this.answer = answer;
14792
- this.advice = advice;
14793
- this.transient = transientStatus(status);
14794
- this.mayHaveReachedDispatch = reachesDispatch(status);
14795
- }
14796
- }
14797
- function asErrorShape(value) {
14798
- return typeof value === "object" && value !== null ? value : {};
14799
- }
14800
- function isJson(response) {
14801
- return response.headers.get("content-type")?.includes("application/json") ?? false;
14525
+ // ../envoy-client/src/tool-contract.ts
14526
+ var DELIVERY_CONTRACT = "Delivery is at-least-once, possibly out of order across topics; use id for dedupe and at for freshness.";
14527
+ var TOPIC_GUIDE = "Topic guide (all are under notifications.): agent.<session_id> (subscribe: own inbox); role.<role> " + "(publish-to; holders claim via envoy_role_set). > matches one or more trailing tokens and does not " + "match the base subject. Envoy registers the concrete base when you subscribe to <subject>.>, so " + "the recommended default remains github.<owner>.<repo>.pr.<n>.>. <owner> and <repo> are one token " + "each, with every dot in the name written _ (sjawhar/.github is github.sjawhar._github; " + "acme/site.io is github.acme.site_io), and a topic spelled with the dot receives nothing. " + "Default PR subscription: " + "github.<owner>.<repo>.pr.<n>.> (it receives the quiet PR family): pr.<n> (lifecycle: " + "opened/synchronize/closed; closed carries merged, merge_commit_sha, merged_by, head_sha), " + "pr.<n>.comment, pr.<n>.review, pr.<n>.mention, pr.<n>.checks (one settlement event per commit whose checks settle, the head or not, naming its sha: " + 'passed/failed/cancelled/skipped with failing names and URLs; re-fires with superseded_settlement: "true" ' + "when new runs appear for the same commit). Other GitHub: issue.<n>, issue.<n>.comment, " + "issue.<n>.mention, mention, push.branch.<name>, push.tag.<name>, workflow.<file>.<action> (only " + "runs without an associated PR); slack.<team>.<channel>.message|mention and " + "slack.<team>.<channel>.thread.<ts>.message|mention; ghostwispr.<session>.<kind>; " + "whatsapp.<phone>.<jid>.<kind>; envoy.exceptions.<original-topic>.";
14528
+ var URGENCY_VALUES = EnvelopeSchema.shape.urgency.unwrap().options;
14529
+ var EXPECTS_REPLY_VALUES = EnvelopeSchema.shape.expects_reply.unwrap().options;
14530
+ function messageMetadataShape(schema) {
14531
+ return {
14532
+ in_reply_to: schema.string().optional(),
14533
+ supersedes: schema.string().optional(),
14534
+ urgency: schema.enum(URGENCY_VALUES).describe("low | med | high | blocking").optional(),
14535
+ expects_reply: schema.enum(EXPECTS_REPLY_VALUES).describe("none | optional | required").optional(),
14536
+ expires_at: schema.number({ int: true }).optional()
14537
+ };
14802
14538
  }
14803
- var DISPATCH_TOOL_DEADLINE_MS = 60000;
14804
- function requestSignal(signal) {
14805
- const deadline = AbortSignal.timeout(DISPATCH_TOOL_DEADLINE_MS);
14806
- return signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
14539
+ var MessageMetadataSchema = exports_external.object(messageMetadataShape(zodSchemaApi(exports_external)));
14540
+ function toMessageMetadata(args) {
14541
+ return {
14542
+ ...args.in_reply_to === undefined ? {} : { inReplyTo: args.in_reply_to },
14543
+ ...args.supersedes === undefined ? {} : { supersedes: args.supersedes },
14544
+ ...args.urgency === undefined ? {} : { urgency: args.urgency },
14545
+ ...args.expects_reply === undefined ? {} : { expectsReply: args.expects_reply },
14546
+ ...args.expires_at === undefined ? {} : { expiresAt: args.expires_at }
14547
+ };
14807
14548
  }
14808
- function whyNotAPage(answer) {
14809
- if (typeof answer === "string") {
14810
- return `text that is not a JSON page, ${GATEWAY_PAGE}, so a retry may succeed.`;
14811
- }
14812
- let what;
14813
- if (Array.isArray(answer)) {
14814
- what = `a bare array of ${answer.length} entries, the unpaged listing, which means that Dispatch ` + "is older than sjawhar/legion#1612 or that change has regressed";
14815
- } else if (answer === null || typeof answer !== "object") {
14816
- what = answer === null ? "null" : `a ${typeof answer}`;
14817
- } else {
14818
- const fields = answer;
14819
- const lacking = [
14820
- ...Array.isArray(fields.issues) ? [] : ["an issues array"],
14821
- ...["total", "limit", "offset"].filter((name) => typeof fields[name] !== "number").map((name) => `a numeric ${name}`)
14822
- ];
14823
- if (lacking.length === 0)
14824
- return;
14825
- what = `an object without ${lacking.join(" or ")}`;
14826
- }
14827
- return `${what}. Retrying will not help: the same request gets the same answer until that ` + "Dispatch is upgraded or fixed.";
14549
+ function messageArguments(schema) {
14550
+ return { message: schema.string(), ...messageMetadataShape(schema) };
14828
14551
  }
14829
- var GATEWAY_PAGE = "which looks like a proxy or gateway page rather than Dispatch's own answer";
14830
- function gatewayAdvice(method, status) {
14831
- if (!transientStatus(status)) {
14832
- return "a retry gets the same answer until the Dispatch URL, or whatever answers in its place, is fixed";
14552
+ var EnvoyToolOperation = {
14553
+ subscribe: "subscribe",
14554
+ unsubscribe: "unsubscribe",
14555
+ listInterests: "listInterests",
14556
+ inbox: "inbox",
14557
+ send: "send",
14558
+ publish: "publish",
14559
+ setRole: "setRole",
14560
+ getRole: "getRole",
14561
+ whoami: "whoami",
14562
+ listSessions: "listSessions"
14563
+ };
14564
+ var envoyToolSpecs = [
14565
+ {
14566
+ name: "envoy_subscribe",
14567
+ description: `Subscribe this session to Envoy notification topics. ${TOPIC_GUIDE}`,
14568
+ arguments: (schema) => ({
14569
+ topics: schema.array(schema.string()).describe("NATS-style topic patterns to subscribe to.")
14570
+ }),
14571
+ operation: EnvoyToolOperation.subscribe
14572
+ },
14573
+ {
14574
+ name: "envoy_unsubscribe",
14575
+ description: "Unsubscribe this session from Envoy topics, or remove all current subscriptions if topics are omitted.",
14576
+ arguments: (schema) => ({ topics: schema.array(schema.string()).optional() }),
14577
+ operation: EnvoyToolOperation.unsubscribe
14578
+ },
14579
+ {
14580
+ name: "envoy_list",
14581
+ description: "List the current Envoy topic subscriptions for this session so you can confirm the exact topic shapes that are active.",
14582
+ arguments: () => ({}),
14583
+ operation: EnvoyToolOperation.listInterests
14584
+ },
14585
+ {
14586
+ name: "envoy_inbox",
14587
+ description: "List this Pi session's 50 most recent rendered Envoy deliveries, newest first.",
14588
+ arguments: () => ({}),
14589
+ operation: EnvoyToolOperation.inbox
14590
+ },
14591
+ {
14592
+ name: "envoy_send",
14593
+ description: `Send an Envoy agent-to-agent message directly to another session by session ID. ${DELIVERY_CONTRACT}`,
14594
+ arguments: (schema) => ({
14595
+ session_id: schema.string().describe("Target session ID; find it with envoy_sessions or envoy_whoami."),
14596
+ ...messageArguments(schema)
14597
+ }),
14598
+ operation: EnvoyToolOperation.send
14599
+ },
14600
+ {
14601
+ name: "envoy_publish",
14602
+ description: `Publish an Envoy message to any topic. ${DELIVERY_CONTRACT}`,
14603
+ arguments: (schema) => ({ topic: schema.string(), ...messageArguments(schema) }),
14604
+ operation: EnvoyToolOperation.publish
14605
+ },
14606
+ {
14607
+ name: "envoy_role_set",
14608
+ description: "Set the current session as the holder of a named role. Messages published to notifications.role.<role> route to this session.",
14609
+ arguments: (schema) => ({ role: schema.string() }),
14610
+ operation: EnvoyToolOperation.setRole
14611
+ },
14612
+ {
14613
+ name: "envoy_role_get",
14614
+ description: "Get the live holder of a named Envoy role.",
14615
+ arguments: (schema) => ({ role: schema.string() }),
14616
+ operation: EnvoyToolOperation.getRole
14617
+ },
14618
+ {
14619
+ name: "envoy_whoami",
14620
+ 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.",
14621
+ arguments: () => ({}),
14622
+ operation: EnvoyToolOperation.whoami
14623
+ },
14624
+ {
14625
+ name: "envoy_sessions",
14626
+ description: "List all live sessions registered with Envoy. Filter by optional machine, directory, or title.",
14627
+ arguments: (schema) => ({
14628
+ machine: schema.string().optional(),
14629
+ dir: schema.string().optional(),
14630
+ title: schema.string().optional()
14631
+ }),
14632
+ operation: EnvoyToolOperation.listSessions
14833
14633
  }
14834
- if (method === "GET")
14835
- return "a retry may succeed";
14836
- return reachesDispatch(status) ? "the write may or may not have reached Dispatch: check whether it took effect before retrying it" : "the write did not reach Dispatch, and a retry may succeed";
14837
- }
14838
- function transientStatus(status) {
14839
- return status >= 500 || status === 408 || status === 429;
14840
- }
14841
- function reachesDispatch(status) {
14842
- return status >= 500;
14843
- }
14844
- var REDACTED = "[redacted]";
14845
- var EXCERPT_SCAN_LIMIT = 64 * 1024;
14846
- var BEARER_PIECE = 8;
14847
- function redactBearer(text, bearer) {
14848
- if (bearer === "")
14849
- return text.slice(0, EXCERPT_SCAN_LIMIT);
14850
- const length = Math.min(BEARER_PIECE, bearer.length);
14851
- const scanned = text.slice(0, EXCERPT_SCAN_LIMIT + length - 1);
14852
- const covered = new Uint8Array(scanned.length);
14853
- const pieces = new Set;
14854
- for (let at = 0;at + length <= bearer.length; at++)
14855
- pieces.add(bearer.slice(at, at + length));
14856
- for (const piece of pieces) {
14857
- let found = scanned.indexOf(piece);
14858
- while (found !== -1) {
14859
- covered.fill(1, found, found + length);
14860
- found = scanned.indexOf(piece, found + 1);
14634
+ ];
14635
+ var envoyToolSchemas = new Map;
14636
+
14637
+ // ../envoy-client/src/transport.ts
14638
+ var DEFAULT_TIMEOUT_MS = 5000;
14639
+ var RETRY_DELAY_MS = 250;
14640
+ var InterestWireSchema = exports_external.object({
14641
+ session_id: exports_external.string(),
14642
+ machine_id: exports_external.string(),
14643
+ dir: exports_external.string(),
14644
+ topics: exports_external.array(exports_external.string()),
14645
+ updated_at: exports_external.number().int().optional(),
14646
+ warnings: exports_external.array(exports_external.string()).optional()
14647
+ });
14648
+ var SessionWireSchema = exports_external.object({
14649
+ session_id: exports_external.string(),
14650
+ machine_id: exports_external.string(),
14651
+ dir: exports_external.string(),
14652
+ port: exports_external.number().int(),
14653
+ title: exports_external.string(),
14654
+ topics: exports_external.array(exports_external.string()),
14655
+ roles: exports_external.array(exports_external.string()).optional(),
14656
+ capabilities: exports_external.array(exports_external.string()).optional(),
14657
+ self_subscribed: exports_external.boolean(),
14658
+ last_seen: exports_external.number().int().optional()
14659
+ });
14660
+ var RoleWireSchema = exports_external.object({
14661
+ role: exports_external.string(),
14662
+ holder: exports_external.string(),
14663
+ last_seen: exports_external.number().int()
14664
+ });
14665
+ var RoleHeldWireSchema = exports_external.object({
14666
+ error: exports_external.string(),
14667
+ role: exports_external.string(),
14668
+ holder: exports_external.string()
14669
+ });
14670
+ var ErrorWireSchema = exports_external.object({
14671
+ error: exports_external.string(),
14672
+ expected: exports_external.array(exports_external.string()).optional()
14673
+ });
14674
+ var EnvelopeResponseSchema = EnvelopeSchema.extend({
14675
+ recipient: exports_external.string().optional(),
14676
+ holder: exports_external.string().optional()
14677
+ });
14678
+ function expandSubscriptionTopics(topics) {
14679
+ const expanded = new Set;
14680
+ for (const topic of topics) {
14681
+ const base = topic.endsWith(".>") ? topic.slice(0, -2) : undefined;
14682
+ if (base === undefined || base.includes("*") || base.includes(">")) {
14683
+ expanded.add(topic);
14684
+ continue;
14861
14685
  }
14686
+ const segments = base.split(".");
14687
+ if (segments.length < 2 || segments.some((segment) => segment.length === 0)) {
14688
+ throw new TypeError(`cannot expand Envoy wildcard topic "${topic}": its concrete base must have at least two non-empty segments`);
14689
+ }
14690
+ expanded.add(base);
14691
+ expanded.add(topic);
14862
14692
  }
14863
- let redacted = "";
14864
- let kept = 0;
14865
- for (let start = covered.indexOf(1);start !== -1; start = covered.indexOf(1, kept)) {
14866
- redacted += `${scanned.slice(kept, start)}${REDACTED}`;
14867
- const end = covered.indexOf(0, start);
14868
- kept = end === -1 ? scanned.length : end;
14869
- }
14870
- return `${redacted}${scanned.slice(kept, EXCERPT_SCAN_LIMIT)}`.slice(0, EXCERPT_SCAN_LIMIT);
14871
- }
14872
- function excerpt(text, token) {
14873
- const plain = redactBearer(text, token.trim()).replace(/<(script|style)\b[^<>]*>(?:[\s\S]*?<\/\1\s*>|[\s\S]*$)/gi, " ").replace(/<[^<>]*>/g, " ");
14874
- return textHead(plain.replace(/(\bauthorization["']?\s*[:=]\s*["']?(?:(?:bearer|basic|digest|token)\s+)?)[^\s"'<>,;]+/gi, `$1${REDACTED}`).replace(/(\bbearer\s+)[^\s"'<>,;]+/gi, `$1${REDACTED}`));
14693
+ return [...expanded];
14875
14694
  }
14876
14695
 
14877
- class DispatchClient {
14878
- token;
14879
- fetchImpl;
14880
- #baseUrl;
14881
- #resolvedIssues = new Map;
14882
- #signal;
14883
- constructor(baseUrl, token, fetchImpl = fetch, signal) {
14884
- this.token = token;
14885
- this.fetchImpl = fetchImpl;
14886
- this.#baseUrl = baseUrl.replace(/\/+$/, "");
14887
- this.#signal = requestSignal(signal);
14888
- }
14889
- async issue(input) {
14890
- return this.#json("POST", ["api", "v1", "issues"], input);
14891
- }
14892
- async resolveIssue(issueReference) {
14893
- return this.#resolveIssue(issueReference);
14894
- }
14895
- async listIssuePage(options, page) {
14896
- const path2 = ["api", "v1", "issues"];
14897
- const query = { ...options, limit: page.limit, offset: page.offset };
14898
- const answer = await this.#json("GET", path2, undefined, query);
14899
- const refusal = whyNotAPage(answer);
14900
- if (refusal === undefined)
14901
- return answer;
14902
- throw new Error(`GET ${this.#url(path2, query)} asked for a page ({issues, total, limit, offset}) and got ` + refusal);
14903
- }
14904
- async listProjectArtifacts(project, unlinked = false) {
14905
- return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
14906
- }
14907
- async projectArtifact(project, input) {
14908
- return this.#upload(["api", "v1", "projects", project, "artifacts"], input);
14909
- }
14910
- async getProjectArtifact(project, slug) {
14911
- return this.#json("GET", ["api", "v1", "projects", project, "artifacts", slug]);
14912
- }
14913
- async agentArtifact(sessionId, input) {
14914
- return this.#upload(["api", "v1", "agents", sessionId, "artifacts"], input);
14915
- }
14916
- async getAgentArtifact(sessionId, slug) {
14917
- return this.#json("GET", ["api", "v1", "agents", sessionId, "artifacts", slug]);
14918
- }
14919
- async getIssueArtifact(issue2, slug) {
14920
- return this.#json("GET", [
14921
- "api",
14922
- "v1",
14923
- "issues",
14924
- await this.#resolveIssue(issue2),
14925
- "artifacts",
14926
- slug
14927
- ]);
14928
- }
14929
- async search(query, options = {}) {
14930
- return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
14931
- }
14932
- async getIssue(issue2) {
14933
- return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue2)]);
14934
- }
14935
- async updateIssue(issue2, input) {
14936
- return this.#json("PATCH", ["api", "v1", "issues", await this.#resolveIssue(issue2)], input);
14937
- }
14938
- async claimIssue(issue2, input = {}) {
14939
- return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "claim"], input);
14940
- }
14941
- async releaseIssueClaim(issue2, input = {}) {
14942
- return this.#json("DELETE", ["api", "v1", "issues", await this.#resolveIssue(issue2), "claim"], input);
14943
- }
14944
- async getIssueEvents(issue2, after = 0, limit = 200) {
14945
- return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue2), "events"], undefined, { after, limit });
14946
- }
14947
- async read(issueReference) {
14948
- const issueKey = await this.#resolveIssue(issueReference);
14949
- const issue2 = await this.getIssue(issueKey);
14950
- const events = await this.getIssueEvents(issueKey, Math.max(0, issue2.last_seq - 10), 10);
14951
- return { issue: issue2, events };
14952
- }
14953
- async ask(issue2, input) {
14954
- return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "asks"], input);
14955
- }
14956
- async openAsks(sessionID, since) {
14957
- return this.#json("GET", ["api", "v1", "asks", "open"], undefined, {
14958
- author_session: sessionID,
14959
- ...since === undefined ? {} : { since }
14960
- });
14961
- }
14962
- async openAsksForProject(project) {
14963
- return this.#json("GET", ["api", "v1", "asks", "open"], undefined, { project });
14964
- }
14965
- async listAgents() {
14966
- return this.#json("GET", ["api", "v1", "agents"]);
14967
- }
14968
- async whoami() {
14969
- return this.#json("GET", ["api", "v1", "whoami"]);
14970
- }
14971
- async syncArchitectureSource(project) {
14972
- return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
14973
- }
14974
- async getArchitectureSource(project) {
14975
- try {
14976
- return await this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
14977
- } catch (error48) {
14978
- if (error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "SOURCE_NOT_FOUND") {
14979
- return null;
14980
- }
14981
- throw error48;
14982
- }
14983
- }
14984
- async resolveAsk(id, input) {
14985
- return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
14986
- }
14987
- async requestApproval(artifactID, input) {
14988
- const answer = await this.#jsonAnswer("POST", ["api", "v1", "artifacts", artifactID, "approval-requests"], input);
14989
- return { ...answer.payload, recorded: answer.status === 201 };
14990
- }
14991
- async editAsk(id, input) {
14992
- return this.#json("PATCH", ["api", "v1", "asks", id], input);
14993
- }
14994
- async comment(issue2, input) {
14995
- return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "comments"], input);
14996
- }
14997
- async listIssueAsks(issue2, state) {
14998
- return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue2), "asks"], undefined, state === undefined ? undefined : { state });
14999
- }
15000
- async getArtifactAsks(id, state) {
15001
- return this.#json("GET", ["api", "v1", "artifacts", id, "asks"], undefined, state === undefined ? undefined : { state });
15002
- }
15003
- async artifactAsk(id, input) {
15004
- return this.#json("POST", ["api", "v1", "artifacts", id, "asks"], input);
15005
- }
15006
- async suggest(issue2, input) {
15007
- const { replace_with, ...comment } = input;
15008
- return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "comments"], {
15009
- ...comment,
15010
- suggestion: { replace_with }
15011
- });
15012
- }
15013
- async getArtifactComments(id) {
15014
- return this.#json("GET", ["api", "v1", "artifacts", id, "comments"]);
15015
- }
15016
- async artifactComment(id, input) {
15017
- return this.#json("POST", ["api", "v1", "artifacts", id, "comments"], input);
15018
- }
15019
- async artifactSuggest(id, input) {
15020
- const { replace_with, ...comment } = input;
15021
- return this.#json("POST", ["api", "v1", "artifacts", id, "comments"], {
15022
- ...comment,
15023
- suggestion: { replace_with }
15024
- });
15025
- }
15026
- async message(issue2, input) {
15027
- return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "messages"], input);
15028
- }
15029
- async messageReply(id, input, options = {}) {
15030
- return this.#json("POST", ["api", "v1", "messages", id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
15031
- }
15032
- async getMessage(issue2, id) {
15033
- return this.#json("GET", [
15034
- "api",
15035
- "v1",
15036
- "issues",
15037
- await this.#resolveIssue(issue2),
15038
- "messages",
15039
- id
15040
- ]);
15041
- }
15042
- async getMessageThread(id, session) {
15043
- return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
15044
- }
15045
- async acceptMessageDelivery(id, attempt, input) {
15046
- return this.#json("POST", ["api", "v1", "messages", id, "deliveries", String(attempt), "accept"], input);
15047
- }
15048
- async artifact(issue2, input) {
15049
- return this.#upload(["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"], input);
15050
- }
15051
- async getArtifact(id) {
15052
- return this.#json("GET", ["api", "v1", "artifacts", id]);
15053
- }
15054
- async docRead(id, version2) {
15055
- return version2 === undefined ? this.#json("GET", ["api", "v1", "artifacts", id, "text"]) : this.#json("GET", ["api", "v1", "artifacts", id, "versions", String(version2)]);
15056
- }
15057
- async fileVersion(id, version2) {
15058
- const url2 = this.#url(["api", "v1", "artifacts", id, "versions", String(version2)]);
15059
- const response = await this.fetchImpl(url2, {
15060
- method: "GET",
15061
- headers: { Accept: "*/*", Authorization: `Bearer ${this.token}` },
15062
- signal: this.#signal
15063
- });
15064
- if (!response.ok)
15065
- return this.#response("GET", url2, response);
15066
- return {
15067
- mime: response.headers.get("Content-Type") ?? "application/octet-stream",
15068
- bytes: new Uint8Array(await response.arrayBuffer())
15069
- };
15070
- }
15071
- async artifactBlocks(id) {
15072
- return this.#json("GET", ["api", "v1", "artifacts", id, "blocks"]);
15073
- }
15074
- async docEdit(id, input) {
15075
- return this.#json("POST", ["api", "v1", "artifacts", id, "edits"], input);
15076
- }
15077
- async nameArtifactVersion(id, input) {
15078
- return this.#json("POST", ["api", "v1", "artifacts", id, "versions"], input);
15079
- }
15080
- async getArtifactEvents(id, after = 0, limit = 200) {
15081
- return this.#json("GET", ["api", "v1", "artifacts", id, "events"], undefined, { after, limit });
15082
- }
15083
- async getArtifactReferences(id) {
15084
- return this.#json("GET", ["api", "v1", "artifacts", id, "references"]);
15085
- }
15086
- async getAsk(id) {
15087
- return this.#json("GET", ["api", "v1", "asks", id]);
15088
- }
15089
- async followAsk(id, sessionId, actor) {
15090
- await this.#json("PUT", ["api", "v1", "asks", id, "followers", sessionId], { actor });
15091
- }
15092
- async unfollowAsk(id, sessionId, actor) {
15093
- await this.#json("DELETE", ["api", "v1", "asks", id, "followers", sessionId], { actor });
15094
- }
15095
- async getComment(id) {
15096
- return this.#json("GET", ["api", "v1", "comments", id]);
15097
- }
15098
- async resolveComment(id, actor) {
15099
- return this.#json("POST", ["api", "v1", "comments", id, "resolve"], { actor });
15100
- }
15101
- async getComments(issue2, artifact) {
15102
- return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue2), "comments"], undefined, artifact ? { artifact } : undefined);
15103
- }
15104
- async getIssueReferences(issue2) {
15105
- return this.#json("GET", [
15106
- "api",
15107
- "v1",
15108
- "issues",
15109
- await this.#resolveIssue(issue2),
15110
- "references"
15111
- ]);
15112
- }
15113
- async getReferences(query) {
15114
- return this.#json("GET", ["api", "v1", "references"], undefined, {
15115
- ..."to" in query ? { to: query.to } : { from: query.from },
15116
- ...query.kind === undefined || query.kind.length === 0 ? {} : { kind: query.kind.join(",") },
15117
- ...query.since === undefined ? {} : { since: query.since }
15118
- });
15119
- }
15120
- async#resolveIssue(issueReference) {
15121
- if (!issueReference.includes("#"))
15122
- return issueReference;
15123
- let resolved = this.#resolvedIssues.get(issueReference);
15124
- if (!resolved) {
15125
- resolved = this.#json("GET", ["api", "v1", "issues", "resolve"], undefined, {
15126
- ref: issueReference
15127
- }).then((resolution) => resolution.key);
15128
- this.#resolvedIssues.set(issueReference, resolved);
15129
- }
15130
- try {
15131
- return await resolved;
15132
- } catch (error48) {
15133
- this.#resolvedIssues.delete(issueReference);
15134
- throw error48;
15135
- }
15136
- }
15137
- async#json(method, path2, body, query) {
15138
- return (await this.#jsonAnswer(method, path2, body, query)).payload;
15139
- }
15140
- async#jsonAnswer(method, path2, body, query) {
15141
- const headers = {
15142
- Accept: "application/json",
15143
- Authorization: `Bearer ${this.token}`
15144
- };
15145
- if (body !== undefined)
15146
- headers["Content-Type"] = "application/json";
15147
- const url2 = this.#url(path2, query);
15148
- const response = await this.fetchImpl(url2, {
15149
- method,
15150
- headers,
15151
- signal: this.#signal,
15152
- ...body === undefined ? {} : { body: JSON.stringify(body) }
15153
- });
15154
- return { status: response.status, payload: await this.#response(method, url2, response) };
15155
- }
15156
- async#upload(path2, input) {
15157
- if ("content" in input)
15158
- return this.#json("POST", path2, input);
15159
- const form = new FormData;
15160
- form.set("name", input.name);
15161
- if (input.summary !== undefined)
15162
- form.set("summary", input.summary);
15163
- if (input.actor !== undefined)
15164
- form.set("actor", JSON.stringify(input.actor));
15165
- form.set("file", input.file, input.name);
15166
- return this.#form("POST", path2, form);
15167
- }
15168
- async#form(method, path2, body) {
15169
- const url2 = this.#url(path2);
15170
- const response = await this.fetchImpl(url2, {
15171
- method,
15172
- headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
15173
- body,
15174
- signal: this.#signal
15175
- });
15176
- return this.#response(method, url2, response);
15177
- }
15178
- #url(path2, query) {
15179
- const url2 = new URL(`${path2.map((segment) => encodeURIComponent(segment)).join("/")}`, `${this.#baseUrl}/`);
15180
- if (query) {
15181
- for (const [name, value] of Object.entries(query)) {
15182
- if (typeof value === "string" || typeof value === "number") {
15183
- url2.searchParams.set(name, String(value));
15184
- } else if (Array.isArray(value)) {
15185
- for (const item of value)
15186
- url2.searchParams.append(name, String(item));
15187
- }
15188
- }
15189
- }
15190
- return url2.toString();
15191
- }
15192
- async#response(method, url2, response) {
15193
- const text = await response.text();
15194
- let payload = text;
15195
- if (text && isJson(response)) {
15196
- try {
15197
- payload = JSON.parse(text);
15198
- } catch {
15199
- payload = text;
15200
- }
15201
- }
15202
- if (!response.ok) {
15203
- const error48 = asErrorShape(payload);
15204
- if (typeof error48.error === "string") {
15205
- throw new DispatchServiceError(error48.code ?? `HTTP_${response.status}`, response.status, error48.error, error48.candidates, error48.current, error48.mismatches);
15206
- }
15207
- const quoted = excerpt(text, this.token);
15208
- let body;
15209
- if (quoted !== "") {
15210
- body = `a body that is not Dispatch's error JSON (${JSON.stringify(quoted)})`;
15211
- } else if (text === "") {
15212
- body = "an empty body";
15213
- } else {
15214
- const within = text.length > EXCERPT_SCAN_LIMIT ? ` in its first ${EXCERPT_SCAN_LIMIT / 1024} KiB` : "";
15215
- const size = Buffer.byteLength(text).toLocaleString("en-US");
15216
- body = `a body of ${size} bytes and no readable text${within}`;
15217
- }
15218
- const reasonPhrase = excerpt(response.statusText, this.token);
15219
- const reason = reasonPhrase === "" ? "" : ` ${reasonPhrase}`;
15220
- throw new DispatchGatewayError(response.status, `${method} ${url2} answered ${response.status}${reason} with ${body}, ${GATEWAY_PAGE}`, gatewayAdvice(method, response.status));
15221
- }
15222
- return payload;
15223
- }
15224
- }
15225
-
15226
- // ../envoy-client/src/dispatch-owner.ts
15227
- function documentLabel(project, slug) {
15228
- return `${project}/${slug}`;
15229
- }
15230
- function issueTopic(key) {
15231
- return { label: key, topic: dispatchIssueSubject(key, ">") };
15232
- }
15233
- function documentTopicOf(project, slug) {
15234
- return {
15235
- label: documentLabel(project, slug),
15236
- topic: dispatchDocumentSubject(project, slug, ">")
15237
- };
15238
- }
15239
- function dispatchIssueRef(key) {
15240
- return `dispatch://${key}`;
15241
- }
15242
- function dispatchDocumentRef(owner, slug) {
15243
- return `dispatch://${owner}/artifact/${slug}`;
15244
- }
15245
- function dispatchChildRef(ownerRef, kind, id) {
15246
- return `${ownerRef}/${kind}/${id}`;
15247
- }
15248
- function parseDispatchRef(ref) {
15249
- const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
15250
- if (projectDocument) {
15251
- const [, project, artifact2, version3, targetKind, targetID] = projectDocument;
15252
- if (project === undefined || artifact2 === undefined || version3 !== undefined && Number(version3) < 1) {
15253
- return null;
15254
- }
15255
- if (targetKind === undefined) {
15256
- return {
15257
- owner: { kind: "project", project },
15258
- kind: "artifact",
15259
- id: artifact2,
15260
- ...version3 === undefined ? {} : { version: Number(version3) }
15261
- };
15262
- }
15263
- if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
15264
- return null;
15265
- return {
15266
- owner: { kind: "project", project },
15267
- kind: targetKind,
15268
- id: targetID,
15269
- artifact: artifact2
15270
- };
15271
- }
15272
- 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\/([^/]+))?$/);
15273
- if (!issueReference)
15274
- return null;
15275
- const [, issue2, spec, log, children, artifact, version2, ask, comment, message] = issueReference;
15276
- if (!issue2 || version2 !== undefined && Number(version2) < 1)
15277
- return null;
15278
- const owner = { kind: "issue", issue: issue2 };
15279
- if (spec)
15280
- return { owner, kind: "spec", id: spec };
15281
- if (log)
15282
- return { owner, kind: "log", id: log };
15283
- if (children)
15284
- return { owner, kind: "children", id: children };
15285
- if (artifact) {
15286
- return {
15287
- owner,
15288
- kind: "artifact",
15289
- id: artifact,
15290
- ...version2 === undefined ? {} : { version: Number(version2) }
15291
- };
15292
- }
15293
- if (ask)
15294
- return { owner, kind: "ask", id: ask };
15295
- if (comment)
15296
- return { owner, kind: "comment", id: comment };
15297
- if (message)
15298
- return { owner, kind: "message", id: message };
15299
- return { owner, kind: "issue", id: issue2 };
15300
- }
15301
-
15302
- // ../envoy-client/src/dispatch-picture-tools.ts
15303
- import { basename, resolve as resolvePath } from "path";
15304
-
15305
- // ../envoy-client/src/dispatch-pictures.ts
15306
- var PICTURE_UPLOAD_MAX_BYTES = 25 * 1024 * 1024;
15307
- var PICTURE_SHOWN_MAX_BYTES = 3750000;
15308
- var pictureShownMaxText = `${PICTURE_SHOWN_MAX_BYTES.toLocaleString("en-US")} bytes (5 MB of base64)`;
15309
- function oversizePictureText(name, bytes) {
15310
- return `${name} is ${bytes.toLocaleString("en-US")} bytes, over the ${pictureShownMaxText} a model is shown`;
15311
- }
15312
- var PICTURES_SHOWN_MAX = 8;
15313
- var PICTURES_SHOWN_MAX_BYTES = 10 * 1024 * 1024;
15314
- var PICTURE_SNIFF_BYTES = 12;
15315
- function bytesAt(bytes, offset, expected) {
15316
- return expected.every((byte, index) => bytes[offset + index] === byte);
15317
- }
15318
- function sniffPictureType(bytes) {
15319
- if (bytesAt(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10]))
15320
- return "image/png";
15321
- if (bytesAt(bytes, 0, [255, 216, 255]))
15322
- return "image/jpeg";
15323
- if (bytesAt(bytes, 0, [71, 73, 70, 56]) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97) {
15324
- return "image/gif";
15325
- }
15326
- if (bytesAt(bytes, 0, [82, 73, 70, 70]) && bytesAt(bytes, 8, [87, 69, 66, 80])) {
15327
- return "image/webp";
15328
- }
15329
- return;
15330
- }
15331
- function toolImage(bytes) {
15332
- const mimeType = sniffPictureType(bytes);
15333
- if (mimeType === undefined || bytes.length > PICTURE_SHOWN_MAX_BYTES)
15334
- return;
15335
- return { data: Buffer.from(bytes).toString("base64"), mimeType };
15336
- }
15337
- function pictureLine(name, address) {
15338
- return `![${pictureCaption(name)}](${address})`;
15339
- }
15340
- function withPictureLines(text, lines) {
15341
- if (lines.length === 0)
15342
- return text;
15343
- const head = text.trimEnd();
15344
- return head === "" ? lines.join(`
15345
- `) : `${head}
15346
-
15347
- ${lines.join(`
15348
- `)}`;
15349
- }
15350
- var PICTURE_SYNTAX = /!\[(?:\\.|[^\\\]])*\]\((dispatch:\/\/[^\s()]+@v[1-9][0-9]*)(?:\s+"(?:\\.|[^\\"])*")?\)/g;
15351
- function pictureAddresses(text) {
15352
- return [...text.matchAll(PICTURE_SYNTAX)].map((match) => match[1]);
15353
- }
15354
- function picturesNewestFirst(texts) {
15355
- 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);
15356
- return [...new Set(ordered.flatMap(({ item }) => pictureAddresses(item.text)))];
15357
- }
15358
-
15359
- // ../envoy-client/src/tool-input-errors.ts
15360
- class ToolInputError extends Error {
15361
- tool;
15362
- problems;
15363
- constructor(tool, problems) {
15364
- const count = problems.length;
15365
- const help = dispatchInputHelp(tool);
15366
- super([
15367
- `${tool} was not called: ${count} problem${count === 1 ? "" : "s"}`,
15368
- ...problems.map((problem) => `- ${problem}`),
15369
- ...help === undefined ? [] : help.map((line) => `- ${line}`)
15370
- ].join(`
15371
- `));
15372
- this.name = "ToolInputError";
15373
- this.tool = tool;
15374
- this.problems = problems;
15375
- }
15376
- }
15377
- function dispatchInputHelp(tool) {
15378
- const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
15379
- if (spec === undefined)
15380
- return;
15381
- const schema = dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
15382
- const allowed = Object.keys(shapeOf(schema) ?? {}).join(", ") || "none";
15383
- return [`Allowed keys: ${allowed}`, `Example: ${tool}(${JSON.stringify(spec.example)})`];
15384
- }
15385
- function unwrap(schema) {
15386
- let current = schema;
15387
- while (current !== undefined) {
15388
- const def = current.def;
15389
- if (!("innerType" in def) || !(def.type === "optional" || def.type === "nullable" || def.type === "default"))
15390
- break;
15391
- current = def.innerType;
15392
- }
15393
- return current;
15394
- }
15395
- function shapeOf(schema) {
15396
- const unwrapped = unwrap(schema);
15397
- if (unwrapped === undefined)
15398
- return;
15399
- const def = unwrapped.def;
15400
- return def.type === "object" && "shape" in def ? def.shape : undefined;
15401
- }
15402
- function schemaAt(schema, path2) {
15403
- let current = schema;
15404
- for (const key of path2) {
15405
- const unwrapped = unwrap(current);
15406
- if (unwrapped === undefined)
15407
- return;
15408
- const def = unwrapped.def;
15409
- if (def.type === "object" && "shape" in def) {
15410
- current = shapeOf(unwrapped)?.[String(key)];
15411
- } else if (def.type === "array" && "element" in def) {
15412
- current = def.element;
15413
- } else {
15414
- return;
15415
- }
15416
- }
15417
- return unwrap(current);
15418
- }
15419
- function describeInput(value) {
15420
- if (value === null)
15421
- return "null";
15422
- if (Array.isArray(value))
15423
- return "an array";
15424
- switch (typeof value) {
15425
- case "number":
15426
- case "boolean":
15427
- return String(value);
15428
- case "string":
15429
- return "a string";
15430
- case "object":
15431
- return "an object";
15432
- default:
15433
- return typeof value;
15434
- }
15435
- }
15436
- function describeExpected(expected, schema) {
15437
- switch (expected) {
15438
- case "int":
15439
- return "an integer";
15440
- case "object": {
15441
- const shape = shapeOf(schema);
15442
- const keys = shape === undefined ? [] : Object.entries(shape).map(([key, field]) => `${key}${field.def.type === "optional" ? "?" : ""}`);
15443
- return keys.length === 0 ? "an object" : `an object {${keys.join(", ")}}`;
15444
- }
15445
- case "array":
15446
- return "an array";
15447
- default:
15448
- return `a ${expected}`;
15449
- }
15450
- }
15451
- function formatZodIssues(issues, schema) {
15452
- const allowed = Object.keys(shapeOf(schema) ?? {}).join(", ");
15453
- return issues.flatMap((issue2) => {
15454
- const path2 = issue2.path.map(String).join(".");
15455
- switch (issue2.code) {
15456
- case "invalid_type":
15457
- return issue2.input === undefined ? [`${path2} is required (${issue2.expected})`] : [
15458
- `${path2} must be ${describeExpected(issue2.expected, schemaAt(schema, issue2.path))}, not ${describeInput(issue2.input)}`
15459
- ];
15460
- case "unrecognized_keys": {
15461
- const here = Object.keys(shapeOf(schemaAt(schema, issue2.path)) ?? {}).join(", ") || allowed;
15462
- const where = path2 === "" ? "" : ` in ${path2}`;
15463
- return issue2.keys.map((key) => `unknown field "${key}"${where}; allowed: ${here}`);
15464
- }
15465
- case "invalid_value":
15466
- return [
15467
- `${path2} must be one of ${issue2.values.map(String).join("|")}; got ${JSON.stringify(issue2.input)}`
15468
- ];
15469
- case "too_big":
15470
- if (issue2.origin === "array" && Array.isArray(issue2.input)) {
15471
- return [`${path2} has ${issue2.input.length} items; the limit is ${issue2.maximum}`];
15472
- }
15473
- return [`${path2} ${issue2.message}`];
15474
- case "custom":
15475
- return [issue2.message];
15476
- default:
15477
- return [path2 === "" ? issue2.message : `${path2}: ${issue2.message}`];
15478
- }
15479
- });
15480
- }
15481
-
15482
- // ../envoy-client/src/dispatch-picture-tools.ts
15483
- var PICTURE_TYPE_NAMES = "a PNG, JPEG, GIF or WebP";
15484
- async function localPictures(images, cwd, problems) {
15485
- if (!Array.isArray(images))
15486
- return [];
15487
- const checked = await Promise.all(images.map((path2) => localPicture(path2, cwd)));
15488
- const pictures = [];
15489
- for (const outcome of checked) {
15490
- if (typeof outcome === "string")
15491
- problems.push(outcome);
15492
- else if (outcome !== undefined)
15493
- pictures.push(outcome);
15494
- }
15495
- return pictures;
15496
- }
15497
- async function localPicture(path2, cwd) {
15498
- if (typeof path2 !== "string" || path2 === "")
15499
- return;
15500
- const absolute = resolvePath(cwd, path2);
15501
- const file2 = Bun.file(absolute);
15502
- let size;
15503
- let head;
15504
- try {
15505
- const stats = await file2.stat();
15506
- if (!stats.isFile())
15507
- return `images: ${path2} is not a file`;
15508
- size = stats.size;
15509
- head = new Uint8Array(await file2.slice(0, PICTURE_SNIFF_BYTES).arrayBuffer());
15510
- } catch (error48) {
15511
- return `images: ${path2} cannot be read: ${messageFor(error48)}`;
15512
- }
15513
- if (size > PICTURE_UPLOAD_MAX_BYTES) {
15514
- return `images: ${path2} is ${size.toLocaleString("en-US")} bytes, over the ${PICTURE_UPLOAD_MAX_BYTES / 1024 / 1024} MiB one Dispatch upload takes`;
15515
- }
15516
- const type = sniffPictureType(head);
15517
- if (type === undefined) {
15518
- return `images: ${path2} is not ${PICTURE_TYPE_NAMES} picture (judged by its bytes, not its name)`;
15519
- }
15520
- return { path: path2, name: basename(absolute), file: Bun.file(absolute, { type }) };
15521
- }
15522
- function uploadSlug(name) {
15523
- const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
15524
- return slug === "" ? "artifact" : slug;
15525
- }
15526
- async function textWithPictures(tool, text, pictures, limit, owner, upload, actor) {
15527
- if (pictures.length === 0)
15528
- return text;
15529
- const counted = `${limit.text} plus ${pictures.length} ${pictures.length === 1 ? "picture" : "pictures"}`;
15530
- const fix = `${limit.shorten} or send fewer pictures`;
15531
- const predicted = withPictureLines(text, pictures.map((picture) => pictureLine(picture.name, `dispatch://${owner}/artifact/${uploadSlug(picture.name)}@v1`)));
15532
- if (predicted.length > limit.cap) {
15533
- throw new ToolInputError(tool, [
15534
- `${counted} ${overCapMessage(predicted.length, limit.cap)}; ${fix}`
15535
- ]);
15536
- }
15537
- const lines = [];
15538
- const addresses = [];
15539
- for (const picture of pictures) {
15540
- let uploaded;
15541
- try {
15542
- uploaded = await upload({ name: picture.name, file: picture.file, actor });
15543
- } catch (error48) {
15544
- const earlier = addresses.length === 0 ? "" : `; the pictures before it are uploaded (${addresses.join(", ")})`;
15545
- throw new Error(`images: ${picture.path} was not uploaded: ${messageFor(error48)}. Nothing was posted${earlier}.`);
15546
- }
15547
- const address = `dispatch://${owner}/artifact/${uploaded.artifact.slug}@v${uploaded.version.number}`;
15548
- addresses.push(address);
15549
- lines.push(pictureLine(picture.name, address));
15550
- }
15551
- const posted = withPictureLines(text, lines);
15552
- if (posted.length > limit.cap) {
15553
- throw new Error(`${counted} ${overCapMessage(posted.length, limit.cap)} with the addresses Dispatch gave ` + `the uploads (${addresses.join(", ")}), so nothing was posted; ${fix}.`);
15554
- }
15555
- return posted;
15556
- }
15557
- var AGENT_ARTIFACT_REF = new RegExp(`^dispatch://agent/(${SESSION_ID_PATTERN})/artifact/([^/@]+)(?:@v(\\d+))?$`, "u");
15558
- function parseAgentArtifactRef(ref) {
15559
- const match = ref.match(AGENT_ARTIFACT_REF);
15560
- if (match === null)
15561
- return null;
15562
- const [, session, slug, version2] = match;
15563
- if (session === undefined || slug === undefined || version2 !== undefined && Number(version2) < 1) {
15564
- return null;
15565
- }
15566
- return { session, slug, ...version2 === undefined ? {} : { version: Number(version2) } };
15567
- }
15568
- function datedBodies(records) {
15569
- return records.map((record2) => ({ text: record2.body, at: record2.created_at }));
15570
- }
15571
- function askTexts({ ask, replies }) {
15572
- return [
15573
- { text: ask.question, at: ask.created_at },
15574
- ...ask.answer?.text ? [{ text: ask.answer.text, at: ask.answer.at }] : [],
15575
- ...ask.resolution === undefined ? [] : [{ text: ask.resolution.reason, at: ask.resolution.at }],
15576
- ...datedBodies(replies)
15577
- ];
15578
- }
15579
- function eventTexts(event) {
15580
- let texts;
15581
- switch (event.type) {
15582
- case "ask.opened":
15583
- case "ask.anchor_refreshed":
15584
- case "ask.edited":
15585
- case "ask.handed_back":
15586
- case "ask.answered":
15587
- case "ask.resolved":
15588
- texts = [
15589
- event.payload.question,
15590
- event.payload.answer?.text,
15591
- event.payload.resolution?.reason
15592
- ];
15593
- break;
15594
- case "comment.created":
15595
- case "comment.answered":
15596
- case "comment.anchor_refreshed":
15597
- case "comment.edited":
15598
- case "comment.resolved":
15599
- case "comment.reopened":
15600
- case "suggestion.accepted":
15601
- case "suggestion.rejected":
15602
- case "message.created":
15603
- case "message.answered":
15604
- texts = [event.payload.body];
15605
- break;
15606
- default:
15607
- texts = [];
15608
- }
15609
- return texts.filter((text) => typeof text === "string" && text !== "").map((text) => ({ text, at: event.created_at }));
15610
- }
15611
- function shownPictureLine(index, address, name, mimeType, bytes) {
15612
- return `- image ${index}: ${address} (${name}, ${mimeType}, ${bytes.toLocaleString("en-US")} bytes)`;
15613
- }
15614
- function pictureTarget(address) {
15615
- const agent = parseAgentArtifactRef(address);
15616
- if (agent !== null) {
15617
- if (agent.version === undefined)
15618
- return;
15619
- return {
15620
- key: `agent/${agent.session}/${agent.slug}`,
15621
- read: (client) => client.getAgentArtifact(agent.session, agent.slug),
15622
- version: agent.version
15623
- };
15624
- }
15625
- const ref = parseDispatchRef(address);
15626
- if (ref?.kind !== "artifact" || ref.version === undefined)
15627
- return;
15628
- const { owner, id: slug } = ref;
15629
- return owner.kind === "issue" ? {
15630
- key: `${owner.issue}/${slug}`,
15631
- read: (client) => client.getIssueArtifact(owner.issue, slug),
15632
- version: ref.version
15633
- } : {
15634
- key: `${owner.project}/${slug}`,
15635
- read: (client) => client.getProjectArtifact(owner.project, slug),
15636
- version: ref.version
15637
- };
15638
- }
15639
- var SESSIONS_SHOWN_MAX = 64;
15640
- var picturesShown = new Map;
15641
- function shownPictures(sessionId) {
15642
- const shown = picturesShown.get(sessionId) ?? new Set;
15643
- picturesShown.delete(sessionId);
15644
- picturesShown.set(sessionId, shown);
15645
- const oldest = picturesShown.keys().next();
15646
- if (picturesShown.size > SESSIONS_SHOWN_MAX && oldest.done !== true) {
15647
- picturesShown.delete(oldest.value);
15648
- }
15649
- return shown;
15650
- }
15651
- async function readPictures(client, addresses, shown) {
15652
- if (addresses.length === 0)
15653
- return { lines: [], images: [], shown: [] };
15654
- const artifacts = new Map;
15655
- const images = [];
15656
- const showing = [];
15657
- const lines = ["Pictures:"];
15658
- let shownBytes = 0;
15659
- const overBudget = `past this read's ${PICTURES_SHOWN_MAX_BYTES / 1024 / 1024} MiB of pictures; dispatch_doc_read shows it`;
15660
- for (const address of addresses) {
15661
- const skip = (reason) => lines.push(`- not shown: ${address} (${reason})`);
15662
- if (shown?.has(address)) {
15663
- skip("shown earlier this session; dispatch_doc_read shows it again");
15664
- continue;
15665
- }
15666
- if (images.length === PICTURES_SHOWN_MAX) {
15667
- skip(`past this read's ${PICTURES_SHOWN_MAX} pictures; dispatch_doc_read shows it`);
15668
- continue;
15669
- }
15670
- const target = pictureTarget(address);
15671
- if (target === undefined) {
15672
- skip("names no versioned Dispatch artifact");
15673
- continue;
15674
- }
15675
- try {
15676
- let read = artifacts.get(target.key);
15677
- if (read === undefined) {
15678
- read = target.read(client);
15679
- artifacts.set(target.key, read);
15680
- }
15681
- const artifact = await read;
15682
- const version2 = artifact.versions.find((candidate) => candidate.number === target.version);
15683
- if (version2 === undefined) {
15684
- skip(`${artifact.name} has no version ${target.version}`);
15685
- continue;
15686
- }
15687
- if (artifact.kind !== "image") {
15688
- skip(`${artifact.name} is a ${artifact.kind}, not a picture`);
15689
- continue;
15690
- }
15691
- const mime = version2.mime?.split(";")[0]?.trim();
15692
- if (mime !== undefined && !isPictureType(mime)) {
15693
- skip(`${artifact.name} is ${mime}, not ${PICTURE_TYPE_NAMES} a model is shown`);
15694
- continue;
15695
- }
15696
- if (version2.size !== undefined && version2.size > PICTURE_SHOWN_MAX_BYTES) {
15697
- skip(oversizePictureText(artifact.name, version2.size));
15698
- continue;
15699
- }
15700
- if (version2.size !== undefined && shownBytes + version2.size > PICTURES_SHOWN_MAX_BYTES) {
15701
- skip(overBudget);
15702
- continue;
15703
- }
15704
- const file2 = await client.fileVersion(artifact.id, target.version);
15705
- const image = toolImage(file2.bytes);
15706
- if (image === undefined) {
15707
- skip(sniffPictureType(file2.bytes) === undefined ? `${artifact.name} is not ${PICTURE_TYPE_NAMES} a model is shown` : oversizePictureText(artifact.name, file2.bytes.length));
15708
- continue;
15709
- }
15710
- if (shownBytes + file2.bytes.length > PICTURES_SHOWN_MAX_BYTES) {
15711
- skip(overBudget);
15712
- continue;
15713
- }
15714
- shownBytes += file2.bytes.length;
15715
- images.push(image);
15716
- showing.push(address);
15717
- lines.push(shownPictureLine(images.length, address, artifact.name, image.mimeType, file2.bytes.length));
15718
- } catch (error48) {
15719
- skip(`unavailable: ${messageFor(error48)}`);
15720
- }
15721
- }
15722
- return { lines, images, shown: showing };
15723
- }
15724
-
15725
- // ../envoy-client/src/search-answer.ts
15726
- function pageSummaryText(offset, count, total) {
15727
- return `showing ${offset + 1}-${offset + count} of ${total}`;
15728
- }
15729
- function searchResultLine(result, baseUrl) {
15730
- const href = new URL(result.href, baseUrl).toString();
15731
- const { owner } = result;
15732
- if (owner.kind === "document") {
15733
- const reference = dispatchDocumentRef(owner.project, owner.slug);
15734
- return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
15735
- }
15736
- const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
15737
- const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
15738
- return `${label}: ${snippetText(result.snippet)} -> ${href}`;
15739
- }
15740
- function searchAnswer(search, query, offset, configUrl) {
15741
- const results = search.results;
15742
- const count = results.length;
15743
- const lines = results.map((result) => searchResultLine(result, configUrl));
15744
- const noun = count === 1 ? "result" : "results";
15745
- const noResults = `No results for "${query}".`;
15746
- const degraded = search.degraded === SEARCH_DEGRADED_EMBEDDER_UNAVAILABLE ? "Searched by keyword only: meaning search was unavailable for this request." : undefined;
15747
- if (typeof search.total !== "number") {
15748
- if (offset !== undefined && offset > 0) {
15749
- throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
15750
- }
15751
- return {
15752
- text: count === 0 ? [noResults, ...degraded === undefined ? [] : [degraded]].join(`
15753
- `) : [
15754
- `${count} ${noun} for "${query}" (${search.took_ms} ms)`,
15755
- ...degraded === undefined ? [] : [degraded],
15756
- ...lines
15757
- ].join(`
15758
- `),
15759
- details: { query, results, ...degraded === undefined ? {} : { degraded: search.degraded } }
15760
- };
15761
- }
15762
- const { total, reachable, offset: pageOffset } = search;
15763
- const end = pageOffset + count;
15764
- 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;
15765
- const details = {
15766
- query,
15767
- results,
15768
- total,
15769
- reachable,
15770
- offset: pageOffset,
15771
- limit: search.limit,
15772
- ...degraded === undefined ? {} : { degraded: search.degraded }
15773
- };
15774
- if (count === 0) {
15775
- return {
15776
- text: total === 0 ? [noResults, ...degraded === undefined ? [] : [degraded]].join(`
15777
- `) : [
15778
- `No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
15779
- ...degraded === undefined ? [] : [degraded],
15780
- ...cut === undefined ? [] : [cut]
15781
- ].join(`
15782
- `),
15783
- details
15784
- };
15785
- }
15786
- const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
15787
- return {
15788
- text: [
15789
- `${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
15790
- ...degraded === undefined ? [] : [degraded],
15791
- ...cut === undefined ? [] : [cut],
15792
- ...lines,
15793
- ...end < reachable ? [`Next page: offset ${end}.`] : []
15794
- ].join(`
15795
- `),
15796
- details
15797
- };
15798
- }
15799
-
15800
- // ../envoy-client/src/dispatch-execute.ts
15801
- function documentTopic(artifact) {
15802
- return documentTopicOf(artifact.project, artifact.slug);
15803
- }
15804
- function resolvedTopic(resolved) {
15805
- if (resolved.owner.kind === "project")
15806
- return documentTopic(resolved.artifact);
15807
- if (resolved.issue === undefined)
15808
- throw new Error("issue document is missing its issue");
15809
- return issueTopic(resolved.issue.key);
15810
- }
15811
- function notSubscribed(owner) {
15812
- return `(not subscribed to ${owner.label}; envoy_subscribe ${owner.topic} for every event on it)`;
15813
- }
15814
- function followsAsk(owner) {
15815
- return `You follow this ask: its answer and replies reach you directly. For every event on ${owner.label}: envoy_subscribe ${owner.topic}`;
15816
- }
15817
- var triageAdviceShown = new Set;
15818
- 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".';
15819
- function renderAdvice(tool, key, advice, opts) {
15820
- if (advice === undefined)
15821
- return [];
15822
- const hasIssueAdvice = advice.issue_status !== undefined;
15823
- const openAsks = advice.your_open_asks;
15824
- const writesSinceHuman = advice.session_writes_since_human;
15825
- const lines = [];
15826
- const unparsed = advice.unparsed_openers;
15827
- if (unparsed !== undefined && unparsed.count > 0 && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
15828
- const quoted = unparsed.examples.map((example) => JSON.stringify(example)).join(", ");
15829
- 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`;
15830
- 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".`);
15831
- }
15832
- if (advice.decision_blocks === 0 && opts.isPrimarySpec === true && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
15833
- 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".');
15834
- }
15835
- if (hasIssueAdvice && writesSinceHuman !== undefined && writesSinceHuman >= 3 && (tool === "dispatch_message" || tool === "dispatch_ask" || tool === "dispatch_comment" && opts.isAskReply !== true)) {
15836
- const middle = writesSinceHuman >= 6 ? "Stop posting here until a human replies." : "Progress ledger or scratchpad? If so, stop.";
15837
- lines.push(`You've sent ${writesSinceHuman} messages on ${key} with no human response. ${middle} See the \`dispatch\` skill, "Structure over stream".`);
15838
- }
15839
- if (advice.issue_status === "triage" && tool !== "dispatch_issue" && !(tool === "dispatch_issue_update" && opts.setsStatus === true) && !triageAdviceShown.has(key)) {
15840
- triageAdviceShown.add(key);
15841
- lines.push(`${key} is still in triage \u2014 nobody can see its development status. See the \`dispatch\` skill, "Issue status is yours to move".`);
15842
- }
15843
- if (hasIssueAdvice && openAsks !== undefined && openAsks.length > 0 && (tool === "dispatch_message" || tool === "dispatch_doc_edit" || tool === "dispatch_issue_update" || tool === "dispatch_comment" && opts.replyToOwnAsk !== true)) {
15844
- const askCount = Math.min(openAsks.length, 2);
15845
- for (let index = 0;index < askCount; index += 1) {
15846
- const ask = openAsks[index];
15847
- if (ask === undefined)
15848
- break;
15849
- 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".`);
15850
- }
15851
- }
15852
- return lines;
15853
- }
15854
- function renderSuggestions(suggestions, configUrl) {
15855
- if (suggestions === undefined)
15856
- return [];
15857
- if (suggestions.missing !== undefined) {
15858
- return [`Related-item search was skipped: ${suggestions.missing}.`];
15859
- }
15860
- const lines = [];
15861
- if (suggestions.related.length > 0) {
15862
- lines.push("Possibly related, found by search:");
15863
- for (const item of suggestions.related) {
15864
- lines.push(`- ${suggestionLabel(item)} \u2192 ${new URL(item.href, configUrl).toString()}`);
15865
- }
15866
- }
15867
- if (suggestions.decision !== undefined) {
15868
- const item = suggestions.decision;
15869
- const when = item.answered_at === undefined ? "" : ` on ${item.answered_at}`;
15870
- lines.push(`A past decision may already answer this: ${suggestionLabel(item)}, answered by ${item.answered_by}${when} \u2192 ${new URL(item.href, configUrl).toString()}`);
15871
- }
15872
- return lines;
15873
- }
15874
- function suggestionLabel(item) {
15875
- if (item.owner.kind === "issue") {
15876
- return `${item.owner.key} [${item.owner.status}] ${item.owner.title}`;
15877
- }
15878
- return `${item.owner.name} (${item.owner.project}/${item.owner.slug})`;
15879
- }
15880
- function documentResultDetails(artifact) {
15881
- return {
15882
- project: artifact.project,
15883
- artifact: artifact.id,
15884
- document: documentLabel(artifact.project, artifact.slug)
15885
- };
15886
- }
15887
- function writeResultDetails(resolved, fields) {
15888
- if (resolved.owner.kind === "project") {
15889
- return { ...documentResultDetails(resolved.artifact), ...fields };
15890
- }
15891
- if (resolved.issue === undefined)
15892
- throw new Error("issue document is missing its issue");
15893
- return { issue: resolved.issue.key, ...fields };
15894
- }
15895
- async function askOwnerDetails(client, ask, artifact) {
15896
- if (ask.issue_key !== null) {
15897
- return { issue: ask.issue_key, ask: ask.id };
15898
- }
15899
- if (ask.artifact_id === undefined || ask.artifact_id === null) {
15900
- throw new Error("document ask is missing its artifact ID");
15901
- }
15902
- const owner = artifact ?? await client.getArtifact(ask.artifact_id);
15903
- return { ...documentResultDetails(owner), ask: ask.id };
15904
- }
15905
- async function commentOwnerResult(client, comment, artifact) {
15906
- if (comment.issue_key !== null) {
15907
- return { label: comment.issue_key, details: { issue: comment.issue_key, comment: comment.id } };
15908
- }
15909
- if (comment.artifact_id === undefined || comment.artifact_id === null) {
15910
- throw new Error("document comment is missing its artifact ID");
15911
- }
15912
- const owner = artifact ?? await client.getArtifact(comment.artifact_id);
15913
- return {
15914
- label: documentLabel(owner.project, owner.slug),
15915
- details: { ...documentResultDetails(owner), comment: comment.id }
15916
- };
15917
- }
15918
- async function followedAskDetails(client, ask, artifact) {
15919
- return { ...await askOwnerDetails(client, ask, artifact), follows: { ask: ask.id } };
15920
- }
15921
- var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
15922
- var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
15923
- var bareIssueNumberPattern = /^[1-9][0-9]*$/;
15924
- var issueFreeTools = {
15925
- dispatch_issue: true,
15926
- dispatch_edit_ask: true,
15927
- dispatch_resolve_ask: true,
15928
- dispatch_resolve_comment: true,
15929
- dispatch_follow: true,
15930
- dispatch_search: true,
15931
- dispatch_issues: true,
15932
- dispatch_open_asks: true,
15933
- dispatch_whoami: true,
15934
- dispatch_architecture_sync: true
15935
- };
15936
- function canonicalExternalIssueRef(value) {
15937
- const match = value.trim().match(externalIssueRefPattern);
15938
- return match ? `${canonicalRepo(match[1] ?? "", match[2] ?? "")}#${match[3]}` : value;
15939
- }
15940
- function stringArg(args, name) {
15941
- const value = args[name];
15942
- if (typeof value !== "string")
15943
- throw new Error(`${name} is required`);
15944
- return value;
15945
- }
15946
- function optionalString(args, name) {
15947
- const value = args[name];
15948
- return typeof value === "string" ? value : undefined;
15949
- }
15950
- function optionalBoolean(args, name) {
15951
- const value = args[name];
15952
- return typeof value === "boolean" ? value : undefined;
15953
- }
15954
- function optionalNumber(args, name) {
15955
- const value = args[name];
15956
- return typeof value === "number" ? value : undefined;
15957
- }
15958
- function optionalPriority(args, name) {
15959
- const value = args[name];
15960
- if (value === null)
15961
- return null;
15962
- return typeof value === "number" ? value : undefined;
15963
- }
15964
- function optionalPriorityFilter(args, name) {
15965
- const value = args[name];
15966
- return Array.isArray(value) ? value.map((item) => item ?? "none") : undefined;
15967
- }
15968
- function optionalComponents(args, name) {
15969
- const value = args[name];
15970
- if (typeof value !== "object" || value === null)
15971
- return;
15972
- const input = value;
15973
- return {
15974
- mode: input.mode,
15975
- ...input.ids === undefined ? {} : { ids: input.ids },
15976
- ...input.reason === undefined ? {} : { reason: input.reason }
15977
- };
15978
- }
15979
- function componentsChange(input, after) {
15980
- switch (input.mode) {
15981
- case "explicit":
15982
- return `components -> explicit [${after.ids.join(", ")}]`;
15983
- case "none":
15984
- return `components -> none (${after.reason ?? input.reason ?? ""})`;
15985
- case "inherit":
15986
- return "components -> inherit";
15987
- }
15988
- }
15989
- 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.';
15990
- async function architectureGuidance(client, project, components) {
15991
- if (components.mode === "none" || components.ids.length > 0)
15992
- return;
15993
- try {
15994
- if (await client.getArchitectureSource(project) === null) {
15995
- return;
15996
- }
15997
- return `Project ${project} has an architecture model, but this issue is not linked to any of its current components. ${architectureComponentsAction}`;
15998
- } catch (error48) {
15999
- return `Could not check whether project ${project} has an architecture model: ${messageFor(error48)}. ${architectureComponentsAction}`;
16000
- }
16001
- }
16002
- function isDuplicateCandidate(value) {
16003
- if (typeof value !== "object" || value === null)
16004
- return false;
16005
- const candidate = value;
16006
- 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";
16007
- }
16008
- function duplicateCandidates(error48) {
16009
- if (error48.candidates === undefined || !error48.candidates.every(isDuplicateCandidate))
16010
- throw error48;
16011
- return error48.candidates;
16012
- }
16013
- function askUrgency(args) {
16014
- const value = args.urgency;
16015
- return ASK_URGENCIES.find((urgency) => urgency === value);
16016
- }
16017
- function questionWithRef(question, ref) {
16018
- return ref === undefined || question.includes(ref) ? question : `${question}
16019
-
16020
- Ref: ${ref}`;
16021
- }
16022
- function askQuestionWithRef(args) {
16023
- return questionWithRef(stringArg(args, "question"), optionalString(args, "ref"));
16024
- }
16025
- function askQuestionProblem(withRef) {
16026
- return withRef.length > ASK_QUESTION_MAX ? `question plus ref ${overCapMessage(withRef.length, ASK_QUESTION_MAX)}; shorten the question or drop the ref` : undefined;
16027
- }
16028
- var pictureSendingTools = {
16029
- dispatch_ask: true,
16030
- dispatch_comment: true,
16031
- dispatch_message: true
16032
- };
16033
- function refTarget(ref, kind, id) {
16034
- const ownerRef = ref.owner.kind === "issue" ? dispatchIssueRef(ref.owner.issue) : dispatchDocumentRef(ref.owner.project, `${ref.artifact}`);
16035
- return dispatchChildRef(ownerRef, kind, id);
16036
- }
16037
- var canonicalUUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16038
- var compactUUIDPattern = /^[0-9a-f]{32}$/i;
16039
- function normalizeUUID(value) {
16040
- let compact = value;
16041
- if (value.slice(0, 9).toLowerCase() === "urn:uuid:") {
16042
- compact = value.slice(9);
16043
- } else if (value.length === 38 && value.startsWith("{") && value.endsWith("}")) {
16044
- compact = value.slice(1, -1);
16045
- }
16046
- if (canonicalUUIDPattern.test(compact))
16047
- compact = compact.replaceAll("-", "");
16048
- if (!compactUUIDPattern.test(compact))
16049
- return;
16050
- return `${compact.slice(0, 8)}-${compact.slice(8, 12)}-${compact.slice(12, 16)}-${compact.slice(16, 20)}-${compact.slice(20)}`.toLowerCase();
16051
- }
16052
- var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
16053
- var commentIdProblem = "comment must be a bare comment id or a dispatch://.../comment/<id> reference";
16054
- var messageIdProblem = "in_reply_to must be a full message id (uuid) or a dispatch://KEY/message/<id> reference";
16055
- function askId(args) {
16056
- const ask = stringArg(args, "ask");
16057
- return ask.startsWith("dispatch://") ? parseDispatchRef(ask)?.id ?? ask : ask;
16058
- }
16059
- function messageIdOf(value) {
16060
- const id = value.startsWith("dispatch://") ? (() => {
16061
- const reference = parseDispatchRef(value);
16062
- return reference?.kind === "message" ? reference.id : undefined;
16063
- })() : value;
16064
- return id === undefined ? undefined : normalizeUUID(id);
16065
- }
16066
- var idPrefixPattern = /^[0-9a-f][0-9a-f-]{7,}$/i;
16067
- function refOwnerName(ref) {
16068
- return ref.owner.kind === "issue" ? ref.owner.issue : `${ref.owner.project}/${ref.artifact}`;
16069
- }
16070
- async function resolveIdPrefix(tool, kind, id, ownerName, list) {
16071
- const fullID = normalizeUUID(id);
16072
- if (fullID !== undefined)
16073
- return fullID;
16074
- if (!idPrefixPattern.test(id)) {
16075
- throw new ToolInputError(tool, [
16076
- `${kind} id ${id} must be a full uuid or a prefix of at least 8 hex characters`
16077
- ]);
16078
- }
16079
- const prefix = id.toLowerCase();
16080
- const matches = (await list()).filter((item) => item.id.toLowerCase().startsWith(prefix));
16081
- if (matches.length === 1 && matches[0] !== undefined)
16082
- return matches[0].id;
16083
- throw new ToolInputError(tool, [
16084
- 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`
16085
- ]);
16086
- }
16087
- var askIdShapeProblem = "ask ids are uuids (a prefix of at least 8 hex characters works)";
16088
- async function sessionOpenAsks(client, sessionId) {
16089
- const session = sessionId?.trim();
16090
- if (!session)
16091
- throw new Error("host session id is required to read this session's open asks");
16092
- return (await client.openAsks(session)).asks;
16093
- }
16094
- function openAskHints(asks) {
16095
- if (asks.length === 0)
16096
- return "you have no open asks";
16097
- const hints = asks.slice(0, askHintLimit).map((ask) => `${ask.id.slice(0, 8)} \u2014 ${textHead(ask.question)}`);
16098
- return `your open asks: ${hints.join("; ")}`;
16099
- }
16100
- async function resolveAskArgument(tool, args, client, sessionId) {
16101
- const id = askId(args);
16102
- if (normalizeUUID(id) === undefined && !idPrefixPattern.test(id)) {
16103
- const asks = await sessionOpenAsks(client, sessionId).catch(() => {
16104
- return;
16105
- });
16106
- throw new ToolInputError(tool, [
16107
- asks === undefined ? askIdShapeProblem : `${askIdShapeProblem}; ${openAskHints(asks)}`
16108
- ]);
16109
- }
16110
- return resolveIdPrefix(tool, "ask", id, "this session", () => sessionOpenAsks(client, sessionId));
16111
- }
16112
- function messageInReplyTo(args) {
16113
- const inReplyTo = optionalString(args, "in_reply_to");
16114
- return inReplyTo === undefined ? undefined : messageIdOf(inReplyTo);
16115
- }
16116
- function argumentProblems(tool, args) {
16117
- const problems = [];
16118
- switch (tool) {
16119
- case "dispatch_ask": {
16120
- const question = optionalString(args, "question");
16121
- const ref = optionalString(args, "ref");
16122
- if (question !== undefined && ref !== undefined && question.length <= ASK_QUESTION_MAX) {
16123
- const problem = askQuestionProblem(questionWithRef(question, ref));
16124
- if (problem !== undefined)
16125
- problems.push(problem);
16126
- }
16127
- break;
16128
- }
16129
- case "dispatch_edit_ask":
16130
- case "dispatch_resolve_ask":
16131
- case "dispatch_follow": {
16132
- const ask = optionalString(args, "ask");
16133
- if (ask?.startsWith("dispatch://") && parseDispatchRef(ask)?.kind !== "ask") {
16134
- problems.push(askIdProblem);
16135
- }
16136
- break;
16137
- }
16138
- case "dispatch_resolve_comment": {
16139
- const comment = optionalString(args, "comment");
16140
- if (comment?.startsWith("dispatch://") && parseDispatchRef(comment)?.kind !== "comment") {
16141
- problems.push(commentIdProblem);
16142
- }
16143
- break;
16144
- }
16145
- case "dispatch_comment": {
16146
- if (optionalString(args, "quote") !== undefined && optionalString(args, "artifact") === undefined) {
16147
- problems.push("artifact is required when quote is supplied");
16148
- }
16149
- if (optionalString(args, "reply_to") !== undefined && optionalString(args, "reply_to_ask") !== undefined) {
16150
- problems.push("reply_to and reply_to_ask cannot both be set");
16151
- }
16152
- break;
16153
- }
16154
- case "dispatch_message": {
16155
- const inReplyTo = optionalString(args, "in_reply_to");
16156
- if (inReplyTo !== undefined && messageIdOf(inReplyTo) === undefined) {
16157
- problems.push(messageIdProblem);
16158
- }
16159
- break;
16160
- }
16161
- case "dispatch_read": {
16162
- const message = optionalString(args, "message");
16163
- if (message !== undefined && messageIdOf(message) === undefined) {
16164
- problems.push("message must be a full message id (uuid) or a dispatch://KEY/message/<id> reference");
16165
- }
16166
- break;
16167
- }
16168
- }
16169
- return problems;
16170
- }
16171
- function dispatchRefFromUrl(value, serverUrl) {
16172
- let url2;
16173
- let origin;
16174
- try {
16175
- url2 = new URL(value);
16176
- origin = new URL(serverUrl).origin;
16177
- } catch {
16178
- return;
16179
- }
16180
- if (url2.origin !== origin)
16181
- return;
16182
- const versionOf = (name) => {
16183
- const value2 = url2.searchParams.get(name);
16184
- return value2 !== null && /^[1-9][0-9]*$/.test(value2) ? `@v${value2}` : "";
16185
- };
16186
- 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\/([^/]+))?\/?$/);
16187
- if (issuePage) {
16188
- const [, key, page, artifact, ask, comment, message] = issuePage;
16189
- const version2 = versionOf("v");
16190
- if (page === "spec" || artifact !== undefined) {
16191
- const item2 = itemFromSearch(url2.search);
16192
- if (item2 === null)
16193
- return;
16194
- if (item2 !== undefined)
16195
- return `dispatch://${key}/${item2.kind}/${item2.id}`;
16196
- }
16197
- if (page === "spec" && version2 !== "")
16198
- return `dispatch://${key}/artifact/spec${version2}`;
16199
- if (page !== undefined)
16200
- return `dispatch://${key}/${page === "conversation" ? "log" : page}`;
16201
- if (artifact !== undefined) {
16202
- return `dispatch://${key}/artifact/${decodeURIComponent(artifact)}${version2}`;
16203
- }
16204
- if (ask !== undefined)
16205
- return `dispatch://${key}/ask/${ask}`;
16206
- if (comment !== undefined)
16207
- return `dispatch://${key}/comment/${comment}`;
16208
- if (message !== undefined)
16209
- return `dispatch://${key}/message/${message}`;
16210
- return `dispatch://${key}`;
16211
- }
16212
- const agentArtifactPage = url2.pathname.match(/^\/agents\/([^/]+)\/artifacts\/([^/]+)\/?$/);
16213
- if (agentArtifactPage) {
16214
- const [, session, slug2] = agentArtifactPage;
16215
- try {
16216
- return `dispatch://agent/${decodeURIComponent(session ?? "")}/artifact/${decodeURIComponent(slug2 ?? "")}${versionOf("v")}`;
16217
- } catch {
16218
- return;
16219
- }
16220
- }
16221
- const documentPage = url2.pathname.match(/^\/projects\/([A-Z][A-Z0-9]{1,9})\/documents\/([^/]+)\/?$/);
16222
- if (!documentPage)
16223
- return;
16224
- const [, project, slug] = documentPage;
16225
- const document = `dispatch://${project}/artifact/${decodeURIComponent(slug ?? "")}${versionOf("version")}`;
16226
- const item = itemFromSearch(url2.search);
16227
- if (item === null)
16228
- return;
16229
- return item === undefined ? document : `${document}/${item.kind}/${item.id}`;
16230
- }
16231
- 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";
16232
- var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
16233
- var toolSchemas = new Map;
16234
- function toolSchema(tool) {
16235
- const cached2 = toolSchemas.get(tool);
16236
- if (cached2 !== undefined)
16237
- return cached2;
16238
- const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
16239
- if (!spec)
16240
- throw new Error(`Unknown Dispatch tool: ${tool}`);
16241
- const schema = dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
16242
- toolSchemas.set(tool, schema);
16243
- return schema;
16244
- }
16245
- async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, problems) {
16246
- if (issueFreeTools[tool] === true)
16247
- return { args: input, ref: null, owner: null };
16248
- const refArgument = input.ref;
16249
- let ref = null;
16250
- let args = input;
16251
- if (typeof refArgument === "string") {
16252
- const refText = dispatchRefFromUrl(refArgument, serverUrl) ?? refArgument;
16253
- if (refText !== refArgument)
16254
- args = { ...input, ref: refText };
16255
- const agentArtifact = parseAgentArtifactRef(refText);
16256
- if (agentArtifact !== null) {
16257
- if (tool !== "dispatch_doc_read") {
16258
- problems.push(`ref ${refText} names a picture in a conversation on the Agents page; only dispatch_doc_read reads one`);
16259
- }
16260
- if (args.issue !== undefined || args.project !== undefined || args.artifact !== undefined) {
16261
- problems.push("a dispatch://agent/... ref names its upload alone: name no issue, project, or artifact with it");
16262
- }
16263
- return { args, ref: null, owner: null, agentArtifact };
16264
- }
16265
- ref = parseDispatchRef(refText);
16266
- if (ref === null) {
16267
- problems.push(tool === "dispatch_ask" && !refText.startsWith("dispatch://") ? "ref must be a dispatch:// reference" : refGrammarProblem);
16268
- }
16269
- }
16270
- const issueArgument = args.issue;
16271
- const projectArgument = args.project;
16272
- const artifactArgument = args.artifact;
16273
- const versionArgument = args.version;
16274
- if (issueArgument !== undefined && projectArgument !== undefined) {
16275
- problems.push("exactly one of issue and project is required");
16276
- }
16277
- if (typeof projectArgument === "string") {
16278
- if (!PROJECT_KEY_PATTERN.test(projectArgument)) {
16279
- problems.push("project must be a project key such as CORE");
16280
- }
16281
- const refDocument = ref?.owner.kind === "project" ? ref.artifact ?? ref.id : undefined;
16282
- if (ref?.owner.kind === "project" && (ref.owner.project !== projectArgument || artifactArgument !== undefined && artifactArgument !== refDocument)) {
16283
- problems.push("project and ref must name the same document");
16284
- }
16285
- return {
16286
- args: {
16287
- ...args,
16288
- ...artifactArgument === undefined && refDocument !== undefined ? { artifact: refDocument } : {},
16289
- ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
16290
- },
16291
- ref,
16292
- owner: { kind: "project", project: projectArgument }
16293
- };
16294
- }
16295
- if (ref?.owner.kind === "project") {
16296
- return {
16297
- args: {
16298
- ...args,
16299
- project: ref.owner.project,
16300
- ...artifactArgument === undefined ? { artifact: ref.artifact ?? ref.id } : {},
16301
- ...versionArgument === undefined && ref.version !== undefined ? { version: ref.version } : {}
16302
- },
16303
- ref,
16304
- owner: ref.owner
16305
- };
16306
- }
16307
- if (issueArgument !== undefined || ref !== null) {
16308
- const issue3 = typeof issueArgument === "string" ? canonicalExternalIssueRef(issueArgument) : ref?.owner.kind === "issue" ? ref.owner.issue : undefined;
16309
- return {
16310
- args: {
16311
- ...args,
16312
- ...issue3 === undefined ? {} : { issue: issue3 },
16313
- ...artifactArgument === undefined && (ref?.kind === "spec" || ref?.kind === "artifact") ? { artifact: ref.id } : {},
16314
- ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
16315
- },
16316
- ref,
16317
- owner: issue3 === undefined ? null : { kind: "issue", issue: issue3 }
16318
- };
16319
- }
16320
- const replyTarget = args.in_reply_to;
16321
- if (tool === "dispatch_message" && typeof replyTarget === "string" && !replyTarget.startsWith("dispatch://")) {
16322
- return { args, ref, owner: null };
16323
- }
16324
- if (tool === "dispatch_read" && typeof args.message === "string") {
16325
- return { args, ref, owner: null };
16326
- }
16327
- const legionIssue = env.LEGION_ISSUE;
16328
- if (!legionIssue) {
16329
- problems.push(ownerRequiredProblem);
16330
- return { args, ref, owner: null };
16331
- }
16332
- if (nativeIssueKeyPattern.test(legionIssue) || externalIssueRefPattern.test(legionIssue)) {
16333
- const issue3 = canonicalExternalIssueRef(legionIssue);
16334
- return { args: { ...args, issue: issue3 }, ref, owner: { kind: "issue", issue: issue3 } };
16335
- }
16336
- if (!bareIssueNumberPattern.test(legionIssue)) {
16337
- 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");
16338
- return { args, ref, owner: null };
16339
- }
16340
- const repo2 = await resolveCwdRepo(cwd, exec);
16341
- if (!repo2) {
16342
- problems.push("issue is required; LEGION_ISSUE needs a GitHub repository in cwd");
16343
- return { args, ref, owner: null };
16344
- }
16345
- const issue2 = `${repo2}#${legionIssue}`;
16346
- return { args: { ...args, issue: issue2 }, ref, owner: { kind: "issue", issue: issue2 } };
16347
- }
16348
- function artifactByReference(artifacts, artifactReference, owner, canonical) {
16349
- const bySlug = artifacts.find((candidate) => candidate.slug === artifactReference);
16350
- if (canonical && bySlug !== undefined)
16351
- return bySlug;
16352
- const byId = artifacts.find((candidate) => candidate.id === artifactReference);
16353
- if (byId !== undefined)
16354
- return byId;
16355
- const byName = artifacts.filter((candidate) => candidate.name === artifactReference);
16356
- if (bySlug !== undefined) {
16357
- const named = byName.filter((candidate) => candidate.id !== bySlug.id && candidate.kind === "doc");
16358
- if (named.length > 0) {
16359
- throw new Error(documentReferenceProblem(artifactReference, [bySlug, ...named], owner, "id"));
16360
- }
16361
- return bySlug;
16362
- }
16363
- if (byName.length > 1) {
16364
- throw new Error(documentReferenceProblem(artifactReference, byName, owner, "slug"));
16365
- }
16366
- const [artifact] = byName;
16367
- if (artifact === undefined) {
16368
- throw new Error(documentReferenceProblem(artifactReference, artifacts, owner));
16369
- }
16370
- return artifact;
16371
- }
16372
- async function resolveArtifact(client, owner, artifactReference, { canonical = false } = {}) {
16373
- if (owner.kind === "project") {
16374
- if (artifactReference === undefined) {
16375
- throw new Error("artifact is required for a project document");
16376
- }
16377
- const routed = await client.getProjectArtifact(owner.project, artifactReference).catch((error48) => {
16378
- if (!dispatchAnswered(error48, 404))
16379
- throw error48;
16380
- return;
16381
- });
16382
- if (canonical && routed !== undefined)
16383
- return { owner, artifact: routed };
16384
- const unlinked = await client.listProjectArtifacts(owner.project, true);
16385
- const artifacts = routed === undefined ? unlinked : [routed, ...unlinked.filter((candidate) => candidate.id !== routed.id)];
16386
- return {
16387
- owner,
16388
- artifact: artifactByReference(artifacts, artifactReference, "project", canonical)
16389
- };
16390
- }
16391
- const issue2 = await client.getIssue(owner.issue);
16392
- let artifact;
16393
- if (artifactReference === undefined || artifactReference === "spec") {
16394
- artifact = issue2.artifacts.find((candidate) => candidate.primary || candidate.id === issue2.primary_artifact_id);
16395
- } else {
16396
- artifact = artifactByReference(issue2.artifacts, artifactReference, "issue", canonical);
16397
- }
16398
- if (!artifact) {
16399
- throw new Error(documentReferenceProblem(artifactReference ?? "spec", issue2.artifacts, "issue"));
16400
- }
16401
- return { owner, issue: issue2, artifact };
16402
- }
16403
- var documentHintLimit = 8;
16404
- function documentReferenceProblem(reference, documents, owner, use) {
16405
- const hints = documents.slice(0, documentHintLimit).map((document) => use === "id" ? `${document.id} (${document.slug}, ${document.name})` : `${document.slug} (${document.name})`);
16406
- const list = hints.length === 0 ? "none" : hints.join(", ");
16407
- 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}`;
16408
- }
16409
- var askHintLimit = 8;
16410
- function askIDInputProblem(asks, scope) {
16411
- const hints = asks.slice(0, askHintLimit).map((ask) => `${ask.id.slice(0, 8)}\u2026 ${textHead(ask.question)}`);
16412
- return `ask IDs are UUIDs; use the full ask ID; ${scope} open asks: ${hints.length === 0 ? "none" : hints.join(", ")}`;
16413
- }
16414
- async function invalidReplyToAskProblem(client, owner, resolved) {
16415
- if (owner.kind === "issue") {
16416
- const issue2 = resolved?.issue ?? await client.getIssue(owner.issue);
16417
- return askIDInputProblem(issue2.open_asks, "this issue's");
16418
- }
16419
- if (resolved === undefined)
16420
- throw new Error("project document is missing its resolved artifact");
16421
- return askIDInputProblem(await client.getArtifactAsks(resolved.artifact.id, "open"), "this document's");
16422
- }
16423
- function anchor(artifact, args) {
16424
- const quote = optionalString(args, "quote");
16425
- if (quote === undefined)
16426
- return;
16427
- const occurrence = optionalNumber(args, "occurrence");
16428
- return { artifact: artifact.id, quote, ...occurrence === undefined ? {} : { occurrence } };
16429
- }
16430
- function toolActor(origin, input) {
16431
- return {
16432
- kind: "session",
16433
- id: input.sessionId ?? "unknown",
16434
- origin: {
16435
- ...origin,
16436
- host: input.host,
16437
- ...input.sessionTitle === undefined ? {} : { session_title: input.sessionTitle }
16438
- }
16439
- };
16440
- }
16441
- function approvalLine(artifact) {
16442
- const approval = artifact.approval;
16443
- if (approval === undefined || approval.state === "draft")
16444
- return;
16445
- switch (approval.state) {
16446
- case "awaiting": {
16447
- const turn = approval.waiting_on === undefined ? "" : `, waiting on ${approval.waiting_on}`;
16448
- return `Approval: awaiting${turn} (requested by ${approval.requested_by?.id ?? "unknown"}, ask ${approval.ask_id ?? "?"})`;
16449
- }
16450
- case "approved":
16451
- return `Approval: approved v${approval.version} by ${approval.by?.id ?? "unknown"}`;
16452
- case "stale":
16453
- 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`;
16454
- case "changes_requested":
16455
- return `Approval: changes requested on v${approval.version} by ${approval.by?.id ?? "unknown"}: ${approval.reason ?? ""}`;
16456
- }
16457
- }
16458
- function componentsLine(components) {
16459
- const inherited = components.inherited_from === null ? "" : ` (inherited from ${components.inherited_from})`;
16460
- const retired = components.unknown.length === 0 ? "" : ` (retired: ${components.unknown.join(", ")})`;
16461
- switch (components.mode) {
16462
- case "explicit":
16463
- return `Components: ${components.ids.length === 0 ? "none live" : components.ids.join(", ")}${inherited}${retired}`;
16464
- case "none":
16465
- return `Components: none \u2014 ${components.reason ?? ""}${inherited}`;
16466
- case "inherit":
16467
- return "Components: unassigned";
16468
- }
16469
- }
16470
- function claimText(claim, titles) {
16471
- const holding = claimHolds(claim, titles);
16472
- const marker = holding === "unknown" ? " \xB7 liveness unknown" : holding === "lapsed" ? " \xB7 not running" : "";
16473
- return `${actorLabel(claim.actor, titles)} since ${claim.at}${marker}`;
16474
- }
16475
- async function liveSessionTitles(client, needed) {
16476
- if (!needed) {
16477
- return;
16478
- }
16479
- try {
16480
- const agents = await client.listAgents();
16481
- return new Map(agents.map((agent) => [agent.session_id, agent.title]));
16482
- } catch {
16483
- return;
16484
- }
16485
- }
16486
- function holdsSession(claim) {
16487
- return claim?.actor.kind === "session";
16488
- }
16489
- function routeHeldBySession(issue2) {
16490
- return issue2.route_status === "live" && issue2.route?.startsWith("role:") === true;
16491
- }
16492
- function routeText(issue2, titles) {
16493
- if (issue2.route === null)
16494
- return "none";
16495
- const holder = issue2.route_holder ?? null;
16496
- const reach = {
16497
- live: routeHeldBySession(issue2) && holder !== null ? ` (held by ${titles?.get(holder) ?? holder})` : "",
16498
- no_holder: issue2.route.startsWith("role:") ? " (nobody holds it right now)" : " (that session is not running right now)",
16499
- unknown: " (the Envoy listener did not answer, so whether it reaches anyone is unknown)"
16500
- };
16501
- return issue2.route + (issue2.route_status == null ? "" : reach[issue2.route_status]);
16502
- }
16503
- function progressText(progress, sep) {
16504
- if (progress === undefined)
16505
- return "";
16506
- const parts = [];
16507
- if (progress.tasks !== null)
16508
- parts.push(`tasks ${progress.tasks.done}/${progress.tasks.total}`);
16509
- if (progress.children !== null) {
16510
- parts.push(`children ${progress.children.done}/${progress.children.total}`);
16511
- }
16512
- return parts.join(sep);
16513
- }
16514
- function issueSummary(issue2, events, references, graph, titles) {
16515
- const asks = issue2.open_asks;
16516
- const spec = issue2.artifacts?.find((artifact) => artifact.primary);
16517
- const specApproval = spec === undefined ? undefined : approvalLine(spec);
16518
- if (issue2.priority === undefined)
16519
- throw new Error("Dispatch issue is missing priority");
16520
- if (issue2.assignee === undefined)
16521
- throw new Error("Dispatch issue is missing assignee");
16522
- if (issue2.components === undefined)
16523
- throw new Error("Dispatch issue is missing components");
16524
- if (issue2.claim === undefined)
16525
- throw new Error("Dispatch issue is missing claim");
16526
- if (issue2.external_links === undefined) {
16527
- throw new Error("Dispatch issue is missing external_links");
16528
- }
16529
- return [
16530
- `Title: ${issue2.title}`,
16531
- `Key: ${issue2.key}`,
16532
- `Status: ${issue2.status}`,
16533
- `Assignee: ${issue2.assignee ?? "unassigned"}`,
16534
- `Claimed by: ${issue2.claim === null ? "nobody" : claimText(issue2.claim, titles)}`,
16535
- ...issue2.priority === null ? [] : [`Priority: P${issue2.priority}`],
16536
- `Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
16537
- componentsLine(issue2.components),
16538
- `Route: ${routeText(issue2, titles)}`,
16539
- `Progress: ${progressText(issue2.progress, ", ") || "none"}`,
16540
- ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
16541
- "External links:",
16542
- ...issue2.external_links.length === 0 ? ["- none"] : issue2.external_links.map((link) => `- ${link.url}${link.kind === undefined ? "" : ` (${link.kind})`}`),
16543
- "Open asks:",
16544
- ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
16545
- "References:",
16546
- ...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}`),
16547
- ...typeof references === "string" || !references.truncated ? [] : ["- more references beyond 8 hops"],
16548
- "Events:",
16549
- ...events.length === 0 ? ["- none"] : events.map(eventLine),
16550
- ...graph
16551
- ].join(`
16552
- `);
16553
- }
16554
- function referenceLines(edges) {
16555
- if (typeof edges === "string")
16556
- return [`- ${edges}`];
16557
- if (edges.length === 0)
16558
- return ["- none"];
16559
- return edges.map((edge) => {
16560
- const excerpt2 = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
16561
- return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt2}${edge.created_at})`;
16562
- });
16563
- }
16564
- function unavailableReason(error48) {
16565
- return dispatchAnswered(error48, 404) ? "unavailable" : `unavailable: ${messageFor(error48)}`;
16566
- }
16567
- async function graphEdges(client, query) {
16568
- try {
16569
- return (await client.getReferences(query)).edges;
16570
- } catch (error48) {
16571
- return unavailableReason(error48);
16572
- }
16573
- }
16574
- async function issueReferencesOrUnavailable(client, key) {
16575
- try {
16576
- return await client.getIssueReferences(key);
16577
- } catch (error48) {
16578
- return unavailableReason(error48);
16579
- }
16580
- }
16581
- async function graphSections(client, ref) {
16582
- const [incoming, outgoing] = await Promise.all([
16583
- graphEdges(client, { to: ref }),
16584
- graphEdges(client, { from: ref })
16585
- ]);
16586
- return ["Referenced by:", ...referenceLines(incoming), "Links:", ...referenceLines(outgoing)];
16587
- }
16588
- function logSummary(issue2, events) {
16589
- return [
16590
- `Key: ${issue2.key}`,
16591
- "Events:",
16592
- ...events.length === 0 ? ["- none"] : events.map(eventLine)
16593
- ].join(`
16594
- `);
16595
- }
16596
- function eventHead(event) {
16597
- switch (event.type) {
16598
- case "ask.opened":
16599
- case "ask.anchor_refreshed":
16600
- case "ask.edited":
16601
- case "ask.handed_back":
16602
- case "ask.resolved":
16603
- return textHead(event.payload.question);
16604
- case "ask.answered": {
16605
- const previousAnswer = event.payload.previous_answer;
16606
- return `${textHead(event.payload.question)} -> ${textHead(askAnswerText(event.payload.answer))}${previousAnswer === undefined ? "" : ` (was: ${textHead(askAnswerText(previousAnswer))})`}`;
16607
- }
16608
- case "comment.created":
16609
- case "comment.answered":
16610
- case "comment.anchor_refreshed":
16611
- case "comment.edited":
16612
- case "comment.resolved":
16613
- case "comment.reopened":
16614
- case "suggestion.accepted":
16615
- case "suggestion.rejected":
16616
- case "message.created":
16617
- case "message.answered":
16618
- return textHead(event.payload.body);
16619
- case "artifact.version":
16620
- return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
16621
- case "issue.created":
16622
- case "issue.updated":
16623
- case "issue.closed":
16624
- return `status ${event.payload.status}`;
16625
- default:
16626
- return;
16627
- }
16628
- }
16629
- function actorText(actor) {
16630
- const service = actor.kind === "session" ? actor.service : undefined;
16631
- if (service === undefined) {
16632
- return `${actor.kind} ${actor.id}`;
16633
- }
16634
- return `${actor.kind} ${actor.id} (as ${serviceSubjectLabel(service)})`;
16635
- }
16636
- function eventLine(event) {
16637
- const head = eventHead(event);
16638
- return `- #${event.seq} ${event.type} \xB7 ${actorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
16639
- }
16640
- function childrenSummary(issue2) {
16641
- return [
16642
- `Key: ${issue2.key}`,
16643
- "Children:",
16644
- ...issue2.children.length === 0 ? ["- none"] : issue2.children.map((child) => `- ${child.key}: ${child.title} (${child.status})`)
16645
- ].join(`
16646
- `);
16647
- }
16648
- function askSummary({ ask, replies, answers }, graph) {
16649
- const answer = ask.answer;
16650
- const earlierAnswers = answers.slice(0, -1);
16651
- const chain = replies.flatMap((reply) => [
16652
- `${reply.id} \xB7 ${actorText(reply.author)}`,
16653
- `Body: ${reply.body}`
16654
- ]);
16655
- return [
16656
- `Question: ${ask.question}`,
16657
- ...anchorLines(ask),
16658
- "Options:",
16659
- ...ask.options.length === 0 ? ["- none"] : ask.options.map((option) => `- ${option.label}${option.description ? ` \u2014 ${option.description}` : ""}`),
16660
- `State: ${ask.state}`,
16661
- "Answer:",
16662
- ...answer === null ? ["- none"] : [
16663
- `- By: ${answer.user}`,
16664
- `- Selected: ${answer.selected.length === 0 ? "none" : answer.selected.join(", ")}`,
16665
- ...answer.text === null ? [] : [`- Text: ${answer.text}`]
16666
- ],
16667
- ...earlierAnswers.length === 0 ? [] : [
16668
- "Earlier answers:",
16669
- ...earlierAnswers.map((earlier) => `- ${earlier.at} \xB7 ${earlier.user} \xB7 ${askAnswerText(earlier)}`)
16670
- ],
16671
- ...ask.resolution === undefined ? [] : [
16672
- "Resolution:",
16673
- `- By: ${ask.resolution.actor.id}`,
16674
- `- Kind: ${ask.resolution.kind}`,
16675
- `- Reason: ${ask.resolution.reason}`
16676
- ],
16677
- "Replies:",
16678
- ...chain.length === 0 ? ["- none"] : chain,
16679
- ...graph
16680
- ].join(`
16681
- `);
16682
- }
16683
- function openAskAge(ageSeconds) {
16684
- const seconds = Math.max(0, Math.floor(ageSeconds));
16685
- if (seconds < 60)
16686
- return `${seconds}s`;
16687
- const minutes = Math.floor(seconds / 60);
16688
- if (minutes < 60)
16689
- return `${minutes}m${seconds % 60 === 0 ? "" : ` ${seconds % 60}s`}`;
16690
- const hours = Math.floor(minutes / 60);
16691
- if (hours < 24)
16692
- return `${hours}h${minutes % 60 === 0 ? "" : ` ${minutes % 60}m`}`;
16693
- const days = Math.floor(hours / 24);
16694
- return `${days}d${hours % 24 === 0 ? "" : ` ${hours % 24}h`}`;
16695
- }
16696
- function openAskOwner(ask) {
16697
- return "issue" in ask.owner ? `${ask.owner.issue.key}: ${ask.owner.issue.title}` : `${ask.owner.document.project} / ${ask.owner.document.name}`;
16698
- }
16699
- function openAskLine(ask, baseUrl) {
16700
- const priority = ask.priority === null ? "" : `P${ask.priority} \xB7 `;
16701
- return `- ${openAskAge(ask.age_seconds)} \xB7 ${priority}${openAskOwner(ask)} \xB7 ${ask.question} \xB7 ${new URL(ask.ref, baseUrl).toString()}`;
16702
- }
16703
- function formatOpenAsksSummary(response, baseUrl) {
16704
- const scope = "active open asks you authored on open issues and project documents";
16705
- if (response.count === 0) {
16706
- return ["There are no unanswered asks for this session.", `Scope: ${scope}.`].join(`
16707
- `);
16708
- }
16709
- const waitingOnHuman = response.asks.filter((ask) => ask.waiting_on === "human");
16710
- const waitingOnAgent = response.asks.filter((ask) => ask.waiting_on === "agent");
16711
- return [
16712
- `${response.count} unanswered ${response.count === 1 ? "ask" : "asks"} you authored on active issues and project documents.`,
16713
- "",
16714
- `Waiting on human (${waitingOnHuman.length}):`,
16715
- ...waitingOnHuman.map((ask) => openAskLine(ask, baseUrl)),
16716
- "",
16717
- `Waiting on agent (${waitingOnAgent.length}):`,
16718
- ...waitingOnAgent.map((ask) => openAskLine(ask, baseUrl))
16719
- ].join(`
16720
- `);
16721
- }
16722
- function commentSummary({ comment, replies }, graph) {
16723
- const root = [
16724
- `${comment.id} \xB7 ${actorText(comment.author)}`,
16725
- ...anchorLines(comment),
16726
- `Body: ${comment.body}`
16727
- ];
16728
- const chain = replies.flatMap((reply) => [
16729
- `${reply.id} \xB7 ${actorText(reply.author)}`,
16730
- ...anchorLines(reply),
16731
- `Body: ${reply.body}`
16732
- ]);
16733
- return [
16734
- "Comment:",
16735
- ...root,
16736
- "Reply chain:",
16737
- ...chain.length === 0 ? ["- none"] : chain,
16738
- ...graph
16739
- ].join(`
16740
- `);
16741
- }
16742
- function anchorLines(record2) {
16743
- return [
16744
- ...record2.anchor?.quote === undefined ? [] : [`> ${record2.anchor.quote}`],
16745
- ...record2.anchor_block === undefined ? [] : [`Position: ${positionText(record2.anchor_block)}`],
16746
- ...record2.anchor_block_error === undefined ? [] : [`Position: unavailable (${record2.anchor_block_error})`]
16747
- ];
16748
- }
16749
- function positionText(block) {
16750
- const { table, path: path2 } = block;
16751
- const segments = path2.map((entry) => `${entry.type}[${entry.index}]`);
16752
- if (table === undefined || table.row === null)
16753
- return segments.join(" \u203A ");
16754
- const tableAt = path2.findIndex((entry) => entry.type === "table");
16755
- const cells = table.cells ?? [];
16756
- const label = (table.column === null ? cells : cells.slice(0, table.column)).map((cell) => cell.trim()).filter((cell) => cell !== "" && !/^\d+$/.test(cell)).join(" \xB7 ");
16757
- const row = label === "" ? `row ${table.row}` : `row ${table.row} (${label})`;
16758
- const header = table.header === null || table.header === "" ? String(table.column) : table.header;
16759
- return [
16760
- ...segments.slice(0, tableAt + 1),
16761
- table.column === null ? row : `${row}, column ${header}`
16762
- ].join(" \u203A ");
16763
- }
16764
- function messageSummary({ message, replies }, graph) {
16765
- const root = [`${message.id} \xB7 ${actorText(message.author)}`, `Body: ${message.body}`];
16766
- const chain = replies.flatMap((reply) => [
16767
- `${reply.id} \xB7 ${actorText(reply.author)}`,
16768
- `Body: ${reply.body}`
16769
- ]);
16770
- return [
16771
- "Message:",
16772
- ...root,
16773
- "Reply chain:",
16774
- ...chain.length === 0 ? ["- none"] : chain,
16775
- ...graph
16776
- ].join(`
16777
- `);
16778
- }
16779
- async function openArtifactMarks(client, resolved) {
16780
- const asksPromise = resolved.owner.kind === "project" ? client.getArtifactAsks(resolved.artifact.id) : Promise.resolve(resolved.issue?.open_asks ?? []);
16781
- const commentsPromise = resolved.owner.kind === "project" ? client.getArtifactComments(resolved.artifact.id) : client.getComments(resolved.issue?.key ?? "", resolved.artifact.id);
16782
- const commentsResultPromise = commentsPromise.then((value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }));
16783
- const asks = await asksPromise;
16784
- const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
16785
- const commentsResult = await commentsResultPromise;
16786
- if (commentsResult.status === "rejected") {
16787
- if (dispatchAnswered(commentsResult.reason, 404))
16788
- return marks;
16789
- throw commentsResult.reason;
16790
- }
16791
- return [
16792
- ...marks,
16793
- ...commentsResult.value.filter((comment) => !comment.resolved && comment.anchor?.artifact_id === resolved.artifact.id).map((comment) => `comment ${comment.id}`)
16794
- ];
16795
- }
16796
- async function blockAsks(client, resolved, state) {
16797
- const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
16798
- return asks.filter((ask) => typeof ask.block_id === "string" && ask.block_artifact?.id === resolved.artifact.id);
16799
- }
16800
- async function readUploadedFile(client, artifact, details, requested, owner, shown) {
16801
- const latest = Math.max(0, ...artifact.versions.map((version2) => version2.number));
16802
- const number4 = requested ?? latest;
16803
- const of = number4 === latest ? "" : ` of ${latest}`;
16804
- const bytesRoute = `GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`;
16805
- const tooLarge = (mime, bytes) => ({
16806
- text: `${oversizePictureText(artifact.name, bytes)}, so dispatch_doc_read cannot show this ` + `uploaded ${mime} picture (version ${number4}${of}). ${bytesRoute}`,
16807
- details
16808
- });
16809
- const stated = artifact.versions.find((version2) => version2.number === number4);
16810
- if (artifact.kind === "image" && stated?.size !== undefined && stated.size > PICTURE_SHOWN_MAX_BYTES) {
16811
- return tooLarge(stated.mime ?? "image", stated.size);
16812
- }
16813
- const file2 = await client.fileVersion(artifact.id, number4);
16814
- const size = `${file2.bytes.length.toLocaleString("en-US")} bytes`;
16815
- if (artifact.kind === "image") {
16816
- const image = toolImage(file2.bytes);
16817
- if (image !== undefined) {
16818
- const address = `dispatch://${owner}/artifact/${artifact.slug}@v${number4}`;
16819
- shown?.add(address);
16820
- return {
16821
- text: `Picture ${artifact.name}: ${image.mimeType}, version ${number4}${of}, ${size}.
16822
- ` + shownPictureLine(1, address, artifact.name, image.mimeType, file2.bytes.length),
16823
- details,
16824
- images: [image]
16825
- };
16826
- }
16827
- if (sniffPictureType(file2.bytes) !== undefined)
16828
- return tooLarge(file2.mime, file2.bytes.length);
16829
- }
16830
- let text;
16831
- try {
16832
- text = new TextDecoder("utf-8", { fatal: true }).decode(file2.bytes);
16833
- } catch {
16834
- return {
16835
- 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}`,
16836
- details
16837
- };
16838
- }
16839
- return {
16840
- text: `File ${artifact.name}: ${file2.mime}, version ${number4}${of}, ${size}.
16841
-
16842
- ${text}`,
16843
- details
16844
- };
16845
- }
16846
- async function refuseOpenDecisionBlocks(client, tool, resolved) {
16847
- const artifact = resolved.artifact;
16848
- const latest = artifact.approval?.latest_version;
16849
- if (latest === undefined || latest < 1 || artifact.approval?.state === "approved")
16850
- return;
16851
- const blocks = (await client.artifactBlocks(artifact.id)).filter((block) => block.type === "ask");
16852
- if (blocks.length === 0)
16853
- return;
16854
- const [documentAsks, version2] = await Promise.all([
16855
- blockAsks(client, resolved),
16856
- client.docRead(artifact.id, latest)
16857
- ]);
16858
- const asks = new Map(documentAsks.map((ask) => [ask.block_id, ask]));
16859
- const lines = version2.markdown.split(`
16860
- `);
16861
- const open = blocks.flatMap((block) => {
16862
- const ask = asks.get(block.id);
16863
- const named = ask === undefined ? `block ${block.id}` : `${JSON.stringify(ask.question)} (block ${block.id}, ask ${ask.id})`;
16864
- const states = lines.filter((line) => line.includes(`ask{#${block.id} `) || line.includes(`ask{#${block.id}}`)).map((line) => /\bstate="(\w+)"/.exec(line)?.[1]);
16865
- if (states.length === 0)
16866
- return [`${named}, which version ${latest} does not hold yet`];
16867
- if (!states.includes("open") && states.some((state) => state !== undefined))
16868
- return [];
16869
- if (ask === undefined)
16870
- return [`${named}, whose ask Dispatch has not opened yet`];
16871
- if (ask.state === "open")
16872
- return [named];
16873
- const next = ask.state === "answered" ? "fold the answer into the text" : "write the decision into the text";
16874
- return [
16875
- `${named}, ${ask.state} but still open in version ${latest}: ${next} with dispatch_doc_edit, which writes a version that carries it`
16876
- ];
16877
- });
16878
- if (open.length === 0)
16879
- return;
16880
- const count = open.length === 1 ? "1 open decision block" : `${open.length} open decision blocks`;
16881
- throw new Error([
16882
- `${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.`,
16883
- ...open.map((line) => `- ${line}`),
16884
- "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."
16885
- ].join(`
16886
- `));
16887
- }
16888
- async function refuseRemovingOpenDecisionBlocks(client, tool, resolved, ops) {
16889
- const removing = ops.filter((operation) => operation.block !== undefined && (operation.op === "delete" || operation.op === "retype" && operation.type !== "ask"));
16890
- if (removing.length === 0)
16891
- return;
16892
- const artifact = resolved.artifact;
16893
- const blocks = await client.artifactBlocks(artifact.id);
16894
- const askBlocks = blocks.filter((block) => block.type === "ask");
16895
- const removed = new Set;
16896
- for (const operation of removing) {
16897
- const target = blocks.find((block) => block.id === operation.block);
16898
- if (target === undefined)
16899
- continue;
16900
- for (const block of askBlocks) {
16901
- if (block.id === target.id || operation.op === "delete" && block.from >= target.from && block.to <= target.to) {
16902
- removed.add(block.id);
16903
- }
16904
- }
16905
- }
16906
- if (removed.size === 0)
16907
- return;
16908
- const asks = await blockAsks(client, resolved, "open");
16909
- const open = asks.filter((ask) => removed.has(ask.block_id));
16910
- if (open.length === 0)
16911
- return;
16912
- const [what, question] = open.length === 1 ? ["a decision block whose ask is", "question"] : [`${open.length} decision blocks whose asks are`, "questions"];
16913
- throw new Error([
16914
- `${tool} was not called: it would remove ${what} still open, and the human's ${question} would leave their Inbox unanswered.`,
16915
- ...open.map((ask) => `- ${JSON.stringify(ask.question)} (block ${ask.block_id}, ask ${ask.id})`),
16916
- "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."
16917
- ].join(`
16918
- `));
16919
- }
16920
- function refusalWithCode(error48, ...clauses) {
16921
- const suffix = clauses.filter((clause) => clause !== "").join("; ");
16922
- const joined = suffix === "" ? "" : `; ${suffix}`;
16923
- if (error48 instanceof DispatchGatewayError) {
16924
- let told = error48.message;
16925
- if (joined !== "") {
16926
- told = error48.mayHaveReachedDispatch ? `${error48.answer}${joined}` : `${error48.answer}, so ${error48.advice}${joined}`;
16927
- }
16928
- return new DispatchGatewayError(error48.status, error48.answer, error48.advice, `${error48.code}: ${told}`);
16929
- }
16930
- if (!(error48 instanceof DispatchServiceError))
16931
- return error48;
16932
- return new DispatchServiceError(error48.code, error48.status, `${error48.code}: ${error48.message}${joined}`, error48.candidates, error48.current, error48.mismatches);
16933
- }
16934
- function dispatchAnswered(error48, status) {
16935
- return error48 instanceof DispatchServiceError && error48.fromDispatch && error48.status === status;
16936
- }
16937
- function writeMayHaveLanded(error48) {
16938
- if (error48 instanceof DispatchGatewayError)
16939
- return error48.mayHaveReachedDispatch;
16940
- return !(error48 instanceof DispatchServiceError) || error48.status >= 500;
16941
- }
16942
- function withAccount(error48, account) {
16943
- const message = messageFor(error48);
16944
- const told = /[.!?]$/.test(message) ? `${message} ${account.charAt(0).toUpperCase()}${account.slice(1)}` : `${message}; ${account}`;
16945
- return new Error(told, { cause: error48 });
16946
- }
16947
- async function executeDispatchTool(input) {
16948
- const configUrl = input.config.url;
16949
- const configToken = input.config.token;
16950
- if (!input.config.enabled || !configUrl || !configToken) {
16951
- throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
16952
- }
16953
- const baseFetch = input.fetchImpl ?? fetch;
16954
- const fetchImpl = Object.assign(async (...args2) => {
16955
- try {
16956
- return await baseFetch(...args2);
16957
- } catch (error48) {
16958
- if (error48 instanceof TypeError) {
16959
- 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.`);
16960
- }
16961
- throw error48;
16962
- }
16963
- }, { preconnect: baseFetch.preconnect });
16964
- const env = input.env ?? process.env;
16965
- const exec = input.exec ?? defaultExec;
16966
- const problems = [];
16967
- const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec, configUrl, problems);
16968
- const ownerMissing = issueFreeTools[input.tool] !== true && ownerArguments.owner === null;
16969
- const schema = toolSchema(input.tool);
16970
- const parsed = schema.safeParse(ownerArguments.args, { reportInput: true });
16971
- if (!parsed.success) {
16972
- 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"))));
16973
- problems.push(...formatZodIssues(issues, schema));
16974
- }
16975
- problems.push(...argumentProblems(input.tool, ownerArguments.args));
16976
- const pictures = pictureSendingTools[input.tool] === true ? await localPictures(ownerArguments.args.images, input.cwd, problems) : [];
16977
- if (problems.length > 0)
16978
- throw new ToolInputError(input.tool, problems);
16979
- const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
16980
- if (input.tool === "dispatch_open_asks") {
16981
- const client2 = dispatchClient();
16982
- const project = optionalString(ownerArguments.args, "project");
16983
- if (project !== undefined) {
16984
- const response2 = await client2.openAsksForProject(project);
16985
- return { text: formatOpenAsksSummary(response2, configUrl), details: { ...response2 } };
16986
- }
16987
- const sessionId = input.sessionId?.trim();
16988
- if (!sessionId)
16989
- throw new Error("host session id is required for dispatch_open_asks");
16990
- const response = await client2.openAsks(sessionId);
16991
- return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
16992
- }
16993
- if (input.tool === "dispatch_whoami") {
16994
- const sessionId = input.sessionId?.trim();
16995
- if (!sessionId)
16996
- throw new Error("host session id is required for dispatch_whoami");
16997
- const client2 = dispatchClient();
16998
- const identity = await client2.whoami();
16999
- const owner2 = identity.kind === "agent" ? identity.owner : identity.login.toLowerCase();
17000
- const service = identity.kind === "agent" ? identity.service ?? null : null;
17001
- const unowned = "no owner, so issues you create without an assignee are unassigned (or inherit their parent's).";
17002
- return {
17003
- 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}`,
17004
- details: { session: sessionId, owner: owner2, service }
17005
- };
17006
- }
17007
- const args = parsed.success ? parsed.data : ownerArguments.args;
17008
- const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
17009
- const client = dispatchClient();
17010
- const refDocument = ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : ownerArguments.ref?.artifact;
17011
- const resolveDocument = (documentOwner2, reference) => resolveArtifact(client, documentOwner2, reference, {
17012
- canonical: reference !== undefined && reference === refDocument
17013
- });
17014
- const owner = ownerArguments.owner?.kind === "issue" ? {
17015
- kind: "issue",
17016
- issue: await resolveExistingIssue(client, ownerArguments.owner.issue)
17017
- } : ownerArguments.owner;
17018
- const issue2 = () => {
17019
- if (owner?.kind !== "issue")
17020
- throw new Error("issue is required");
17021
- return owner.issue;
17022
- };
17023
- const documentOwner = () => {
17024
- if (owner === null)
17025
- throw new Error("issue or project is required");
17026
- return owner;
17027
- };
17028
- const postWithPictures = (text, limit, owner2, upload) => textWithPictures(input.tool, text, pictures, limit, owner2, upload, actor);
17029
- const writePictures = (resolved, text, limit) => {
17030
- if (resolved?.owner.kind === "project") {
17031
- const { project } = resolved.owner;
17032
- return postWithPictures(text, limit, project, (upload) => client.projectArtifact(project, upload));
17033
- }
17034
- const issueKey = issue2();
17035
- return postWithPictures(text, limit, issueKey, (upload) => client.artifact(issueKey, upload));
17036
- };
17037
- const hostSession = input.sessionId?.trim();
17038
- const shown = hostSession ? shownPictures(hostSession) : undefined;
17039
- switch (input.tool) {
17040
- case "dispatch_issue": {
17041
- const project = stringArg(args, "project");
17042
- const title = stringArg(args, "title");
17043
- const parent = optionalString(args, "parent");
17044
- const external2 = optionalString(args, "external");
17045
- const force = optionalBoolean(args, "force");
17046
- const spec = optionalString(args, "spec");
17047
- const priority = optionalPriority(args, "priority");
17048
- const assignee = optionalString(args, "assignee");
17049
- const components = optionalComponents(args, "components");
17050
- const labels = args.labels;
17051
- try {
17052
- const created = await client.issue({
17053
- project,
17054
- title,
17055
- ...parent === undefined ? {} : { parent },
17056
- ...external2 === undefined ? {} : { external: external2 },
17057
- ...force === undefined ? {} : { force },
17058
- ...spec === undefined ? {} : { spec },
17059
- ...priority === undefined ? {} : { priority },
17060
- ...assignee === undefined ? {} : { assignee },
17061
- ...components === undefined ? {} : { components },
17062
- ...Array.isArray(labels) ? { labels } : {},
17063
- actor
17064
- });
17065
- const componentGuidance = await architectureGuidance(client, created.project, created.components);
17066
- const adviceLines = [
17067
- ...renderAdvice(input.tool, created.key, created.advice, {
17068
- isPrimarySpec: spec !== undefined
17069
- }),
17070
- ...componentGuidance === undefined ? [] : [componentGuidance],
17071
- ...renderSuggestions(created.advice?.suggestions, configUrl)
17072
- ];
17073
- return {
17074
- text: [
17075
- `Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
17076
- ...spec === undefined ? [] : [SPEC_CHECK_REMINDER],
17077
- ...adviceLines
17078
- ].join(`
17079
- `),
17080
- details: {
17081
- issue: created.key,
17082
- ...created.advice === undefined ? {} : { advice: created.advice }
17083
- }
17084
- };
17085
- } catch (error48) {
17086
- if (!(error48 instanceof DispatchServiceError) || error48.code !== "POSSIBLE_DUPLICATE") {
17087
- throw error48;
17088
- }
17089
- const candidates = duplicateCandidates(error48);
17090
- return {
17091
- text: [
17092
- `Not created: "${title}" looks like a duplicate.`,
17093
- ...candidates.map((candidate) => {
17094
- const href = new URL(candidate.href, configUrl).toString();
17095
- return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
17096
- }),
17097
- "Reference the existing issue, or call dispatch_issue again with force: true after reading it."
17098
- ].join(`
17099
- `),
17100
- details: { duplicates: candidates }
17101
- };
17102
- }
17103
- }
17104
- case "dispatch_issue_update": {
17105
- const issueKey = issue2();
17106
- const status = optionalString(args, "status");
17107
- const reason = optionalString(args, "reason");
17108
- const title = optionalString(args, "title");
17109
- const route = optionalString(args, "route");
17110
- const parent = optionalString(args, "parent");
17111
- const components = optionalComponents(args, "components");
17112
- const priority = optionalPriority(args, "priority");
17113
- const labels = Array.isArray(args.labels) ? args.labels : undefined;
17114
- const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
17115
- let before;
17116
- try {
17117
- before = await client.getIssue(issueKey);
17118
- } catch (error48) {
17119
- throw refusalWithCode(error48);
17120
- }
17121
- let closingNote;
17122
- if (reason !== undefined) {
17123
- try {
17124
- const message = await client.message(issueKey, { body: reason, actor });
17125
- closingNote = {
17126
- id: message.id,
17127
- ref: dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id)
17128
- };
17129
- } catch (error48) {
17130
- 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";
17131
- if (error48 instanceof DispatchServiceError)
17132
- throw refusalWithCode(error48, told);
17133
- throw withAccount(error48, told);
17134
- }
17135
- }
17136
- const linked = before.external_links.map((link) => link.url);
17137
- const newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
17138
- let after;
17139
- try {
17140
- after = await client.updateIssue(issueKey, {
17141
- ...status === undefined ? {} : { status },
17142
- ...title === undefined ? {} : { title },
17143
- ...labels === undefined ? {} : { labels },
17144
- ...priority === undefined ? {} : { priority },
17145
- ...route === undefined ? {} : { route },
17146
- ...parent === undefined ? {} : { parent: parent === "" ? null : parent },
17147
- ...components === undefined ? {} : { components },
17148
- ...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url2) => ({ url: url2 }))] },
17149
- actor
17150
- });
17151
- } catch (error48) {
17152
- 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)` : "";
17153
- if (closingNote === undefined)
17154
- throw refusalWithCode(error48, taken);
17155
- const posted = `the reason already landed as message ${closingNote.id} (${closingNote.ref})`;
17156
- const fix = error48 instanceof DispatchGatewayError && error48.transient ? "" : "fix what refused the close, then ";
17157
- 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}`;
17158
- if (error48 instanceof DispatchServiceError)
17159
- throw refusalWithCode(error48, taken, landed);
17160
- throw withAccount(error48, landed);
17161
- }
17162
- const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
17163
- const changes = [
17164
- ...closingNote === undefined ? [] : [`reason posted as message ${closingNote.id} (${closingNote.ref})`],
17165
- ...status === undefined ? [] : [`status ${before.status} -> ${after.status}`],
17166
- ...title === undefined ? [] : [`title "${after.title}"`],
17167
- ...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
17168
- ...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
17169
- ...requestedLinks === undefined ? [] : [
17170
- newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
17171
- ],
17172
- ...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
17173
- ...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
17174
- ...components === undefined ? [] : [componentsChange(components, after.components)]
17175
- ];
17176
- const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
17177
- setsStatus: status !== undefined
17178
- });
17179
- return {
17180
- text: [
17181
- `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
17182
- ...adviceLines
17183
- ].join(`
17184
- `),
17185
- details: {
17186
- issue: after.key,
17187
- status: after.status,
17188
- external_links: after.external_links.map((link) => link.url),
17189
- ...closingNote === undefined ? {} : { message: closingNote.id },
17190
- ...after.advice === undefined ? {} : { advice: after.advice }
17191
- }
17192
- };
17193
- }
17194
- case "dispatch_claim": {
17195
- const issueKey = issue2();
17196
- const release = optionalBoolean(args, "release") ?? false;
17197
- let after;
17198
- try {
17199
- after = release ? await client.releaseIssueClaim(issueKey, { actor }) : await client.claimIssue(issueKey, { actor });
17200
- } catch (error48) {
17201
- throw refusalWithCode(error48);
17202
- }
17203
- const held = after.claim;
17204
- let text;
17205
- if (release) {
17206
- 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.`;
17207
- } else {
17208
- if (held === null) {
17209
- throw new Error(`Dispatch claimed ${issueKey} but answered with no claim`);
17210
- }
17211
- 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.`;
17212
- }
17213
- return {
17214
- text: [text, notSubscribed(issueTopic(issueKey))].join(`
17215
- `),
17216
- details: { issue: issueKey, status: after.status, claim: held }
17217
- };
17218
- }
17219
- case "dispatch_search": {
17220
- const query = stringArg(args, "query");
17221
- const project = optionalString(args, "project");
17222
- const limit = optionalNumber(args, "limit");
17223
- const offset = optionalNumber(args, "offset");
17224
- const search = await client.search(query, {
17225
- ...project === undefined ? {} : { project },
17226
- ...limit === undefined ? {} : { limit },
17227
- ...offset === undefined ? {} : { offset }
17228
- });
17229
- return searchAnswer(search, query, offset, configUrl);
17230
- }
17231
- case "dispatch_issues": {
17232
- const project = stringArg(args, "project");
17233
- const status = optionalString(args, "status");
17234
- const parent = optionalString(args, "parent");
17235
- const label = optionalString(args, "label");
17236
- const priority = optionalPriorityFilter(args, "priority");
17237
- const updatedSince = optionalString(args, "updated_since");
17238
- const routeStatus = optionalString(args, "route_status");
17239
- const page = await client.listIssuePage({
17240
- project,
17241
- ...status === undefined ? {} : { status },
17242
- ...parent === undefined ? {} : { parent },
17243
- ...label === undefined ? {} : { label },
17244
- ...priority === undefined ? {} : { priority },
17245
- ...updatedSince === undefined ? {} : { updated_since: updatedSince },
17246
- ...routeStatus === undefined ? {} : { route_status: routeStatus }
17247
- }, {
17248
- limit: optionalNumber(args, "limit") ?? DEFAULT_ISSUE_PAGE_LIMIT,
17249
- offset: optionalNumber(args, "offset") ?? 0
17250
- });
17251
- const { total, limit, offset } = page;
17252
- const rows = page.issues.map((row) => ({
17253
- key: row.key,
17254
- title: row.title,
17255
- status: row.status,
17256
- priority: row.priority,
17257
- parent: row.parent,
17258
- labels: row.labels ?? [],
17259
- open_asks: row.open_asks,
17260
- claim: row.claim ?? null,
17261
- route: row.route ?? null,
17262
- route_status: row.route_status ?? null,
17263
- route_holder: row.route_holder ?? null,
17264
- updated_at: row.updated_at,
17265
- progress: row.progress
17266
- }));
17267
- const titles = await liveSessionTitles(client, rows.some((row) => holdsSession(row.claim)));
17268
- const isPartial = offset !== 0 || rows.length !== total;
17269
- const showing = !isPartial ? "" : rows.length === 0 ? `showing 0-0 of ${total}` : pageSummaryText(offset, rows.length, total);
17270
- return {
17271
- text: rows.length === 0 ? `No issues in ${project}.${isPartial ? ` (${showing})` : ""}` : [
17272
- `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (isPartial ? ` (${showing})` : ""),
17273
- ...rows.map((row) => {
17274
- const progress = progressText(row.progress, " \xB7 ");
17275
- 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)}`);
17276
- })
17277
- ].join(`
17278
- `),
17279
- details: { issues: rows, total, offset, limit }
17280
- };
17281
- }
17282
- case "dispatch_architecture_sync": {
17283
- const project = stringArg(args, "project");
17284
- const source = await client.syncArchitectureSource(project);
17285
- const at = source.last_sync_at ?? "unknown time";
17286
- return {
17287
- text: source.last_error === null ? `Synced ${project} architecture from ${source.repo}@${source.branch}: commit ${source.last_commit ?? "unknown"} (${at}).` : [
17288
- `Sync failed for ${project} (${source.repo}@${source.branch}): ${source.last_error}`,
17289
- source.last_commit === null ? "No model has ever imported for this project." : `The previous model stays up (commit ${source.last_commit}).`
17290
- ].join(`
17291
- `),
17292
- details: {
17293
- project,
17294
- repo: source.repo,
17295
- branch: source.branch,
17296
- commit: source.last_commit,
17297
- error: source.last_error
17298
- }
17299
- };
17300
- }
17301
- case "dispatch_resolve_ask": {
17302
- const kind = stringArg(args, "kind");
17303
- const id = await resolveAskArgument(input.tool, args, client, input.sessionId);
17304
- const ask = await client.resolveAsk(id, {
17305
- kind,
17306
- reason: stringArg(args, "reason"),
17307
- actor
17308
- });
17309
- if (ask.resolution === undefined)
17310
- throw new Error("resolved ask is missing its resolution");
17311
- return {
17312
- text: `${kind === "retracted" ? "Retracted" : "Resolved"} ask ${ask.id}: ${ask.resolution.reason}`,
17313
- details: await askOwnerDetails(client, ask)
17314
- };
17315
- }
17316
- case "dispatch_resolve_comment": {
17317
- const reference = stringArg(args, "comment");
17318
- const ref = reference.startsWith("dispatch://") ? parseDispatchRef(reference) : null;
17319
- let id = reference;
17320
- let document;
17321
- if (ref?.owner.kind === "issue") {
17322
- const issueKey = ref.owner.issue;
17323
- id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), () => client.getComments(issueKey));
17324
- } else if (ref !== null) {
17325
- const artifact = (await resolveArtifact(client, ref.owner, ref.artifact, { canonical: true })).artifact;
17326
- document = artifact;
17327
- id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), () => client.getArtifactComments(artifact.id));
17328
- }
17329
- const comment = await client.resolveComment(id, actor);
17330
- const { label, details } = await commentOwnerResult(client, comment, document);
17331
- return { text: `Resolved comment ${comment.id} on ${label}.`, details };
17332
- }
17333
- case "dispatch_ask": {
17334
- const anchorArgs = asObject(args.anchor);
17335
- const owner2 = documentOwner();
17336
- const artifactReference = optionalString(args, "artifact") ?? (anchorArgs === null ? undefined : optionalString(anchorArgs, "artifact"));
17337
- const resolved = owner2.kind === "project" || artifactReference === undefined ? owner2.kind === "project" ? await resolveDocument(owner2, artifactReference) : undefined : await resolveDocument(owner2, artifactReference);
17338
- const options = args.options;
17339
- const multiple = optionalBoolean(args, "multiple");
17340
- const urgency = askUrgency(args);
17341
- const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
17342
- const withRef = askQuestionWithRef(args);
17343
- const refAppended = withRef !== stringArg(args, "question");
17344
- const question = await writePictures(resolved, withRef, {
17345
- cap: ASK_QUESTION_MAX,
17346
- text: refAppended ? "question plus ref" : "question",
17347
- shorten: refAppended ? "shorten the question, drop the ref" : "shorten the question"
17348
- });
17349
- const askInput = {
17350
- question,
17351
- ...Array.isArray(options) ? { options } : {},
17352
- ...multiple === undefined ? {} : { multiple },
17353
- ...urgency === undefined ? {} : { urgency },
17354
- ...anchored === undefined ? {} : { anchor: anchored },
17355
- actor
17356
- };
17357
- const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue2(), askInput);
17358
- const askOwner = ask.issue_key !== null ? issueTopic(ask.issue_key) : resolved === undefined ? issueTopic(issue2()) : documentTopic(resolved.artifact);
17359
- const adviceLines = [
17360
- ...renderAdvice(input.tool, askOwner.label, ask.advice, {}),
17361
- ...renderSuggestions(ask.advice?.suggestions, configUrl)
17362
- ];
17363
- return {
17364
- text: [
17365
- `Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
17366
- ${followsAsk(askOwner)}`,
17367
- ...adviceLines
17368
- ].join(`
17369
- `),
17370
- details: {
17371
- ...await followedAskDetails(client, ask, resolved?.artifact),
17372
- ...ask.advice === undefined ? {} : { advice: ask.advice }
17373
- }
17374
- };
17375
- }
17376
- case "dispatch_edit_ask": {
17377
- const question = optionalString(args, "question");
17378
- const options = args.options;
17379
- const multiple = optionalBoolean(args, "multiple");
17380
- const urgency = askUrgency(args);
17381
- const id = await resolveAskArgument(input.tool, args, client, input.sessionId);
17382
- const ask = await client.editAsk(id, {
17383
- ...question === undefined ? {} : { question },
17384
- ...Array.isArray(options) ? { options } : {},
17385
- ...multiple === undefined ? {} : { multiple },
17386
- ...urgency === undefined ? {} : { urgency },
17387
- actor
17388
- });
17389
- return {
17390
- text: `Ask edited: ${ask.question}`,
17391
- details: await askOwnerDetails(client, ask)
17392
- };
17393
- }
17394
- case "dispatch_comment": {
17395
- const artifactReference = optionalString(args, "artifact");
17396
- const owner2 = documentOwner();
17397
- const resolved = owner2.kind === "project" || artifactReference === undefined ? owner2.kind === "project" ? await resolveDocument(owner2, artifactReference) : undefined : await resolveDocument(owner2, artifactReference);
17398
- const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
17399
- const replyTo = optionalString(args, "reply_to");
17400
- const replyToAskReference = optionalString(args, "reply_to_ask");
17401
- const replyToAsk = replyToAskReference === undefined ? undefined : normalizeUUID(replyToAskReference);
17402
- if (replyToAskReference !== undefined && replyToAsk === undefined) {
17403
- throw new ToolInputError(input.tool, [
17404
- await invalidReplyToAskProblem(client, owner2, resolved)
17405
- ]);
17406
- }
17407
- const requestedTurn = optionalString(args, "turn");
17408
- const body = await writePictures(resolved, stringArg(args, "body"), {
17409
- cap: DISPATCH_BODY_MAX,
17410
- text: "body",
17411
- shorten: "shorten the body"
17412
- });
17413
- const commentInput = {
17414
- body,
17415
- ...anchored === undefined ? {} : { anchor: anchored },
17416
- ...replyTo === undefined ? {} : { reply_to: replyTo },
17417
- ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
17418
- ...requestedTurn === undefined ? {} : { turn: requestedTurn },
17419
- actor
17420
- };
17421
- const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue2(), commentInput);
17422
- const commentOwner2 = resolved === undefined ? issueTopic(issue2()) : resolvedTopic(resolved);
17423
- const commentDetails = resolved === undefined ? { issue: comment.issue_key, comment: comment.id } : writeResultDetails(resolved, { comment: comment.id });
17424
- const adviceLines = renderAdvice(input.tool, commentOwner2.label, comment.advice, {
17425
- isAskReply: replyToAsk !== undefined,
17426
- replyToOwnAsk: replyToAsk !== undefined && (comment.advice?.your_open_asks?.some((ask) => ask.id === replyToAsk) ?? false)
17427
- });
17428
- if (replyToAsk !== undefined) {
17429
- const askState = comment.ask_waiting_on === undefined ? "" : `; ask now waiting on ${comment.ask_waiting_on}`;
17430
- return {
17431
- text: [
17432
- `Replied on ask ${replyToAsk} (comment ${comment.id}${askState}). ${followsAsk(commentOwner2)}`,
17433
- ...adviceLines
17434
- ].join(`
17435
- `),
17436
- details: {
17437
- ...commentDetails,
17438
- ask: replyToAsk,
17439
- follows: { ask: replyToAsk },
17440
- ...comment.ask_waiting_on === undefined ? {} : { ask_waiting_on: comment.ask_waiting_on },
17441
- ...comment.advice === undefined ? {} : { advice: comment.advice }
17442
- }
17443
- };
17444
- }
17445
- return {
17446
- text: [`Posted comment ${comment.id} ${notSubscribed(commentOwner2)}`, ...adviceLines].join(`
17447
- `),
17448
- details: {
17449
- ...commentDetails,
17450
- ...comment.advice === undefined ? {} : { advice: comment.advice }
17451
- }
17452
- };
17453
- }
17454
- case "dispatch_suggest": {
17455
- const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
17456
- const anchored = anchor(resolved.artifact, args);
17457
- if (anchored === undefined)
17458
- throw new Error("quote is required");
17459
- const body = optionalString(args, "body");
17460
- const suggestionInput = {
17461
- ...body === undefined ? {} : { body },
17462
- anchor: anchored,
17463
- replace_with: stringArg(args, "replace_with"),
17464
- actor
17465
- };
17466
- const comment = resolved.owner.kind === "project" ? await client.artifactSuggest(resolved.artifact.id, suggestionInput) : await client.suggest(issue2(), suggestionInput);
17467
- return {
17468
- text: `Posted suggestion ${comment.id} ${notSubscribed(resolvedTopic(resolved))}`,
17469
- details: writeResultDetails(resolved, { comment: comment.id })
17470
- };
17471
- }
17472
- case "dispatch_message": {
17473
- const inReplyTo = messageInReplyTo(args);
17474
- const written = stringArg(args, "body");
17475
- const bodyLimit = {
17476
- cap: DISPATCH_BODY_MAX,
17477
- text: "body",
17478
- shorten: "shorten the body"
17479
- };
17480
- if (owner === null && inReplyTo !== undefined) {
17481
- let body2 = written;
17482
- if (pictures.length > 0) {
17483
- const sessionId = input.sessionId?.trim();
17484
- if (!sessionId) {
17485
- throw new Error("host session id is required to send pictures in a direct-message reply");
17486
- }
17487
- body2 = await postWithPictures(written, bodyLimit, `agent/${sessionId}`, (upload) => client.agentArtifact(sessionId, upload));
17488
- }
17489
- const reply = await client.messageReply(inReplyTo, { body: body2, attempt: 1, actor }, { followUp: true });
17490
- if (reply.duplicate === true && reply.body === body2) {
17491
- return {
17492
- 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.",
17493
- details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
17494
- };
17495
- }
17496
- if (reply.body !== body2) {
17497
- return {
17498
- 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.",
17499
- details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
17500
- };
17501
- }
17502
- const readBack = `dispatch_read({message: "${inReplyTo}"}) reads the conversation back.`;
17503
- const parent = reply.in_reply_to ?? undefined;
17504
- const follows = parent === inReplyTo ? undefined : parent;
17505
- return {
17506
- 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}`,
17507
- details: {
17508
- message: reply.id,
17509
- in_reply_to: inReplyTo,
17510
- posted: true,
17511
- ...follows === undefined ? {} : { follows }
17512
- }
17513
- };
17514
- }
17515
- const issueKey = issue2();
17516
- const body = await postWithPictures(written, bodyLimit, issueKey, (upload) => client.artifact(issueKey, upload));
17517
- const message = await client.message(issueKey, {
17518
- body,
17519
- ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
17520
- actor
17521
- });
17522
- const messageRef = dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id);
17523
- const adviceLines = renderAdvice(input.tool, issueKey, message.advice, {});
17524
- return {
17525
- text: [
17526
- `Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
17527
- ...adviceLines
17528
- ].join(`
17529
- `),
17530
- details: {
17531
- issue: issueKey,
17532
- message: message.id,
17533
- ...message.advice === undefined ? {} : { advice: message.advice }
17534
- }
17535
- };
17536
- }
17537
- case "dispatch_doc_edit": {
17538
- const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
17539
- const ops = args.ops;
17540
- const summary = optionalString(args, "summary");
17541
- const { precondition: rawPrecondition } = args;
17542
- const precondition = rawPrecondition;
17543
- await refuseRemovingOpenDecisionBlocks(client, input.tool, resolved, ops);
17544
- const edited = await client.docEdit(resolved.artifact.id, {
17545
- ops,
17546
- ...summary === undefined ? {} : { summary },
17547
- ...precondition === undefined ? {} : { precondition },
17548
- actor
17549
- });
17550
- const retyped = ops.filter((operation) => operation.op === "retype").length;
17551
- const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
17552
- const retypedText = retyped === 0 ? "" : `; retyped ${retyped} block${retyped === 1 ? "" : "s"}`;
17553
- const nothingChanged = edited.changed === false;
17554
- const head = nothingChanged ? `Applied ${edited.applied} ops${retypedText}; nothing changed (${versionText})` : `Applied ${edited.applied} ops${retypedText} (${versionText})`;
17555
- const unchangedOps = edited.unchanged_ops ?? [];
17556
- const unchangedText = unchangedOps.length === 0 ? "" : `; ${unchangedOps.length === 1 ? "operation" : "operations"} ${unchangedOps.join(", ")} changed nothing`;
17557
- const lostOps = edited.lost_ops;
17558
- 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`;
17559
- const applied = `${head}${unchangedText}${lostText}`;
17560
- const adviceLines = renderAdvice(input.tool, resolvedTopic(resolved).label, edited.advice, {});
17561
- const tokenTrailer = edited.token === undefined ? [] : [`Document token: ${edited.token}`];
17562
- return {
17563
- text: [
17564
- `${applied} ${notSubscribed(resolvedTopic(resolved))}`,
17565
- ...tokenTrailer,
17566
- ...adviceLines
17567
- ].join(`
17568
- `),
17569
- details: writeResultDetails(resolved, {
17570
- applied: edited.applied,
17571
- ...edited.version === null ? {} : { version: edited.version.number },
17572
- ...nothingChanged ? { changed: false } : {},
17573
- ...lostOps === undefined ? {} : { lost_ops: lostOps },
17574
- ...edited.token === undefined ? {} : { token: edited.token },
17575
- ...edited.advice === undefined ? {} : { advice: edited.advice }
17576
- })
17577
- };
17578
- }
17579
- case "dispatch_doc_read": {
17580
- const { agentArtifact } = ownerArguments;
17581
- if (agentArtifact !== undefined) {
17582
- const artifact = await client.getAgentArtifact(agentArtifact.session, agentArtifact.slug);
17583
- return readUploadedFile(client, artifact, { session: agentArtifact.session, artifact: artifact.slug }, optionalNumber(args, "version") ?? agentArtifact.version, `agent/${agentArtifact.session}`, shown);
17584
- }
17585
- const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
17586
- const resolved = await resolveDocument(documentOwner(), artifactReference);
17587
- const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
17588
- if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
17589
- const { artifact } = resolved;
17590
- return readUploadedFile(client, artifact, resolved.owner.kind === "project" ? {
17591
- project: artifact.project,
17592
- document: documentLabel(artifact.project, artifact.slug)
17593
- } : { issue: resolved.issue?.key }, version2, resolved.owner.kind === "project" ? resolved.owner.project : resolved.owner.issue, shown);
17594
- }
17595
- const documentPromise = client.docRead(resolved.artifact.id, version2);
17596
- const marksPromise = openArtifactMarks(client, resolved);
17597
- const marksResultPromise = marksPromise.then((value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }));
17598
- const document = await documentPromise;
17599
- const marksResult = await marksResultPromise;
17600
- if (marksResult.status === "rejected")
17601
- throw marksResult.reason;
17602
- const marks = marksResult.value;
17603
- const approval = approvalLine(resolved.artifact);
17604
- const trailer = [
17605
- ..."token" in document && document.token !== undefined ? [`Document token: ${document.token}`] : [],
17606
- ...marks.length === 0 ? [] : [`Open anchored asks/comments: ${marks.join(", ")}`],
17607
- ...approval === undefined ? [] : [approval]
17608
- ];
17609
- return {
17610
- text: trailer.length === 0 ? document.markdown : `${document.markdown}
17611
-
17612
- ${trailer.join(`
17613
- `)}`,
17614
- details: resolved.owner.kind === "project" ? {
17615
- project: resolved.artifact.project,
17616
- document: `${resolved.artifact.project}/${resolved.artifact.slug}`
17617
- } : { issue: resolved.issue?.key }
17618
- };
17619
- }
17620
- case "dispatch_request_approval": {
17621
- const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
17622
- const resolved = await resolveDocument(documentOwner(), artifactReference);
17623
- await refuseOpenDecisionBlocks(client, input.tool, resolved);
17624
- const result = await client.requestApproval(resolved.artifact.id, {
17625
- actor,
17626
- summary: stringArg(args, "summary")
17627
- });
17628
- if (result.ask === null) {
17629
- return {
17630
- 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.`,
17631
- details: {
17632
- ...resolved.owner.kind === "project" ? documentResultDetails(resolved.artifact) : { issue: resolved.issue?.key },
17633
- artifact: resolved.artifact.id,
17634
- version: result.version
17635
- }
17636
- };
17637
- }
17638
- const details = await followedAskDetails(client, result.ask, resolved.artifact);
17639
- 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.`;
17640
- return {
17641
- 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 ? `
17642
- ${SPEC_CHECK_REMINDER}` : ""}`,
17643
- details: { ...details, artifact: resolved.artifact.id, version: result.version }
17644
- };
17645
- }
17646
- case "dispatch_artifact": {
17647
- const summary = optionalString(args, "summary");
17648
- const name = stringArg(args, "name");
17649
- const content = optionalString(args, "content");
17650
- const artifactInput = content === undefined ? {
17651
- name,
17652
- file: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))),
17653
- ...summary === undefined ? {} : { summary },
17654
- actor
17655
- } : {
17656
- name,
17657
- content,
17658
- ...summary === undefined ? {} : { summary },
17659
- actor
17660
- };
17661
- const artifactOwner = documentOwner();
17662
- const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue2(), artifactInput);
17663
- const artifactRef = dispatchDocumentRef(artifactOwner.kind === "project" ? artifactOwner.project : issue2(), result.artifact.slug);
17664
- const uploadOwner = artifactOwner.kind === "project" ? documentTopic(result.artifact) : issueTopic(issue2());
17665
- const adviceLines = renderAdvice(input.tool, uploadOwner.label, result.advice, {
17666
- isPrimarySpec: result.artifact.primary || result.artifact.name === "spec.md"
17667
- });
17668
- return {
17669
- text: [
17670
- `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef}) ${notSubscribed(uploadOwner)}`,
17671
- ...adviceLines
17672
- ].join(`
17673
- `),
17674
- details: artifactOwner.kind === "project" ? {
17675
- ...documentResultDetails(result.artifact),
17676
- version: result.version.number,
17677
- ...result.advice === undefined ? {} : { advice: result.advice }
17678
- } : {
17679
- issue: issue2(),
17680
- artifact: result.artifact.id,
17681
- version: result.version.number,
17682
- ...result.advice === undefined ? {} : { advice: result.advice }
17683
- }
17684
- };
17685
- }
17686
- case "dispatch_follow": {
17687
- const sessionId = input.sessionId?.trim();
17688
- if (!sessionId)
17689
- throw new Error("host session id is required for dispatch_follow");
17690
- const ask = await resolveAskArgument(input.tool, args, client, sessionId);
17691
- const action = stringArg(args, "action");
17692
- if (action === "unfollow") {
17693
- await client.unfollowAsk(ask, sessionId, actor);
17694
- return { text: `Unfollowed ask ${ask}.`, details: { ask } };
17695
- }
17696
- const read = await client.getAsk(ask);
17697
- await client.followAsk(ask, sessionId, actor);
17698
- return {
17699
- text: `Following ask ${ask}: its answer and replies reach this session directly.`,
17700
- details: await followedAskDetails(client, read.ask)
17701
- };
17702
- }
17703
- case "dispatch_read": {
17704
- const message = optionalString(args, "message");
17705
- const picturesOf = (texts) => readPictures(client, picturesNewestFirst(texts), shown);
17706
- const showImages = (pictures3) => {
17707
- for (const address of pictures3.shown)
17708
- shown?.add(address);
17709
- return pictures3.images.length === 0 ? {} : { images: pictures3.images };
17710
- };
17711
- if (message !== undefined) {
17712
- const sessionId = input.sessionId?.trim();
17713
- if (!sessionId)
17714
- throw new Error("host session id is required for dispatch_read({message})");
17715
- const thread = await client.getMessageThread(messageIdOf(message), sessionId);
17716
- const issueKey2 = thread.message.issue_key;
17717
- const [pictures3, graph2] = await Promise.all([
17718
- picturesOf(datedBodies([thread.message, ...thread.replies])),
17719
- issueKey2 === null ? [] : graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))
17720
- ]);
17721
- return {
17722
- text: messageSummary(thread, [...pictures3.lines, ...graph2]),
17723
- details: {
17724
- message: thread.message.id,
17725
- ...issueKey2 === null ? {} : { issue: issueKey2 }
17726
- },
17727
- ...showImages(pictures3)
17728
- };
17729
- }
17730
- if (ownerArguments.ref?.kind === "ask") {
17731
- const ref = ownerArguments.ref;
17732
- 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"));
17733
- const askRead = await client.getAsk(id);
17734
- const askRef = refTarget(ref, "ask", id);
17735
- const [pictures3, graph2] = await Promise.all([
17736
- picturesOf(askTexts(askRead)),
17737
- graphSections(client, askRef)
17738
- ]);
17739
- return {
17740
- text: askSummary(askRead, [...pictures3.lines, ...graph2]),
17741
- details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue },
17742
- ...showImages(pictures3)
17743
- };
17744
- }
17745
- if (ownerArguments.ref?.kind === "comment") {
17746
- const ref = ownerArguments.ref;
17747
- 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));
17748
- const comment = await client.getComment(id);
17749
- const commentRef = refTarget(ref, "comment", id);
17750
- const [pictures3, graph2] = await Promise.all([
17751
- picturesOf(datedBodies([comment.comment, ...comment.replies])),
17752
- graphSections(client, commentRef)
17753
- ]);
17754
- return {
17755
- text: commentSummary(comment, [...pictures3.lines, ...graph2]),
17756
- details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key },
17757
- ...showImages(pictures3)
17758
- };
17759
- }
17760
- if (ownerArguments.ref?.kind === "message") {
17761
- if (ownerArguments.ref.owner.kind !== "issue") {
17762
- throw new Error("message references are issue-scoped");
17763
- }
17764
- const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
17765
- const messageRef = dispatchChildRef(dispatchIssueRef(ownerArguments.ref.owner.issue), "message", ownerArguments.ref.id);
17766
- const [pictures3, graph2] = await Promise.all([
17767
- picturesOf(datedBodies([messageRead.message, ...messageRead.replies])),
17768
- graphSections(client, messageRef)
17769
- ]);
17770
- return {
17771
- text: messageSummary(messageRead, [...pictures3.lines, ...graph2]),
17772
- details: { issue: messageRead.message.issue_key },
17773
- ...showImages(pictures3)
17774
- };
17775
- }
17776
- if (documentOwner().kind === "project") {
17777
- const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
17778
- const documentRef = dispatchDocumentRef(resolved.artifact.project, resolved.artifact.slug);
17779
- return {
17780
- text: [
17781
- `Document: ${resolved.artifact.project} / ${resolved.artifact.name}`,
17782
- `Reference: ${documentRef}`,
17783
- `Versions: ${resolved.artifact.versions.length}`,
17784
- ...approvalLine(resolved.artifact) === undefined ? [] : [approvalLine(resolved.artifact)],
17785
- ...await graphSections(client, documentRef)
17786
- ].join(`
17787
- `),
17788
- details: {
17789
- project: resolved.artifact.project,
17790
- document: documentLabel(resolved.artifact.project, resolved.artifact.slug)
17791
- }
17792
- };
17793
- }
17794
- const issueKey = issue2();
17795
- if (ownerArguments.ref?.kind === "log") {
17796
- const read2 = await client.read(issueKey);
17797
- const pictures3 = await picturesOf(read2.events.flatMap(eventTexts));
17798
- return {
17799
- text: [logSummary(read2.issue, read2.events), ...pictures3.lines].join(`
17800
- `),
17801
- details: { issue: read2.issue.key },
17802
- ...showImages(pictures3)
17803
- };
17804
- }
17805
- if (ownerArguments.ref?.kind === "children") {
17806
- const read2 = await client.read(issueKey);
17807
- return {
17808
- text: childrenSummary(read2.issue),
17809
- details: { issue: read2.issue.key }
17810
- };
17811
- }
17812
- const readPromise = client.read(issueKey);
17813
- const referencesPromise = issueReferencesOrUnavailable(client, issueKey);
17814
- const read = await readPromise;
17815
- const [references, graph, titles, pictures2] = await Promise.all([
17816
- referencesPromise,
17817
- graphSections(client, dispatchIssueRef(read.issue.key)),
17818
- liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue)),
17819
- picturesOf([
17820
- ...read.issue.open_asks.map((ask) => ({ text: ask.question, at: ask.created_at })),
17821
- ...read.events.flatMap(eventTexts)
17822
- ])
17823
- ]);
17824
- return {
17825
- text: issueSummary(read.issue, read.events, references, [...pictures2.lines, ...graph], titles),
17826
- details: { issue: read.issue.key },
17827
- ...showImages(pictures2)
17828
- };
17829
- }
17830
- default:
17831
- throw new Error(`Unknown Dispatch tool: ${input.tool}`);
17832
- }
17833
- }
17834
- async function resolveExistingIssue(client, issueReference) {
17835
- try {
17836
- return await client.resolveIssue(issueReference);
17837
- } catch (error48) {
17838
- if (dispatchAnswered(error48, 404)) {
17839
- throw new Error(`no Dispatch issue is linked to ${issueReference}; create it first with ` + `dispatch_issue({ external: "${issueReference}", ... })`);
17840
- }
17841
- throw error48;
17842
- }
17843
- }
17844
- function asObject(value) {
17845
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
17846
- }
17847
-
17848
- // ../envoy-client/src/dispatch-first.ts
17849
- import { join as join2 } from "path";
17850
- function dispatchFirstSkillFile(skillsDirectory) {
17851
- return join2(skillsDirectory, "dispatch-first", "SKILL.md");
17852
- }
17853
-
17854
- // ../envoy-client/src/tool-contract.ts
17855
- var DELIVERY_CONTRACT = "Delivery is at-least-once, possibly out of order across topics; use id for dedupe and at for freshness.";
17856
- var TOPIC_GUIDE = "Topic guide (all are under notifications.): agent.<session_id> (subscribe: own inbox); role.<role> " + "(publish-to; holders claim via envoy_role_set). > matches one or more trailing tokens and does not " + "match the base subject. Envoy registers the concrete base when you subscribe to <subject>.>, so " + "the recommended default remains github.<owner>.<repo>.pr.<n>.>. <owner> and <repo> are one token " + "each, with every dot in the name written _ (sjawhar/.github is github.sjawhar._github; " + "acme/site.io is github.acme.site_io), and a topic spelled with the dot receives nothing. " + "Default PR subscription: " + "github.<owner>.<repo>.pr.<n>.> (it receives the quiet PR family): pr.<n> (lifecycle: " + "opened/synchronize/closed; closed carries merged, merge_commit_sha, merged_by, head_sha), " + "pr.<n>.comment, pr.<n>.review, pr.<n>.mention, pr.<n>.checks (one settlement event per commit whose checks settle, the head or not, naming its sha: " + 'passed/failed/cancelled/skipped with failing names and URLs; re-fires with superseded_settlement: "true" ' + "when new runs appear for the same commit). Other GitHub: issue.<n>, issue.<n>.comment, " + "issue.<n>.mention, mention, push.branch.<name>, push.tag.<name>, workflow.<file>.<action> (only " + "runs without an associated PR); slack.<team>.<channel>.message|mention and " + "slack.<team>.<channel>.thread.<ts>.message|mention; ghostwispr.<session>.<kind>; " + "whatsapp.<phone>.<jid>.<kind>; envoy.exceptions.<original-topic>.";
17857
- var URGENCY_VALUES = EnvelopeSchema.shape.urgency.unwrap().options;
17858
- var EXPECTS_REPLY_VALUES = EnvelopeSchema.shape.expects_reply.unwrap().options;
17859
- function messageMetadataShape(schema) {
17860
- return {
17861
- in_reply_to: schema.string().optional(),
17862
- supersedes: schema.string().optional(),
17863
- urgency: schema.enum(URGENCY_VALUES).describe("low | med | high | blocking").optional(),
17864
- expects_reply: schema.enum(EXPECTS_REPLY_VALUES).describe("none | optional | required").optional(),
17865
- expires_at: schema.number({ int: true }).optional()
17866
- };
17867
- }
17868
- var MessageMetadataSchema = exports_external.object(messageMetadataShape(zodSchemaApi(exports_external)));
17869
- function toMessageMetadata(args) {
17870
- return {
17871
- ...args.in_reply_to === undefined ? {} : { inReplyTo: args.in_reply_to },
17872
- ...args.supersedes === undefined ? {} : { supersedes: args.supersedes },
17873
- ...args.urgency === undefined ? {} : { urgency: args.urgency },
17874
- ...args.expects_reply === undefined ? {} : { expectsReply: args.expects_reply },
17875
- ...args.expires_at === undefined ? {} : { expiresAt: args.expires_at }
17876
- };
17877
- }
17878
- function messageArguments(schema) {
17879
- return { message: schema.string(), ...messageMetadataShape(schema) };
17880
- }
17881
- var EnvoyToolOperation = {
17882
- subscribe: "subscribe",
17883
- unsubscribe: "unsubscribe",
17884
- listInterests: "listInterests",
17885
- inbox: "inbox",
17886
- send: "send",
17887
- publish: "publish",
17888
- setRole: "setRole",
17889
- getRole: "getRole",
17890
- whoami: "whoami",
17891
- listSessions: "listSessions"
17892
- };
17893
- var envoyToolSpecs = [
17894
- {
17895
- name: "envoy_subscribe",
17896
- description: `Subscribe this session to Envoy notification topics. ${TOPIC_GUIDE}`,
17897
- arguments: (schema) => ({
17898
- topics: schema.array(schema.string()).describe("NATS-style topic patterns to subscribe to.")
17899
- }),
17900
- operation: EnvoyToolOperation.subscribe,
17901
- requiresSubscriptionCapability: true
17902
- },
17903
- {
17904
- name: "envoy_unsubscribe",
17905
- description: "Unsubscribe this session from Envoy topics, or remove all current subscriptions if topics are omitted.",
17906
- arguments: (schema) => ({ topics: schema.array(schema.string()).optional() }),
17907
- operation: EnvoyToolOperation.unsubscribe,
17908
- requiresSubscriptionCapability: true
17909
- },
17910
- {
17911
- name: "envoy_list",
17912
- description: "List the current Envoy topic subscriptions for this session so you can confirm the exact topic shapes that are active.",
17913
- arguments: () => ({}),
17914
- operation: EnvoyToolOperation.listInterests,
17915
- requiresSubscriptionCapability: false
17916
- },
17917
- {
17918
- name: "envoy_inbox",
17919
- description: "List this Pi session's 50 most recent rendered Envoy deliveries, newest first.",
17920
- arguments: () => ({}),
17921
- operation: EnvoyToolOperation.inbox,
17922
- requiresSubscriptionCapability: false
17923
- },
17924
- {
17925
- name: "envoy_send",
17926
- description: `Send an Envoy agent-to-agent message directly to another session by session ID. ${DELIVERY_CONTRACT}`,
17927
- arguments: (schema) => ({
17928
- session_id: schema.string().describe("Target session ID; find it with envoy_sessions or envoy_whoami."),
17929
- ...messageArguments(schema)
17930
- }),
17931
- operation: EnvoyToolOperation.send,
17932
- requiresSubscriptionCapability: false
17933
- },
17934
- {
17935
- name: "envoy_publish",
17936
- description: `Publish an Envoy message to any topic. ${DELIVERY_CONTRACT}`,
17937
- arguments: (schema) => ({ topic: schema.string(), ...messageArguments(schema) }),
17938
- operation: EnvoyToolOperation.publish,
17939
- requiresSubscriptionCapability: false
17940
- },
17941
- {
17942
- name: "envoy_role_set",
17943
- description: "Set the current session as the holder of a named role. Messages published to notifications.role.<role> route to this session.",
17944
- arguments: (schema) => ({ role: schema.string() }),
17945
- operation: EnvoyToolOperation.setRole,
17946
- requiresSubscriptionCapability: false
17947
- },
17948
- {
17949
- name: "envoy_role_get",
17950
- description: "Get the live holder of a named Envoy role.",
17951
- arguments: (schema) => ({ role: schema.string() }),
17952
- operation: EnvoyToolOperation.getRole,
17953
- requiresSubscriptionCapability: false
17954
- },
17955
- {
17956
- name: "envoy_whoami",
17957
- 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.",
17958
- arguments: () => ({}),
17959
- operation: EnvoyToolOperation.whoami,
17960
- requiresSubscriptionCapability: false
17961
- },
17962
- {
17963
- name: "envoy_sessions",
17964
- description: "List all live sessions registered with Envoy. Filter by optional machine, directory, or title.",
17965
- arguments: (schema) => ({
17966
- machine: schema.string().optional(),
17967
- dir: schema.string().optional(),
17968
- title: schema.string().optional()
17969
- }),
17970
- operation: EnvoyToolOperation.listSessions,
17971
- requiresSubscriptionCapability: false
17972
- }
17973
- ];
17974
- var envoyToolSchemas = new Map;
17975
-
17976
- // ../envoy-client/src/transport.ts
17977
- var DEFAULT_TIMEOUT_MS = 5000;
17978
- var RETRY_DELAY_MS = 250;
17979
- var InterestWireSchema = exports_external.object({
17980
- session_id: exports_external.string(),
17981
- machine_id: exports_external.string(),
17982
- dir: exports_external.string(),
17983
- topics: exports_external.array(exports_external.string()),
17984
- updated_at: exports_external.number().int().optional(),
17985
- warnings: exports_external.array(exports_external.string()).optional()
17986
- });
17987
- var SessionWireSchema = exports_external.object({
17988
- session_id: exports_external.string(),
17989
- machine_id: exports_external.string(),
17990
- dir: exports_external.string(),
17991
- port: exports_external.number().int(),
17992
- title: exports_external.string(),
17993
- topics: exports_external.array(exports_external.string()),
17994
- roles: exports_external.array(exports_external.string()).optional(),
17995
- capabilities: exports_external.array(exports_external.string()).optional(),
17996
- self_subscribed: exports_external.boolean(),
17997
- last_seen: exports_external.number().int().optional()
17998
- });
17999
- var RoleWireSchema = exports_external.object({
18000
- role: exports_external.string(),
18001
- holder: exports_external.string(),
18002
- last_seen: exports_external.number().int()
18003
- });
18004
- var RoleHeldWireSchema = exports_external.object({
18005
- error: exports_external.string(),
18006
- role: exports_external.string(),
18007
- holder: exports_external.string()
18008
- });
18009
- var ErrorWireSchema = exports_external.object({
18010
- error: exports_external.string(),
18011
- expected: exports_external.array(exports_external.string()).optional()
18012
- });
18013
- var EnvelopeResponseSchema = EnvelopeSchema.extend({
18014
- recipient: exports_external.string().optional(),
18015
- holder: exports_external.string().optional()
18016
- });
18017
- function expandSubscriptionTopics(topics) {
18018
- const expanded = new Set;
18019
- for (const topic of topics) {
18020
- const base = topic.endsWith(".>") ? topic.slice(0, -2) : undefined;
18021
- if (base === undefined || base.includes("*") || base.includes(">")) {
18022
- expanded.add(topic);
18023
- continue;
18024
- }
18025
- const segments = base.split(".");
18026
- if (segments.length < 2 || segments.some((segment) => segment.length === 0)) {
18027
- throw new TypeError(`cannot expand Envoy wildcard topic "${topic}": its concrete base must have at least two non-empty segments`);
18028
- }
18029
- expanded.add(base);
18030
- expanded.add(topic);
18031
- }
18032
- return [...expanded];
18033
- }
18034
-
18035
- class EnvoyApiError extends Error {
18036
- details;
18037
- name = "EnvoyApiError";
18038
- expected;
18039
- constructor(details) {
18040
- let parsed;
18041
- try {
18042
- const result = ErrorWireSchema.safeParse(JSON.parse(details.responseBody));
18043
- if (result.success)
18044
- parsed = result.data;
18045
- } catch {}
18046
- super(parsed === undefined ? `${details.method} ${details.url} failed with ${details.status}: ${details.responseBody}` : `${parsed.error}${parsed.expected === undefined ? "" : ` (expected: ${parsed.expected.join(", ")})`}`);
18047
- this.details = details;
18048
- this.expected = parsed?.expected;
14696
+ class EnvoyApiError extends Error {
14697
+ details;
14698
+ name = "EnvoyApiError";
14699
+ expected;
14700
+ constructor(details) {
14701
+ let parsed;
14702
+ try {
14703
+ const result = ErrorWireSchema.safeParse(JSON.parse(details.responseBody));
14704
+ if (result.success)
14705
+ parsed = result.data;
14706
+ } catch {}
14707
+ super(parsed === undefined ? `${details.method} ${details.url} failed with ${details.status}: ${details.responseBody}` : `${parsed.error}${parsed.expected === undefined ? "" : ` (expected: ${parsed.expected.join(", ")})`}`);
14708
+ this.details = details;
14709
+ this.expected = parsed?.expected;
18049
14710
  }
18050
14711
  }
18051
14712
  function resolveEnvoyToken(env) {
@@ -18247,7 +14908,7 @@ function createLogger(options = {}) {
18247
14908
  var logger = createLogger();
18248
14909
 
18249
14910
  // src/port.ts
18250
- import { execFile as execFile2 } from "child_process";
14911
+ import { execFile } from "child_process";
18251
14912
 
18252
14913
  // src/ss.ts
18253
14914
  function portFromSsOutput(output, pid) {
@@ -18269,15 +14930,15 @@ function portFromSsOutput(output, pid) {
18269
14930
  }
18270
14931
 
18271
14932
  // src/port.ts
18272
- var defaultExec2 = (command, args, options) => new Promise((resolve, reject) => {
18273
- execFile2(command, args, { encoding: options.encoding }, (error48, stdout) => {
14933
+ var defaultExec = (command, args, options) => new Promise((resolve, reject) => {
14934
+ execFile(command, args, { encoding: options.encoding }, (error48, stdout) => {
18274
14935
  if (error48)
18275
14936
  reject(error48);
18276
14937
  else
18277
14938
  resolve(stdout);
18278
14939
  });
18279
14940
  });
18280
- async function resolvePort(serverUrl, exec = defaultExec2) {
14941
+ async function resolvePort(serverUrl, exec = defaultExec) {
18281
14942
  const urlPort = Number.parseInt(serverUrl.port, 10);
18282
14943
  if (Number.isFinite(urlPort) && urlPort > 0)
18283
14944
  return urlPort;
@@ -18296,6 +14957,25 @@ var skillsDirectory = [
18296
14957
  path3.resolve(moduleDirectory, "../../skills"),
18297
14958
  path3.resolve(moduleDirectory, "../../../skills")
18298
14959
  ].find((dir) => existsSync(dir));
14960
+ var PACKAGE_NAME = "@sjawhar/opencode-legion-envoy";
14961
+ function packageRoot(from) {
14962
+ for (let directory = from;; directory = path3.dirname(directory)) {
14963
+ if (manifestName(path3.join(directory, "package.json")) === PACKAGE_NAME)
14964
+ return directory;
14965
+ if (path3.dirname(directory) === directory) {
14966
+ throw new Error(`envoy: no ${PACKAGE_NAME} package.json above ${from}`);
14967
+ }
14968
+ }
14969
+ }
14970
+ function manifestName(manifest) {
14971
+ try {
14972
+ const parsed = JSON.parse(readFileSync3(manifest, "utf8"));
14973
+ return typeof parsed === "object" && parsed !== null && "name" in parsed ? parsed.name : undefined;
14974
+ } catch {
14975
+ return;
14976
+ }
14977
+ }
14978
+ var PLUGIN_BIN = path3.join(packageRoot(moduleDirectory), "bin");
18299
14979
  function toolSpec(name) {
18300
14980
  const spec = envoyToolSpecs.find((candidate) => candidate.name === name);
18301
14981
  if (spec === undefined)
@@ -18314,7 +14994,7 @@ var server_default = async (input) => {
18314
14994
  const cwd = process.cwd();
18315
14995
  const dispatchConfig = resolveDispatchConfig(process.env, { cwd });
18316
14996
  if (!dispatchConfig.enabled) {
18317
- logger.warn(`envoy: dispatch tools disabled \u2014 ${dispatchConfig.error ?? "no Dispatch URL configured"}`);
14997
+ logger.warn(`envoy: Dispatch disabled \u2014 ${dispatchConfig.error ?? "no Dispatch URL configured"}`);
18318
14998
  }
18319
14999
  let dispatchFirst;
18320
15000
  if (dispatchConfig.enabled) {
@@ -18396,29 +15076,6 @@ var server_default = async (input) => {
18396
15076
  clearInterval(timer);
18397
15077
  clearInterval(heartbeatInterval);
18398
15078
  });
18399
- const dispatchTools = {};
18400
- if (dispatchConfig.enabled) {
18401
- for (const spec of dispatchToolSpecs) {
18402
- dispatchTools[spec.name] = tool({
18403
- description: spec.description,
18404
- args: spec.arguments(zodSchemaApi(tool.schema)),
18405
- async execute(args, ctx) {
18406
- ctx.metadata({ title: "Dispatch" });
18407
- const result = await executeDispatchTool({
18408
- tool: spec.name,
18409
- args,
18410
- cwd: ctx.directory,
18411
- host: "opencode",
18412
- sessionId: ctx.sessionID,
18413
- sessionTitle: await fetchTitle(ctx.sessionID) ?? undefined,
18414
- config: dispatchConfig,
18415
- env: process.env
18416
- });
18417
- return { title: "Dispatch", output: result.text, metadata: result.details };
18418
- }
18419
- });
18420
- }
18421
- }
18422
15079
  return {
18423
15080
  config: (cfg) => {
18424
15081
  if (skillsDirectory) {
@@ -18434,6 +15091,18 @@ var server_default = async (input) => {
18434
15091
  cfg.instructions.push(dispatchFirst);
18435
15092
  }
18436
15093
  },
15094
+ "shell.env": async (input2, output) => {
15095
+ output.env.PATH = `${PLUGIN_BIN}${path3.delimiter}${output.env.PATH ?? process.env.PATH ?? ""}`;
15096
+ output.env.DISPATCH_HOST = "opencode";
15097
+ output.env.DISPATCH_SESSION_ID = "";
15098
+ output.env.DISPATCH_SESSION_TITLE = "";
15099
+ if (input2.sessionID) {
15100
+ output.env.DISPATCH_SESSION_ID = input2.sessionID;
15101
+ const title = trackedSessions.get(input2.sessionID)?.title;
15102
+ if (title)
15103
+ output.env.DISPATCH_SESSION_TITLE = title;
15104
+ }
15105
+ },
18437
15106
  event: async ({
18438
15107
  event
18439
15108
  }) => {
@@ -18488,7 +15157,6 @@ var server_default = async (input) => {
18488
15157
  clearInterval(heartbeatInterval);
18489
15158
  },
18490
15159
  tool: {
18491
- ...dispatchTools,
18492
15160
  envoy_subscribe: tool({
18493
15161
  description: subscribeSpec.description,
18494
15162
  args: subscribeSpec.arguments(zodSchemaApi(tool.schema)),