@sjawhar/pi-legion-envoy 1.42.3 → 1.44.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(),
@@ -29926,14 +29926,13 @@ var dispatchToolSpecs = [
29926
29926
  },
29927
29927
  {
29928
29928
  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}`,
29929
+ 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
29930
  arguments: (z) => ({
29931
29931
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29932
29932
  project: z.string().describe("Project key owning the document.").optional(),
29933
29933
  artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
29934
29934
  ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
29935
29935
  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
29936
  options: z.array(z.object({
29938
29937
  label: z.string().describe("Selectable option label."),
29939
29938
  description: z.string().describe("Optional option context.").optional()
@@ -30345,6 +30344,12 @@ var stateTreeLocator = discriminatedUnion("runtime", [
30345
30344
  stateTmuxLocator.extend({ ompSessionFile: nonEmptyString.optional() }),
30346
30345
  stateK8sLocator.extend({ ompSessionFile: nonEmptyString.optional() })
30347
30346
  ]);
30347
+ var stateExternalControllerLocator = strictObject({
30348
+ runtime: literal("kubernetes"),
30349
+ external: literal(true),
30350
+ sessionId: nonEmptyString,
30351
+ registeredAt: number2().int().nonnegative()
30352
+ });
30348
30353
  var stateIssue = strictObject({
30349
30354
  key: nonEmptyString,
30350
30355
  title: string2(),
@@ -30400,7 +30405,7 @@ var LegionDaemonApi = {
30400
30405
  queue: array(nonEmptyString)
30401
30406
  }),
30402
30407
  gates: record(string2(), stateGate),
30403
- controllerLocator: stateTreeLocator.optional(),
30408
+ controllerLocator: union([stateTreeLocator, stateExternalControllerLocator]).optional(),
30404
30409
  roles: record(string2(), stateRole),
30405
30410
  controllerPendingNotices: number2().int().nonnegative(),
30406
30411
  pendingStatusWrites: array(nonEmptyString),
@@ -30415,6 +30420,10 @@ var LegionDaemonApi = {
30415
30420
  }),
30416
30421
  response: object({})
30417
30422
  },
30423
+ ControllerSecret: {
30424
+ request: strictObject({}),
30425
+ response: object({ secret: nonEmptyString })
30426
+ },
30418
30427
  ProcessStarted: {
30419
30428
  request: strictObject({
30420
30429
  tree: nonEmptyString,
@@ -32376,9 +32385,6 @@ function askUrgency(args) {
32376
32385
  const value = args.urgency;
32377
32386
  return ASK_URGENCIES.find((urgency) => urgency === value);
32378
32387
  }
32379
- function askKind(args) {
32380
- return optionalString(args, "kind") === "action" ? "action" : undefined;
32381
- }
32382
32388
  var maxAskQuestion16 = 800;
32383
32389
  function questionWithRef(question, ref) {
32384
32390
  return ref === undefined || question.includes(ref) ? question : `${question}
