@sjawhar/opencode-legion-envoy 3.5.0 → 3.7.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/dist/src/server.js +74 -39
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +11 -1
package/dist/src/server.js
CHANGED
|
@@ -13978,11 +13978,16 @@ var dispatchToolSpecs = [
|
|
|
13978
13978
|
},
|
|
13979
13979
|
{
|
|
13980
13980
|
name: "dispatch_issue_update",
|
|
13981
|
-
example: {
|
|
13982
|
-
|
|
13981
|
+
example: {
|
|
13982
|
+
issue: "DSP-1",
|
|
13983
|
+
status: "done",
|
|
13984
|
+
reason: "Shipped in owner/repo#7; verified on the production dashboard."
|
|
13985
|
+
},
|
|
13986
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, or attach it to architecture components. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. external_links are " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link. components replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only rank, components, " + "and a reopening status (any status but done); everything else, priority included, waits " + "for the reopen. " + "priority is yours to set and a human overrides it; rank, the board's own order, is not " + "settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
13983
13987
|
arguments: (z2) => ({
|
|
13984
13988
|
issue: z2.string().describe(ISSUE_REFERENCE),
|
|
13985
13989
|
status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
13990
|
+
reason: z2.string({ max: 2000 }).describe("Required with status done, and only with it: why the issue is closing, at most 2,000 characters. Posted on the issue as a message before it closes.").optional(),
|
|
13986
13991
|
title: z2.string({ min: 1 }).describe("Replacement title.").optional(),
|
|
13987
13992
|
labels: z2.array(z2.string({ min: 1, max: 40 }), { max: 20 }).describe("Replacement label set, at most 20 labels of up to 40 characters; replaces every existing label.").optional(),
|
|
13988
13993
|
priority: z2.number({ int: true, min: 0, max: 3 }).nullable().optional().describe("Coarse priority: 0 is P0 (highest) through 3 is P3 (lowest); null clears it."),
|
|
@@ -13994,9 +13999,10 @@ var dispatchToolSpecs = [
|
|
|
13994
13999
|
validation: {
|
|
13995
14000
|
check: (value) => {
|
|
13996
14001
|
const input = value;
|
|
13997
|
-
|
|
14002
|
+
const reasonFits = input.status === "done" ? typeof input.reason === "string" && input.reason.trim() !== "" : input.reason === undefined;
|
|
14003
|
+
return reasonFits && (typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || typeof input.components === "object" && input.components !== null);
|
|
13998
14004
|
},
|
|
13999
|
-
message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, or components."
|
|
14005
|
+
message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, or components. " + "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts; reason goes only with status done."
|
|
14000
14006
|
},
|
|
14001
14007
|
strict: true
|
|
14002
14008
|
},
|
|
@@ -16548,6 +16554,7 @@ async function executeDispatchTool(input) {
|
|
|
16548
16554
|
case "dispatch_issue_update": {
|
|
16549
16555
|
const issueKey = issue2();
|
|
16550
16556
|
const status = optionalString(args, "status");
|
|
16557
|
+
const reason = optionalString(args, "reason");
|
|
16551
16558
|
const title = optionalString(args, "title");
|
|
16552
16559
|
const route = optionalString(args, "route");
|
|
16553
16560
|
const parent = optionalString(args, "parent");
|
|
@@ -16555,12 +16562,29 @@ async function executeDispatchTool(input) {
|
|
|
16555
16562
|
const priority = optionalPriority(args, "priority");
|
|
16556
16563
|
const labels = Array.isArray(args.labels) ? args.labels : undefined;
|
|
16557
16564
|
const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
|
|
16558
|
-
let
|
|
16565
|
+
let before;
|
|
16566
|
+
try {
|
|
16567
|
+
before = await client.getIssue(issueKey);
|
|
16568
|
+
} catch (error48) {
|
|
16569
|
+
throw refusalWithCode(error48);
|
|
16570
|
+
}
|
|
16571
|
+
let closingNote;
|
|
16572
|
+
if (reason !== undefined) {
|
|
16573
|
+
try {
|
|
16574
|
+
const message = await client.message(issueKey, { body: reason, actor });
|
|
16575
|
+
closingNote = {
|
|
16576
|
+
id: message.id,
|
|
16577
|
+
ref: dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id)
|
|
16578
|
+
};
|
|
16579
|
+
} catch (error48) {
|
|
16580
|
+
throw refusalWithCode(error48, "; the reason was not posted, so the close was not sent");
|
|
16581
|
+
}
|
|
16582
|
+
}
|
|
16583
|
+
const linked = before.external_links.map((link) => link.url);
|
|
16584
|
+
const newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
|
|
16585
|
+
let after;
|
|
16559
16586
|
try {
|
|
16560
|
-
|
|
16561
|
-
const linked = before.external_links.map((link) => link.url);
|
|
16562
|
-
newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
|
|
16563
|
-
const after = await client.updateIssue(issueKey, {
|
|
16587
|
+
after = await client.updateIssue(issueKey, {
|
|
16564
16588
|
...status === undefined ? {} : { status },
|
|
16565
16589
|
...title === undefined ? {} : { title },
|
|
16566
16590
|
...labels === undefined ? {} : { labels },
|
|
@@ -16571,39 +16595,50 @@ async function executeDispatchTool(input) {
|
|
|
16571
16595
|
...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url2) => ({ url: url2 }))] },
|
|
16572
16596
|
actor
|
|
16573
16597
|
});
|
|
16574
|
-
const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
|
|
16575
|
-
const changes = [
|
|
16576
|
-
...status === undefined ? [] : [`status ${before.status} -> ${after.status}`],
|
|
16577
|
-
...title === undefined ? [] : [`title "${after.title}"`],
|
|
16578
|
-
...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
|
|
16579
|
-
...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
|
|
16580
|
-
...requestedLinks === undefined ? [] : [
|
|
16581
|
-
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
16582
|
-
],
|
|
16583
|
-
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
|
|
16584
|
-
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
|
16585
|
-
...components === undefined ? [] : [componentsChange(components, after.components)]
|
|
16586
|
-
];
|
|
16587
|
-
const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
|
|
16588
|
-
setsStatus: status !== undefined
|
|
16589
|
-
});
|
|
16590
|
-
return {
|
|
16591
|
-
text: [
|
|
16592
|
-
`${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
16593
|
-
...adviceLines
|
|
16594
|
-
].join(`
|
|
16595
|
-
`),
|
|
16596
|
-
details: {
|
|
16597
|
-
issue: after.key,
|
|
16598
|
-
status: after.status,
|
|
16599
|
-
external_links: after.external_links.map((link) => link.url),
|
|
16600
|
-
...after.advice === undefined ? {} : { advice: after.advice }
|
|
16601
|
-
}
|
|
16602
|
-
};
|
|
16603
16598
|
} catch (error48) {
|
|
16604
16599
|
const taken = error48 instanceof DispatchServiceError && error48.status === 500 && newLinks.length > 0 ? `; one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
|
|
16605
|
-
|
|
16600
|
+
if (closingNote === undefined)
|
|
16601
|
+
throw refusalWithCode(error48, taken);
|
|
16602
|
+
const refused = error48 instanceof DispatchServiceError && error48.status < 500;
|
|
16603
|
+
const posted = `; the reason already landed as message ${closingNote.id} (${closingNote.ref})`;
|
|
16604
|
+
const landed = refused ? `${posted} but the issue did not close. Retrying this call posts its reason again, so fix what refused the close, then retry with a reason that points at message ${closingNote.id}` : `${posted}, and the close may or may not have taken effect. Read the issue's status before retrying: done means it closed; otherwise retry with a reason that points at message ${closingNote.id}, since retrying this call posts its reason again`;
|
|
16605
|
+
if (error48 instanceof DispatchServiceError)
|
|
16606
|
+
throw refusalWithCode(error48, taken + landed);
|
|
16607
|
+
throw new Error(`${error48 instanceof Error ? error48.message : String(error48)}${landed}`, {
|
|
16608
|
+
cause: error48
|
|
16609
|
+
});
|
|
16606
16610
|
}
|
|
16611
|
+
const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
|
|
16612
|
+
const changes = [
|
|
16613
|
+
...closingNote === undefined ? [] : [`reason posted as message ${closingNote.id} (${closingNote.ref})`],
|
|
16614
|
+
...status === undefined ? [] : [`status ${before.status} -> ${after.status}`],
|
|
16615
|
+
...title === undefined ? [] : [`title "${after.title}"`],
|
|
16616
|
+
...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
|
|
16617
|
+
...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
|
|
16618
|
+
...requestedLinks === undefined ? [] : [
|
|
16619
|
+
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
16620
|
+
],
|
|
16621
|
+
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
|
|
16622
|
+
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
|
16623
|
+
...components === undefined ? [] : [componentsChange(components, after.components)]
|
|
16624
|
+
];
|
|
16625
|
+
const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
|
|
16626
|
+
setsStatus: status !== undefined
|
|
16627
|
+
});
|
|
16628
|
+
return {
|
|
16629
|
+
text: [
|
|
16630
|
+
`${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
16631
|
+
...adviceLines
|
|
16632
|
+
].join(`
|
|
16633
|
+
`),
|
|
16634
|
+
details: {
|
|
16635
|
+
issue: after.key,
|
|
16636
|
+
status: after.status,
|
|
16637
|
+
external_links: after.external_links.map((link) => link.url),
|
|
16638
|
+
...closingNote === undefined ? {} : { message: closingNote.id },
|
|
16639
|
+
...after.advice === undefined ? {} : { advice: after.advice }
|
|
16640
|
+
}
|
|
16641
|
+
};
|
|
16607
16642
|
}
|
|
16608
16643
|
case "dispatch_claim": {
|
|
16609
16644
|
const issueKey = issue2();
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -224,11 +224,19 @@ status is." Waiting for the deploy lane is not a status and is never announced.
|
|
|
224
224
|
```ts
|
|
225
225
|
// PATCH /api/v1/issues/{key} — status, title, labels, priority, external_links (merged by URL), route, parent
|
|
226
226
|
dispatch_issue_update({ issue: "AGENTC-175", status: "testing" })
|
|
227
|
+
dispatch_issue_update({ issue: "AGENTC-175", status: "done", reason: "Shipped in owner/repo#7; verified on the production dashboard." })
|
|
227
228
|
dispatch_issue_update({ issue: "AGENTC-175", priority: 1 }) // 0–3; see Priority is yours to set
|
|
228
229
|
dispatch_issue_update({ issue: "AGENTC-175", external_links: ["https://github.com/owner/repo/pull/7"] })
|
|
229
230
|
dispatch_issue_update({ issue: "AGENTC-175", parent: "AGENTC-170" }) // same-project key; "" clears the parent
|
|
230
231
|
```
|
|
231
232
|
|
|
233
|
+
Closing takes a `reason`, and the tool refuses `status: "done"` without one: it posts the reason on
|
|
234
|
+
the issue as a message, then closes it, because a closed issue refuses messages, comments, and
|
|
235
|
+
artifacts, so a reason left for later has nowhere to go. When the close fails after the post, the
|
|
236
|
+
error names the posted message; after a timeout or a server error it also says the close may have
|
|
237
|
+
landed, so read the issue's status first. A retry points its reason at the posted message rather
|
|
238
|
+
than repeating it.
|
|
239
|
+
|
|
232
240
|
The two clears differ: `priority` clears with `null`, while `parent` and `route` clear with `""`.
|
|
233
241
|
Guessing the other one is a refusal either way.
|
|
234
242
|
|
|
@@ -833,7 +841,9 @@ message ref, it returns that message and its reply chain. Reads do not subscribe
|
|
|
833
841
|
Every read ends with two sections from the reference graph. `Referenced by:` lists what points at the node — every document, ask,
|
|
834
842
|
comment, or message that cites it, plus its structure: child issues, attached documents, anchored and owned asks and comments, replies,
|
|
835
843
|
followers — and `Links:` lists what it cites. Each row is `- <edge kind> <node kind> dispatch://… (<excerpt> · <when>)`; for a
|
|
836
|
-
document source the excerpt is the block
|
|
844
|
+
document source the excerpt is the start of the block holding the mention, and a whole list is one block, so every issue named in
|
|
845
|
+
one list previews the list's first item. When a document references many issues and each backlink should read right, give each
|
|
846
|
+
issue its own paragraph (or block), not an item of one list. Cross-project, always: a message on another project's issue that
|
|
837
847
|
cites an ask shows up under that ask. So "what led to this decision" is one `dispatch_read` on the ask, and "who relies on this
|
|
838
848
|
document" one read on the document. Cite with `dispatch://` references (below) whenever you name a node in a body — a bare id or
|
|
839
849
|
title is invisible to the graph.
|