@sjawhar/pi-legion-envoy 1.36.0 → 1.37.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 +15 -13
- package/dist/envoy.js +151 -75
- package/dist/legion.js +14 -0
- package/dist/skills/dispatch/SKILL.md +32 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,10 +85,10 @@ extension files it does not contain.
|
|
|
85
85
|
|
|
86
86
|
## Native Dispatch tools
|
|
87
87
|
|
|
88
|
-
The extension registers
|
|
89
|
-
`dispatch_resolve_ask`, `dispatch_comment`, `dispatch_suggest`, `dispatch_message`,
|
|
90
|
-
`dispatch_doc_edit`, `dispatch_doc_read`, `dispatch_artifact`, `dispatch_read`,
|
|
91
|
-
`dispatch_search`, when Dispatch configuration resolves both a base URL and bearer token.
|
|
88
|
+
The extension registers fifteen native Dispatch tools: `dispatch_issue`, `dispatch_ask`, `dispatch_edit_ask`,
|
|
89
|
+
`dispatch_resolve_ask`, `dispatch_follow`, `dispatch_comment`, `dispatch_suggest`, `dispatch_message`,
|
|
90
|
+
`dispatch_doc_edit`, `dispatch_doc_read`, `dispatch_request_approval`, `dispatch_artifact`, `dispatch_read`,
|
|
91
|
+
`dispatch_search`, and `dispatch_open_asks`, when Dispatch configuration resolves both a base URL and bearer token.
|
|
92
92
|
|
|
93
93
|
Configure the shared `envoy.json` with:
|
|
94
94
|
|
|
@@ -112,21 +112,23 @@ whose trimmed contents are the token — how the Legion daemon delivers it to a
|
|
|
112
112
|
pane) wins over every other token source and never falls back when unreadable.
|
|
113
113
|
Omitting `dispatch.serverUrl` while `dispatch.enabled` is true targets
|
|
114
114
|
`http://localhost:8766`, the Go server's listen address. Invalid configuration,
|
|
115
|
-
an invalid URL, or an empty token leaves the
|
|
115
|
+
an invalid URL, or an empty token leaves the fifteen tools unavailable and
|
|
116
116
|
reports the source of the error.
|
|
117
117
|
|
|
118
118
|
Owner-scoped calls use either an issue (a native `KEY` or external `owner/repo#n` reference) or
|
|
119
119
|
an unlinked project document (`project` plus its `artifact` slug). A Legion session may omit
|
|
120
120
|
`issue` when `LEGION_ISSUE` identifies its root issue and its working directory resolves to a
|
|
121
121
|
repository. `dispatch_doc_read` and `dispatch_read` also accept `dispatch://` references,
|
|
122
|
-
including `dispatch://PROJECT/artifact/<slug>`. `dispatch_search` needs only its query
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
including `dispatch://PROJECT/artifact/<slug>`. `dispatch_search` needs only its query.
|
|
123
|
+
`dispatch_edit_ask`, `dispatch_resolve_ask`, and `dispatch_follow` instead identify an existing
|
|
124
|
+
ask with `ask`. No result carries a subscription topic: opening an ask or replying to one with
|
|
125
|
+
`dispatch_comment({ reply_to_ask })` makes the session a follower of that ask (its answer and
|
|
126
|
+
replies reach the session directly, server-side), the result carries `details.follows.ask`, and
|
|
127
|
+
the extension's `tool_result` hook tells the model so once per ask. Whole-issue or
|
|
128
|
+
whole-document subscription is the model's own `envoy_subscribe` of the topic every write
|
|
129
|
+
result names (`notifications.dispatch.issue.<KEY>.>` or
|
|
130
|
+
`notifications.dispatch.document.<PROJECT>.<SLUG>.>`). `dispatch_doc_read` and
|
|
131
|
+
`dispatch_read` return owner details only.
|
|
130
132
|
|
|
131
133
|
The shared contract supplies the model-facing schemas and descriptions. The
|
|
132
134
|
`dispatch` skill describes when to use each operation for issues, asks, review
|
package/dist/envoy.js
CHANGED
|
@@ -29779,6 +29779,11 @@ var SubscriptionRemovedEventPayloadSchema = object({
|
|
|
29779
29779
|
pending: boolean2().optional(),
|
|
29780
29780
|
request_event_id: number2().int().positive().optional()
|
|
29781
29781
|
});
|
|
29782
|
+
var AskFollowerEventPayloadSchema = object({
|
|
29783
|
+
ask_id: string2().optional(),
|
|
29784
|
+
session_id: string2().optional(),
|
|
29785
|
+
by: object({ kind: string2(), id: string2().optional() }).passthrough().optional()
|
|
29786
|
+
});
|
|
29782
29787
|
// ../contracts/src/dispatch-snippet.ts
|
|
29783
29788
|
var HTML_ENTITIES = [
|
|
29784
29789
|
["<", "<"],
|
|
@@ -29944,6 +29949,15 @@ var dispatchToolSpecs = [
|
|
|
29944
29949
|
reason: z.string({ min: 1 }).describe("Why the open ask no longer needs a human answer.")
|
|
29945
29950
|
})
|
|
29946
29951
|
},
|
|
29952
|
+
{
|
|
29953
|
+
name: "dispatch_follow",
|
|
29954
|
+
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>.>",
|
|
29955
|
+
arguments: (z) => ({
|
|
29956
|
+
ask: z.string().describe("Full ask id (uuid)."),
|
|
29957
|
+
action: z.enum(["follow", "unfollow"]).describe("follow | unfollow")
|
|
29958
|
+
}),
|
|
29959
|
+
strict: true
|
|
29960
|
+
},
|
|
29947
29961
|
{
|
|
29948
29962
|
name: "dispatch_comment",
|
|
29949
29963
|
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}`,
|
|
@@ -31119,6 +31133,9 @@ function askAnswerText(answer) {
|
|
|
31119
31133
|
// ../envoy-client/src/delivery.ts
|
|
31120
31134
|
var KNOWN_SOURCES = EnvelopeSchema.shape.source.enum;
|
|
31121
31135
|
var FOREIGN_SESSION_ID = /\b01a0[0-9a-f]{4}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}\b/g;
|
|
31136
|
+
var AskAuthorPayloadSchema = object({
|
|
31137
|
+
author: object({ kind: string2(), id: string2() }).passthrough()
|
|
31138
|
+
});
|
|
31122
31139
|
var InboundSenderSchema = object({
|
|
31123
31140
|
session_id: string2().optional(),
|
|
31124
31141
|
machine: string2().optional(),
|
|
@@ -31381,6 +31398,7 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31381
31398
|
let rejectedDelivery;
|
|
31382
31399
|
let malformedDelivery = false;
|
|
31383
31400
|
let inReplyTo = envelope.in_reply_to;
|
|
31401
|
+
let askAuthor;
|
|
31384
31402
|
const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
|
|
31385
31403
|
if (envelope.source === "dispatch") {
|
|
31386
31404
|
if (envelope.payload === undefined) {
|
|
@@ -31406,8 +31424,28 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31406
31424
|
envelope
|
|
31407
31425
|
};
|
|
31408
31426
|
}
|
|
31427
|
+
if (frame.event.type === "ask.follower_added" || frame.event.type === "ask.follower_removed") {
|
|
31428
|
+
const follower = AskFollowerEventPayloadSchema.safeParse(frame.event.payload);
|
|
31429
|
+
if (!follower.success || follower.data.session_id !== sessionID) {
|
|
31430
|
+
return { skip: true, content: "", envelope };
|
|
31431
|
+
}
|
|
31432
|
+
const who = follower.data.by?.id ?? "someone";
|
|
31433
|
+
const owner = dispatchOwner(frame.event, subject ?? envelope.topic);
|
|
31434
|
+
const ask = follower.data.ask_id ?? "?";
|
|
31435
|
+
return {
|
|
31436
|
+
skip: false,
|
|
31437
|
+
content: frame.event.type === "ask.follower_added" ? `Now following ask ${ask} on ${owner} (added by ${who}): its answer and replies reach you directly; dispatch_follow unfollow to stop.` : `No longer following ask ${ask} on ${owner} (removed by ${who}).`,
|
|
31438
|
+
envelope
|
|
31439
|
+
};
|
|
31440
|
+
}
|
|
31409
31441
|
const answered = dispatchAskAnswer(frame.event);
|
|
31410
31442
|
const question = answered?.question ?? dispatchAskQuestion(frame.event);
|
|
31443
|
+
if (frame.event.type.startsWith("ask.")) {
|
|
31444
|
+
const asked = AskAuthorPayloadSchema.safeParse(frame.event.payload);
|
|
31445
|
+
if (asked.success && asked.data.author.kind === "session") {
|
|
31446
|
+
askAuthor = asked.data.author.id;
|
|
31447
|
+
}
|
|
31448
|
+
}
|
|
31411
31449
|
if (inReplyTo !== undefined) {
|
|
31412
31450
|
inReplyTo = dispatchReplyRef(frame.event, subject ?? envelope.topic, inReplyTo);
|
|
31413
31451
|
}
|
|
@@ -31468,7 +31506,7 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31468
31506
|
${envelope.payload ?? ""}`;
|
|
31469
31507
|
let foreignSession;
|
|
31470
31508
|
for (const match of body.matchAll(FOREIGN_SESSION_ID)) {
|
|
31471
|
-
if (match[0] !== envelope.source_session && match[0] !== sessionID) {
|
|
31509
|
+
if (match[0] !== envelope.source_session && match[0] !== sessionID && match[0] !== askAuthor) {
|
|
31472
31510
|
foreignSession = match[0];
|
|
31473
31511
|
break;
|
|
31474
31512
|
}
|
|
@@ -31921,6 +31959,12 @@ class DispatchClient {
|
|
|
31921
31959
|
async getAsk(id) {
|
|
31922
31960
|
return this.#json("GET", ["api", "v1", "asks", id]);
|
|
31923
31961
|
}
|
|
31962
|
+
async followAsk(id, sessionId, actor) {
|
|
31963
|
+
await this.#json("PUT", ["api", "v1", "asks", id, "followers", sessionId], { actor });
|
|
31964
|
+
}
|
|
31965
|
+
async unfollowAsk(id, sessionId, actor) {
|
|
31966
|
+
await this.#json("DELETE", ["api", "v1", "asks", id, "followers", sessionId], { actor });
|
|
31967
|
+
}
|
|
31924
31968
|
async getComment(id) {
|
|
31925
31969
|
return this.#json("GET", ["api", "v1", "comments", id]);
|
|
31926
31970
|
}
|
|
@@ -32154,12 +32198,33 @@ function formatZodIssues(issues, schema) {
|
|
|
32154
32198
|
}
|
|
32155
32199
|
|
|
32156
32200
|
// ../envoy-client/src/dispatch-execute.ts
|
|
32201
|
+
function issueTopic(key) {
|
|
32202
|
+
return { label: key, topic: dispatchIssueSubject(key, ">") };
|
|
32203
|
+
}
|
|
32204
|
+
function documentTopic(artifact) {
|
|
32205
|
+
return {
|
|
32206
|
+
label: `${artifact.project}/${artifact.slug}`,
|
|
32207
|
+
topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
|
|
32208
|
+
};
|
|
32209
|
+
}
|
|
32210
|
+
function resolvedTopic(resolved) {
|
|
32211
|
+
if (resolved.owner.kind === "project")
|
|
32212
|
+
return documentTopic(resolved.artifact);
|
|
32213
|
+
if (resolved.issue === undefined)
|
|
32214
|
+
throw new Error("issue document is missing its issue");
|
|
32215
|
+
return issueTopic(resolved.issue.key);
|
|
32216
|
+
}
|
|
32217
|
+
function notSubscribed(owner) {
|
|
32218
|
+
return `(not subscribed to ${owner.label}; envoy_subscribe ${owner.topic} for every event on it)`;
|
|
32219
|
+
}
|
|
32220
|
+
function followsAsk(owner) {
|
|
32221
|
+
return `You follow this ask: its answer and replies reach you directly. For every event on ${owner.label}: envoy_subscribe ${owner.topic}`;
|
|
32222
|
+
}
|
|
32157
32223
|
function documentResultDetails(artifact) {
|
|
32158
32224
|
return {
|
|
32159
32225
|
project: artifact.project,
|
|
32160
32226
|
artifact: artifact.id,
|
|
32161
|
-
document: `${artifact.project}/${artifact.slug}
|
|
32162
|
-
topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
|
|
32227
|
+
document: `${artifact.project}/${artifact.slug}`
|
|
32163
32228
|
};
|
|
32164
32229
|
}
|
|
32165
32230
|
function writeResultDetails(resolved, fields) {
|
|
@@ -32168,19 +32233,11 @@ function writeResultDetails(resolved, fields) {
|
|
|
32168
32233
|
}
|
|
32169
32234
|
if (resolved.issue === undefined)
|
|
32170
32235
|
throw new Error("issue document is missing its issue");
|
|
32171
|
-
return {
|
|
32172
|
-
issue: resolved.issue.key,
|
|
32173
|
-
topic: dispatchIssueSubject(resolved.issue.key, ">"),
|
|
32174
|
-
...fields
|
|
32175
|
-
};
|
|
32236
|
+
return { issue: resolved.issue.key, ...fields };
|
|
32176
32237
|
}
|
|
32177
|
-
async function
|
|
32238
|
+
async function askOwnerDetails(client, ask, resolved) {
|
|
32178
32239
|
if (ask.issue_key !== null) {
|
|
32179
|
-
return {
|
|
32180
|
-
issue: ask.issue_key,
|
|
32181
|
-
topic: dispatchIssueSubject(ask.issue_key, ">"),
|
|
32182
|
-
ask: ask.id
|
|
32183
|
-
};
|
|
32240
|
+
return { issue: ask.issue_key, ask: ask.id };
|
|
32184
32241
|
}
|
|
32185
32242
|
if (ask.artifact_id === undefined || ask.artifact_id === null) {
|
|
32186
32243
|
throw new Error("document ask is missing its artifact ID");
|
|
@@ -32188,6 +32245,9 @@ async function askResultDetails(client, ask, resolved) {
|
|
|
32188
32245
|
const artifact = resolved?.artifact ?? await client.getArtifact(ask.artifact_id);
|
|
32189
32246
|
return { ...documentResultDetails(artifact), ask: ask.id };
|
|
32190
32247
|
}
|
|
32248
|
+
async function followedAskDetails(client, ask, resolved) {
|
|
32249
|
+
return { ...await askOwnerDetails(client, ask, resolved), follows: { ask: ask.id } };
|
|
32250
|
+
}
|
|
32191
32251
|
var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
32192
32252
|
var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
|
|
32193
32253
|
var bareIssueNumberPattern = /^[1-9][0-9]*$/;
|
|
@@ -32195,6 +32255,7 @@ var issueFreeTools = {
|
|
|
32195
32255
|
dispatch_issue: true,
|
|
32196
32256
|
dispatch_edit_ask: true,
|
|
32197
32257
|
dispatch_resolve_ask: true,
|
|
32258
|
+
dispatch_follow: true,
|
|
32198
32259
|
dispatch_search: true,
|
|
32199
32260
|
dispatch_open_asks: true
|
|
32200
32261
|
};
|
|
@@ -32857,8 +32918,8 @@ async function executeDispatchTool(input) {
|
|
|
32857
32918
|
actor
|
|
32858
32919
|
});
|
|
32859
32920
|
return {
|
|
32860
|
-
text: `Created ${created.key}: ${created.title}`,
|
|
32861
|
-
details: { issue: created.key
|
|
32921
|
+
text: `Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
|
|
32922
|
+
details: { issue: created.key }
|
|
32862
32923
|
};
|
|
32863
32924
|
} catch (error) {
|
|
32864
32925
|
if (!(error instanceof DispatchServiceError) || error.code !== "POSSIBLE_DUPLICATE") {
|
|
@@ -32909,7 +32970,7 @@ async function executeDispatchTool(input) {
|
|
|
32909
32970
|
throw new Error("resolved ask is missing its resolution");
|
|
32910
32971
|
return {
|
|
32911
32972
|
text: `${kind === "retracted" ? "Retracted" : "Resolved"} ask ${ask.id}: ${ask.resolution.reason}`,
|
|
32912
|
-
details: await
|
|
32973
|
+
details: await askOwnerDetails(client, ask)
|
|
32913
32974
|
};
|
|
32914
32975
|
}
|
|
32915
32976
|
case "dispatch_ask": {
|
|
@@ -32932,9 +32993,11 @@ async function executeDispatchTool(input) {
|
|
|
32932
32993
|
actor
|
|
32933
32994
|
};
|
|
32934
32995
|
const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue(), askInput);
|
|
32996
|
+
const askOwner = ask.issue_key !== null ? issueTopic(ask.issue_key) : resolved === undefined ? issueTopic(issue()) : documentTopic(resolved.artifact);
|
|
32935
32997
|
return {
|
|
32936
|
-
text: `
|
|
32937
|
-
|
|
32998
|
+
text: `Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
|
|
32999
|
+
${followsAsk(askOwner)}`,
|
|
33000
|
+
details: await followedAskDetails(client, ask, resolved)
|
|
32938
33001
|
};
|
|
32939
33002
|
}
|
|
32940
33003
|
case "dispatch_edit_ask": {
|
|
@@ -32951,7 +33014,7 @@ async function executeDispatchTool(input) {
|
|
|
32951
33014
|
});
|
|
32952
33015
|
return {
|
|
32953
33016
|
text: `Ask edited: ${ask.question}`,
|
|
32954
|
-
details: await
|
|
33017
|
+
details: await askOwnerDetails(client, ask)
|
|
32955
33018
|
};
|
|
32956
33019
|
}
|
|
32957
33020
|
case "dispatch_comment": {
|
|
@@ -32971,18 +33034,23 @@ async function executeDispatchTool(input) {
|
|
|
32971
33034
|
actor
|
|
32972
33035
|
};
|
|
32973
33036
|
const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
|
|
32974
|
-
const
|
|
33037
|
+
const commentOwner = resolved === undefined ? issueTopic(issue()) : resolvedTopic(resolved);
|
|
33038
|
+
const commentDetails = resolved === undefined ? { issue: comment.issue_key, comment: comment.id } : writeResultDetails(resolved, { comment: comment.id });
|
|
33039
|
+
if (replyToAsk !== undefined) {
|
|
33040
|
+
const askState = comment.turn === null ? "" : `; ask now waiting on ${comment.turn}`;
|
|
33041
|
+
return {
|
|
33042
|
+
text: `Replied on ask ${replyToAsk} (comment ${comment.id}${askState}). ${followsAsk(commentOwner)}`,
|
|
33043
|
+
details: {
|
|
33044
|
+
...commentDetails,
|
|
33045
|
+
ask: replyToAsk,
|
|
33046
|
+
follows: { ask: replyToAsk },
|
|
33047
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
33048
|
+
}
|
|
33049
|
+
};
|
|
33050
|
+
}
|
|
32975
33051
|
return {
|
|
32976
|
-
text: `Posted comment ${comment.id}${
|
|
32977
|
-
details:
|
|
32978
|
-
issue: comment.issue_key,
|
|
32979
|
-
topic: dispatchIssueSubject(issue(), ">"),
|
|
32980
|
-
comment: comment.id,
|
|
32981
|
-
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
32982
|
-
} : writeResultDetails(resolved, {
|
|
32983
|
-
comment: comment.id,
|
|
32984
|
-
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
32985
|
-
})
|
|
33052
|
+
text: `Posted comment ${comment.id} ${notSubscribed(commentOwner)}`,
|
|
33053
|
+
details: commentDetails
|
|
32986
33054
|
};
|
|
32987
33055
|
}
|
|
32988
33056
|
case "dispatch_suggest": {
|
|
@@ -32999,7 +33067,7 @@ async function executeDispatchTool(input) {
|
|
|
32999
33067
|
};
|
|
33000
33068
|
const comment = resolved.owner.kind === "project" ? await client.artifactSuggest(resolved.artifact.id, suggestionInput) : await client.suggest(issue(), suggestionInput);
|
|
33001
33069
|
return {
|
|
33002
|
-
text: `Posted suggestion ${comment.id}`,
|
|
33070
|
+
text: `Posted suggestion ${comment.id} ${notSubscribed(resolvedTopic(resolved))}`,
|
|
33003
33071
|
details: writeResultDetails(resolved, { comment: comment.id })
|
|
33004
33072
|
};
|
|
33005
33073
|
}
|
|
@@ -33013,12 +33081,8 @@ async function executeDispatchTool(input) {
|
|
|
33013
33081
|
});
|
|
33014
33082
|
const messageRef = `dispatch://${issueKey}/message/${message.id}`;
|
|
33015
33083
|
return {
|
|
33016
|
-
text: `Posted message ${message.id} (${messageRef})`,
|
|
33017
|
-
details: {
|
|
33018
|
-
issue: issueKey,
|
|
33019
|
-
topic: dispatchIssueSubject(issueKey, ">"),
|
|
33020
|
-
message: message.id
|
|
33021
|
-
}
|
|
33084
|
+
text: `Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
|
|
33085
|
+
details: { issue: issueKey, message: message.id }
|
|
33022
33086
|
};
|
|
33023
33087
|
}
|
|
33024
33088
|
case "dispatch_doc_edit": {
|
|
@@ -33032,8 +33096,9 @@ async function executeDispatchTool(input) {
|
|
|
33032
33096
|
});
|
|
33033
33097
|
const retyped = ops.filter((operation) => operation.op === "retype").length;
|
|
33034
33098
|
const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
|
|
33099
|
+
const applied = retyped === 0 ? `Applied ${edited.applied} ops (${versionText})` : `Applied ${edited.applied} ops; retyped ${retyped} block${retyped === 1 ? "" : "s"} (${versionText})`;
|
|
33035
33100
|
return {
|
|
33036
|
-
text:
|
|
33101
|
+
text: `${applied} ${notSubscribed(resolvedTopic(resolved))}`,
|
|
33037
33102
|
details: writeResultDetails(resolved, {
|
|
33038
33103
|
applied: edited.applied,
|
|
33039
33104
|
...edited.version === null ? {} : { version: edited.version.number }
|
|
@@ -33076,7 +33141,7 @@ ${trailer.join(`
|
|
|
33076
33141
|
}
|
|
33077
33142
|
};
|
|
33078
33143
|
}
|
|
33079
|
-
const details = await
|
|
33144
|
+
const details = await followedAskDetails(client, result.ask, resolved);
|
|
33080
33145
|
return {
|
|
33081
33146
|
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.`,
|
|
33082
33147
|
details: { ...details, artifact: resolved.artifact.id, version: result.version }
|
|
@@ -33100,19 +33165,36 @@ ${trailer.join(`
|
|
|
33100
33165
|
const artifactOwner = documentOwner();
|
|
33101
33166
|
const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
|
|
33102
33167
|
const artifactRef = artifactOwner.kind === "project" ? `dispatch://${artifactOwner.project}/artifact/${result.artifact.slug}` : `dispatch://${issue()}/artifact/${result.artifact.slug}`;
|
|
33168
|
+
const uploadOwner = artifactOwner.kind === "project" ? documentTopic(result.artifact) : issueTopic(issue());
|
|
33103
33169
|
return {
|
|
33104
|
-
text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef})`,
|
|
33170
|
+
text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef}) ${notSubscribed(uploadOwner)}`,
|
|
33105
33171
|
details: artifactOwner.kind === "project" ? {
|
|
33106
33172
|
...documentResultDetails(result.artifact),
|
|
33107
33173
|
version: result.version.number
|
|
33108
33174
|
} : {
|
|
33109
33175
|
issue: issue(),
|
|
33110
|
-
topic: dispatchIssueSubject(issue(), ">"),
|
|
33111
33176
|
artifact: result.artifact.id,
|
|
33112
33177
|
version: result.version.number
|
|
33113
33178
|
}
|
|
33114
33179
|
};
|
|
33115
33180
|
}
|
|
33181
|
+
case "dispatch_follow": {
|
|
33182
|
+
const sessionId = input.sessionId?.trim();
|
|
33183
|
+
if (!sessionId)
|
|
33184
|
+
throw new Error("host session id is required for dispatch_follow");
|
|
33185
|
+
const ask = askId(args);
|
|
33186
|
+
const action = stringArg(args, "action");
|
|
33187
|
+
if (action === "unfollow") {
|
|
33188
|
+
await client.unfollowAsk(ask, sessionId, actor);
|
|
33189
|
+
return { text: `Unfollowed ask ${ask}.`, details: { ask } };
|
|
33190
|
+
}
|
|
33191
|
+
const read = await client.getAsk(ask);
|
|
33192
|
+
await client.followAsk(ask, sessionId, actor);
|
|
33193
|
+
return {
|
|
33194
|
+
text: `Following ask ${ask}: its answer and replies reach this session directly.`,
|
|
33195
|
+
details: await followedAskDetails(client, read.ask)
|
|
33196
|
+
};
|
|
33197
|
+
}
|
|
33116
33198
|
case "dispatch_read": {
|
|
33117
33199
|
if (ownerArguments.ref?.kind === "ask") {
|
|
33118
33200
|
const ref = ownerArguments.ref;
|
|
@@ -33202,25 +33284,30 @@ function asObject(value) {
|
|
|
33202
33284
|
}
|
|
33203
33285
|
|
|
33204
33286
|
// ../envoy-client/src/dispatch-subscribe.ts
|
|
33205
|
-
function
|
|
33206
|
-
if (typeof details !== "object" || details === null
|
|
33287
|
+
function dispatchFollowNotice(details) {
|
|
33288
|
+
if (typeof details !== "object" || details === null)
|
|
33289
|
+
return null;
|
|
33290
|
+
const { follows, issue, document } = details;
|
|
33291
|
+
if (typeof follows !== "object" || follows === null)
|
|
33292
|
+
return null;
|
|
33293
|
+
const { ask } = follows;
|
|
33294
|
+
if (typeof ask !== "string" || ask === "")
|
|
33295
|
+
return null;
|
|
33296
|
+
let owner;
|
|
33297
|
+
if (typeof issue === "string" && issue !== "") {
|
|
33298
|
+
owner = { label: issue, topic: dispatchIssueSubject(issue, ">") };
|
|
33299
|
+
} else if (typeof document === "string") {
|
|
33300
|
+
const [project, slug] = document.split("/", 2);
|
|
33301
|
+
if (!project || !slug)
|
|
33302
|
+
return null;
|
|
33303
|
+
owner = { label: document, topic: dispatchDocumentSubject(project, slug, ">") };
|
|
33304
|
+
} else {
|
|
33207
33305
|
return null;
|
|
33208
|
-
const { topic } = details;
|
|
33209
|
-
return typeof topic === "string" && topic.startsWith("notifications.dispatch.") ? topic : null;
|
|
33210
|
-
}
|
|
33211
|
-
function dispatchTopicLabel(topic) {
|
|
33212
|
-
if (topic.startsWith(DISPATCH_ISSUE_TOPIC_PREFIX)) {
|
|
33213
|
-
const [key] = topic.slice(DISPATCH_ISSUE_TOPIC_PREFIX.length).split(".", 2);
|
|
33214
|
-
if (key !== undefined && key !== "")
|
|
33215
|
-
return key;
|
|
33216
|
-
}
|
|
33217
|
-
if (topic.startsWith(DISPATCH_DOCUMENT_TOPIC_PREFIX)) {
|
|
33218
|
-
const [project, slug] = topic.slice(DISPATCH_DOCUMENT_TOPIC_PREFIX.length).split(".", 3);
|
|
33219
|
-
if (project !== undefined && project !== "" && slug !== undefined && slug !== "") {
|
|
33220
|
-
return `${project}/${slug}`;
|
|
33221
|
-
}
|
|
33222
33306
|
}
|
|
33223
|
-
return
|
|
33307
|
+
return {
|
|
33308
|
+
ask,
|
|
33309
|
+
text: `Following ask ${ask} on ${owner.label}: its answer and replies reach you directly (dispatch_follow unfollow to stop). For every event on ${owner.label}: envoy_subscribe ${owner.topic}.`
|
|
33310
|
+
};
|
|
33224
33311
|
}
|
|
33225
33312
|
function subscriptionRemovedTopics(raw, sessionID) {
|
|
33226
33313
|
let envelope;
|
|
@@ -34326,26 +34413,15 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
|
|
|
34326
34413
|
};
|
|
34327
34414
|
}
|
|
34328
34415
|
});
|
|
34416
|
+
const announcedFollows = new Set;
|
|
34329
34417
|
pi.on("tool_result", async (event) => {
|
|
34330
34418
|
if (event.isError)
|
|
34331
34419
|
return;
|
|
34332
|
-
const
|
|
34333
|
-
if (
|
|
34420
|
+
const notice = dispatchFollowNotice(event.details);
|
|
34421
|
+
if (notice === null || announcedFollows.has(notice.ask))
|
|
34334
34422
|
return;
|
|
34335
|
-
|
|
34336
|
-
|
|
34337
|
-
if (isNew)
|
|
34338
|
-
await registerSession();
|
|
34339
|
-
if (isNew) {
|
|
34340
|
-
pi.sendMessage({
|
|
34341
|
-
customType: "envoy-message",
|
|
34342
|
-
content: `Subscribed to ${dispatchTopicLabel(topic)} (every event on this issue reaches you; envoy_unsubscribe ${topic} to stop).`,
|
|
34343
|
-
display: true
|
|
34344
|
-
}, { deliverAs: "steer", triggerTurn: false });
|
|
34345
|
-
}
|
|
34346
|
-
} catch (error) {
|
|
34347
|
-
activeSessionContext?.ui.notify(`envoy: dispatch reply auto-subscribe failed (${messageFor(error)}); run envoy_subscribe ${topic}`, "warning");
|
|
34348
|
-
}
|
|
34423
|
+
announcedFollows.add(notice.ask);
|
|
34424
|
+
pi.sendMessage({ customType: "envoy-message", content: notice.text, display: true }, { deliverAs: "steer", triggerTurn: false });
|
|
34349
34425
|
});
|
|
34350
34426
|
async function execute(operation, rawParameters) {
|
|
34351
34427
|
try {
|
package/dist/legion.js
CHANGED
|
@@ -29053,6 +29053,11 @@ var SubscriptionRemovedEventPayloadSchema = object({
|
|
|
29053
29053
|
pending: boolean2().optional(),
|
|
29054
29054
|
request_event_id: number2().int().positive().optional()
|
|
29055
29055
|
});
|
|
29056
|
+
var AskFollowerEventPayloadSchema = object({
|
|
29057
|
+
ask_id: string2().optional(),
|
|
29058
|
+
session_id: string2().optional(),
|
|
29059
|
+
by: object({ kind: string2(), id: string2().optional() }).passthrough().optional()
|
|
29060
|
+
});
|
|
29056
29061
|
// ../contracts/src/dispatch-snippet.ts
|
|
29057
29062
|
var HTML_ENTITIES = [
|
|
29058
29063
|
["<", "<"],
|
|
@@ -29218,6 +29223,15 @@ var dispatchToolSpecs = [
|
|
|
29218
29223
|
reason: z.string({ min: 1 }).describe("Why the open ask no longer needs a human answer.")
|
|
29219
29224
|
})
|
|
29220
29225
|
},
|
|
29226
|
+
{
|
|
29227
|
+
name: "dispatch_follow",
|
|
29228
|
+
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>.>",
|
|
29229
|
+
arguments: (z) => ({
|
|
29230
|
+
ask: z.string().describe("Full ask id (uuid)."),
|
|
29231
|
+
action: z.enum(["follow", "unfollow"]).describe("follow | unfollow")
|
|
29232
|
+
}),
|
|
29233
|
+
strict: true
|
|
29234
|
+
},
|
|
29221
29235
|
{
|
|
29222
29236
|
name: "dispatch_comment",
|
|
29223
29237
|
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}`,
|
|
@@ -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? })
|