@sjawhar/opencode-legion-envoy 1.26.0 → 1.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/src/server.js +99 -67
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +32 -13
- 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
|
["<", "<"],
|
|
@@ -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}`,
|
|
@@ -14882,6 +14896,12 @@ class DispatchClient {
|
|
|
14882
14896
|
async getAsk(id) {
|
|
14883
14897
|
return this.#json("GET", ["api", "v1", "asks", id]);
|
|
14884
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
|
+
}
|
|
14885
14905
|
async getComment(id) {
|
|
14886
14906
|
return this.#json("GET", ["api", "v1", "comments", id]);
|
|
14887
14907
|
}
|
|
@@ -15115,12 +15135,33 @@ function formatZodIssues(issues, schema) {
|
|
|
15115
15135
|
}
|
|
15116
15136
|
|
|
15117
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
|
+
}
|
|
15118
15160
|
function documentResultDetails(artifact) {
|
|
15119
15161
|
return {
|
|
15120
15162
|
project: artifact.project,
|
|
15121
15163
|
artifact: artifact.id,
|
|
15122
|
-
document: `${artifact.project}/${artifact.slug}
|
|
15123
|
-
topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
|
|
15164
|
+
document: `${artifact.project}/${artifact.slug}`
|
|
15124
15165
|
};
|
|
15125
15166
|
}
|
|
15126
15167
|
function writeResultDetails(resolved, fields) {
|
|
@@ -15129,19 +15170,11 @@ function writeResultDetails(resolved, fields) {
|
|
|
15129
15170
|
}
|
|
15130
15171
|
if (resolved.issue === undefined)
|
|
15131
15172
|
throw new Error("issue document is missing its issue");
|
|
15132
|
-
return {
|
|
15133
|
-
issue: resolved.issue.key,
|
|
15134
|
-
topic: dispatchIssueSubject(resolved.issue.key, ">"),
|
|
15135
|
-
...fields
|
|
15136
|
-
};
|
|
15173
|
+
return { issue: resolved.issue.key, ...fields };
|
|
15137
15174
|
}
|
|
15138
|
-
async function
|
|
15175
|
+
async function askOwnerDetails(client, ask, resolved) {
|
|
15139
15176
|
if (ask.issue_key !== null) {
|
|
15140
|
-
return {
|
|
15141
|
-
issue: ask.issue_key,
|
|
15142
|
-
topic: dispatchIssueSubject(ask.issue_key, ">"),
|
|
15143
|
-
ask: ask.id
|
|
15144
|
-
};
|
|
15177
|
+
return { issue: ask.issue_key, ask: ask.id };
|
|
15145
15178
|
}
|
|
15146
15179
|
if (ask.artifact_id === undefined || ask.artifact_id === null) {
|
|
15147
15180
|
throw new Error("document ask is missing its artifact ID");
|
|
@@ -15149,6 +15182,9 @@ async function askResultDetails(client, ask, resolved) {
|
|
|
15149
15182
|
const artifact = resolved?.artifact ?? await client.getArtifact(ask.artifact_id);
|
|
15150
15183
|
return { ...documentResultDetails(artifact), ask: ask.id };
|
|
15151
15184
|
}
|
|
15185
|
+
async function followedAskDetails(client, ask, resolved) {
|
|
15186
|
+
return { ...await askOwnerDetails(client, ask, resolved), follows: { ask: ask.id } };
|
|
15187
|
+
}
|
|
15152
15188
|
var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
15153
15189
|
var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
|
|
15154
15190
|
var bareIssueNumberPattern = /^[1-9][0-9]*$/;
|
|
@@ -15156,6 +15192,7 @@ var issueFreeTools = {
|
|
|
15156
15192
|
dispatch_issue: true,
|
|
15157
15193
|
dispatch_edit_ask: true,
|
|
15158
15194
|
dispatch_resolve_ask: true,
|
|
15195
|
+
dispatch_follow: true,
|
|
15159
15196
|
dispatch_search: true,
|
|
15160
15197
|
dispatch_open_asks: true
|
|
15161
15198
|
};
|
|
@@ -15818,8 +15855,8 @@ async function executeDispatchTool(input) {
|
|
|
15818
15855
|
actor
|
|
15819
15856
|
});
|
|
15820
15857
|
return {
|
|
15821
|
-
text: `Created ${created.key}: ${created.title}`,
|
|
15822
|
-
details: { issue: created.key
|
|
15858
|
+
text: `Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
|
|
15859
|
+
details: { issue: created.key }
|
|
15823
15860
|
};
|
|
15824
15861
|
} catch (error) {
|
|
15825
15862
|
if (!(error instanceof DispatchServiceError) || error.code !== "POSSIBLE_DUPLICATE") {
|
|
@@ -15870,7 +15907,7 @@ async function executeDispatchTool(input) {
|
|
|
15870
15907
|
throw new Error("resolved ask is missing its resolution");
|
|
15871
15908
|
return {
|
|
15872
15909
|
text: `${kind === "retracted" ? "Retracted" : "Resolved"} ask ${ask.id}: ${ask.resolution.reason}`,
|
|
15873
|
-
details: await
|
|
15910
|
+
details: await askOwnerDetails(client, ask)
|
|
15874
15911
|
};
|
|
15875
15912
|
}
|
|
15876
15913
|
case "dispatch_ask": {
|
|
@@ -15893,9 +15930,11 @@ async function executeDispatchTool(input) {
|
|
|
15893
15930
|
actor
|
|
15894
15931
|
};
|
|
15895
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);
|
|
15896
15934
|
return {
|
|
15897
|
-
text: `
|
|
15898
|
-
|
|
15935
|
+
text: `Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
|
|
15936
|
+
${followsAsk(askOwner)}`,
|
|
15937
|
+
details: await followedAskDetails(client, ask, resolved)
|
|
15899
15938
|
};
|
|
15900
15939
|
}
|
|
15901
15940
|
case "dispatch_edit_ask": {
|
|
@@ -15912,7 +15951,7 @@ async function executeDispatchTool(input) {
|
|
|
15912
15951
|
});
|
|
15913
15952
|
return {
|
|
15914
15953
|
text: `Ask edited: ${ask.question}`,
|
|
15915
|
-
details: await
|
|
15954
|
+
details: await askOwnerDetails(client, ask)
|
|
15916
15955
|
};
|
|
15917
15956
|
}
|
|
15918
15957
|
case "dispatch_comment": {
|
|
@@ -15932,18 +15971,23 @@ async function executeDispatchTool(input) {
|
|
|
15932
15971
|
actor
|
|
15933
15972
|
};
|
|
15934
15973
|
const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
|
|
15935
|
-
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
|
+
}
|
|
15936
15988
|
return {
|
|
15937
|
-
text: `Posted comment ${comment.id}${
|
|
15938
|
-
details:
|
|
15939
|
-
issue: comment.issue_key,
|
|
15940
|
-
topic: dispatchIssueSubject(issue(), ">"),
|
|
15941
|
-
comment: comment.id,
|
|
15942
|
-
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
15943
|
-
} : writeResultDetails(resolved, {
|
|
15944
|
-
comment: comment.id,
|
|
15945
|
-
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
15946
|
-
})
|
|
15989
|
+
text: `Posted comment ${comment.id} ${notSubscribed(commentOwner)}`,
|
|
15990
|
+
details: commentDetails
|
|
15947
15991
|
};
|
|
15948
15992
|
}
|
|
15949
15993
|
case "dispatch_suggest": {
|
|
@@ -15960,7 +16004,7 @@ async function executeDispatchTool(input) {
|
|
|
15960
16004
|
};
|
|
15961
16005
|
const comment = resolved.owner.kind === "project" ? await client.artifactSuggest(resolved.artifact.id, suggestionInput) : await client.suggest(issue(), suggestionInput);
|
|
15962
16006
|
return {
|
|
15963
|
-
text: `Posted suggestion ${comment.id}`,
|
|
16007
|
+
text: `Posted suggestion ${comment.id} ${notSubscribed(resolvedTopic(resolved))}`,
|
|
15964
16008
|
details: writeResultDetails(resolved, { comment: comment.id })
|
|
15965
16009
|
};
|
|
15966
16010
|
}
|
|
@@ -15974,12 +16018,8 @@ async function executeDispatchTool(input) {
|
|
|
15974
16018
|
});
|
|
15975
16019
|
const messageRef = `dispatch://${issueKey}/message/${message.id}`;
|
|
15976
16020
|
return {
|
|
15977
|
-
text: `Posted message ${message.id} (${messageRef})`,
|
|
15978
|
-
details: {
|
|
15979
|
-
issue: issueKey,
|
|
15980
|
-
topic: dispatchIssueSubject(issueKey, ">"),
|
|
15981
|
-
message: message.id
|
|
15982
|
-
}
|
|
16021
|
+
text: `Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
|
|
16022
|
+
details: { issue: issueKey, message: message.id }
|
|
15983
16023
|
};
|
|
15984
16024
|
}
|
|
15985
16025
|
case "dispatch_doc_edit": {
|
|
@@ -15993,8 +16033,9 @@ async function executeDispatchTool(input) {
|
|
|
15993
16033
|
});
|
|
15994
16034
|
const retyped = ops.filter((operation) => operation.op === "retype").length;
|
|
15995
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})`;
|
|
15996
16037
|
return {
|
|
15997
|
-
text:
|
|
16038
|
+
text: `${applied} ${notSubscribed(resolvedTopic(resolved))}`,
|
|
15998
16039
|
details: writeResultDetails(resolved, {
|
|
15999
16040
|
applied: edited.applied,
|
|
16000
16041
|
...edited.version === null ? {} : { version: edited.version.number }
|
|
@@ -16037,7 +16078,7 @@ ${trailer.join(`
|
|
|
16037
16078
|
}
|
|
16038
16079
|
};
|
|
16039
16080
|
}
|
|
16040
|
-
const details = await
|
|
16081
|
+
const details = await followedAskDetails(client, result.ask, resolved);
|
|
16041
16082
|
return {
|
|
16042
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.`,
|
|
16043
16084
|
details: { ...details, artifact: resolved.artifact.id, version: result.version }
|
|
@@ -16061,19 +16102,36 @@ ${trailer.join(`
|
|
|
16061
16102
|
const artifactOwner = documentOwner();
|
|
16062
16103
|
const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
|
|
16063
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());
|
|
16064
16106
|
return {
|
|
16065
|
-
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)}`,
|
|
16066
16108
|
details: artifactOwner.kind === "project" ? {
|
|
16067
16109
|
...documentResultDetails(result.artifact),
|
|
16068
16110
|
version: result.version.number
|
|
16069
16111
|
} : {
|
|
16070
16112
|
issue: issue(),
|
|
16071
|
-
topic: dispatchIssueSubject(issue(), ">"),
|
|
16072
16113
|
artifact: result.artifact.id,
|
|
16073
16114
|
version: result.version.number
|
|
16074
16115
|
}
|
|
16075
16116
|
};
|
|
16076
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
|
+
}
|
|
16077
16135
|
case "dispatch_read": {
|
|
16078
16136
|
if (ownerArguments.ref?.kind === "ask") {
|
|
16079
16137
|
const ref = ownerArguments.ref;
|
|
@@ -16162,14 +16220,6 @@ function asObject(value) {
|
|
|
16162
16220
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
16163
16221
|
}
|
|
16164
16222
|
|
|
16165
|
-
// ../envoy-client/src/dispatch-subscribe.ts
|
|
16166
|
-
function dispatchSubscriptionTopic(details) {
|
|
16167
|
-
if (typeof details !== "object" || details === null || !("topic" in details))
|
|
16168
|
-
return null;
|
|
16169
|
-
const { topic } = details;
|
|
16170
|
-
return typeof topic === "string" && topic.startsWith("notifications.dispatch.") ? topic : null;
|
|
16171
|
-
}
|
|
16172
|
-
|
|
16173
16223
|
// ../envoy-client/src/tool-contract.ts
|
|
16174
16224
|
var DELIVERY_CONTRACT = "Delivery is at-least-once, possibly out of order across topics; use id for dedupe and at for freshness.";
|
|
16175
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>.";
|
|
@@ -16783,24 +16833,6 @@ var server_default = async (input) => {
|
|
|
16783
16833
|
}
|
|
16784
16834
|
}
|
|
16785
16835
|
},
|
|
16786
|
-
"tool.execute.after": async (input, output) => {
|
|
16787
|
-
const topic = dispatchSubscriptionTopic(output.metadata);
|
|
16788
|
-
if (!topic)
|
|
16789
|
-
return;
|
|
16790
|
-
try {
|
|
16791
|
-
await envoy.subscribe({
|
|
16792
|
-
sessionID: input.sessionID,
|
|
16793
|
-
directory: cwd,
|
|
16794
|
-
topics: [topic],
|
|
16795
|
-
port: currentPort() ?? 0,
|
|
16796
|
-
title: activeSessionTitle ?? "",
|
|
16797
|
-
driving: true
|
|
16798
|
-
});
|
|
16799
|
-
} catch (err) {
|
|
16800
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
16801
|
-
logger.warn(`[envoy-plugin] dispatch auto-subscribe failed: ${message}`);
|
|
16802
|
-
}
|
|
16803
|
-
},
|
|
16804
16836
|
dispose: () => {
|
|
16805
16837
|
clearInterval(timer);
|
|
16806
16838
|
clearInterval(heartbeatInterval);
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -97,7 +97,7 @@ Architects create newly tracked child work with:
|
|
|
97
97
|
dispatch_issue({ project, title, parent?, external?, spec?, force?, labels?: string[], priority?: 0 | 1 | 2 | 3 })
|
|
98
98
|
```
|
|
99
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
|
|
100
|
-
`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,
|
|
101
101
|
follow [Writing a spec](#writing-a-spec).
|
|
102
102
|
|
|
103
103
|
## Search first
|
|
@@ -131,7 +131,7 @@ dispatch_ask({
|
|
|
131
131
|
anchor?: { artifact, quote, occurrence? },
|
|
132
132
|
})
|
|
133
133
|
```
|
|
134
|
-
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)).
|
|
135
135
|
|
|
136
136
|
References belong in the question text; `ref` is sugar that appends its `dispatch://` value to the question as a rendered link.
|
|
137
137
|
|
|
@@ -248,7 +248,7 @@ It returns live or versioned markdown with open marks. `issue` with an omitted `
|
|
|
248
248
|
```ts
|
|
249
249
|
dispatch_doc_edit({ issue?, project?, artifact, ops, summary? })
|
|
250
250
|
```
|
|
251
|
-
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
|
|
252
252
|
exact `EditOp` shape:
|
|
253
253
|
|
|
254
254
|
```ts
|
|
@@ -324,7 +324,8 @@ Add feedback with:
|
|
|
324
324
|
dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask?, turn? })
|
|
325
325
|
```
|
|
326
326
|
|
|
327
|
-
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)).
|
|
328
329
|
`ref` names the owner (an issue or project-document reference) in place of `issue`/`project`.
|
|
329
330
|
`quote` requires `artifact`; its anchor is pinned to the containing block while retaining the quote
|
|
330
331
|
for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
|
|
@@ -342,7 +343,7 @@ Propose an exact replacement instead of describing it:
|
|
|
342
343
|
dispatch_suggest({ issue?, project?, artifact, ref?, quote, replace_with, body?, occurrence? })
|
|
343
344
|
```
|
|
344
345
|
|
|
345
|
-
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.
|
|
346
347
|
Errors: `TARGET_AMBIGUOUS` (add `occurrence`), `TARGET_NOT_FOUND` (re-read first), `INVALID_ANCHOR`/`ANCHOR_MISSING`/`ANCHOR_ORPHANED`
|
|
347
348
|
(bad, unwritten, or stale quote), `INVALID_MARKDOWN`/`DOC_SCHEMA` (malformed content), `CAP_EXCEEDED`, `ISSUE_CLOSED`.
|
|
348
349
|
|
|
@@ -361,8 +362,8 @@ dispatch_artifact({ issue?, project?, name: "load-test-results.md", content: "#
|
|
|
361
362
|
```
|
|
362
363
|
|
|
363
364
|
Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
|
|
364
|
-
Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact
|
|
365
|
-
|
|
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**
|
|
366
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
|
|
367
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
|
|
368
369
|
slug also arrives on `artifact.created` events.
|
|
@@ -396,7 +397,7 @@ pull request is where it is summarised. One message that a human reads beats ten
|
|
|
396
397
|
dispatch_message({ issue, body })
|
|
397
398
|
```
|
|
398
399
|
|
|
399
|
-
It returns `details` `{ issue,
|
|
400
|
+
It returns `details` `{ issue, message }`. `body` is capped at 2,000 characters. A message is not a decision
|
|
400
401
|
(`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
|
|
401
402
|
|
|
402
403
|
### Targeted agent messages
|
|
@@ -423,13 +424,31 @@ through Envoy or the hub. A bearer that targets over HTTP names its own session
|
|
|
423
424
|
target only a session that advertises the mode you want. Sending to a session with no issue
|
|
424
425
|
(`POST /api/v1/agents/{session_id}/messages`) stays human-only.
|
|
425
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
|
+
|
|
426
449
|
## What comes back
|
|
427
450
|
|
|
428
|
-
|
|
429
|
-
you so (an already-subscribed write stays quiet — no repeat notice). Issue writes use `notifications.dispatch.issue.<KEY>.>`;
|
|
430
|
-
project-document writes use `notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The owner topic carries every Dispatch event; `notify`
|
|
431
|
-
only controls agent wake and routed delivery. A human may unsubscribe you from the issue or document header; you are told with a
|
|
432
|
-
`subscription.removed` notice when that happens. After a restart, catch up with:
|
|
451
|
+
After a restart, catch up with:
|
|
433
452
|
|
|
434
453
|
```ts
|
|
435
454
|
dispatch_read({ issue?, project?, artifact?, ref? })
|
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);
|