@sjawhar/pi-legion-envoy 1.43.0 → 1.45.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
@@ -29686,7 +29686,7 @@ var ArtifactReviewEventPayloadSchema = object({
29686
29686
  var askEventPayloadFields = {
29687
29687
  id: string2().optional(),
29688
29688
  opened_event_id: number2().int().positive(),
29689
- kind: _enum2(["question", "approval", "action"]).optional(),
29689
+ kind: string2().optional(),
29690
29690
  question: string2().optional(),
29691
29691
  options: array(object({ label: string2().optional() })).optional(),
29692
29692
  answer: object({ selected: array(string2()).nullish(), text: string2().nullish() }).nullish(),
@@ -29906,34 +29906,34 @@ var dispatchToolSpecs = [
29906
29906
  },
29907
29907
  {
29908
29908
  name: "dispatch_issue_update",
29909
- description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), or set its route. 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. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
29909
+ description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), set its route, or set or clear its parent. " + "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. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
29910
29910
  arguments: (z) => ({
29911
29911
  issue: z.string().describe(ISSUE_REFERENCE),
29912
29912
  status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
29913
29913
  title: z.string({ min: 1 }).describe("Replacement title.").optional(),
29914
29914
  labels: z.array(z.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(),
29915
29915
  external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
29916
- route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional()
29916
+ route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
29917
+ parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional()
29917
29918
  }),
29918
29919
  validation: {
29919
29920
  check: (value) => {
29920
29921
  const input = value;
29921
- return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string";
29922
+ return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string";
29922
29923
  },
29923
- message: "Issue update requires at least one field besides issue: status, title, labels, external_links, or route."
29924
+ message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, or parent."
29924
29925
  },
29925
29926
  strict: true
29926
29927
  },
29927
29928
  {
29928
29929
  name: "dispatch_ask",
29929
- description: "Open a durable, answerable decision or human to-do on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Use kind: action for a to-do a human must complete; it has fixed Done / Can't answers. " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29930
+ description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29930
29931
  arguments: (z) => ({
29931
29932
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29932
29933
  project: z.string().describe("Project key owning the document.").optional(),
29933
29934
  artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
29934
29935
  ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
29935
29936
  question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
29936
- kind: z.enum(["action"]).describe("Optional human to-do ask kind.").optional(),
29937
29937
  options: z.array(z.object({
29938
29938
  label: z.string().describe("Selectable option label."),
29939
29939
  description: z.string().describe("Optional option context.").optional()
@@ -32386,9 +32386,6 @@ function askUrgency(args) {
32386
32386
  const value = args.urgency;
32387
32387
  return ASK_URGENCIES.find((urgency) => urgency === value);
32388
32388
  }
32389
- function askKind(args) {
32390
- return optionalString(args, "kind") === "action" ? "action" : undefined;
32391
- }
32392
32389
  var maxAskQuestion16 = 800;
32393
32390
  function questionWithRef(question, ref) {
32394
32391
  return ref === undefined || question.includes(ref) ? question : `${question}
@@ -33087,6 +33084,7 @@ async function executeDispatchTool(input) {
33087
33084
  const status = optionalString(args, "status");
33088
33085
  const title = optionalString(args, "title");
33089
33086
  const route = optionalString(args, "route");
33087
+ const parent = optionalString(args, "parent");
33090
33088
  const labels = Array.isArray(args.labels) ? args.labels : undefined;
33091
33089
  const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
33092
33090
  let newLinks = [];
@@ -33099,6 +33097,7 @@ async function executeDispatchTool(input) {
33099
33097
  ...title === undefined ? {} : { title },
33100
33098
  ...labels === undefined ? {} : { labels },
33101
33099
  ...route === undefined ? {} : { route },
33100
+ ...parent === undefined ? {} : { parent: parent === "" ? null : parent },
33102
33101
  ...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url) => ({ url }))] },
33103
33102
  actor
33104
33103
  });
@@ -33110,7 +33109,8 @@ async function executeDispatchTool(input) {
33110
33109
  ...requestedLinks === undefined ? [] : [
33111
33110
  newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
33112
33111
  ],
33113
- ...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`]
33112
+ ...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
33113
+ ...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`]
33114
33114
  ];
33115
33115
  return {
33116
33116
  text: `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
@@ -33185,11 +33185,9 @@ async function executeDispatchTool(input) {
33185
33185
  const options = args.options;
33186
33186
  const multiple = optionalBoolean(args, "multiple");
33187
33187
  const urgency = askUrgency(args);
33188
- const kind = askKind(args);
33189
33188
  const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
33190
33189
  const askInput = {
33191
33190
  question: askQuestionWithRef(args),
33192
- ...kind === undefined ? {} : { kind },
33193
33191
  ...Array.isArray(options) ? { options } : {},
33194
33192
  ...multiple === undefined ? {} : { multiple },
33195
33193
  ...urgency === undefined ? {} : { urgency },
package/dist/legion.js CHANGED
@@ -28960,7 +28960,7 @@ var ArtifactReviewEventPayloadSchema = object({
28960
28960
  var askEventPayloadFields = {
28961
28961
  id: string2().optional(),
28962
28962
  opened_event_id: number2().int().positive(),
28963
- kind: _enum2(["question", "approval", "action"]).optional(),
28963
+ kind: string2().optional(),
28964
28964
  question: string2().optional(),
28965
28965
  options: array(object({ label: string2().optional() })).optional(),
28966
28966
  answer: object({ selected: array(string2()).nullish(), text: string2().nullish() }).nullish(),
@@ -29180,34 +29180,34 @@ var dispatchToolSpecs = [
29180
29180
  },
29181
29181
  {
29182
29182
  name: "dispatch_issue_update",
29183
- description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), or set its route. 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. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
29183
+ description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), set its route, or set or clear its parent. " + "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. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
29184
29184
  arguments: (z) => ({
29185
29185
  issue: z.string().describe(ISSUE_REFERENCE),
29186
29186
  status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
29187
29187
  title: z.string({ min: 1 }).describe("Replacement title.").optional(),
29188
29188
  labels: z.array(z.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(),
29189
29189
  external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
29190
- route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional()
29190
+ route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
29191
+ parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional()
29191
29192
  }),
29192
29193
  validation: {
29193
29194
  check: (value) => {
29194
29195
  const input = value;
29195
- return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string";
29196
+ return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string";
29196
29197
  },
29197
- message: "Issue update requires at least one field besides issue: status, title, labels, external_links, or route."
29198
+ message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, or parent."
29198
29199
  },
29199
29200
  strict: true
29200
29201
  },
29201
29202
  {
29202
29203
  name: "dispatch_ask",
29203
- description: "Open a durable, answerable decision or human to-do on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Use kind: action for a to-do a human must complete; it has fixed Done / Can't answers. " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29204
+ description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29204
29205
  arguments: (z) => ({
29205
29206
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29206
29207
  project: z.string().describe("Project key owning the document.").optional(),
29207
29208
  artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
29208
29209
  ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
29209
29210
  question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
29210
- kind: z.enum(["action"]).describe("Optional human to-do ask kind.").optional(),
29211
29211
  options: z.array(z.object({
29212
29212
  label: z.string().describe("Selectable option label."),
29213
29213
  description: z.string().describe("Optional option context.").optional()
@@ -75,8 +75,9 @@ rest. Use these headings in this order.
75
75
 
76
76
  - The spec is the issue's one primary document. Extend it in place — a new version that keeps the
77
77
  human's own text — never a second "spec" artifact beside it.
78
- - No hedging ("might", "could consider"). No TBD, TODO, or placeholders: an open item is a
79
- Decision needed.
78
+ - No hedging ("might", "could consider"). No TBD, TODO, or placeholders: an open item is either a
79
+ Decision needed or a question for the platform PO whose ruling becomes a Requirement (see
80
+ [Before you ask](#before-you-ask) under Asking).
80
81
  - Keep each section to one screen; work that exceeds one screen per section is two specs.
81
82
  - Update the spec as decisions land: the spec is the record, comments are the discussion.
82
83
  - Before sending it: no sections conflict, every requirement has exactly one reading, and the
@@ -121,9 +122,10 @@ status is." Waiting for the deploy lane is not a status and is never announced.
121
122
  human's: set it on creation only when their intent is clear, and change it only on their word.
122
123
 
123
124
  ```ts
124
- // PATCH /api/v1/issues/{key} — status, title, labels, external_links (merged by URL), route
125
+ // PATCH /api/v1/issues/{key} — status, title, labels, external_links (merged by URL), route, parent
125
126
  dispatch_issue_update({ issue: "AGENTC-175", status: "testing" })
126
127
  dispatch_issue_update({ issue: "AGENTC-175", external_links: ["https://github.com/owner/repo/pull/7"] })
128
+ dispatch_issue_update({ issue: "AGENTC-175", parent: "AGENTC-170" }) // same-project key; "" clears the parent
127
129
  ```
128
130
 
129
131
  Link the pull request that delivers the issue in `external_links` when you open it; the issue page
@@ -148,6 +150,30 @@ Read them; reference the existing issue, or repeat the call with `force: true` w
148
150
 
149
151
  ## Asking
150
152
 
153
+ ### Before you ask
154
+
155
+ Sami, 2026-09-16, verbatim, rejecting two asks the same night: "All of these \"decisions\" are
156
+ completely disconnected from any discussion of design or trade-offs. This is not a very useful way
157
+ of having this discussion" (on a report-table shape), and "What's a fenced PutObject or phantom
158
+ eval_id? What's an R4 model header? What exactly is the question or uncertainty here?" (on a
159
+ production import). Every `dispatch_ask` passes three gates first:
160
+
161
+ 1. **Does it need his authority, taste, or risk appetite?** The same bar as a spec's Decisions
162
+ needed ([Writing a spec](#writing-a-spec)). Schema shapes, table layouts, field names, migration
163
+ internals, and contracts between lanes do not: they go to the platform PO over Envoy, who rules.
164
+ 2. **Is there genuine uncertainty?** If not, it is a plan you execute. The one legitimate ask
165
+ without uncertainty is permission for an action only a human can authorise — a production
166
+ write, an external send, a console action — and then the question is that action in one
167
+ sentence with `Done` / `Can't` options (below).
168
+ 3. **Can someone who has not read the code answer it on a phone?** What he can see today, what
169
+ changes for a reader, two options with what each costs, your recommendation. No slice or
170
+ decision numbers, no coined nouns, no internal identifiers he has never used, no jargon you
171
+ would have to define. This is the phone test in [Writing for the human](#writing-for-the-human).
172
+ If you cannot write it that way, you do not understand it well enough to ask.
173
+
174
+ The platform PO audits open asks. One that fails a gate is retracted, with the PO's answer as the
175
+ record.
176
+
151
177
  Open a decision with:
152
178
  ```ts
153
179
  dispatch_ask({
@@ -191,20 +217,22 @@ Before saying you are waiting for human input, call `dispatch_open_asks`. It lis
191
217
 
192
218
  **Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
193
219
  a setting only they can change, a review click, a conflict between two of their own rules - if
194
- your work waits on it, open a `dispatch_ask` with `kind: "action"` the moment you know, the
195
- action as the question (the server supplies the fixed `Done` / `Can't` options; `Can't` requires
196
- an explanation). Never write it into a spec, a comment reply, a message, or a
220
+ your work waits on it, open a `dispatch_ask` the moment you know, the action as the question.
221
+ Never write it into a spec, a comment reply, a message, or a
197
222
  pull-request body: nothing in those paths reaches the human's Inbox, and a human who is not
198
223
  reading your document does not know they are the blocker. Before asking, try to remove the
199
224
  step: a value already on the machine, a permission you already hold, an API that replaces the
200
225
  click. One ask per item, `urgency: "high"` when work is stopped on it; while it is open, keep
201
226
  working on everything that is not.
202
227
 
203
- Use `kind: "action"` for a to-do handed to a human. It has fixed `Done` / `Can't` options;
204
- `Can't` requires an explanation, while the asker can still correct the action's wording:
228
+ A to-do handed to a human is an ordinary question: phrase the to-do as the question and give it
229
+ the options you want, typically `Done` / `Can't`. Nothing about the options is special to the
230
+ server; if you need a reason with `Can't`, say so in the option's description, and the human's
231
+ free-text answer carries it:
205
232
  ```ts
206
- dispatch_ask({ issue: "DSP-42", kind: "action",
207
- question: "Confirm the deployment is complete." })
233
+ dispatch_ask({ issue: "DSP-42",
234
+ question: "Confirm the deployment is complete.",
235
+ options: [{ label: "Done" }, { label: "Can't", description: "Say what is missing." }] })
208
236
  ```
209
237
 
210
238
  Correct or refine an open ask in place instead of opening a second question:
@@ -258,7 +258,8 @@ Preserve this order exactly:
258
258
  production-check task. It drives the changed path in production through the user's own access
259
259
  path and records what it saw on the pull request and on this issue. Close only after the implementer's production report exists.
260
260
  A defect it finds is a corrective child issue of this tree, not a note on a closed one; a deploy
261
- the implementer cannot perform is its action ask, and the issue waits for it.
261
+ the implementer cannot perform is its `dispatch_ask` with `Done` / `Can't` options, and the
262
+ issue waits for it.
262
263
 
263
264
  What returns the tree to review: a changed diff — a commit above the approved head that
264
265
  touches anything outside `docs/solutions/`, or a rebase whose fingerprint (the `legion-worker`
@@ -274,7 +275,7 @@ PR gets no CI and no wake announces it, and send the implementer to rebase the m
274
275
  it. Do not let the merger publish `READY` for an obsolete approval.
275
276
 
276
277
  If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
277
- to resolve, open an action ask (`dispatch_ask` with `kind: "action"`) that names the thread's URL
278
+ to resolve, open a `dispatch_ask` with `Done` / `Can't` options that names the thread's URL
278
279
  and GitHub's message for a human to resolve it by hand; the merger does not publish while it is
279
280
  open. That is the one review-thread step a human takes: the review App cannot resolve threads,
280
281
  and the implementer's and merger's runs of the command close every accepted one.
@@ -315,7 +315,7 @@ this proof.
315
315
  whose newest comment is the opener's own `Accepted:` reply, one `resolveReviewThread` per
316
316
  thread, prints `resolved <url>` or `left open <url> — newest reply by <login> is not an acceptance`,
317
317
  and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one; report that
318
- exit to the architect, which opens an action ask for a human to resolve the thread by hand —
318
+ exit to the architect, which opens a `Done` / `Can't` ask for a human to resolve the thread by hand —
319
319
  never skip it silently. The merger runs the same command once more before publishing READY
320
320
  and does not publish while any `left open` line remains.
321
321
  - **Correctness fixes land in this PR; cleanup is one named fast-follow.** A finding that
@@ -428,8 +428,8 @@ this proof.
428
428
  carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
429
429
  read GitHub, the architect reads the issue. When the deploy that carries the merge has not
430
430
  happened (a shared profile still holding the previous plugin release, a daemon still running
431
- the previous commit, a slot nobody has run), open an action ask — `dispatch_ask` with
432
- `kind: "action"` — naming the exact install or restart step, keep the `Production:` line at
431
+ the previous commit, a slot nobody has run), open a `dispatch_ask` with `Done` / `Can't`
432
+ options naming the exact install or restart step, keep the `Production:` line at
433
433
  `pending <what is missing>`, and complete the check once the human answers Done. Never record
434
434
  a staging pass as the production check, and never let the architect sign off on a `pending`
435
435
  line.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.43.0",
3
+ "version": "1.45.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [