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