@@ -33175,11 +33181,9 @@ async function executeDispatchTool(input) {
33175
33181
  const options = args.options;
33176
33182
  const multiple = optionalBoolean(args, "multiple");
33177
33183
  const urgency = askUrgency(args);
33178
- const kind = askKind(args);
33179
33184
  const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
33180
33185
  const askInput = {
33181
33186
  question: askQuestionWithRef(args),
33182
- ...kind === undefined ? {} : { kind },
33183
33187
  ...Array.isArray(options) ? { options } : {},
33184
33188
  ...multiple === undefined ? {} : { multiple },
33185
33189
  ...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(),
@@ -29200,14 +29200,13 @@ var dispatchToolSpecs = [
29200
29200
  },
29201
29201
  {
29202
29202
  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}`,
29203
+ 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
29204
  arguments: (z) => ({
29205
29205
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29206
29206
  project: z.string().describe("Project key owning the document.").optional(),
29207
29207
  artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
29208
29208
  ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
29209
29209
  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
29210
  options: z.array(z.object({
29212
29211
  label: z.string().describe("Selectable option label."),
29213
29212
  description: z.string().describe("Optional option context.").optional()
@@ -29619,6 +29618,12 @@ var stateTreeLocator = discriminatedUnion("runtime", [
29619
29618
  stateTmuxLocator.extend({ ompSessionFile: nonEmptyString.optional() }),
29620
29619
  stateK8sLocator.extend({ ompSessionFile: nonEmptyString.optional() })
29621
29620
  ]);
29621
+ var stateExternalControllerLocator = strictObject({
29622
+ runtime: literal("kubernetes"),
29623
+ external: literal(true),
29624
+ sessionId: nonEmptyString,
29625
+ registeredAt: number2().int().nonnegative()
29626
+ });
29622
29627
  var stateIssue = strictObject({
29623
29628
  key: nonEmptyString,
29624
29629
  title: string2(),
@@ -29674,7 +29679,7 @@ var LegionDaemonApi = {
29674
29679
  queue: array(nonEmptyString)
29675
29680
  }),
29676
29681
  gates: record(string2(), stateGate),
29677
- controllerLocator: stateTreeLocator.optional(),
29682
+ controllerLocator: union([stateTreeLocator, stateExternalControllerLocator]).optional(),
29678
29683
  roles: record(string2(), stateRole),
29679
29684
  controllerPendingNotices: number2().int().nonnegative(),
29680
29685
  pendingStatusWrites: array(nonEmptyString),
@@ -29689,6 +29694,10 @@ var LegionDaemonApi = {
29689
29694
  }),
29690
29695
  response: object({})
29691
29696
  },
29697
+ ControllerSecret: {
29698
+ request: strictObject({}),
29699
+ response: object({ secret: nonEmptyString })
29700
+ },
29692
29701
  ProcessStarted: {
29693
29702
  request: strictObject({
29694
29703
  tree: nonEmptyString,
@@ -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
@@ -148,6 +149,30 @@ Read them; reference the existing issue, or repeat the call with `force: true` w
148
149
 
149
150
  ## Asking
150
151
 
152
+ ### Before you ask
153
+
154
+ Sami, 2026-09-16, verbatim, rejecting two asks the same night: "All of these \"decisions\" are
155
+ completely disconnected from any discussion of design or trade-offs. This is not a very useful way
156
+ of having this discussion" (on a report-table shape), and "What's a fenced PutObject or phantom
157
+ eval_id? What's an R4 model header? What exactly is the question or uncertainty here?" (on a
158
+ production import). Every `dispatch_ask` passes three gates first:
159
+
160
+ 1. **Does it need his authority, taste, or risk appetite?** The same bar as a spec's Decisions
161
+ needed ([Writing a spec](#writing-a-spec)). Schema shapes, table layouts, field names, migration
162
+ internals, and contracts between lanes do not: they go to the platform PO over Envoy, who rules.
163
+ 2. **Is there genuine uncertainty?** If not, it is a plan you execute. The one legitimate ask
164
+ without uncertainty is permission for an action only a human can authorise — a production
165
+ write, an external send, a console action — and then the question is that action in one
166
+ sentence with `Done` / `Can't` options (below).
167
+ 3. **Can someone who has not read the code answer it on a phone?** What he can see today, what
168
+ changes for a reader, two options with what each costs, your recommendation. No slice or
169
+ decision numbers, no coined nouns, no internal identifiers he has never used, no jargon you
170
+ would have to define. This is the phone test in [Writing for the human](#writing-for-the-human).
171
+ If you cannot write it that way, you do not understand it well enough to ask.
172
+
173
+ The platform PO audits open asks. One that fails a gate is retracted, with the PO's answer as the
174
+ record.
175
+
151
176
  Open a decision with:
152
177
  ```ts
153
178
  dispatch_ask({
@@ -191,20 +216,22 @@ Before saying you are waiting for human input, call `dispatch_open_asks`. It lis
191
216
 
192
217
  **Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
193
218
  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
219
+ your work waits on it, open a `dispatch_ask` the moment you know, the action as the question.
220
+ Never write it into a spec, a comment reply, a message, or a
197
221
  pull-request body: nothing in those paths reaches the human's Inbox, and a human who is not
198
222
  reading your document does not know they are the blocker. Before asking, try to remove the
199
223
  step: a value already on the machine, a permission you already hold, an API that replaces the
200
224
  click. One ask per item, `urgency: "high"` when work is stopped on it; while it is open, keep
201
225
  working on everything that is not.
202
226
 
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:
227
+ A to-do handed to a human is an ordinary question: phrase the to-do as the question and give it
228
+ the options you want, typically `Done` / `Can't`. Nothing about the options is special to the
229
+ server; if you need a reason with `Can't`, say so in the option's description, and the human's
230
+ free-text answer carries it:
205
231
  ```ts
206
- dispatch_ask({ issue: "DSP-42", kind: "action",
207
- question: "Confirm the deployment is complete." })
232
+ dispatch_ask({ issue: "DSP-42",
233
+ question: "Confirm the deployment is complete.",
234
+ options: [{ label: "Done" }, { label: "Can't", description: "Say what is missing." }] })
208
235
  ```
209
236
 
210
237
  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.
@@ -64,6 +64,26 @@ command run `/legion-claim-controller` again.
64
64
  This handshake lets the daemon redeliver held controller work. It does not turn the controller
65
65
  into a state holder: daemon state and the Dispatch project remain authoritative.
66
66
 
67
+ ### Started by the operator (runtime: kubernetes)
68
+
69
+ When the daemon runs inside a Kubernetes cluster it cannot open a terminal anywhere, so nobody
70
+ launched your pane: the operator ran `legion controller start --config controller.yaml
71
+ [--daemon-url <port-forward>]` on their own machine, and you are that foreground OMP session.
72
+ The command fetched a fresh controller secret from the daemon with the operator's token, wrote it
73
+ to a 0600 file under `LEGION_STATE_DIR` (`~/.local/state/legion/<project>-controller` by default)
74
+ beside the `gh` shim and the `legion` launcher, and started you with `LEGION_CONTROLLER=1` and
75
+ the same environment a tmux controller pane carries — so the extension claims the role and calls
76
+ `/controller/ready` exactly as under tmux, and nothing changes in how you handle wakes. The
77
+ daemon records you as `controllerLocator: {runtime: "kubernetes", external: true, sessionId,
78
+ registeredAt}` and reads your liveness from the Envoy role registry (the holder of
79
+ `legion-<project>-controller` and its `last_seen`), not from a pane: keep the session running.
80
+ Exiting it leaves the project without a controller until the operator runs the command again —
81
+ the daemon logs `controller not registered; run legion controller start` once per boot-timeout
82
+ interval and launches nothing itself. `legion state`, `legion gh -- <args>`, and
83
+ `legion status <KEY> <status>` work here over `LEGION_DAEMON_URL` (the port-forward). A second
84
+ `legion controller start` replaces you: it mints a new secret, so your grants stop working and
85
+ the role moves to the new session.
86
+
67
87
  ## Deployment instructions
68
88
 
69
89
  Deployment instructions, when present, are the operator's standing rules for this repository —
@@ -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.42.3",
3
+ "version": "1.44.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "legion": {
15
- "daemonApiVersion": 5
15
+ "daemonApiVersion": 6
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",