@sjawhar/opencode-legion-envoy 6.4.2 → 6.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -36
- package/bin/dispatch +6 -0
- package/dist/dispatch.js +18648 -0
- package/dist/src/server.js +238 -3570
- package/package.json +4 -3
- package/skills/dispatch/SKILL.md +85 -97
- package/skills/dispatch/references/api.md +4 -4
- package/skills/dispatch/references/asks.md +34 -54
- package/skills/dispatch/references/document-edits.md +24 -21
- package/skills/dispatch/references/documents.md +75 -72
- package/skills/dispatch/references/examples.md +37 -67
- package/skills/dispatch/references/issues.md +48 -48
- package/skills/dispatch/references/messages.md +18 -25
- package/skills/dispatch/references/reading.md +10 -10
- package/skills/dispatch-brainstorming/SKILL.md +19 -15
- package/skills/dispatch-first/SKILL.md +7 -7
- package/skills/envoy/SKILL.md +3 -2
- package/skills/legion-architect/SKILL.md +47 -49
- package/skills/legion-controller/SKILL.md +38 -32
- package/skills/legion-retro/SKILL.md +7 -8
- package/skills/legion-worker/SKILL.md +20 -15
- package/skills/legion-worker/references/merge-gate.md +3 -3
- package/src/server.ts +54 -36
package/dist/src/server.js
CHANGED
|
@@ -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
|
-
["<", "<"],
|
|
13849
|
-
[">", ">"],
|
|
13850
|
-
["'", "'"],
|
|
13851
|
-
[""", '"'],
|
|
13852
|
-
["&", "&"]
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 " + `
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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 " + "
|
|
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
|
|
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;
|
|
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-
|
|
14662
|
-
import {
|
|
14663
|
-
|
|
14664
|
-
|
|
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-
|
|
14693
|
-
|
|
14694
|
-
|
|
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
|
-
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
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
|
|
14804
|
-
function
|
|
14805
|
-
|
|
14806
|
-
|
|
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
|
|
14809
|
-
|
|
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
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
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
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
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
|
-
|
|
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
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
|
|
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 ``;
|
|
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
|
|
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
|
|
18273
|
-
|
|
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 =
|
|
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:
|
|
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)),
|