@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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
195
|
-
|
|
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
|
-
|
|
204
|
-
`Can't
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
432
|
-
|
|
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.
|