@sjawhar/pi-legion-envoy 5.9.0 → 5.10.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 CHANGED
@@ -30092,11 +30092,16 @@ var dispatchToolSpecs = [
30092
30092
  },
30093
30093
  {
30094
30094
  name: "dispatch_issue_update",
30095
- example: { issue: "DSP-1", status: "in_progress" },
30096
- 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. 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}`,
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
- return 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;
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 newLinks = [];
33766
+ let before;
33760
33767
  try {
33761
- const before = await client.getIssue(issueKey);
33762
- const linked = before.external_links.map((link) => link.url);
33763
- newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
33764
- const after = await client.updateIssue(issueKey, {
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
- throw refusalWithCode(error48, taken);
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.9.0",
16195
+ version: "5.10.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: { issue: "DSP-1", status: "in_progress" },
30219
- 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. 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}`,
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
- return 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;
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 containing the mention. Cross-project, always: a message on another project's issue that
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.9.0",
3
+ "version": "5.10.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [