@sjawhar/pi-legion-envoy 5.9.0 → 5.11.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/envoy.js +74 -39
- package/dist/legion.js +11 -5
- package/dist/skills/dispatch/SKILL.md +11 -1
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -30092,11 +30092,16 @@ var dispatchToolSpecs = [
|
|
|
30092
30092
|
},
|
|
30093
30093
|
{
|
|
30094
30094
|
name: "dispatch_issue_update",
|
|
30095
|
-
example: {
|
|
30096
|
-
|
|
30095
|
+
example: {
|
|
30096
|
+
issue: "DSP-1",
|
|
30097
|
+
status: "done",
|
|
30098
|
+
reason: "Shipped in owner/repo#7; verified on the production dashboard."
|
|
30099
|
+
},
|
|
30100
|
+
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}`,
|
|
30097
30101
|
arguments: (z2) => ({
|
|
30098
30102
|
issue: z2.string().describe(ISSUE_REFERENCE),
|
|
30099
30103
|
status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
30104
|
+
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(),
|
|
30100
30105
|
title: z2.string({ min: 1 }).describe("Replacement title.").optional(),
|
|
30101
30106
|
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(),
|
|
30102
30107
|
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."),
|
|
@@ -30108,9 +30113,10 @@ var dispatchToolSpecs = [
|
|
|
30108
30113
|
validation: {
|
|
30109
30114
|
check: (value) => {
|
|
30110
30115
|
const input = value;
|
|
30111
|
-
|
|
30116
|
+
const reasonFits = input.status === "done" ? typeof input.reason === "string" && input.reason.trim() !== "" : input.reason === undefined;
|
|
30117
|
+
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);
|
|
30112
30118
|
},
|
|
30113
|
-
message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, or components."
|
|
30119
|
+
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."
|
|
30114
30120
|
},
|
|
30115
30121
|
strict: true
|
|
30116
30122
|
},
|
|
@@ -33749,6 +33755,7 @@ async function executeDispatchTool(input) {
|
|
|
33749
33755
|
case "dispatch_issue_update": {
|
|
33750
33756
|
const issueKey = issue2();
|
|
33751
33757
|
const status = optionalString(args, "status");
|
|
33758
|
+
const reason = optionalString(args, "reason");
|
|
33752
33759
|
const title = optionalString(args, "title");
|
|
33753
33760
|
const route = optionalString(args, "route");
|
|
33754
33761
|
const parent = optionalString(args, "parent");
|
|
@@ -33756,12 +33763,29 @@ async function executeDispatchTool(input) {
|
|
|
33756
33763
|
const priority = optionalPriority(args, "priority");
|
|
33757
33764
|
const labels = Array.isArray(args.labels) ? args.labels : undefined;
|
|
33758
33765
|
const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
|
|
33759
|
-
let
|
|
33766
|
+
let before;
|
|
33760
33767
|
try {
|
|
33761
|
-
|
|
33762
|
-
|
|
33763
|
-
|
|
33764
|
-
|
|
33768
|
+
before = await client.getIssue(issueKey);
|
|
33769
|
+
} catch (error48) {
|
|
33770
|
+
throw refusalWithCode(error48);
|
|
33771
|
+
}
|
|
33772
|
+
let closingNote;
|
|
33773
|
+
if (reason !== undefined) {
|
|
33774
|
+
try {
|
|
33775
|
+
const message = await client.message(issueKey, { body: reason, actor });
|
|
33776
|
+
closingNote = {
|
|
33777
|
+
id: message.id,
|
|
33778
|
+
ref: dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id)
|
|
33779
|
+
};
|
|
33780
|
+
} catch (error48) {
|
|
33781
|
+
throw refusalWithCode(error48, "; the reason was not posted, so the close was not sent");
|
|
33782
|
+
}
|
|
33783
|
+
}
|
|
33784
|
+
const linked = before.external_links.map((link) => link.url);
|
|
33785
|
+
const newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
|
|
33786
|
+
let after;
|
|
33787
|
+
try {
|
|
33788
|
+
after = await client.updateIssue(issueKey, {
|
|
33765
33789
|
...status === undefined ? {} : { status },
|
|
33766
33790
|
...title === undefined ? {} : { title },
|
|
33767
33791
|
...labels === undefined ? {} : { labels },
|
|
@@ -33772,39 +33796,50 @@ async function executeDispatchTool(input) {
|
|
|
33772
33796
|
...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url2) => ({ url: url2 }))] },
|
|
33773
33797
|
actor
|
|
33774
33798
|
});
|
|
33775
|
-
const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
|
|
33776
|
-
const changes = [
|
|
33777
|
-
...status === undefined ? [] : [`status ${before.status} -> ${after.status}`],
|
|
33778
|
-
...title === undefined ? [] : [`title "${after.title}"`],
|
|
33779
|
-
...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
|
|
33780
|
-
...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
|
|
33781
|
-
...requestedLinks === undefined ? [] : [
|
|
33782
|
-
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
33783
|
-
],
|
|
33784
|
-
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
|
|
33785
|
-
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
|
33786
|
-
...components === undefined ? [] : [componentsChange(components, after.components)]
|
|
33787
|
-
];
|
|
33788
|
-
const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
|
|
33789
|
-
setsStatus: status !== undefined
|
|
33790
|
-
});
|
|
33791
|
-
return {
|
|
33792
|
-
text: [
|
|
33793
|
-
`${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
33794
|
-
...adviceLines
|
|
33795
|
-
].join(`
|
|
33796
|
-
`),
|
|
33797
|
-
details: {
|
|
33798
|
-
issue: after.key,
|
|
33799
|
-
status: after.status,
|
|
33800
|
-
external_links: after.external_links.map((link) => link.url),
|
|
33801
|
-
...after.advice === undefined ? {} : { advice: after.advice }
|
|
33802
|
-
}
|
|
33803
|
-
};
|
|
33804
33799
|
} catch (error48) {
|
|
33805
33800
|
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)` : "";
|
|
33806
|
-
|
|
33801
|
+
if (closingNote === undefined)
|
|
33802
|
+
throw refusalWithCode(error48, taken);
|
|
33803
|
+
const refused = error48 instanceof DispatchServiceError && error48.status < 500;
|
|
33804
|
+
const posted = `; the reason already landed as message ${closingNote.id} (${closingNote.ref})`;
|
|
33805
|
+
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`;
|
|
33806
|
+
if (error48 instanceof DispatchServiceError)
|
|
33807
|
+
throw refusalWithCode(error48, taken + landed);
|
|
33808
|
+
throw new Error(`${error48 instanceof Error ? error48.message : String(error48)}${landed}`, {
|
|
33809
|
+
cause: error48
|
|
33810
|
+
});
|
|
33807
33811
|
}
|
|
33812
|
+
const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
|
|
33813
|
+
const changes = [
|
|
33814
|
+
...closingNote === undefined ? [] : [`reason posted as message ${closingNote.id} (${closingNote.ref})`],
|
|
33815
|
+
...status === undefined ? [] : [`status ${before.status} -> ${after.status}`],
|
|
33816
|
+
...title === undefined ? [] : [`title "${after.title}"`],
|
|
33817
|
+
...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
|
|
33818
|
+
...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
|
|
33819
|
+
...requestedLinks === undefined ? [] : [
|
|
33820
|
+
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
33821
|
+
],
|
|
33822
|
+
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
|
|
33823
|
+
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
|
33824
|
+
...components === undefined ? [] : [componentsChange(components, after.components)]
|
|
33825
|
+
];
|
|
33826
|
+
const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
|
|
33827
|
+
setsStatus: status !== undefined
|
|
33828
|
+
});
|
|
33829
|
+
return {
|
|
33830
|
+
text: [
|
|
33831
|
+
`${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
33832
|
+
...adviceLines
|
|
33833
|
+
].join(`
|
|
33834
|
+
`),
|
|
33835
|
+
details: {
|
|
33836
|
+
issue: after.key,
|
|
33837
|
+
status: after.status,
|
|
33838
|
+
external_links: after.external_links.map((link) => link.url),
|
|
33839
|
+
...closingNote === undefined ? {} : { message: closingNote.id },
|
|
33840
|
+
...after.advice === undefined ? {} : { advice: after.advice }
|
|
33841
|
+
}
|
|
33842
|
+
};
|
|
33808
33843
|
}
|
|
33809
33844
|
case "dispatch_claim": {
|
|
33810
33845
|
const issueKey = issue2();
|
package/dist/legion.js
CHANGED
|
@@ -16192,7 +16192,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
16192
16192
|
// package.json
|
|
16193
16193
|
var package_default = {
|
|
16194
16194
|
name: "@sjawhar/pi-legion-envoy",
|
|
16195
|
-
version: "5.
|
|
16195
|
+
version: "5.11.0",
|
|
16196
16196
|
type: "module",
|
|
16197
16197
|
omp: {
|
|
16198
16198
|
extensions: [
|
|
@@ -30215,11 +30215,16 @@ var dispatchToolSpecs = [
|
|
|
30215
30215
|
},
|
|
30216
30216
|
{
|
|
30217
30217
|
name: "dispatch_issue_update",
|
|
30218
|
-
example: {
|
|
30219
|
-
|
|
30218
|
+
example: {
|
|
30219
|
+
issue: "DSP-1",
|
|
30220
|
+
status: "done",
|
|
30221
|
+
reason: "Shipped in owner/repo#7; verified on the production dashboard."
|
|
30222
|
+
},
|
|
30223
|
+
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}`,
|
|
30220
30224
|
arguments: (z2) => ({
|
|
30221
30225
|
issue: z2.string().describe(ISSUE_REFERENCE),
|
|
30222
30226
|
status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
30227
|
+
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(),
|
|
30223
30228
|
title: z2.string({ min: 1 }).describe("Replacement title.").optional(),
|
|
30224
30229
|
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(),
|
|
30225
30230
|
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."),
|
|
@@ -30231,9 +30236,10 @@ var dispatchToolSpecs = [
|
|
|
30231
30236
|
validation: {
|
|
30232
30237
|
check: (value) => {
|
|
30233
30238
|
const input = value;
|
|
30234
|
-
|
|
30239
|
+
const reasonFits = input.status === "done" ? typeof input.reason === "string" && input.reason.trim() !== "" : input.reason === undefined;
|
|
30240
|
+
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);
|
|
30235
30241
|
},
|
|
30236
|
-
message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, or components."
|
|
30242
|
+
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."
|
|
30237
30243
|
},
|
|
30238
30244
|
strict: true
|
|
30239
30245
|
},
|
|
@@ -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.
|