@sjawhar/opencode-legion-envoy 1.25.0 → 1.27.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 +9 -4
- package/dist/src/server.js +467 -152
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +43 -17
- package/skills/envoy/SKILL.md +15 -3
- package/src/server.ts +0 -24
package/README.md
CHANGED
|
@@ -16,23 +16,28 @@ This package exposes:
|
|
|
16
16
|
- `dispatch_ask`
|
|
17
17
|
- `dispatch_edit_ask`
|
|
18
18
|
- `dispatch_resolve_ask`
|
|
19
|
+
- `dispatch_follow`
|
|
19
20
|
- `dispatch_comment`
|
|
20
21
|
- `dispatch_suggest`
|
|
21
22
|
- `dispatch_message`
|
|
22
23
|
- `dispatch_doc_edit`
|
|
23
24
|
- `dispatch_doc_read`
|
|
25
|
+
- `dispatch_request_approval`
|
|
24
26
|
- `dispatch_artifact`
|
|
25
27
|
- `dispatch_read`
|
|
26
28
|
- `dispatch_search`
|
|
29
|
+
- `dispatch_open_asks`
|
|
27
30
|
|
|
28
|
-
The
|
|
31
|
+
The fifteen native `dispatch_*` tools create and read Dispatch issues, asks, comments,
|
|
29
32
|
documents, and artifacts, or search all of them. They are present when `dispatch.enabled`
|
|
30
33
|
resolves a server URL and bearer token from envoy.json (`~/.config/opencode/envoy.json`, merged
|
|
31
34
|
with `<repo>/.opencode/envoy.json`) or the `DISPATCH_URL` and `DISPATCH_TOKEN` environment
|
|
32
35
|
variables; `dispatch.enabled: true` without `dispatch.serverUrl` targets `http://localhost:8766`.
|
|
33
|
-
Each issue-scoped call fills the target issue from the session working directory
|
|
34
|
-
the OpenCode session id and title
|
|
35
|
-
|
|
36
|
+
Each issue-scoped call fills the target issue from the session working directory and stamps it with
|
|
37
|
+
the OpenCode session id and title. No write subscribes the session to an issue or document: a
|
|
38
|
+
session follows the asks it opens or replies to (their answers and replies reach it directly),
|
|
39
|
+
`dispatch_follow` leaves or rejoins one, and every write result names the `envoy_subscribe` line
|
|
40
|
+
for the whole owner.
|
|
36
41
|
|
|
37
42
|
`dispatch_artifact` accepts exactly one upload source: a local `path`, or inline `content`.
|
|
38
43
|
An architect can post a specification directly with
|
package/dist/src/server.js
CHANGED
|
@@ -13682,6 +13682,11 @@ var SubscriptionRemovedEventPayloadSchema = object({
|
|
|
13682
13682
|
pending: boolean2().optional(),
|
|
13683
13683
|
request_event_id: number2().int().positive().optional()
|
|
13684
13684
|
});
|
|
13685
|
+
var AskFollowerEventPayloadSchema = object({
|
|
13686
|
+
ask_id: string2().optional(),
|
|
13687
|
+
session_id: string2().optional(),
|
|
13688
|
+
by: object({ kind: string2(), id: string2().optional() }).passthrough().optional()
|
|
13689
|
+
});
|
|
13685
13690
|
// ../contracts/src/dispatch-snippet.ts
|
|
13686
13691
|
var HTML_ENTITIES = [
|
|
13687
13692
|
["<", "<"],
|
|
@@ -13806,7 +13811,7 @@ var dispatchToolSpecs = [
|
|
|
13806
13811
|
options: z.array(z.object({
|
|
13807
13812
|
label: z.string().describe("Selectable option label."),
|
|
13808
13813
|
description: z.string().describe("Optional option context.").optional()
|
|
13809
|
-
}), { max: 8 }).describe("
|
|
13814
|
+
}), { max: 8 }).describe("Up to 8 choices, each an object { label, description? } (never a bare string).").optional(),
|
|
13810
13815
|
multiple: z.boolean().describe("Whether multiple choices may be selected.").optional(),
|
|
13811
13816
|
urgency: z.enum(ASK_URGENCIES).describe("Optional decision urgency.").optional(),
|
|
13812
13817
|
anchor: z.object({
|
|
@@ -13847,6 +13852,15 @@ var dispatchToolSpecs = [
|
|
|
13847
13852
|
reason: z.string({ min: 1 }).describe("Why the open ask no longer needs a human answer.")
|
|
13848
13853
|
})
|
|
13849
13854
|
},
|
|
13855
|
+
{
|
|
13856
|
+
name: "dispatch_follow",
|
|
13857
|
+
description: "Follow or unfollow an ask. Every session that opens or replies to an ask follows it: its answer, " + "edits, resolution, and replies reach that session directly. Unfollow to stop; follow to rejoin or " + "to hear an ask you never wrote to. Whole-issue subscription is separate: envoy_subscribe " + "notifications.dispatch.issue.<KEY>.>",
|
|
13858
|
+
arguments: (z) => ({
|
|
13859
|
+
ask: z.string().describe("Full ask id (uuid)."),
|
|
13860
|
+
action: z.enum(["follow", "unfollow"]).describe("follow | unfollow")
|
|
13861
|
+
}),
|
|
13862
|
+
strict: true
|
|
13863
|
+
},
|
|
13850
13864
|
{
|
|
13851
13865
|
name: "dispatch_comment",
|
|
13852
13866
|
description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters. ${OWNER_REFERENCE}`,
|
|
@@ -13858,7 +13872,7 @@ var dispatchToolSpecs = [
|
|
|
13858
13872
|
quote: z.string().describe("Optional exact quoted document text.").optional(),
|
|
13859
13873
|
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
|
|
13860
13874
|
body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
|
|
13861
|
-
reply_to: z.string().describe("
|
|
13875
|
+
reply_to: z.string().describe("A comment id (uuid); replying to any comment in a thread continues that thread (an " + "ask's clarification thread included). To reply to an ask, use reply_to_ask with the " + "ask id instead.").optional(),
|
|
13862
13876
|
reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
|
|
13863
13877
|
turn: z.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
|
|
13864
13878
|
}),
|
|
@@ -13890,7 +13904,7 @@ var dispatchToolSpecs = [
|
|
|
13890
13904
|
},
|
|
13891
13905
|
{
|
|
13892
13906
|
name: "dispatch_doc_edit",
|
|
13893
|
-
description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " +
|
|
13907
|
+
description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "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 insert anchors, find text as rendered: inline Markdown (**bold**, `code`) is tolerated; a leading \'# \' matches a heading. Insert anchors also accept "start", "end", and "heading:<exact heading text>". ' + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
13894
13908
|
arguments: (z) => ({
|
|
13895
13909
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
13896
13910
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -13898,12 +13912,12 @@ var dispatchToolSpecs = [
|
|
|
13898
13912
|
ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
|
|
13899
13913
|
ops: z.array(z.object({
|
|
13900
13914
|
op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
|
|
13901
|
-
find: z.string().describe("Text
|
|
13915
|
+
find: z.string().describe("Text of the target block as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading.").optional(),
|
|
13902
13916
|
with: z.string().describe("Replacement text for replace.").optional(),
|
|
13903
13917
|
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
|
|
13904
13918
|
markdown: z.string().describe("Markdown to insert.").optional(),
|
|
13905
|
-
after: z.string().describe(
|
|
13906
|
-
before: z.string().describe(
|
|
13919
|
+
after: z.string().describe(`Insert after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>".`).optional(),
|
|
13920
|
+
before: z.string().describe(`Insert before this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>".`).optional(),
|
|
13907
13921
|
block: z.string().describe("Block id to retype.").optional(),
|
|
13908
13922
|
type: z.string().describe("Typed block name for retype.").optional(),
|
|
13909
13923
|
attributes: z.unknown().describe("Typed block attributes for retype.").optional()
|
|
@@ -14390,6 +14404,9 @@ function canonicalRepo(owner, repo) {
|
|
|
14390
14404
|
return `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase().replace(/\.git$/, "")}`;
|
|
14391
14405
|
}
|
|
14392
14406
|
// ../contracts/src/tool-schema.ts
|
|
14407
|
+
function overCapMessage(length, max) {
|
|
14408
|
+
return `is ${length - max} characters over the ${max}-character limit (${length}/${max})`;
|
|
14409
|
+
}
|
|
14393
14410
|
function zodSchemaApi(zod) {
|
|
14394
14411
|
const api = zod;
|
|
14395
14412
|
return {
|
|
@@ -14397,8 +14414,12 @@ function zodSchemaApi(zod) {
|
|
|
14397
14414
|
let schema = api.string();
|
|
14398
14415
|
if (opts.min !== undefined)
|
|
14399
14416
|
schema = schema.min(opts.min);
|
|
14400
|
-
if (opts.max !== undefined)
|
|
14401
|
-
|
|
14417
|
+
if (opts.max !== undefined) {
|
|
14418
|
+
const max = opts.max;
|
|
14419
|
+
schema = schema.max(max, {
|
|
14420
|
+
error: (issue) => overCapMessage(typeof issue.input === "string" ? issue.input.length : max + 1, max)
|
|
14421
|
+
});
|
|
14422
|
+
}
|
|
14402
14423
|
return schema;
|
|
14403
14424
|
},
|
|
14404
14425
|
number: (opts = {}) => {
|
|
@@ -14597,6 +14618,24 @@ function resolveDispatchConfig(env, options = {}) {
|
|
|
14597
14618
|
// ../envoy-client/src/dispatch-execute.ts
|
|
14598
14619
|
import { resolve as resolvePath } from "path";
|
|
14599
14620
|
|
|
14621
|
+
// ../envoy-client/src/ask-answer.ts
|
|
14622
|
+
var HEAD_LENGTH = 120;
|
|
14623
|
+
function textHead(text, limit = HEAD_LENGTH) {
|
|
14624
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
14625
|
+
return flat.length > limit ? `${flat.slice(0, limit)}\u2026` : flat;
|
|
14626
|
+
}
|
|
14627
|
+
function askAnswerText(answer) {
|
|
14628
|
+
if (answer === null || answer === undefined)
|
|
14629
|
+
return "";
|
|
14630
|
+
const selected = (answer.selected ?? []).join(", ");
|
|
14631
|
+
const text = answer.text ?? "";
|
|
14632
|
+
if (text === "")
|
|
14633
|
+
return selected;
|
|
14634
|
+
if (selected === "")
|
|
14635
|
+
return text;
|
|
14636
|
+
return `${selected} - ${text}`;
|
|
14637
|
+
}
|
|
14638
|
+
|
|
14600
14639
|
// ../envoy-client/src/dispatch-cwd.ts
|
|
14601
14640
|
import { execFile } from "child_process";
|
|
14602
14641
|
import { promisify } from "util";
|
|
@@ -14778,6 +14817,9 @@ class DispatchClient {
|
|
|
14778
14817
|
async comment(issue, input) {
|
|
14779
14818
|
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], input);
|
|
14780
14819
|
}
|
|
14820
|
+
async listIssueAsks(issue, state) {
|
|
14821
|
+
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "asks"], undefined, state === undefined ? undefined : { state });
|
|
14822
|
+
}
|
|
14781
14823
|
async getArtifactAsks(id, state) {
|
|
14782
14824
|
return this.#json("GET", ["api", "v1", "artifacts", id, "asks"], undefined, state === undefined ? undefined : { state });
|
|
14783
14825
|
}
|
|
@@ -14854,6 +14896,12 @@ class DispatchClient {
|
|
|
14854
14896
|
async getAsk(id) {
|
|
14855
14897
|
return this.#json("GET", ["api", "v1", "asks", id]);
|
|
14856
14898
|
}
|
|
14899
|
+
async followAsk(id, sessionId, actor) {
|
|
14900
|
+
await this.#json("PUT", ["api", "v1", "asks", id, "followers", sessionId], { actor });
|
|
14901
|
+
}
|
|
14902
|
+
async unfollowAsk(id, sessionId, actor) {
|
|
14903
|
+
await this.#json("DELETE", ["api", "v1", "asks", id, "followers", sessionId], { actor });
|
|
14904
|
+
}
|
|
14857
14905
|
async getComment(id) {
|
|
14858
14906
|
return this.#json("GET", ["api", "v1", "comments", id]);
|
|
14859
14907
|
}
|
|
@@ -14976,13 +15024,144 @@ class DispatchClient {
|
|
|
14976
15024
|
}
|
|
14977
15025
|
}
|
|
14978
15026
|
|
|
15027
|
+
// ../envoy-client/src/tool-input-errors.ts
|
|
15028
|
+
class ToolInputError extends Error {
|
|
15029
|
+
tool;
|
|
15030
|
+
problems;
|
|
15031
|
+
constructor(tool, problems) {
|
|
15032
|
+
const count = problems.length;
|
|
15033
|
+
super([
|
|
15034
|
+
`${tool} was not called: ${count} problem${count === 1 ? "" : "s"}`,
|
|
15035
|
+
...problems.map((problem) => `- ${problem}`)
|
|
15036
|
+
].join(`
|
|
15037
|
+
`));
|
|
15038
|
+
this.name = "ToolInputError";
|
|
15039
|
+
this.tool = tool;
|
|
15040
|
+
this.problems = problems;
|
|
15041
|
+
}
|
|
15042
|
+
}
|
|
15043
|
+
function unwrap(schema) {
|
|
15044
|
+
let current = schema;
|
|
15045
|
+
while (current !== undefined) {
|
|
15046
|
+
const def = current.def;
|
|
15047
|
+
if (!("innerType" in def) || !(def.type === "optional" || def.type === "nullable" || def.type === "default"))
|
|
15048
|
+
break;
|
|
15049
|
+
current = def.innerType;
|
|
15050
|
+
}
|
|
15051
|
+
return current;
|
|
15052
|
+
}
|
|
15053
|
+
function shapeOf(schema) {
|
|
15054
|
+
const unwrapped = unwrap(schema);
|
|
15055
|
+
if (unwrapped === undefined)
|
|
15056
|
+
return;
|
|
15057
|
+
const def = unwrapped.def;
|
|
15058
|
+
return def.type === "object" && "shape" in def ? def.shape : undefined;
|
|
15059
|
+
}
|
|
15060
|
+
function schemaAt(schema, path) {
|
|
15061
|
+
let current = schema;
|
|
15062
|
+
for (const key of path) {
|
|
15063
|
+
const unwrapped = unwrap(current);
|
|
15064
|
+
if (unwrapped === undefined)
|
|
15065
|
+
return;
|
|
15066
|
+
const def = unwrapped.def;
|
|
15067
|
+
if (def.type === "object" && "shape" in def) {
|
|
15068
|
+
current = shapeOf(unwrapped)?.[String(key)];
|
|
15069
|
+
} else if (def.type === "array" && "element" in def) {
|
|
15070
|
+
current = def.element;
|
|
15071
|
+
} else {
|
|
15072
|
+
return;
|
|
15073
|
+
}
|
|
15074
|
+
}
|
|
15075
|
+
return unwrap(current);
|
|
15076
|
+
}
|
|
15077
|
+
function describeInput(value) {
|
|
15078
|
+
if (value === null)
|
|
15079
|
+
return "null";
|
|
15080
|
+
if (Array.isArray(value))
|
|
15081
|
+
return "an array";
|
|
15082
|
+
switch (typeof value) {
|
|
15083
|
+
case "number":
|
|
15084
|
+
case "boolean":
|
|
15085
|
+
return String(value);
|
|
15086
|
+
case "string":
|
|
15087
|
+
return "a string";
|
|
15088
|
+
case "object":
|
|
15089
|
+
return "an object";
|
|
15090
|
+
default:
|
|
15091
|
+
return typeof value;
|
|
15092
|
+
}
|
|
15093
|
+
}
|
|
15094
|
+
function describeExpected(expected, schema) {
|
|
15095
|
+
switch (expected) {
|
|
15096
|
+
case "int":
|
|
15097
|
+
return "an integer";
|
|
15098
|
+
case "object": {
|
|
15099
|
+
const shape = shapeOf(schema);
|
|
15100
|
+
const keys = shape === undefined ? [] : Object.entries(shape).map(([key, field]) => `${key}${field.def.type === "optional" ? "?" : ""}`);
|
|
15101
|
+
return keys.length === 0 ? "an object" : `an object {${keys.join(", ")}}`;
|
|
15102
|
+
}
|
|
15103
|
+
case "array":
|
|
15104
|
+
return "an array";
|
|
15105
|
+
default:
|
|
15106
|
+
return `a ${expected}`;
|
|
15107
|
+
}
|
|
15108
|
+
}
|
|
15109
|
+
function formatZodIssues(issues, schema) {
|
|
15110
|
+
const allowed = Object.keys(shapeOf(schema) ?? {}).join(", ");
|
|
15111
|
+
return issues.flatMap((issue) => {
|
|
15112
|
+
const path = issue.path.map(String).join(".");
|
|
15113
|
+
switch (issue.code) {
|
|
15114
|
+
case "invalid_type":
|
|
15115
|
+
return issue.input === undefined ? [`${path} is required (${issue.expected})`] : [
|
|
15116
|
+
`${path} must be ${describeExpected(issue.expected, schemaAt(schema, issue.path))}, not ${describeInput(issue.input)}`
|
|
15117
|
+
];
|
|
15118
|
+
case "unrecognized_keys":
|
|
15119
|
+
return issue.keys.map((key) => `unknown field "${key}"; allowed: ${allowed}`);
|
|
15120
|
+
case "invalid_value":
|
|
15121
|
+
return [
|
|
15122
|
+
`${path} must be one of ${issue.values.map(String).join("|")}; got ${JSON.stringify(issue.input)}`
|
|
15123
|
+
];
|
|
15124
|
+
case "too_big":
|
|
15125
|
+
if (issue.origin === "array" && Array.isArray(issue.input)) {
|
|
15126
|
+
return [`${path} has ${issue.input.length} items; the limit is ${issue.maximum}`];
|
|
15127
|
+
}
|
|
15128
|
+
return [`${path} ${issue.message}`];
|
|
15129
|
+
case "custom":
|
|
15130
|
+
return [issue.message];
|
|
15131
|
+
default:
|
|
15132
|
+
return [path === "" ? issue.message : `${path}: ${issue.message}`];
|
|
15133
|
+
}
|
|
15134
|
+
});
|
|
15135
|
+
}
|
|
15136
|
+
|
|
14979
15137
|
// ../envoy-client/src/dispatch-execute.ts
|
|
15138
|
+
function issueTopic(key) {
|
|
15139
|
+
return { label: key, topic: dispatchIssueSubject(key, ">") };
|
|
15140
|
+
}
|
|
15141
|
+
function documentTopic(artifact) {
|
|
15142
|
+
return {
|
|
15143
|
+
label: `${artifact.project}/${artifact.slug}`,
|
|
15144
|
+
topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
|
|
15145
|
+
};
|
|
15146
|
+
}
|
|
15147
|
+
function resolvedTopic(resolved) {
|
|
15148
|
+
if (resolved.owner.kind === "project")
|
|
15149
|
+
return documentTopic(resolved.artifact);
|
|
15150
|
+
if (resolved.issue === undefined)
|
|
15151
|
+
throw new Error("issue document is missing its issue");
|
|
15152
|
+
return issueTopic(resolved.issue.key);
|
|
15153
|
+
}
|
|
15154
|
+
function notSubscribed(owner) {
|
|
15155
|
+
return `(not subscribed to ${owner.label}; envoy_subscribe ${owner.topic} for every event on it)`;
|
|
15156
|
+
}
|
|
15157
|
+
function followsAsk(owner) {
|
|
15158
|
+
return `You follow this ask: its answer and replies reach you directly. For every event on ${owner.label}: envoy_subscribe ${owner.topic}`;
|
|
15159
|
+
}
|
|
14980
15160
|
function documentResultDetails(artifact) {
|
|
14981
15161
|
return {
|
|
14982
15162
|
project: artifact.project,
|
|
14983
15163
|
artifact: artifact.id,
|
|
14984
|
-
document: `${artifact.project}/${artifact.slug}
|
|
14985
|
-
topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
|
|
15164
|
+
document: `${artifact.project}/${artifact.slug}`
|
|
14986
15165
|
};
|
|
14987
15166
|
}
|
|
14988
15167
|
function writeResultDetails(resolved, fields) {
|
|
@@ -14991,19 +15170,11 @@ function writeResultDetails(resolved, fields) {
|
|
|
14991
15170
|
}
|
|
14992
15171
|
if (resolved.issue === undefined)
|
|
14993
15172
|
throw new Error("issue document is missing its issue");
|
|
14994
|
-
return {
|
|
14995
|
-
issue: resolved.issue.key,
|
|
14996
|
-
topic: dispatchIssueSubject(resolved.issue.key, ">"),
|
|
14997
|
-
...fields
|
|
14998
|
-
};
|
|
15173
|
+
return { issue: resolved.issue.key, ...fields };
|
|
14999
15174
|
}
|
|
15000
|
-
async function
|
|
15175
|
+
async function askOwnerDetails(client, ask, resolved) {
|
|
15001
15176
|
if (ask.issue_key !== null) {
|
|
15002
|
-
return {
|
|
15003
|
-
issue: ask.issue_key,
|
|
15004
|
-
topic: dispatchIssueSubject(ask.issue_key, ">"),
|
|
15005
|
-
ask: ask.id
|
|
15006
|
-
};
|
|
15177
|
+
return { issue: ask.issue_key, ask: ask.id };
|
|
15007
15178
|
}
|
|
15008
15179
|
if (ask.artifact_id === undefined || ask.artifact_id === null) {
|
|
15009
15180
|
throw new Error("document ask is missing its artifact ID");
|
|
@@ -15011,6 +15182,9 @@ async function askResultDetails(client, ask, resolved) {
|
|
|
15011
15182
|
const artifact = resolved?.artifact ?? await client.getArtifact(ask.artifact_id);
|
|
15012
15183
|
return { ...documentResultDetails(artifact), ask: ask.id };
|
|
15013
15184
|
}
|
|
15185
|
+
async function followedAskDetails(client, ask, resolved) {
|
|
15186
|
+
return { ...await askOwnerDetails(client, ask, resolved), follows: { ask: ask.id } };
|
|
15187
|
+
}
|
|
15014
15188
|
var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
15015
15189
|
var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
|
|
15016
15190
|
var bareIssueNumberPattern = /^[1-9][0-9]*$/;
|
|
@@ -15018,6 +15192,7 @@ var issueFreeTools = {
|
|
|
15018
15192
|
dispatch_issue: true,
|
|
15019
15193
|
dispatch_edit_ask: true,
|
|
15020
15194
|
dispatch_resolve_ask: true,
|
|
15195
|
+
dispatch_follow: true,
|
|
15021
15196
|
dispatch_search: true,
|
|
15022
15197
|
dispatch_open_asks: true
|
|
15023
15198
|
};
|
|
@@ -15070,23 +15245,19 @@ function askUrgency(args) {
|
|
|
15070
15245
|
return ASK_URGENCIES.find((urgency) => urgency === value);
|
|
15071
15246
|
}
|
|
15072
15247
|
function askKind(args) {
|
|
15073
|
-
|
|
15074
|
-
if (value === undefined || value === "action")
|
|
15075
|
-
return value;
|
|
15076
|
-
throw new Error("kind must be action");
|
|
15248
|
+
return optionalString(args, "kind") === "action" ? "action" : undefined;
|
|
15077
15249
|
}
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
|
|
15081
|
-
if (ref === undefined || question.includes(ref))
|
|
15082
|
-
return question;
|
|
15083
|
-
const withRef = `${question}
|
|
15250
|
+
var maxAskQuestion16 = 800;
|
|
15251
|
+
function questionWithRef(question, ref) {
|
|
15252
|
+
return ref === undefined || question.includes(ref) ? question : `${question}
|
|
15084
15253
|
|
|
15085
15254
|
Ref: ${ref}`;
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15255
|
+
}
|
|
15256
|
+
function askQuestionWithRef(args) {
|
|
15257
|
+
return questionWithRef(stringArg(args, "question"), optionalString(args, "ref"));
|
|
15258
|
+
}
|
|
15259
|
+
function askQuestionProblem(withRef) {
|
|
15260
|
+
return withRef.length > maxAskQuestion16 ? `question plus ref ${overCapMessage(withRef.length, maxAskQuestion16)}; shorten the question or drop the ref` : undefined;
|
|
15090
15261
|
}
|
|
15091
15262
|
function parseDispatchRef(ref) {
|
|
15092
15263
|
const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
|
|
@@ -15108,7 +15279,8 @@ function parseDispatchRef(ref) {
|
|
|
15108
15279
|
return {
|
|
15109
15280
|
owner: { kind: "project", project },
|
|
15110
15281
|
kind: targetKind,
|
|
15111
|
-
id: targetID
|
|
15282
|
+
id: targetID,
|
|
15283
|
+
artifact
|
|
15112
15284
|
};
|
|
15113
15285
|
}
|
|
15114
15286
|
const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
|
|
@@ -15140,60 +15312,171 @@ function parseDispatchRef(ref) {
|
|
|
15140
15312
|
return { owner, kind: "message", id: message };
|
|
15141
15313
|
return { owner, kind: "issue", id: issue };
|
|
15142
15314
|
}
|
|
15315
|
+
var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15316
|
+
var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
|
|
15317
|
+
var messageIdProblem = "in_reply_to must be a full message id (uuid) or a dispatch://KEY/message/<id> reference";
|
|
15143
15318
|
function askId(args) {
|
|
15144
15319
|
const ask = stringArg(args, "ask");
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
15320
|
+
return ask.startsWith("dispatch://") ? parseDispatchRef(ask)?.id ?? ask : ask;
|
|
15321
|
+
}
|
|
15322
|
+
function messageIdOf(value) {
|
|
15323
|
+
const id = value.startsWith("dispatch://") ? (() => {
|
|
15324
|
+
const reference = parseDispatchRef(value);
|
|
15325
|
+
return reference?.kind === "message" ? reference.id : undefined;
|
|
15326
|
+
})() : value;
|
|
15327
|
+
return id !== undefined && uuidPattern.test(id) ? id : undefined;
|
|
15328
|
+
}
|
|
15329
|
+
var idPrefixPattern = /^[0-9a-f][0-9a-f-]{7,}$/i;
|
|
15330
|
+
async function resolveIdPrefix(tool, kind, ref, list) {
|
|
15331
|
+
if (uuidPattern.test(ref.id))
|
|
15332
|
+
return ref.id;
|
|
15333
|
+
const ownerName = ref.owner.kind === "issue" ? ref.owner.issue : `${ref.owner.project}/${ref.artifact}`;
|
|
15334
|
+
if (!idPrefixPattern.test(ref.id)) {
|
|
15335
|
+
throw new ToolInputError(tool, [
|
|
15336
|
+
`${kind} id ${ref.id} must be a full uuid or a prefix of at least 8 hex characters`
|
|
15337
|
+
]);
|
|
15150
15338
|
}
|
|
15151
|
-
|
|
15339
|
+
const prefix = ref.id.toLowerCase();
|
|
15340
|
+
const matches = (await list()).filter((item) => item.id.toLowerCase().startsWith(prefix));
|
|
15341
|
+
if (matches.length === 1 && matches[0] !== undefined)
|
|
15342
|
+
return matches[0].id;
|
|
15343
|
+
throw new ToolInputError(tool, [
|
|
15344
|
+
matches.length === 0 ? `${kind} id ${ref.id} matches none of the ${kind}s on ${ownerName}; use the full id` : `${kind} id ${ref.id} matches ${matches.length} ${kind}s on ${ownerName}; use the full id`
|
|
15345
|
+
]);
|
|
15152
15346
|
}
|
|
15153
15347
|
function messageInReplyTo(args) {
|
|
15154
15348
|
const inReplyTo = optionalString(args, "in_reply_to");
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
|
|
15349
|
+
return inReplyTo === undefined ? undefined : messageIdOf(inReplyTo);
|
|
15350
|
+
}
|
|
15351
|
+
function argumentProblems(tool, args) {
|
|
15352
|
+
const problems = [];
|
|
15353
|
+
switch (tool) {
|
|
15354
|
+
case "dispatch_ask": {
|
|
15355
|
+
const question = optionalString(args, "question");
|
|
15356
|
+
const ref = optionalString(args, "ref");
|
|
15357
|
+
if (question !== undefined && ref !== undefined && question.length <= maxAskQuestion16) {
|
|
15358
|
+
const problem = askQuestionProblem(questionWithRef(question, ref));
|
|
15359
|
+
if (problem !== undefined)
|
|
15360
|
+
problems.push(problem);
|
|
15361
|
+
}
|
|
15362
|
+
break;
|
|
15363
|
+
}
|
|
15364
|
+
case "dispatch_edit_ask":
|
|
15365
|
+
case "dispatch_resolve_ask": {
|
|
15366
|
+
const ask = optionalString(args, "ask");
|
|
15367
|
+
if (ask?.startsWith("dispatch://") && parseDispatchRef(ask)?.kind !== "ask") {
|
|
15368
|
+
problems.push(askIdProblem);
|
|
15369
|
+
}
|
|
15370
|
+
break;
|
|
15371
|
+
}
|
|
15372
|
+
case "dispatch_comment": {
|
|
15373
|
+
if (optionalString(args, "quote") !== undefined && optionalString(args, "artifact") === undefined) {
|
|
15374
|
+
problems.push("artifact is required when quote is supplied");
|
|
15375
|
+
}
|
|
15376
|
+
if (optionalString(args, "reply_to") !== undefined && optionalString(args, "reply_to_ask") !== undefined) {
|
|
15377
|
+
problems.push("reply_to and reply_to_ask cannot both be set");
|
|
15378
|
+
}
|
|
15379
|
+
break;
|
|
15380
|
+
}
|
|
15381
|
+
case "dispatch_message": {
|
|
15382
|
+
const inReplyTo = optionalString(args, "in_reply_to");
|
|
15383
|
+
if (inReplyTo !== undefined && messageIdOf(inReplyTo) === undefined) {
|
|
15384
|
+
problems.push(messageIdProblem);
|
|
15385
|
+
}
|
|
15386
|
+
break;
|
|
15387
|
+
}
|
|
15160
15388
|
}
|
|
15161
|
-
return
|
|
15389
|
+
return problems;
|
|
15162
15390
|
}
|
|
15391
|
+
function dispatchRefFromUrl(value, serverUrl) {
|
|
15392
|
+
let url;
|
|
15393
|
+
let origin;
|
|
15394
|
+
try {
|
|
15395
|
+
url = new URL(value);
|
|
15396
|
+
origin = new URL(serverUrl).origin;
|
|
15397
|
+
} catch {
|
|
15398
|
+
return;
|
|
15399
|
+
}
|
|
15400
|
+
if (url.origin !== origin)
|
|
15401
|
+
return;
|
|
15402
|
+
const versionOf = (name) => {
|
|
15403
|
+
const value = url.searchParams.get(name);
|
|
15404
|
+
return value !== null && /^[1-9][0-9]*$/.test(value) ? `@v${value}` : "";
|
|
15405
|
+
};
|
|
15406
|
+
const issuePage = url.pathname.match(/^\/issues\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec|log|conversation|children)|\/artifacts\/([^/]+)|\/asks\/([^/]+)|\/comments\/([^/]+)|\/messages\/([^/]+))?\/?$/);
|
|
15407
|
+
if (issuePage) {
|
|
15408
|
+
const [, key, page, artifact, ask, comment, message] = issuePage;
|
|
15409
|
+
const version = versionOf("v");
|
|
15410
|
+
if (page === "spec" && version !== "")
|
|
15411
|
+
return `dispatch://${key}/artifact/spec${version}`;
|
|
15412
|
+
if (page !== undefined)
|
|
15413
|
+
return `dispatch://${key}/${page === "conversation" ? "log" : page}`;
|
|
15414
|
+
if (artifact !== undefined) {
|
|
15415
|
+
return `dispatch://${key}/artifact/${decodeURIComponent(artifact)}${version}`;
|
|
15416
|
+
}
|
|
15417
|
+
if (ask !== undefined)
|
|
15418
|
+
return `dispatch://${key}/ask/${ask}`;
|
|
15419
|
+
if (comment !== undefined)
|
|
15420
|
+
return `dispatch://${key}/comment/${comment}`;
|
|
15421
|
+
if (message !== undefined)
|
|
15422
|
+
return `dispatch://${key}/message/${message}`;
|
|
15423
|
+
return `dispatch://${key}`;
|
|
15424
|
+
}
|
|
15425
|
+
const documentPage = url.pathname.match(/^\/projects\/([A-Z][A-Z0-9]{1,9})\/documents\/([^/]+)\/?$/);
|
|
15426
|
+
if (!documentPage)
|
|
15427
|
+
return;
|
|
15428
|
+
const [, project, slug] = documentPage;
|
|
15429
|
+
const document = `dispatch://${project}/artifact/${decodeURIComponent(slug ?? "")}${versionOf("version")}`;
|
|
15430
|
+
const ask = url.searchParams.get("ask");
|
|
15431
|
+
if (ask !== null)
|
|
15432
|
+
return `${document}/ask/${ask}`;
|
|
15433
|
+
const comment = url.searchParams.get("comment");
|
|
15434
|
+
if (comment !== null)
|
|
15435
|
+
return `${document}/comment/${comment}`;
|
|
15436
|
+
return document;
|
|
15437
|
+
}
|
|
15438
|
+
var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, or " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename); " + "a dashboard URL on this Dispatch server is accepted too";
|
|
15439
|
+
var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
|
|
15163
15440
|
function toolSchema(tool) {
|
|
15164
15441
|
const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
|
|
15165
15442
|
if (!spec)
|
|
15166
15443
|
throw new Error(`Unknown Dispatch tool: ${tool}`);
|
|
15167
15444
|
return dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
|
|
15168
15445
|
}
|
|
15169
|
-
async function resolveOwnerArguments(tool,
|
|
15446
|
+
async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, problems) {
|
|
15170
15447
|
if (issueFreeTools[tool] === true)
|
|
15171
|
-
return { args, ref: null, owner: null };
|
|
15172
|
-
const refArgument =
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15448
|
+
return { args: input, ref: null, owner: null };
|
|
15449
|
+
const refArgument = input.ref;
|
|
15450
|
+
let ref = null;
|
|
15451
|
+
let args = input;
|
|
15452
|
+
if (typeof refArgument === "string") {
|
|
15453
|
+
const refText = dispatchRefFromUrl(refArgument, serverUrl) ?? refArgument;
|
|
15454
|
+
if (refText !== refArgument)
|
|
15455
|
+
args = { ...input, ref: refText };
|
|
15456
|
+
ref = parseDispatchRef(refText);
|
|
15457
|
+
if (ref === null) {
|
|
15458
|
+
problems.push(tool === "dispatch_ask" && !refText.startsWith("dispatch://") ? "ref must be a dispatch:// reference" : refGrammarProblem);
|
|
15459
|
+
}
|
|
15460
|
+
}
|
|
15179
15461
|
const issueArgument = args.issue;
|
|
15180
15462
|
const projectArgument = args.project;
|
|
15181
15463
|
const artifactArgument = args.artifact;
|
|
15182
15464
|
const versionArgument = args.version;
|
|
15183
15465
|
if (issueArgument !== undefined && projectArgument !== undefined) {
|
|
15184
|
-
|
|
15466
|
+
problems.push("exactly one of issue and project is required");
|
|
15185
15467
|
}
|
|
15186
15468
|
if (typeof projectArgument === "string") {
|
|
15187
15469
|
if (!/^[A-Z][A-Z0-9]{1,9}$/.test(projectArgument)) {
|
|
15188
|
-
|
|
15470
|
+
problems.push("project must be a project key such as CORE");
|
|
15189
15471
|
}
|
|
15190
|
-
|
|
15191
|
-
|
|
15472
|
+
const refDocument = ref?.owner.kind === "project" ? ref.artifact ?? ref.id : undefined;
|
|
15473
|
+
if (ref?.owner.kind === "project" && (ref.owner.project !== projectArgument || artifactArgument !== undefined && artifactArgument !== refDocument)) {
|
|
15474
|
+
problems.push("project and ref must name the same document");
|
|
15192
15475
|
}
|
|
15193
15476
|
return {
|
|
15194
15477
|
args: {
|
|
15195
15478
|
...args,
|
|
15196
|
-
...artifactArgument === undefined &&
|
|
15479
|
+
...artifactArgument === undefined && refDocument !== undefined ? { artifact: refDocument } : {},
|
|
15197
15480
|
...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
|
|
15198
15481
|
},
|
|
15199
15482
|
ref,
|
|
@@ -15205,7 +15488,7 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
|
|
|
15205
15488
|
args: {
|
|
15206
15489
|
...args,
|
|
15207
15490
|
project: ref.owner.project,
|
|
15208
|
-
...artifactArgument === undefined ? { artifact: ref.id } : {},
|
|
15491
|
+
...artifactArgument === undefined ? { artifact: ref.artifact ?? ref.id } : {},
|
|
15209
15492
|
...versionArgument === undefined && ref.version !== undefined ? { version: ref.version } : {}
|
|
15210
15493
|
},
|
|
15211
15494
|
ref,
|
|
@@ -15226,20 +15509,25 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
|
|
|
15226
15509
|
};
|
|
15227
15510
|
}
|
|
15228
15511
|
const legionIssue = env.LEGION_ISSUE;
|
|
15229
|
-
if (!legionIssue)
|
|
15230
|
-
|
|
15512
|
+
if (!legionIssue) {
|
|
15513
|
+
problems.push(ownerRequiredProblem);
|
|
15514
|
+
return { args, ref, owner: null };
|
|
15515
|
+
}
|
|
15231
15516
|
if (nativeIssueKeyPattern.test(legionIssue) || externalIssueRefPattern.test(legionIssue)) {
|
|
15232
15517
|
const issue = canonicalExternalIssueRef(legionIssue);
|
|
15233
|
-
return { args: { ...args, issue }, ref
|
|
15518
|
+
return { args: { ...args, issue }, ref, owner: { kind: "issue", issue } };
|
|
15234
15519
|
}
|
|
15235
15520
|
if (!bareIssueNumberPattern.test(legionIssue)) {
|
|
15236
|
-
|
|
15521
|
+
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");
|
|
15522
|
+
return { args, ref, owner: null };
|
|
15237
15523
|
}
|
|
15238
15524
|
const repo = await resolveCwdRepo(cwd, exec);
|
|
15239
|
-
if (!repo)
|
|
15240
|
-
|
|
15525
|
+
if (!repo) {
|
|
15526
|
+
problems.push("issue is required; LEGION_ISSUE needs a GitHub repository in cwd");
|
|
15527
|
+
return { args, ref, owner: null };
|
|
15528
|
+
}
|
|
15241
15529
|
const issue = `${repo}#${legionIssue}`;
|
|
15242
|
-
return { args: { ...args, issue }, ref
|
|
15530
|
+
return { args: { ...args, issue }, ref, owner: { kind: "issue", issue } };
|
|
15243
15531
|
}
|
|
15244
15532
|
async function resolveArtifact(client, owner, artifactReference) {
|
|
15245
15533
|
if (owner.kind === "project") {
|
|
@@ -15270,7 +15558,8 @@ async function resolveArtifact(client, owner, artifactReference) {
|
|
|
15270
15558
|
const issue = await client.getIssue(owner.issue);
|
|
15271
15559
|
const artifact = artifactReference === undefined || artifactReference === "spec" ? issue.artifacts.find((candidate) => candidate.primary || candidate.id === issue.primary_artifact_id) : issue.artifacts.find((candidate) => candidate.id === artifactReference || candidate.slug === artifactReference || candidate.name === artifactReference);
|
|
15272
15560
|
if (!artifact) {
|
|
15273
|
-
|
|
15561
|
+
const slugs = issue.artifacts.map((candidate) => `${candidate.slug}${candidate.primary || candidate.id === issue.primary_artifact_id ? " (primary)" : ""}`);
|
|
15562
|
+
throw new Error(`artifact "${artifactReference ?? "spec"}" was not found on issue ${issue.key}; artifacts: ${slugs.length === 0 ? "none" : slugs.join(", ")}`);
|
|
15274
15563
|
}
|
|
15275
15564
|
return { owner, issue, artifact };
|
|
15276
15565
|
}
|
|
@@ -15327,7 +15616,7 @@ function issueSummary(issue, events, references) {
|
|
|
15327
15616
|
...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}`),
|
|
15328
15617
|
...typeof references === "string" || !references.truncated ? [] : ["- more references beyond 8 hops"],
|
|
15329
15618
|
"Events:",
|
|
15330
|
-
...events.length === 0 ? ["- none"] : events.map(
|
|
15619
|
+
...events.length === 0 ? ["- none"] : events.map(eventLine)
|
|
15331
15620
|
].join(`
|
|
15332
15621
|
`);
|
|
15333
15622
|
}
|
|
@@ -15335,10 +15624,41 @@ function logSummary(issue, events) {
|
|
|
15335
15624
|
return [
|
|
15336
15625
|
`Key: ${issue.key}`,
|
|
15337
15626
|
"Events:",
|
|
15338
|
-
...events.length === 0 ? ["- none"] : events.map(
|
|
15627
|
+
...events.length === 0 ? ["- none"] : events.map(eventLine)
|
|
15339
15628
|
].join(`
|
|
15340
15629
|
`);
|
|
15341
15630
|
}
|
|
15631
|
+
function eventHead(event) {
|
|
15632
|
+
switch (event.type) {
|
|
15633
|
+
case "ask.opened":
|
|
15634
|
+
case "ask.edited":
|
|
15635
|
+
case "ask.resolved":
|
|
15636
|
+
return textHead(event.payload.question);
|
|
15637
|
+
case "ask.answered":
|
|
15638
|
+
return `${textHead(event.payload.question)} -> ${textHead(askAnswerText(event.payload.answer))}`;
|
|
15639
|
+
case "comment.created":
|
|
15640
|
+
case "comment.edited":
|
|
15641
|
+
case "comment.resolved":
|
|
15642
|
+
case "comment.reopened":
|
|
15643
|
+
case "suggestion.accepted":
|
|
15644
|
+
case "suggestion.rejected":
|
|
15645
|
+
case "message.created":
|
|
15646
|
+
case "message.answered":
|
|
15647
|
+
return textHead(event.payload.body);
|
|
15648
|
+
case "artifact.version":
|
|
15649
|
+
return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
|
|
15650
|
+
case "issue.created":
|
|
15651
|
+
case "issue.updated":
|
|
15652
|
+
case "issue.closed":
|
|
15653
|
+
return `status ${event.payload.status}`;
|
|
15654
|
+
default:
|
|
15655
|
+
return;
|
|
15656
|
+
}
|
|
15657
|
+
}
|
|
15658
|
+
function eventLine(event) {
|
|
15659
|
+
const head = eventHead(event);
|
|
15660
|
+
return `- #${event.seq} ${event.type} \xB7 ${event.actor.kind} ${event.actor.id} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
|
|
15661
|
+
}
|
|
15342
15662
|
function childrenSummary(issue) {
|
|
15343
15663
|
return [
|
|
15344
15664
|
`Key: ${issue.key}`,
|
|
@@ -15473,19 +15793,29 @@ async function executeDispatchTool(input) {
|
|
|
15473
15793
|
throw error;
|
|
15474
15794
|
}
|
|
15475
15795
|
}, { preconnect: baseFetch.preconnect });
|
|
15796
|
+
const env = input.env ?? process.env;
|
|
15797
|
+
const exec = input.exec ?? defaultExec;
|
|
15798
|
+
const problems = [];
|
|
15799
|
+
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec, configUrl, problems);
|
|
15800
|
+
const ownerMissing = issueFreeTools[input.tool] !== true && ownerArguments.owner === null;
|
|
15801
|
+
const schema = toolSchema(input.tool);
|
|
15802
|
+
const parsed = schema.safeParse(ownerArguments.args, { reportInput: true });
|
|
15803
|
+
if (!parsed.success) {
|
|
15804
|
+
const issues = parsed.error.issues.filter((issue) => !ownerMissing || !(issue.code === "invalid_type" && issue.path.length === 1 && issue.path[0] === "issue" && issue.input === undefined || issue.code === "custom" && issue.path.length === 0 && issue.message.startsWith("Exactly one of issue and project is required")));
|
|
15805
|
+
problems.push(...formatZodIssues(issues, schema));
|
|
15806
|
+
}
|
|
15807
|
+
problems.push(...argumentProblems(input.tool, ownerArguments.args));
|
|
15808
|
+
if (problems.length > 0)
|
|
15809
|
+
throw new ToolInputError(input.tool, problems);
|
|
15476
15810
|
if (input.tool === "dispatch_open_asks") {
|
|
15477
15811
|
const sessionId = input.sessionId?.trim();
|
|
15478
15812
|
if (!sessionId)
|
|
15479
15813
|
throw new Error("host session id is required for dispatch_open_asks");
|
|
15480
|
-
toolSchema(input.tool).parse(input.args);
|
|
15481
15814
|
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
15482
15815
|
const response = await client.openAsks(sessionId);
|
|
15483
15816
|
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
15484
15817
|
}
|
|
15485
|
-
const
|
|
15486
|
-
const exec = input.exec ?? defaultExec;
|
|
15487
|
-
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
|
|
15488
|
-
const args = toolSchema(input.tool).parse(ownerArguments.args);
|
|
15818
|
+
const args = parsed.success ? parsed.data : ownerArguments.args;
|
|
15489
15819
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
15490
15820
|
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
15491
15821
|
const owner = ownerArguments.owner?.kind === "issue" ? {
|
|
@@ -15525,8 +15855,8 @@ async function executeDispatchTool(input) {
|
|
|
15525
15855
|
actor
|
|
15526
15856
|
});
|
|
15527
15857
|
return {
|
|
15528
|
-
text: `Created ${created.key}: ${created.title}`,
|
|
15529
|
-
details: { issue: created.key
|
|
15858
|
+
text: `Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
|
|
15859
|
+
details: { issue: created.key }
|
|
15530
15860
|
};
|
|
15531
15861
|
} catch (error) {
|
|
15532
15862
|
if (!(error instanceof DispatchServiceError) || error.code !== "POSSIBLE_DUPLICATE") {
|
|
@@ -15568,8 +15898,6 @@ async function executeDispatchTool(input) {
|
|
|
15568
15898
|
}
|
|
15569
15899
|
case "dispatch_resolve_ask": {
|
|
15570
15900
|
const kind = stringArg(args, "kind");
|
|
15571
|
-
if (kind !== "retracted" && kind !== "resolved")
|
|
15572
|
-
throw new Error("kind must be retracted or resolved");
|
|
15573
15901
|
const ask = await client.resolveAsk(stringArg(args, "ask"), {
|
|
15574
15902
|
kind,
|
|
15575
15903
|
reason: stringArg(args, "reason"),
|
|
@@ -15579,7 +15907,7 @@ async function executeDispatchTool(input) {
|
|
|
15579
15907
|
throw new Error("resolved ask is missing its resolution");
|
|
15580
15908
|
return {
|
|
15581
15909
|
text: `${kind === "retracted" ? "Retracted" : "Resolved"} ask ${ask.id}: ${ask.resolution.reason}`,
|
|
15582
|
-
details: await
|
|
15910
|
+
details: await askOwnerDetails(client, ask)
|
|
15583
15911
|
};
|
|
15584
15912
|
}
|
|
15585
15913
|
case "dispatch_ask": {
|
|
@@ -15602,9 +15930,11 @@ async function executeDispatchTool(input) {
|
|
|
15602
15930
|
actor
|
|
15603
15931
|
};
|
|
15604
15932
|
const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue(), askInput);
|
|
15933
|
+
const askOwner = ask.issue_key !== null ? issueTopic(ask.issue_key) : resolved === undefined ? issueTopic(issue()) : documentTopic(resolved.artifact);
|
|
15605
15934
|
return {
|
|
15606
|
-
text: `
|
|
15607
|
-
|
|
15935
|
+
text: `Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
|
|
15936
|
+
${followsAsk(askOwner)}`,
|
|
15937
|
+
details: await followedAskDetails(client, ask, resolved)
|
|
15608
15938
|
};
|
|
15609
15939
|
}
|
|
15610
15940
|
case "dispatch_edit_ask": {
|
|
@@ -15621,29 +15951,17 @@ async function executeDispatchTool(input) {
|
|
|
15621
15951
|
});
|
|
15622
15952
|
return {
|
|
15623
15953
|
text: `Ask edited: ${ask.question}`,
|
|
15624
|
-
details: await
|
|
15954
|
+
details: await askOwnerDetails(client, ask)
|
|
15625
15955
|
};
|
|
15626
15956
|
}
|
|
15627
15957
|
case "dispatch_comment": {
|
|
15628
15958
|
const artifactReference = optionalString(args, "artifact");
|
|
15629
|
-
if (optionalString(args, "quote") !== undefined && artifactReference === undefined) {
|
|
15630
|
-
throw new Error("artifact is required when quote is supplied");
|
|
15631
|
-
}
|
|
15632
15959
|
const owner = documentOwner();
|
|
15633
15960
|
const resolved = owner.kind === "project" || artifactReference === undefined ? owner.kind === "project" ? await resolveArtifact(client, owner, artifactReference) : undefined : await resolveArtifact(client, owner, artifactReference);
|
|
15634
15961
|
const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
|
|
15635
15962
|
const replyTo = optionalString(args, "reply_to");
|
|
15636
15963
|
const replyToAsk = optionalString(args, "reply_to_ask");
|
|
15637
|
-
if (replyTo !== undefined && replyToAsk !== undefined) {
|
|
15638
|
-
throw new Error("reply_to and reply_to_ask cannot both be set");
|
|
15639
|
-
}
|
|
15640
15964
|
const requestedTurn = optionalString(args, "turn");
|
|
15641
|
-
if (requestedTurn !== undefined && replyToAsk === undefined) {
|
|
15642
|
-
throw new Error("turn requires reply_to_ask");
|
|
15643
|
-
}
|
|
15644
|
-
if (requestedTurn !== undefined && requestedTurn !== "agent" && requestedTurn !== "human") {
|
|
15645
|
-
throw new Error("turn must be agent or human");
|
|
15646
|
-
}
|
|
15647
15965
|
const commentInput = {
|
|
15648
15966
|
body: stringArg(args, "body"),
|
|
15649
15967
|
...anchored === undefined ? {} : { anchor: anchored },
|
|
@@ -15653,18 +15971,23 @@ async function executeDispatchTool(input) {
|
|
|
15653
15971
|
actor
|
|
15654
15972
|
};
|
|
15655
15973
|
const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
|
|
15656
|
-
const
|
|
15974
|
+
const commentOwner = resolved === undefined ? issueTopic(issue()) : resolvedTopic(resolved);
|
|
15975
|
+
const commentDetails = resolved === undefined ? { issue: comment.issue_key, comment: comment.id } : writeResultDetails(resolved, { comment: comment.id });
|
|
15976
|
+
if (replyToAsk !== undefined) {
|
|
15977
|
+
const askState = comment.turn === null ? "" : `; ask now waiting on ${comment.turn}`;
|
|
15978
|
+
return {
|
|
15979
|
+
text: `Replied on ask ${replyToAsk} (comment ${comment.id}${askState}). ${followsAsk(commentOwner)}`,
|
|
15980
|
+
details: {
|
|
15981
|
+
...commentDetails,
|
|
15982
|
+
ask: replyToAsk,
|
|
15983
|
+
follows: { ask: replyToAsk },
|
|
15984
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
15985
|
+
}
|
|
15986
|
+
};
|
|
15987
|
+
}
|
|
15657
15988
|
return {
|
|
15658
|
-
text: `Posted comment ${comment.id}${
|
|
15659
|
-
details:
|
|
15660
|
-
issue: comment.issue_key,
|
|
15661
|
-
topic: dispatchIssueSubject(issue(), ">"),
|
|
15662
|
-
comment: comment.id,
|
|
15663
|
-
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
15664
|
-
} : writeResultDetails(resolved, {
|
|
15665
|
-
comment: comment.id,
|
|
15666
|
-
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
15667
|
-
})
|
|
15989
|
+
text: `Posted comment ${comment.id} ${notSubscribed(commentOwner)}`,
|
|
15990
|
+
details: commentDetails
|
|
15668
15991
|
};
|
|
15669
15992
|
}
|
|
15670
15993
|
case "dispatch_suggest": {
|
|
@@ -15681,7 +16004,7 @@ async function executeDispatchTool(input) {
|
|
|
15681
16004
|
};
|
|
15682
16005
|
const comment = resolved.owner.kind === "project" ? await client.artifactSuggest(resolved.artifact.id, suggestionInput) : await client.suggest(issue(), suggestionInput);
|
|
15683
16006
|
return {
|
|
15684
|
-
text: `Posted suggestion ${comment.id}`,
|
|
16007
|
+
text: `Posted suggestion ${comment.id} ${notSubscribed(resolvedTopic(resolved))}`,
|
|
15685
16008
|
details: writeResultDetails(resolved, { comment: comment.id })
|
|
15686
16009
|
};
|
|
15687
16010
|
}
|
|
@@ -15695,12 +16018,8 @@ async function executeDispatchTool(input) {
|
|
|
15695
16018
|
});
|
|
15696
16019
|
const messageRef = `dispatch://${issueKey}/message/${message.id}`;
|
|
15697
16020
|
return {
|
|
15698
|
-
text: `Posted message ${message.id} (${messageRef})`,
|
|
15699
|
-
details: {
|
|
15700
|
-
issue: issueKey,
|
|
15701
|
-
topic: dispatchIssueSubject(issueKey, ">"),
|
|
15702
|
-
message: message.id
|
|
15703
|
-
}
|
|
16021
|
+
text: `Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
|
|
16022
|
+
details: { issue: issueKey, message: message.id }
|
|
15704
16023
|
};
|
|
15705
16024
|
}
|
|
15706
16025
|
case "dispatch_doc_edit": {
|
|
@@ -15714,8 +16033,9 @@ async function executeDispatchTool(input) {
|
|
|
15714
16033
|
});
|
|
15715
16034
|
const retyped = ops.filter((operation) => operation.op === "retype").length;
|
|
15716
16035
|
const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
|
|
16036
|
+
const applied = retyped === 0 ? `Applied ${edited.applied} ops (${versionText})` : `Applied ${edited.applied} ops; retyped ${retyped} block${retyped === 1 ? "" : "s"} (${versionText})`;
|
|
15717
16037
|
return {
|
|
15718
|
-
text:
|
|
16038
|
+
text: `${applied} ${notSubscribed(resolvedTopic(resolved))}`,
|
|
15719
16039
|
details: writeResultDetails(resolved, {
|
|
15720
16040
|
applied: edited.applied,
|
|
15721
16041
|
...edited.version === null ? {} : { version: edited.version.number }
|
|
@@ -15758,7 +16078,7 @@ ${trailer.join(`
|
|
|
15758
16078
|
}
|
|
15759
16079
|
};
|
|
15760
16080
|
}
|
|
15761
|
-
const details = await
|
|
16081
|
+
const details = await followedAskDetails(client, result.ask, resolved);
|
|
15762
16082
|
return {
|
|
15763
16083
|
text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
|
|
15764
16084
|
details: { ...details, artifact: resolved.artifact.id, version: result.version }
|
|
@@ -15782,32 +16102,53 @@ ${trailer.join(`
|
|
|
15782
16102
|
const artifactOwner = documentOwner();
|
|
15783
16103
|
const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
|
|
15784
16104
|
const artifactRef = artifactOwner.kind === "project" ? `dispatch://${artifactOwner.project}/artifact/${result.artifact.slug}` : `dispatch://${issue()}/artifact/${result.artifact.slug}`;
|
|
16105
|
+
const uploadOwner = artifactOwner.kind === "project" ? documentTopic(result.artifact) : issueTopic(issue());
|
|
15785
16106
|
return {
|
|
15786
|
-
text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef})`,
|
|
16107
|
+
text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef}) ${notSubscribed(uploadOwner)}`,
|
|
15787
16108
|
details: artifactOwner.kind === "project" ? {
|
|
15788
16109
|
...documentResultDetails(result.artifact),
|
|
15789
16110
|
version: result.version.number
|
|
15790
16111
|
} : {
|
|
15791
16112
|
issue: issue(),
|
|
15792
|
-
topic: dispatchIssueSubject(issue(), ">"),
|
|
15793
16113
|
artifact: result.artifact.id,
|
|
15794
16114
|
version: result.version.number
|
|
15795
16115
|
}
|
|
15796
16116
|
};
|
|
15797
16117
|
}
|
|
16118
|
+
case "dispatch_follow": {
|
|
16119
|
+
const sessionId = input.sessionId?.trim();
|
|
16120
|
+
if (!sessionId)
|
|
16121
|
+
throw new Error("host session id is required for dispatch_follow");
|
|
16122
|
+
const ask = askId(args);
|
|
16123
|
+
const action = stringArg(args, "action");
|
|
16124
|
+
if (action === "unfollow") {
|
|
16125
|
+
await client.unfollowAsk(ask, sessionId, actor);
|
|
16126
|
+
return { text: `Unfollowed ask ${ask}.`, details: { ask } };
|
|
16127
|
+
}
|
|
16128
|
+
const read = await client.getAsk(ask);
|
|
16129
|
+
await client.followAsk(ask, sessionId, actor);
|
|
16130
|
+
return {
|
|
16131
|
+
text: `Following ask ${ask}: its answer and replies reach this session directly.`,
|
|
16132
|
+
details: await followedAskDetails(client, read.ask)
|
|
16133
|
+
};
|
|
16134
|
+
}
|
|
15798
16135
|
case "dispatch_read": {
|
|
15799
16136
|
if (ownerArguments.ref?.kind === "ask") {
|
|
15800
|
-
const
|
|
16137
|
+
const ref = ownerArguments.ref;
|
|
16138
|
+
const id = await resolveIdPrefix(input.tool, "ask", ref, async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id, "all"));
|
|
16139
|
+
const askRead = await client.getAsk(id);
|
|
15801
16140
|
return {
|
|
15802
16141
|
text: askSummary(askRead),
|
|
15803
|
-
details:
|
|
16142
|
+
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue }
|
|
15804
16143
|
};
|
|
15805
16144
|
}
|
|
15806
16145
|
if (ownerArguments.ref?.kind === "comment") {
|
|
15807
|
-
const
|
|
16146
|
+
const ref = ownerArguments.ref;
|
|
16147
|
+
const id = await resolveIdPrefix(input.tool, "comment", ref, async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id));
|
|
16148
|
+
const comment = await client.getComment(id);
|
|
15808
16149
|
return {
|
|
15809
16150
|
text: commentSummary(comment),
|
|
15810
|
-
details:
|
|
16151
|
+
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key }
|
|
15811
16152
|
};
|
|
15812
16153
|
}
|
|
15813
16154
|
if (ownerArguments.ref?.kind === "message") {
|
|
@@ -15879,14 +16220,6 @@ function asObject(value) {
|
|
|
15879
16220
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
15880
16221
|
}
|
|
15881
16222
|
|
|
15882
|
-
// ../envoy-client/src/dispatch-subscribe.ts
|
|
15883
|
-
function dispatchSubscriptionTopic(details) {
|
|
15884
|
-
if (typeof details !== "object" || details === null || !("topic" in details))
|
|
15885
|
-
return null;
|
|
15886
|
-
const { topic } = details;
|
|
15887
|
-
return typeof topic === "string" && topic.startsWith("notifications.dispatch.") ? topic : null;
|
|
15888
|
-
}
|
|
15889
|
-
|
|
15890
16223
|
// ../envoy-client/src/tool-contract.ts
|
|
15891
16224
|
var DELIVERY_CONTRACT = "Delivery is at-least-once, possibly out of order across topics; use id for dedupe and at for freshness.";
|
|
15892
16225
|
var TOPIC_GUIDE = "Topic guide (all are under notifications.): agent.<session_id> (subscribe: own inbox); role.<role> " + "(publish-to; holders claim via envoy_role_set). > matches one or more trailing tokens and does not " + "match the base subject. Envoy registers the concrete base when you subscribe to <subject>.>, so " + "the recommended default remains github.<owner>.<repo>.pr.<n>.>. Default PR subscription: " + "github.<owner>.<repo>.pr.<n>.> (it receives the quiet PR family): pr.<n> (lifecycle: " + "opened/synchronize/closed; closed carries merged, merge_commit_sha, merged_by, head_sha), " + "pr.<n>.comment, pr.<n>.review, pr.<n>.mention, pr.<n>.checks (one head-checks settlement event: " + 'passed/failed/cancelled/skipped with failing names and URLs; re-fires with superseded_settlement: "true" ' + "when new runs appear for the same head). Other GitHub: issue.<n>, issue.<n>.comment, " + "issue.<n>.mention, mention, push.branch.<name>, push.tag.<name>, workflow.<file>.<action> (only " + "runs without an associated PR); slack.<team>.<channel>.message|mention and " + "slack.<team>.<channel>.thread.<ts>.message|mention; ghostwispr.<session>.<kind>; " + "whatsapp.<phone>.<jid>.<kind>; envoy.exceptions.<original-topic>.";
|
|
@@ -15896,8 +16229,8 @@ function messageMetadataShape(schema) {
|
|
|
15896
16229
|
return {
|
|
15897
16230
|
in_reply_to: schema.string().optional(),
|
|
15898
16231
|
supersedes: schema.string().optional(),
|
|
15899
|
-
urgency: schema.enum(URGENCY_VALUES).optional(),
|
|
15900
|
-
expects_reply: schema.enum(EXPECTS_REPLY_VALUES).optional(),
|
|
16232
|
+
urgency: schema.enum(URGENCY_VALUES).describe("low | med | high | blocking").optional(),
|
|
16233
|
+
expects_reply: schema.enum(EXPECTS_REPLY_VALUES).describe("none | optional | required").optional(),
|
|
15901
16234
|
expires_at: schema.number({ int: true }).optional()
|
|
15902
16235
|
};
|
|
15903
16236
|
}
|
|
@@ -16500,24 +16833,6 @@ var server_default = async (input) => {
|
|
|
16500
16833
|
}
|
|
16501
16834
|
}
|
|
16502
16835
|
},
|
|
16503
|
-
"tool.execute.after": async (input, output) => {
|
|
16504
|
-
const topic = dispatchSubscriptionTopic(output.metadata);
|
|
16505
|
-
if (!topic)
|
|
16506
|
-
return;
|
|
16507
|
-
try {
|
|
16508
|
-
await envoy.subscribe({
|
|
16509
|
-
sessionID: input.sessionID,
|
|
16510
|
-
directory: cwd,
|
|
16511
|
-
topics: [topic],
|
|
16512
|
-
port: currentPort() ?? 0,
|
|
16513
|
-
title: activeSessionTitle ?? "",
|
|
16514
|
-
driving: true
|
|
16515
|
-
});
|
|
16516
|
-
} catch (err) {
|
|
16517
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
16518
|
-
logger.warn(`[envoy-plugin] dispatch auto-subscribe failed: ${message}`);
|
|
16519
|
-
}
|
|
16520
|
-
},
|
|
16521
16836
|
dispose: () => {
|
|
16522
16837
|
clearInterval(timer);
|
|
16523
16838
|
clearInterval(heartbeatInterval);
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -10,7 +10,9 @@ decide, never a log of your work. The transcript is your scratch pad; progress a
|
|
|
10
10
|
goes through a `dispatch_*` tool.
|
|
11
11
|
|
|
12
12
|
The server enforces high signal: an ask question is at most 800 characters with at most eight options; comment and message bodies are at
|
|
13
|
-
most 2,000 characters; an artifact is at most 25 MiB. It refuses over-limit input
|
|
13
|
+
most 2,000 characters; an artifact is at most 25 MiB. It refuses over-limit input with the number to trim (`question is 50 characters over
|
|
14
|
+
the 800-character limit (850/800)`); it never truncates it. A tool call with several problems is refused once, every problem listed
|
|
15
|
+
(`<tool> was not called: N problems`), so one corrected call lands. GitHub threads and markers no
|
|
14
16
|
longer exist.
|
|
15
17
|
|
|
16
18
|
## Writing for the human
|
|
@@ -95,7 +97,7 @@ Architects create newly tracked child work with:
|
|
|
95
97
|
dispatch_issue({ project, title, parent?, external?, spec?, force?, labels?: string[], priority?: 0 | 1 | 2 | 3 })
|
|
96
98
|
```
|
|
97
99
|
`labels` are optional initial labels: Dispatch trims them, preserves their case, and removes case-insensitive duplicates. Set `priority` on creation only when the human's intent makes the bucket clear; otherwise priority remains the human's decision. It returns
|
|
98
|
-
`details` `{ issue
|
|
100
|
+
`details` `{ issue }`; creating an issue does not subscribe you to it (see [Following](#following)). Use `dispatch_issue` only to create an issue; never use it to park a question. When `spec` is supplied,
|
|
99
101
|
follow [Writing a spec](#writing-a-spec).
|
|
100
102
|
|
|
101
103
|
## Search first
|
|
@@ -129,7 +131,7 @@ dispatch_ask({
|
|
|
129
131
|
anchor?: { artifact, quote, occurrence? },
|
|
130
132
|
})
|
|
131
133
|
```
|
|
132
|
-
It returns `details` `{ issue,
|
|
134
|
+
It returns `details` `{ issue, ask, follows: { ask } }` for an issue or `{ project, artifact, document, ask, follows: { ask } }` for a project document: you follow the ask you opened (see [Following](#following)).
|
|
133
135
|
|
|
134
136
|
References belong in the question text; `ref` is sugar that appends its `dispatch://` value to the question as a rendered link.
|
|
135
137
|
|
|
@@ -246,7 +248,7 @@ It returns live or versioned markdown with open marks. `issue` with an omitted `
|
|
|
246
248
|
```ts
|
|
247
249
|
dispatch_doc_edit({ issue?, project?, artifact, ops, summary? })
|
|
248
250
|
```
|
|
249
|
-
It returns issue or project-document owner details plus `applied
|
|
251
|
+
It returns issue or project-document owner details plus `applied` and optional `version`. `ops` is an array of this
|
|
250
252
|
exact `EditOp` shape:
|
|
251
253
|
|
|
252
254
|
```ts
|
|
@@ -273,7 +275,8 @@ within one textblock; split changes that span separate blocks into separate oper
|
|
|
273
275
|
insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Ordinary inserts create a sibling block before or after the quote or
|
|
274
276
|
heading's enclosing document block; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no
|
|
275
277
|
header or delimiter rows) extends that table before or after the matched row instead; short rows are padded, wider rows are rejected,
|
|
276
|
-
and deleting a cell's quoted text removes only that text.
|
|
278
|
+
and deleting a cell's quoted text removes only that text. A `find` or quote anchor tolerates inline Markdown (`**bold**`, `` `code` ``)
|
|
279
|
+
and a leading `# ` selects a heading by its text; a miss names the three nearest blocks so the next quote lands.
|
|
277
280
|
|
|
278
281
|
Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target; re-read a missing or ambiguous target before
|
|
279
282
|
retrying. Pass `summary` to name the version when recording a decision.
|
|
@@ -321,7 +324,8 @@ Add feedback with:
|
|
|
321
324
|
dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask?, turn? })
|
|
322
325
|
```
|
|
323
326
|
|
|
324
|
-
It returns issue or project-document owner details plus `comment` and
|
|
327
|
+
It returns issue or project-document owner details plus `comment`; a `reply_to_ask` reply also returns `ask` and `follows: { ask }`,
|
|
328
|
+
because replying to an ask makes you one of its followers (see [Following](#following)).
|
|
325
329
|
`ref` names the owner (an issue or project-document reference) in place of `issue`/`project`.
|
|
326
330
|
`quote` requires `artifact`; its anchor is pinned to the containing block while retaining the quote
|
|
327
331
|
for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
|
|
@@ -339,7 +343,7 @@ Propose an exact replacement instead of describing it:
|
|
|
339
343
|
dispatch_suggest({ issue?, project?, artifact, ref?, quote, replace_with, body?, occurrence? })
|
|
340
344
|
```
|
|
341
345
|
|
|
342
|
-
It returns issue or project-document owner details plus `comment
|
|
346
|
+
It returns issue or project-document owner details plus `comment`. A human accepts or rejects a suggestion.
|
|
343
347
|
Errors: `TARGET_AMBIGUOUS` (add `occurrence`), `TARGET_NOT_FOUND` (re-read first), `INVALID_ANCHOR`/`ANCHOR_MISSING`/`ANCHOR_ORPHANED`
|
|
344
348
|
(bad, unwritten, or stale quote), `INVALID_MARKDOWN`/`DOC_SCHEMA` (malformed content), `CAP_EXCEEDED`, `ISSUE_CLOSED`.
|
|
345
349
|
|
|
@@ -358,8 +362,8 @@ dispatch_artifact({ issue?, project?, name: "load-test-results.md", content: "#
|
|
|
358
362
|
```
|
|
359
363
|
|
|
360
364
|
Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
|
|
361
|
-
Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact
|
|
362
|
-
|
|
365
|
+
Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact` and `version`.
|
|
366
|
+
Uploading the same `name` creates its next version — so uploading `spec.md` **replaces the issue's own specification**
|
|
363
367
|
with your text. Never do that: the spec is edited in place with `dispatch_doc_edit` (see [The Spec](#the-spec)). Address an existing
|
|
364
368
|
artifact by the slug shown in the upload result or by its filename, and a project document by its artifact id, slug, or filename; the
|
|
365
369
|
slug also arrives on `artifact.created` events.
|
|
@@ -393,14 +397,14 @@ pull request is where it is summarised. One message that a human reads beats ten
|
|
|
393
397
|
dispatch_message({ issue, body })
|
|
394
398
|
```
|
|
395
399
|
|
|
396
|
-
It returns `details` `{ issue,
|
|
400
|
+
It returns `details` `{ issue, message }`. `body` is capped at 2,000 characters. A message is not a decision
|
|
397
401
|
(`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
|
|
398
402
|
|
|
399
403
|
### Targeted agent messages
|
|
400
404
|
|
|
401
405
|
A human — or any bearer caller over HTTP, such as a test rig — can target the issue message at a
|
|
402
406
|
live Envoy session or role as **BTW**, **Aside**, or **Steer**. The incoming Dispatch frame names
|
|
403
|
-
the issue and includes a `reply_with`
|
|
407
|
+
the issue and includes a `reply_with` hint (`{ tool, args }`, ready to issue on any host); reply on the same open issue with the existing
|
|
404
408
|
tool, never a new targeted send:
|
|
405
409
|
|
|
406
410
|
```ts
|
|
@@ -420,13 +424,31 @@ through Envoy or the hub. A bearer that targets over HTTP names its own session
|
|
|
420
424
|
target only a session that advertises the mode you want. Sending to a session with no issue
|
|
421
425
|
(`POST /api/v1/agents/{session_id}/messages`) stays human-only.
|
|
422
426
|
|
|
427
|
+
## Following
|
|
428
|
+
|
|
429
|
+
An ask has followers: every session that wrote to it — the session that opened it and every session that replied with
|
|
430
|
+
`dispatch_comment({ reply_to_ask })` — plus any session a human adds from the ask card. The ask's answer, edits, resolution, and
|
|
431
|
+
every reply on it reach each follower's own agent topic directly, whether or not the writer was a human and whatever the issue's
|
|
432
|
+
route. The tool result says so (`You follow this ask: its answer and replies reach you directly.`) and carries `details.follows.ask`;
|
|
433
|
+
the host tells you once per ask. Leave a thread you no longer need, or rejoin one, with:
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
dispatch_follow({ ask, action: "follow" | "unfollow" })
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
`ask` is the full ask id or a `dispatch://KEY/ask/<id>` reference. A human may also remove you from the ask card; either way you are
|
|
440
|
+
told with an `ask.follower_removed` notice, and a human adding you arrives as `ask.follower_added`.
|
|
441
|
+
|
|
442
|
+
No write subscribes you to an issue or document. Following covers your own asks and the threads you joined; everything else on the
|
|
443
|
+
owner — other sessions' asks, comments, messages, status changes — reaches you only if you subscribe to the owner topic yourself.
|
|
444
|
+
Every write result names that line: `envoy_subscribe notifications.dispatch.issue.<KEY>.>` for an issue,
|
|
445
|
+
`envoy_subscribe notifications.dispatch.document.<PROJECT>.<SLUG>.>` for a project document. The owner topic carries every Dispatch
|
|
446
|
+
event; `notify` only controls agent wake and routed delivery. A human may unsubscribe you from the issue or document header; you
|
|
447
|
+
are told with a `subscription.removed` notice when that happens.
|
|
448
|
+
|
|
423
449
|
## What comes back
|
|
424
450
|
|
|
425
|
-
|
|
426
|
-
you so (an already-subscribed write stays quiet — no repeat notice). Issue writes use `notifications.dispatch.issue.<KEY>.>`;
|
|
427
|
-
project-document writes use `notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The owner topic carries every Dispatch event; `notify`
|
|
428
|
-
only controls agent wake and routed delivery. A human may unsubscribe you from the issue or document header; you are told with a
|
|
429
|
-
`subscription.removed` notice when that happens. After a restart, catch up with:
|
|
451
|
+
After a restart, catch up with:
|
|
430
452
|
|
|
431
453
|
```ts
|
|
432
454
|
dispatch_read({ issue?, project?, artifact?, ref? })
|
|
@@ -458,7 +480,11 @@ dispatch://PROJECT/artifact/<document-ref>/ask/<id>
|
|
|
458
480
|
dispatch://PROJECT/artifact/<document-ref>/comment/<id>
|
|
459
481
|
```
|
|
460
482
|
|
|
461
|
-
A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records.
|
|
483
|
+
A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records. `dispatch_read` also
|
|
484
|
+
accepts the dashboard URL of an issue, spec, artifact, ask, comment, or project document on the configured server (it maps to the
|
|
485
|
+
`dispatch://` form above), and an ask or comment id may be a unique prefix of at least 8 hex characters; a message id is always the
|
|
486
|
+
full uuid. A non-uuid id on `GET /asks/{id}`, `/comments/{id}`, or `/issues/{key}/messages/{id}` is a 400 `ASK_ID_INPUT` /
|
|
487
|
+
`COMMENT_ID_INPUT` / `MESSAGE_ID_INPUT`, never a 500.
|
|
462
488
|
|
|
463
489
|
## Before / after
|
|
464
490
|
|
package/skills/envoy/SKILL.md
CHANGED
|
@@ -47,8 +47,17 @@ envoy:
|
|
|
47
47
|
expects_reply: required
|
|
48
48
|
re: agent-message-1
|
|
49
49
|
supersedes: agent-message-0
|
|
50
|
-
reply_with:
|
|
51
|
-
|
|
50
|
+
reply_with:
|
|
51
|
+
tool: envoy_send
|
|
52
|
+
args:
|
|
53
|
+
session_id: 01a0bbbb-cccc-7ddd-eeee-0123456789ab
|
|
54
|
+
in_reply_to: agent-message-2
|
|
55
|
+
message: ...
|
|
56
|
+
reply_role:
|
|
57
|
+
tool: envoy_publish
|
|
58
|
+
args:
|
|
59
|
+
topic: notifications.role.legion-reviewer
|
|
60
|
+
message: ...
|
|
52
61
|
summary: Deployment needs confirmation.
|
|
53
62
|
message: "Confirm the listener health check passed.\n\nThen publish the release."
|
|
54
63
|
note: body names session 01a0cccc-dddd-7eee-ffff-0123456789ab; the sender is 01a0bbbb-cccc-7ddd-eeee-0123456789ab
|
|
@@ -62,7 +71,10 @@ envoy:
|
|
|
62
71
|
- `by` is the expiry deadline, when the sender supplied one.
|
|
63
72
|
- `urgency` is the sender's priority classification.
|
|
64
73
|
- `expects_reply` states whether a reply is `none`, `optional`, or `required`.
|
|
65
|
-
- `re` names the delivery this message replies to.
|
|
74
|
+
- `re` names the delivery this message replies to. A Dispatch frame names it as a `dispatch://`
|
|
75
|
+
ref — the ask (`dispatch://KEY/ask/<id>`, or `dispatch://PROJECT/artifact/<slug>/ask/<id>` for a
|
|
76
|
+
document ask) or the message (`dispatch://KEY/message/<id>`) — never the quoted text; the
|
|
77
|
+
question head, when there is one, is `dispatch.question`.
|
|
66
78
|
- `supersedes` names an earlier delivery this one replaces.
|
|
67
79
|
- `reply_with` is the direct-reply call for the sender.
|
|
68
80
|
- `reply_role` is the role-publish reply call when the sender has a role.
|
package/src/server.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { agentSubject, dispatchToolSpecs, zodSchemaApi } from "@legion/contracts
|
|
|
5
5
|
import { envoyDefaultsFromEnvironment } from "@legion/envoy-client/defaults";
|
|
6
6
|
import { resolveDispatchConfig } from "@legion/envoy-client/dispatch-config";
|
|
7
7
|
import { executeDispatchTool } from "@legion/envoy-client/dispatch-execute";
|
|
8
|
-
import { dispatchSubscriptionTopic } from "@legion/envoy-client/dispatch-subscribe";
|
|
9
8
|
import { machineID } from "@legion/envoy-client/machine";
|
|
10
9
|
import {
|
|
11
10
|
envoyToolSpecs,
|
|
@@ -269,29 +268,6 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
269
268
|
}
|
|
270
269
|
}
|
|
271
270
|
},
|
|
272
|
-
"tool.execute.after": async (
|
|
273
|
-
input: { tool: string; sessionID: string; callID: string; args: unknown },
|
|
274
|
-
output: { title: string; output: string; metadata: unknown }
|
|
275
|
-
) => {
|
|
276
|
-
// Native Dispatch tools return `DispatchToolResult.details` in OpenCode
|
|
277
|
-
// output metadata, so only mutations with a Dispatch topic are followed.
|
|
278
|
-
// Best-effort — a subscribe failure must never surface to the model.
|
|
279
|
-
const topic = dispatchSubscriptionTopic(output.metadata);
|
|
280
|
-
if (!topic) return;
|
|
281
|
-
try {
|
|
282
|
-
await envoy.subscribe({
|
|
283
|
-
sessionID: input.sessionID,
|
|
284
|
-
directory: cwd,
|
|
285
|
-
topics: [topic],
|
|
286
|
-
port: currentPort() ?? 0,
|
|
287
|
-
title: activeSessionTitle ?? "",
|
|
288
|
-
driving: true,
|
|
289
|
-
});
|
|
290
|
-
} catch (err) {
|
|
291
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
292
|
-
logger.warn(`[envoy-plugin] dispatch auto-subscribe failed: ${message}`);
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
271
|
// Cleanup hook (used by tests; production relies on process 'exit').
|
|
296
272
|
dispose: () => {
|
|
297
273
|
clearInterval(timer);
|