@sjawhar/pi-legion-envoy 1.42.2 → 1.43.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
@@ -30345,6 +30345,12 @@ var stateTreeLocator = discriminatedUnion("runtime", [
30345
30345
  stateTmuxLocator.extend({ ompSessionFile: nonEmptyString.optional() }),
30346
30346
  stateK8sLocator.extend({ ompSessionFile: nonEmptyString.optional() })
30347
30347
  ]);
30348
+ var stateExternalControllerLocator = strictObject({
30349
+ runtime: literal("kubernetes"),
30350
+ external: literal(true),
30351
+ sessionId: nonEmptyString,
30352
+ registeredAt: number2().int().nonnegative()
30353
+ });
30348
30354
  var stateIssue = strictObject({
30349
30355
  key: nonEmptyString,
30350
30356
  title: string2(),
@@ -30400,7 +30406,7 @@ var LegionDaemonApi = {
30400
30406
  queue: array(nonEmptyString)
30401
30407
  }),
30402
30408
  gates: record(string2(), stateGate),
30403
- controllerLocator: stateTreeLocator.optional(),
30409
+ controllerLocator: union([stateTreeLocator, stateExternalControllerLocator]).optional(),
30404
30410
  roles: record(string2(), stateRole),
30405
30411
  controllerPendingNotices: number2().int().nonnegative(),
30406
30412
  pendingStatusWrites: array(nonEmptyString),
@@ -30415,6 +30421,10 @@ var LegionDaemonApi = {
30415
30421
  }),
30416
30422
  response: object({})
30417
30423
  },
30424
+ ControllerSecret: {
30425
+ request: strictObject({}),
30426
+ response: object({ secret: nonEmptyString })
30427
+ },
30418
30428
  ProcessStarted: {
30419
30429
  request: strictObject({
30420
30430
  tree: nonEmptyString,
package/dist/legion.js CHANGED
@@ -29619,6 +29619,12 @@ var stateTreeLocator = discriminatedUnion("runtime", [
29619
29619
  stateTmuxLocator.extend({ ompSessionFile: nonEmptyString.optional() }),
29620
29620
  stateK8sLocator.extend({ ompSessionFile: nonEmptyString.optional() })
29621
29621
  ]);
29622
+ var stateExternalControllerLocator = strictObject({
29623
+ runtime: literal("kubernetes"),
29624
+ external: literal(true),
29625
+ sessionId: nonEmptyString,
29626
+ registeredAt: number2().int().nonnegative()
29627
+ });
29622
29628
  var stateIssue = strictObject({
29623
29629
  key: nonEmptyString,
29624
29630
  title: string2(),
@@ -29674,7 +29680,7 @@ var LegionDaemonApi = {
29674
29680
  queue: array(nonEmptyString)
29675
29681
  }),
29676
29682
  gates: record(string2(), stateGate),
29677
- controllerLocator: stateTreeLocator.optional(),
29683
+ controllerLocator: union([stateTreeLocator, stateExternalControllerLocator]).optional(),
29678
29684
  roles: record(string2(), stateRole),
29679
29685
  controllerPendingNotices: number2().int().nonnegative(),
29680
29686
  pendingStatusWrites: array(nonEmptyString),
@@ -29689,6 +29695,10 @@ var LegionDaemonApi = {
29689
29695
  }),
29690
29696
  response: object({})
29691
29697
  },
29698
+ ControllerSecret: {
29699
+ request: strictObject({}),
29700
+ response: object({ secret: nonEmptyString })
29701
+ },
29692
29702
  ProcessStarted: {
29693
29703
  request: strictObject({
29694
29704
  tree: nonEmptyString,
@@ -183,6 +183,10 @@ passage with `anchor`. Follow up on an ask or comment with `dispatch_comment`; c
183
183
  with a `dispatch://` reference (see [References](#references)). Never write "see above", "the
184
184
  message above", or "as attached".
185
185
 
186
+ **A decision about an uploaded artifact links it.** If the human must read an artifact to answer,
187
+ the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
188
+ they must read to decide belongs in the spec in the first place — see [Artifacts](#artifacts).
189
+
186
190
  Before saying you are waiting for human input, call `dispatch_open_asks`. It lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply.
187
191
 
188
192
  **Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
@@ -431,6 +435,15 @@ with your text. Never do that: the spec is edited in place with `dispatch_doc_ed
431
435
  artifact by the slug shown in the upload result or by its filename, and a project document by its artifact id, slug, or filename; the
432
436
  slug also arrives on `artifact.created` events.
433
437
 
438
+ **Where a deliverable goes.** Text the human must read to decide — a draft message, a proposal,
439
+ a summary — goes in the spec as a section: the spec is the one document they open. A separate
440
+ artifact is for a real file: something sent as-is, a long report, a binary, a screenshot.
441
+
442
+ When you do upload one, the spec links it as `dispatch://KEY/artifact/<slug>` (the `slug` from the
443
+ upload result; it renders as a link) at the place the reader needs it, and the ask that needs the
444
+ decision carries the same reference. A heading or a sentence naming the filename is not a
445
+ reference.
446
+
434
447
  Documents are CommonMark. A bare `<https://example.com|text>` is a CommonMark autolink and is normalised: the angle brackets are
435
448
  dropped and the URL keeps `|text`. A backslash-escaped `\<https://example.com|text>` displays as `<https://example.com|text>` in the
436
449
  document but comes back re-escaped (`\<`) from `dispatch_doc_read`. A Slack mrkdwn draft, or any other payload that is not Markdown,
@@ -607,3 +620,38 @@ dispatch_message({
607
620
  body: "Release 1.4 is live on the devbox (dispatch://LEGION-815/artifact/release-notes). Nothing needed from you.",
608
621
  })
609
622
  ```
623
+
624
+ Before — a draft the human must read is uploaded as a separate file, the spec only names it, and
625
+ the ask does not point at it, so the reader has to go looking:
626
+
627
+ ```ts
628
+ dispatch_artifact({ issue: "OPS-52", name: "cu-update-2026-09-15.md", content: "Hi team, ..." })
629
+ dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
630
+ { op: "insert", after: "## Context", markdown: "## Draft (artifact cu-update-2026-09-15.md)" },
631
+ ]})
632
+ dispatch_ask({ issue: "OPS-52", question: "Send the customer update as drafted?", options: [...] })
633
+ ```
634
+
635
+ After — the draft is a section of the spec, and the ask anchors there. If it really must be a
636
+ file (something to send as-is), the spec and the ask both link the slug from the upload result:
637
+
638
+ ```ts
639
+ dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
640
+ { op: "insert", after: "## Context", markdown: "## Draft\n\nHi team, ..." },
641
+ ]})
642
+ dispatch_ask({
643
+ issue: "OPS-52",
644
+ question: "Send the customer update as drafted?",
645
+ options: [...],
646
+ anchor: { artifact: "spec", quote: "Hi team," },
647
+ })
648
+ // or, for a real file — the spec links it where the reader needs it, and so does the ask:
649
+ dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
650
+ { op: "insert", after: "## Context", markdown: "## Draft\n\nThe update to send as-is: dispatch://OPS-52/artifact/cu-update-2026-09-15-md" },
651
+ ]})
652
+ dispatch_ask({
653
+ issue: "OPS-52",
654
+ question: "Send this customer update as-is? dispatch://OPS-52/artifact/cu-update-2026-09-15-md",
655
+ options: [...],
656
+ })
657
+ ```
@@ -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 —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.42.2",
3
+ "version": "1.43.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",