@sjawhar/opencode-legion-envoy 1.31.2 → 1.33.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/src/server.js
CHANGED
|
@@ -13589,7 +13589,7 @@ var ArtifactReviewEventPayloadSchema = object({
|
|
|
13589
13589
|
var askEventPayloadFields = {
|
|
13590
13590
|
id: string2().optional(),
|
|
13591
13591
|
opened_event_id: number2().int().positive(),
|
|
13592
|
-
kind:
|
|
13592
|
+
kind: string2().optional(),
|
|
13593
13593
|
question: string2().optional(),
|
|
13594
13594
|
options: array(object({ label: string2().optional() })).optional(),
|
|
13595
13595
|
answer: object({ selected: array(string2()).nullish(), text: string2().nullish() }).nullish(),
|
|
@@ -13829,14 +13829,13 @@ var dispatchToolSpecs = [
|
|
|
13829
13829
|
},
|
|
13830
13830
|
{
|
|
13831
13831
|
name: "dispatch_ask",
|
|
13832
|
-
description: "Open a durable, answerable decision
|
|
13832
|
+
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}`,
|
|
13833
13833
|
arguments: (z) => ({
|
|
13834
13834
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
13835
13835
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
13836
13836
|
artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
|
|
13837
13837
|
ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
|
|
13838
13838
|
question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
|
|
13839
|
-
kind: z.enum(["action"]).describe("Optional human to-do ask kind.").optional(),
|
|
13840
13839
|
options: z.array(z.object({
|
|
13841
13840
|
label: z.string().describe("Selectable option label."),
|
|
13842
13841
|
description: z.string().describe("Optional option context.").optional()
|
|
@@ -14231,6 +14230,12 @@ var stateTreeLocator = discriminatedUnion("runtime", [
|
|
|
14231
14230
|
stateTmuxLocator.extend({ ompSessionFile: nonEmptyString.optional() }),
|
|
14232
14231
|
stateK8sLocator.extend({ ompSessionFile: nonEmptyString.optional() })
|
|
14233
14232
|
]);
|
|
14233
|
+
var stateExternalControllerLocator = strictObject({
|
|
14234
|
+
runtime: literal("kubernetes"),
|
|
14235
|
+
external: literal(true),
|
|
14236
|
+
sessionId: nonEmptyString,
|
|
14237
|
+
registeredAt: number2().int().nonnegative()
|
|
14238
|
+
});
|
|
14234
14239
|
var stateIssue = strictObject({
|
|
14235
14240
|
key: nonEmptyString,
|
|
14236
14241
|
title: string2(),
|
|
@@ -14286,7 +14291,7 @@ var LegionDaemonApi = {
|
|
|
14286
14291
|
queue: array(nonEmptyString)
|
|
14287
14292
|
}),
|
|
14288
14293
|
gates: record(string2(), stateGate),
|
|
14289
|
-
controllerLocator: stateTreeLocator.optional(),
|
|
14294
|
+
controllerLocator: union([stateTreeLocator, stateExternalControllerLocator]).optional(),
|
|
14290
14295
|
roles: record(string2(), stateRole),
|
|
14291
14296
|
controllerPendingNotices: number2().int().nonnegative(),
|
|
14292
14297
|
pendingStatusWrites: array(nonEmptyString),
|
|
@@ -14301,6 +14306,10 @@ var LegionDaemonApi = {
|
|
|
14301
14306
|
}),
|
|
14302
14307
|
response: object({})
|
|
14303
14308
|
},
|
|
14309
|
+
ControllerSecret: {
|
|
14310
|
+
request: strictObject({}),
|
|
14311
|
+
response: object({ secret: nonEmptyString })
|
|
14312
|
+
},
|
|
14304
14313
|
ProcessStarted: {
|
|
14305
14314
|
request: strictObject({
|
|
14306
14315
|
tree: nonEmptyString,
|
|
@@ -15313,9 +15322,6 @@ function askUrgency(args) {
|
|
|
15313
15322
|
const value = args.urgency;
|
|
15314
15323
|
return ASK_URGENCIES.find((urgency) => urgency === value);
|
|
15315
15324
|
}
|
|
15316
|
-
function askKind(args) {
|
|
15317
|
-
return optionalString(args, "kind") === "action" ? "action" : undefined;
|
|
15318
|
-
}
|
|
15319
15325
|
var maxAskQuestion16 = 800;
|
|
15320
15326
|
function questionWithRef(question, ref) {
|
|
15321
15327
|
return ref === undefined || question.includes(ref) ? question : `${question}
|
|
@@ -16112,11 +16118,9 @@ async function executeDispatchTool(input) {
|
|
|
16112
16118
|
const options = args.options;
|
|
16113
16119
|
const multiple = optionalBoolean(args, "multiple");
|
|
16114
16120
|
const urgency = askUrgency(args);
|
|
16115
|
-
const kind = askKind(args);
|
|
16116
16121
|
const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
|
|
16117
16122
|
const askInput = {
|
|
16118
16123
|
question: askQuestionWithRef(args),
|
|
16119
|
-
...kind === undefined ? {} : { kind },
|
|
16120
16124
|
...Array.isArray(options) ? { options } : {},
|
|
16121
16125
|
...multiple === undefined ? {} : { multiple },
|
|
16122
16126
|
...urgency === undefined ? {} : { urgency },
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -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`
|
|
195
|
-
|
|
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
|
-
|
|
204
|
-
`Can't
|
|
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",
|
|
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
|
|
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.
|
|
@@ -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
|
|
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.
|