@sjawhar/opencode-legion-envoy 1.17.1 → 1.18.1
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 +11 -6
- package/package.json +1 -1
- package/skills/AGENTS.md +5 -2
- package/skills/dispatch/SKILL.md +9 -3
- package/skills/legion-architect/SKILL.md +89 -39
- package/skills/legion-controller/SKILL.md +7 -3
- package/skills/legion-retro/SKILL.md +31 -16
- package/skills/legion-worker/SKILL.md +176 -35
package/dist/src/server.js
CHANGED
|
@@ -13875,7 +13875,7 @@ var dispatchToolSpecs = [
|
|
|
13875
13875
|
},
|
|
13876
13876
|
{
|
|
13877
13877
|
name: "dispatch_doc_edit",
|
|
13878
|
-
description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
13878
|
+
description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + "For replace, delete, and quote insert anchors, find text exactly as rendered: omit Markdown markers such as backticks or asterisks. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
13879
13879
|
arguments: (z) => ({
|
|
13880
13880
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
13881
13881
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -14165,8 +14165,9 @@ var stateTree = strictObject({
|
|
|
14165
14165
|
locator: stateTreeLocator.optional()
|
|
14166
14166
|
});
|
|
14167
14167
|
var stateGate = strictObject({
|
|
14168
|
-
|
|
14169
|
-
|
|
14168
|
+
artifactId: nonEmptyString,
|
|
14169
|
+
latestVersion: number2().int().positive(),
|
|
14170
|
+
approvedVersion: number2().int().positive().optional()
|
|
14170
14171
|
});
|
|
14171
14172
|
var stateRole = strictObject({
|
|
14172
14173
|
role: nonEmptyString,
|
|
@@ -14308,7 +14309,11 @@ var LegionDaemonApi = {
|
|
|
14308
14309
|
response: object({})
|
|
14309
14310
|
},
|
|
14310
14311
|
GatesRegister: {
|
|
14311
|
-
request: architectCapability.extend({
|
|
14312
|
+
request: architectCapability.extend({
|
|
14313
|
+
issue: nonEmptyString,
|
|
14314
|
+
artifactId: uuid2(),
|
|
14315
|
+
version: number2().int().positive()
|
|
14316
|
+
}),
|
|
14312
14317
|
response: object({})
|
|
14313
14318
|
},
|
|
14314
14319
|
Grant: {
|
|
@@ -15649,7 +15654,7 @@ ${trailer.join(`
|
|
|
15649
15654
|
const result = await client.requestApproval(resolved.artifact.id, { actor });
|
|
15650
15655
|
if (result.ask === null) {
|
|
15651
15656
|
return {
|
|
15652
|
-
text: `${resolved.artifact.name} is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. An edit after approval makes it stale, so request again only for a new version.`,
|
|
15657
|
+
text: `${resolved.artifact.name} (document id ${resolved.artifact.id}) is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. An edit after approval makes it stale, so request again only for a new version.`,
|
|
15653
15658
|
details: {
|
|
15654
15659
|
...resolved.owner.kind === "project" ? documentResultDetails(resolved.artifact) : { issue: resolved.issue?.key },
|
|
15655
15660
|
artifact: resolved.artifact.id,
|
|
@@ -15659,7 +15664,7 @@ ${trailer.join(`
|
|
|
15659
15664
|
}
|
|
15660
15665
|
const details = await askResultDetails(client, result.ask, resolved);
|
|
15661
15666
|
return {
|
|
15662
|
-
text: `Approval requested for ${resolved.artifact.name} at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
|
|
15667
|
+
text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
|
|
15663
15668
|
details: { ...details, artifact: resolved.artifact.id, version: result.version }
|
|
15664
15669
|
};
|
|
15665
15670
|
}
|
package/package.json
CHANGED
package/skills/AGENTS.md
CHANGED
|
@@ -34,8 +34,11 @@ durable human question; replies come back to the worker's own session.
|
|
|
34
34
|
## Durable artifacts
|
|
35
35
|
|
|
36
36
|
Phase handoffs are committed in lifecycle order: architect, plan, implement, test, and review.
|
|
37
|
+
Only the implementer pushes them: it and the merger act as the code-writing GitHub App, while
|
|
38
|
+
the planner, tester, reviewer, and architects act as the review App (`appRoleForLegionRole`,
|
|
39
|
+
`packages/daemon/src/daemon/github-apps.ts`), which holds no `contents` permission — their
|
|
40
|
+
handoff commits stay on the shared workspace's issue branch and ride the implementer's next push.
|
|
37
41
|
A clean review ends with the `.legion/` deletion pushed by the implementer at the reviewer's
|
|
38
|
-
direction
|
|
39
|
-
then approves; retro records its learning in
|
|
42
|
+
direction, which the reviewer then approves; retro records its learning in
|
|
40
43
|
`docs/solutions/` and writes no handoff. GitHub comments and reviews carry the required Legion
|
|
41
44
|
footer so the daemon can attribute artifacts to their worker session.
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -243,9 +243,10 @@ type EditOp = {
|
|
|
243
243
|
};
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
Target `replace` and
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
Target `replace`, `delete`, and quote insert anchors by a block's text as rendered: write inline
|
|
247
|
+
code without backticks, bold without asterisks, and link text without link syntax. A table-cell
|
|
248
|
+
anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay
|
|
249
|
+
within one textblock; split changes that span separate blocks into separate operations.
|
|
249
250
|
|
|
250
251
|
`replace` requires `find` and `with`; `delete` requires `find`; `insert` requires `markdown` and exactly one of `after` or `before`. An
|
|
251
252
|
insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Ordinary inserts create a sibling block before or after the quote or
|
|
@@ -286,6 +287,11 @@ Should we ship the migration?
|
|
|
286
287
|
:::
|
|
287
288
|
```
|
|
288
289
|
|
|
290
|
+
When a human answers a decision written as an ask block, the answer lives on that ask. Use
|
|
291
|
+
`dispatch_resolve_ask` when the decision is resolved without a human response, or preserve the
|
|
292
|
+
human's answer; never rewrite the question into its answer or blank its options. An edit that leaves
|
|
293
|
+
an ask block without a question or with a blank option is rejected with `INVALID_ASK_BLOCK`.
|
|
294
|
+
|
|
289
295
|
## Comments and suggestions
|
|
290
296
|
|
|
291
297
|
Add feedback with:
|
|
@@ -18,7 +18,11 @@ separate coordinator to finish necessary work.
|
|
|
18
18
|
Message a known phase worker with `envoy_publish` to `notifications.role.` followed by
|
|
19
19
|
its encoded role token (the token `spawn_worker` returned for it); re-assign it by
|
|
20
20
|
calling `spawn_worker` again on the same existing role, which resumes the same process
|
|
21
|
-
instead of starting a fresh one.
|
|
21
|
+
instead of starting a fresh one. Phases on one issue are strictly sequential -- one role
|
|
22
|
+
is the issue's active phase at a time, and calling `spawn_worker` for a different role
|
|
23
|
+
while a phase is active supersedes that phase: the superseded worker's
|
|
24
|
+
`legion handoff complete` is then refused, so finish (or deliberately abandon) one role
|
|
25
|
+
before assigning the next. Phase workers escalate lifecycle, scope, and
|
|
22
26
|
cross-phase matters the same way: `envoy_publish` to your own encoded token. Any role
|
|
23
27
|
may use `dispatch_ask` directly for a standalone human question; replies return to the
|
|
24
28
|
asking session.
|
|
@@ -29,7 +33,8 @@ separate coordinator to finish necessary work.
|
|
|
29
33
|
hold (your own, or one `spawn_worker` returned) or compute another with the
|
|
30
34
|
`roleToken` helper from `@legion/contracts` exactly the way the daemon does.
|
|
31
35
|
- There is no label vocabulary. Dispatch status replaces the board, and the design gate
|
|
32
|
-
is a
|
|
36
|
+
is a human approving the root spec document at a version in Dispatch, requested with
|
|
37
|
+
`dispatch_request_approval` — not a label and not an ask. Never attempt to apply a label.
|
|
33
38
|
- Deferring necessary work is failure. The sole valid deferral is a new child issue you
|
|
34
39
|
create and continue to own. Re-file a genuinely independent child through the
|
|
35
40
|
controller rather than treating it as an abandoned dependency.
|
|
@@ -83,34 +88,51 @@ adoption/decomposition and waves, acceptance criteria, and the integration test
|
|
|
83
88
|
"spec" artifact beside it (`dispatch_artifact` with the primary document's name replaces the
|
|
84
89
|
human's document; do not do that). Both readers described in
|
|
85
90
|
[Writing for the human](../dispatch/SKILL.md#writing-for-the-human) must be able to follow it.
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
The design gate runs only when the "Design gate policy" line at the end of your system prompt
|
|
92
|
+
says `gates.design: root-issues`. When it says `gates.design: off`, write the spec and continue
|
|
93
|
+
to section 2 with no approval step at all: do not request approval, do not register a gate, and
|
|
94
|
+
do not wait for `design-approved`. A sub-architect on a child issue has no policy line and never
|
|
95
|
+
runs the gate either: the root approval covers the tree. When the gate is armed, run this exact
|
|
96
|
+
sequence **before any Legion-role spawn**, including a sub-architect:
|
|
88
97
|
|
|
89
98
|
```text
|
|
90
99
|
dispatch_doc_edit({ issue: "<root issue>", ... }) // extend the primary document in place
|
|
91
|
-
|
|
100
|
+
result = dispatch_request_approval({ issue: "<root issue>" }) // the primary document by default
|
|
101
|
+
legion({
|
|
102
|
+
op: "register_gate",
|
|
92
103
|
issue: "<root issue>",
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{ label: "Approve", description: "Work starts as described; the first worker is spawned now." },
|
|
96
|
-
{ label: "Hold", description: "Nothing starts; reply on the issue with what should change first." },
|
|
97
|
-
]
|
|
104
|
+
artifactId: result.details.artifact, // the document id, a UUID such as 4e0aca36-77b3-43bd-96cf-d58890ae64e4
|
|
105
|
+
version: result.details.version, // the version number the human is asked to approve
|
|
98
106
|
})
|
|
99
|
-
legion({ op: "register_gate", issue: "<root issue>", askId })
|
|
100
107
|
```
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
`dispatch_request_approval` opens a system question on the document with the fixed options
|
|
110
|
+
`Approve` and `Request changes`; a human answers it from the Inbox or approves from the
|
|
111
|
+
document's own header. Never open a `dispatch_ask` with an `Approve` option yourself: an
|
|
112
|
+
ordinary question is not a gate and the daemon ignores its answer. Copy `artifactId` and
|
|
113
|
+
`version` from the result of `dispatch_request_approval` — its text reads "Approval requested for
|
|
114
|
+
spec.md (document id <UUID>) at version <N>" and its `details.artifact` / `details.version` carry
|
|
115
|
+
the same two values. The document id is never the slug or file name you passed in (`spec`,
|
|
116
|
+
`spec.md`): the daemon recognizes the document's approval events by that id, and both the
|
|
117
|
+
`legion` tool and the daemon refuse a value that is not a UUID. Calling
|
|
118
|
+
`dispatch_request_approval` again while a request is open returns the same open request, so it
|
|
119
|
+
is safe to repeat. If its text instead reads "spec.md (document id <UUID>) is already approved at
|
|
120
|
+
version <N>" — a human approved from the document header before you asked — still call
|
|
121
|
+
`register_gate` with that id and version: the daemon reads the approval from Dispatch as it
|
|
122
|
+
registers, opens the gate, and delivers `design-approved` at once. The same read covers a human
|
|
123
|
+
who answers the question between your `dispatch_request_approval` and `register_gate` calls, so
|
|
124
|
+
an approval is never lost to timing; you never approve anything yourself.
|
|
125
|
+
|
|
126
|
+
Then park. Do not release a wave or spawn a Legion role until a later delivered wake shows
|
|
127
|
+
`design-approved` on the root. On `design-changes-requested`, revise the spec (a new version of
|
|
128
|
+
the primary document), call `dispatch_request_approval` again — it re-opens the request at the
|
|
129
|
+
new version — and stay parked. Approval is pinned to the spec version: editing the root spec
|
|
130
|
+
after approval closes the gate again with no wake (you made the edit, or the `artifact.version`
|
|
131
|
+
event on your issue tells you), so call `dispatch_request_approval` again, and release no new
|
|
132
|
+
wave and spawn no new role until the next `design-approved` arrives — work already in flight
|
|
133
|
+
continues. Later waves, re-scopes, and integration-failure children that leave the root spec
|
|
134
|
+
untouched need no new approval, and a child issue's spec is never gated: the root approval covers
|
|
135
|
+
the tree.
|
|
114
136
|
|
|
115
137
|
## 2. Children in flight
|
|
116
138
|
|
|
@@ -141,6 +163,12 @@ in flight. On each child closure, re-scope open work, close obsolete work with a
|
|
|
141
163
|
release the next wave only when it now makes sense. There is no inter-child dependency
|
|
142
164
|
mechanism to encode.
|
|
143
165
|
|
|
166
|
+
Release admits nothing. A child never takes an admission slot or becomes a root tree of its
|
|
167
|
+
own: the daemon ignores a child's `todo` while your tree is live, and this `spawn_worker` is
|
|
168
|
+
what starts the child — the daemon writes its Dispatch status `in_progress` on the first
|
|
169
|
+
sub-architect spawn while the child is at `todo`. A released child with no sub-architect stays
|
|
170
|
+
at `todo` until you spawn one.
|
|
171
|
+
|
|
144
172
|
## 3. Children complete
|
|
145
173
|
|
|
146
174
|
Treat `children-complete` as the edge into the end-game, not as a reason to close the
|
|
@@ -182,13 +210,13 @@ legion({
|
|
|
182
210
|
op: "spawn_worker",
|
|
183
211
|
issue: "LEGION-40",
|
|
184
212
|
role: "implementer",
|
|
185
|
-
task: "Run the legion-retro skill now. Capture durable learnings and post the issue
|
|
213
|
+
task: "Run the legion-retro skill now. Capture durable learnings and post the retro message on the Dispatch issue with dispatch_message; do not create a .legion handoff file."
|
|
186
214
|
})
|
|
187
215
|
```
|
|
188
216
|
|
|
189
217
|
Wait for the implementer to report its durable retro result. Retro output is
|
|
190
|
-
`docs/solutions/` plus
|
|
191
|
-
the reviewer-approved head after cleanup.
|
|
218
|
+
`docs/solutions/` plus one `dispatch_message` on the issue; it must not create a `.legion`
|
|
219
|
+
file or rewrite the reviewer-approved head after cleanup.
|
|
192
220
|
|
|
193
221
|
## 6. Architect sign-off and merge
|
|
194
222
|
|
|
@@ -202,19 +230,37 @@ Preserve this order exactly:
|
|
|
202
230
|
2. on a clean review, `spawn_worker` the implementer once more to push only the `.legion/`
|
|
203
231
|
deletion (the review App holds no `contents` permission), then the reviewer approves that
|
|
204
232
|
head. The deletion must land before that approval, which is head-pinned. An implementer
|
|
205
|
-
completion
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
3. retro
|
|
210
|
-
|
|
211
|
-
|
|
233
|
+
completion advances the status only from `in_progress` to `testing`; this push, like retro
|
|
234
|
+
later, leaves the status where it is, so you set nothing by hand — on its `phase-complete`
|
|
235
|
+
wake, `spawn_worker` the reviewer to approve that head (a finished reviewer may already be
|
|
236
|
+
retired; `spawn_worker` resumes it);
|
|
237
|
+
3. retro commits its learnings under `docs/solutions/` on top of the approved head; that
|
|
238
|
+
commit does not void the approval and never returns the tree to the tester or reviewer;
|
|
239
|
+
4. the merger verifies the current head is the reviewer-approved head plus only commits that
|
|
240
|
+
change `docs/solutions/` (`jj diff --from <approved-sha> --to <tip-sha> --summary`, quoted in READY)
|
|
241
|
+
and publishes `READY #<n> at <sha>` to `notifications.role.pr-queue`; it never
|
|
212
242
|
merges. The merge queue merges under its own authority and the repository's own rules
|
|
213
243
|
(branch protection, CODEOWNERS); whether a human must approve first is that repository's
|
|
214
244
|
setting, not Legion's, and you never ask for or wait on such an approval.
|
|
215
245
|
|
|
216
|
-
|
|
217
|
-
|
|
246
|
+
What returns the tree to review: a changed diff — a commit above the approved head that
|
|
247
|
+
touches anything outside `docs/solutions/`, or a rebase whose fingerprint (the `legion-worker`
|
|
248
|
+
skill's unchanged-diff check) differs from the approved head's. What does not: retro's
|
|
249
|
+
`docs/solutions/` commit, and a rebase forced by a GitHub-reported conflict whose fingerprint
|
|
250
|
+
is unchanged. For that rebase the order is: the implementer rebases and posts the before/after
|
|
251
|
+
fingerprints; the tester re-runs the bare gates only; the reviewer confirms and approves the new
|
|
252
|
+
head by SHA (or continues its round if it had not approved); the merger republishes READY.
|
|
253
|
+
Retro does not re-run. A rebase happens only when GitHub reports `CONFLICTING`
|
|
254
|
+
(`legion gh -- pr view <n> --json mergeable,mergeStateStatus`); read that on every end-game
|
|
255
|
+
wake — `pr-ready`, `pr-review`, `phase-complete`, `catchup-overseer` — because a `CONFLICTING`
|
|
256
|
+
PR gets no CI and no wake announces it, and send the implementer to rebase the moment you see
|
|
257
|
+
it. Do not let the merger publish `READY` for an obsolete approval.
|
|
258
|
+
|
|
259
|
+
If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
|
|
260
|
+
to resolve, open an action ask (`dispatch_ask` with `kind: "action"`) that names the thread's URL
|
|
261
|
+
and GitHub's message for a human to resolve it by hand; the merger does not publish while it is
|
|
262
|
+
open. That is the one review-thread step a human takes: the review App cannot resolve threads,
|
|
263
|
+
and the implementer's and merger's runs of the command close every accepted one.
|
|
218
264
|
|
|
219
265
|
## 7. Close
|
|
220
266
|
|
|
@@ -236,19 +282,23 @@ corresponding lifecycle procedure.
|
|
|
236
282
|
|
|
237
283
|
| Wake | Procedure |
|
|
238
284
|
| --- | --- |
|
|
285
|
+
| `child-adopted` | Payload `{type:"child-adopted", child, remaining}`. A child is now in your tree — one created under this issue (by you or a human), or one a daemon upgrade moved back into your tree from a root tree of its own (LEGION-57). If Dispatch shows it released **and open** — `todo` through `retro`, never `done`; `remaining` counts exactly those — and `legion state` shows no `roles` entry with `issue` = the child and `role: "architect"`, `spawn_worker` its architect now. An unreleased child waits for its wave; a `done` child is finished and gets nothing, whatever stray tree of its own `legion state` may still show. |
|
|
286
|
+
| `child-status` | Payload `{type:"child-status", child, from, to}`. Your child's Dispatch status changed. `to: "todo"` with no architect claim for the child (`legion state`) means it is released and unowned — your own `release_wave` echo, or a human's move — so `spawn_worker` its architect. `to: "backlog"` or `"icebox"` means the child was de-prioritised (a human's move, or your own `set_status`): a child has no tree of its own, so the daemon stops nothing on that move — tell its sub-architect (`envoy_publish` to its role topic) to finish the step in flight and park, or re-scope it; its finished workers idle-retire, and it resumes from its session on your next `spawn_worker` once the child is released again. Any other transition is information for re-scoping. |
|
|
239
287
|
| `child-closed` | Read the child completion and remaining open children. Re-scope or close obsolete open work; release an appropriate next wave, or await `children-complete`. |
|
|
240
288
|
| `children-complete` | Execute steps 3–4: parent integration verification; failures become a new child wave, success advances to review and retro. |
|
|
241
289
|
| `child-reopened` | Treat the completion edge as reset. Reassess the reopened child and return the tree to children-in-flight; do not continue an already-started end-game. |
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
290
|
+
| `design-approved` | Payload `{type:"design-approved"}`. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
|
|
291
|
+
| `design-changes-requested` | Payload `{type:"design-changes-requested", version, reason, author?}`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec, call `dispatch_request_approval` again, and stay parked; the gate is closed. |
|
|
292
|
+
| `phase-complete` | Payload `{type:"phase-complete", issue, role, summary}`. May arrive live or via `catchup-overseer`'s `phaseCompletions`. Read the committed handoff for that phase, then spawn the next phase's owner, or `spawn_worker` on the same role again to resume it with corrections if the handoff shows unresolved gaps. A `reviewer` completion whose GitHub review is `CHANGES_REQUESTED` (the daemon returns the issue's Dispatch status to `in_progress` for this, on the reviewer's completion and again when you spawn the corrective implementer unless the daemon already knows the issue is `in_progress`) means `spawn_worker` the **implementer** again with the review findings — thread URLs and blocking items — as its task, then route back through tester and reviewer in order; never `spawn_worker` the reviewer directly off this wake and never proceed to retro on this verdict. A reviewer completion with an `APPROVED` review proceeds to retro (step 5). A `reviewer` completion after a conflict-forced rebase whose review body names an unchanged fingerprint is a confirmation, not a round: if retro already completed, `spawn_worker` the merger; otherwise resume the step you were on. |
|
|
293
|
+
| `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. |
|
|
244
294
|
| `worker-started` | Payload `{type:"worker-started", issue, role}`. A previously queued role has been promoted and is now running. Treat it exactly as a normal spawn: resume tracking that role's live session. |
|
|
245
295
|
| `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
|
|
246
|
-
| `pr-review` | Payload `{type:"pr-review", state, author, body}`. Delivered to whichever role is currently active for the issue, falling back to you when no worker phase is active. Follows the same verdict rule as a reviewer's `phase-complete`: `state: "changes_requested"` sends the implementer back in with the review findings, then tester, then reviewer — never the reviewer again and never retro; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. |
|
|
296
|
+
| `pr-review` | Payload `{type:"pr-review", state, author, body}`. Delivered to whichever role is currently active for the issue, falling back to you when no worker phase is active. Follows the same verdict rule as a reviewer's `phase-complete`: `state: "changes_requested"` sends the implementer back in with the review findings, then tester, then reviewer — never the reviewer again and never retro; that `spawn_worker` returns the issue to `in_progress` on its own (the daemon writes it for a corrective implementer whenever the PR's latest recorded review is changes requested, a human's after approval included), so you set nothing by hand; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. `state: "approved"` on a rebased head whose body names an unchanged fingerprint is that confirmation: proceed to retro if it has not run, otherwise to the merger — never to a second retro or test round. |
|
|
247
297
|
| `pr-blocked` | Payload `{type:"pr-blocked", pr, attempts}`. `attempts` counts heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count; a push the daemon cannot classify (a listener without `changed_paths`, a list capped at 100, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. Assign a focused implementer or corrective child, then return it through testing and review; do not treat the blocked PR as final. |
|
|
248
298
|
| `pr-merged` | Payload `{type:"pr-merged", pr, mergeCommitSha}`. The merge queue landed the PR. This is your cue for step 7: post the sign-off comment naming that merge commit and set the issue `done`. Nothing else follows a merge. |
|
|
249
299
|
| `pr-closed-unmerged` | Decide from current scope whether to reopen the work, send a fresh implementer, or cancel it with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
|
|
250
300
|
| `issue-comment` | Interpret the comment in the issue's design context. Answer it, adjust the plan, or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
|
|
251
|
-
| `catchup-overseer` | Verify its gates, child counts, and PR verdicts against current artifacts, then resume the applicable numbered lifecycle step. It is a current-state snapshot, not a raw-event replay. For each entry in its `phaseCompletions` (`{issue, role, summary, at}`, phases that completed while you were not live), handle it exactly as a `phase-complete` wake. |
|
|
301
|
+
| `catchup-overseer` | Verify its gates, child counts, and PR verdicts against current artifacts, then resume the applicable numbered lifecycle step. It is a current-state snapshot, not a raw-event replay. `gates[LEGION_TREE].open` is the design gate's current state: `true` means the root spec is approved at its current version and you may spawn; `false` (or no `open` key, meaning no gate is registered) means the sequence in section 1 still applies. For each entry in its `phaseCompletions` (`{issue, role, summary, at}`, phases that completed while you were not live), handle it exactly as a `phase-complete` wake. Then compare `childCounts[LEGION_ISSUE].open` (the children not `done`) with `legion state` and Dispatch: any **open** released child — `todo` through `retro` — with no architect role claim gets `spawn_worker` for its architect, a `child-adopted` or `child-status` wake you missed while not live; a `done` child gets nothing, whether or not a lingering legacy tree of its own still shows in `legion state`. |
|
|
252
302
|
| `worker-died` | Payload `{type:"worker-died", issue, role}`. The daemon probed and retried this role's worker through `MAX_LAUNCH_FAILURES` attempts and could not confirm a boot — never a raw-event replay or a silent revive. Reassess the work and `spawn_worker` again for the role (it resumes the same agent via `--resume` if a session file survived) or reassign it if the failure looks environmental, not agent-specific. |
|
|
253
303
|
| `reopened` | Reopen the root lifecycle: inspect the reason and current artifacts, reassess scope and children, and resume at the first applicable numbered step. |
|
|
254
304
|
|
|
@@ -129,7 +129,11 @@ on stale entries until their source artifact explains the anomaly.
|
|
|
129
129
|
## Mentions
|
|
130
130
|
|
|
131
131
|
Read the mention and its artifact. Answer it when it asks the controller for triage or
|
|
132
|
-
human-facing information.
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
human-facing information. A human asking how to let a root proceed past its design gate
|
|
133
|
+
approves the root issue's spec document in Dispatch — the `Approve` control in the document's
|
|
134
|
+
header, or the approval question the architect's request opened in the Inbox. The controller
|
|
135
|
+
never opens a gate and there is no operator command for it; a project that does not want the
|
|
136
|
+
gate at all runs `gates.design: off` in its `legion.yaml`. Otherwise resolve the authoritative
|
|
137
|
+
owning architect role and route the verified context with `envoy_publish`. Do not route raw
|
|
138
|
+
event traffic or invent a role token from a partial issue reference.
|
|
135
139
|
|
|
@@ -17,15 +17,23 @@ retrospective's durable output.
|
|
|
17
17
|
1. Tester green and all code-review cycles finish.
|
|
18
18
|
2. The implementer pushes the `.legion/` deletion at the reviewer's direction, and the reviewer
|
|
19
19
|
approves that head.
|
|
20
|
-
3. Run this retro: commit durable learnings to `docs/solutions/` and post the
|
|
20
|
+
3. Run this retro: commit durable learnings to `docs/solutions/` and post the retro message on
|
|
21
|
+
the Dispatch issue.
|
|
21
22
|
Retro writes **no `.legion` file**, so it never re-dirties the cleaned handoff tree.
|
|
22
|
-
4. The merger verifies the
|
|
23
|
-
|
|
23
|
+
4. The merger verifies the tip is the approved head plus commits that change only
|
|
24
|
+
`docs/solutions/` — `jj diff --from <approved-sha> --to <tip-sha> --summary`, quoted in READY —
|
|
25
|
+
publishes `READY`, and pushes nothing; the merge queue merges under the repository's own
|
|
26
|
+
rules.
|
|
24
27
|
5. After the merge lands, the implementer — not the reviewer, the merger, or the queue — verifies
|
|
25
28
|
the change in production and records it on the PR and the issue (Sami, 2026-09-13, verbatim:
|
|
26
29
|
"the agent that developed it should be responsible for testing in production"). The
|
|
27
30
|
architect's sign-off waits for that record.
|
|
28
31
|
|
|
32
|
+
Retro's commit sits above the reviewer's approved head and the approval stands: a commit that
|
|
33
|
+
changes only `docs/solutions/` does not void it, and the tree goes from retro to the merger —
|
|
34
|
+
never back to the tester or reviewer. A conflict-forced rebase after retro moves these documents
|
|
35
|
+
with the branch; retro does not re-run.
|
|
36
|
+
|
|
29
37
|
Do not start retro before step 2, skip it because the change seems mechanical, or publish `READY`
|
|
30
38
|
before step 3. The design gate is not a substitute for review and retro.
|
|
31
39
|
|
|
@@ -59,18 +67,22 @@ date: YYYY-MM-DD
|
|
|
59
67
|
status: active
|
|
60
68
|
module: affected-module
|
|
61
69
|
related_issues:
|
|
62
|
-
- "
|
|
70
|
+
- "LEGION-123" # the Dispatch issue
|
|
71
|
+
- "owner/repo#456" # the pull request
|
|
63
72
|
---
|
|
64
73
|
```
|
|
65
74
|
|
|
66
75
|
Commit the documentation on the existing issue branch, advance its existing bookmark, and push
|
|
67
|
-
that branch. Do not create a replacement branch or bookmark. Then post
|
|
68
|
-
the
|
|
69
|
-
|
|
76
|
+
that branch. Do not create a replacement branch or bookmark. Then post one Dispatch message on
|
|
77
|
+
the issue — `issue` is your `LEGION_ISSUE`; Legion issues live on Dispatch, never on a GitHub
|
|
78
|
+
issue, and the `gh` shim refuses every GitHub-issue write — naming the documents and the
|
|
79
|
+
one-to-three most useful takeaways. The message must carry this revived implementer's structured
|
|
80
|
+
attribution footer with `phase` set to `retro`; the body is capped at 2,000 characters:
|
|
70
81
|
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
```ts
|
|
83
|
+
dispatch_message({
|
|
84
|
+
issue: "<KEY>",
|
|
85
|
+
body: `## Retro Complete
|
|
74
86
|
|
|
75
87
|
**Learnings documented in:**
|
|
76
88
|
- docs/solutions/<path>.md
|
|
@@ -78,19 +90,22 @@ legion gh -- issue comment <issue-number> \
|
|
|
78
90
|
**Key takeaways:**
|
|
79
91
|
- <reusable lesson>
|
|
80
92
|
|
|
81
|
-
<!-- legion: {"session":"<session-id>","phase":"retro"}
|
|
82
|
-
|
|
93
|
+
<!-- legion: {"session":"<session-id>","phase":"retro"} -->`,
|
|
94
|
+
})
|
|
83
95
|
```
|
|
84
96
|
|
|
85
|
-
The
|
|
86
|
-
handoff, phase artifact, local feedback log, or completion label
|
|
97
|
+
The Dispatch message and the `docs/solutions/` commit are the only retro outputs. Never write a
|
|
98
|
+
handoff, phase artifact, local feedback log, or completion label; `.legion/` was deleted before
|
|
99
|
+
retro and nothing recreates it. Report completion with `legion handoff complete` alone (its
|
|
100
|
+
summary: two sentences for the architect) — no `legion handoff write`.
|
|
87
101
|
|
|
88
102
|
## Completion check
|
|
89
103
|
|
|
90
104
|
Before returning, verify all of the following:
|
|
91
105
|
|
|
92
|
-
- The reviewer cleanup commit remains below the retro documentation commit
|
|
93
|
-
|
|
106
|
+
- The reviewer cleanup commit remains below the retro documentation commit, and the reviewer's
|
|
107
|
+
approval of that cleanup head stands: the merger accepts the approved head plus this commit.
|
|
108
|
+
- The learning documents and the Dispatch message both exist.
|
|
94
109
|
- No `.legion` file was created or modified by retro.
|
|
95
110
|
- The fresh-eyes analysis was considered alongside the implementer's context.
|
|
96
111
|
- The merger remains a subsequent step, not work performed by retro.
|
|
@@ -89,7 +89,7 @@ Concurrent issues have disjoint workspaces; only the currently active phase muta
|
|
|
89
89
|
one. After you complete and go idle, treat `$LEGION_WORKSPACE` as read-only: you are kept
|
|
90
90
|
alive to answer questions, not to keep editing. Do not create new commits, run
|
|
91
91
|
`jj -R "$LEGION_WORKSPACE" new`, or touch tracked files once your own handoff is committed
|
|
92
|
-
and pushed — a code change belongs to whichever phase is active now.
|
|
92
|
+
(and, for the implementer, pushed) — a code change belongs to whichever phase is active now.
|
|
93
93
|
|
|
94
94
|
On every start, and especially after revival or re-creation, read the issue and then the
|
|
95
95
|
committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legion/`:
|
|
@@ -120,10 +120,21 @@ transcript, the committed file wins: it is the copy that survived.
|
|
|
120
120
|
- **Before pushing, check ancestry:** `jj -R "$LEGION_WORKSPACE" log -r 'ancestors(@, 5)'`
|
|
121
121
|
— verify only your issue's commits are in the chain, not unrelated work.
|
|
122
122
|
|
|
123
|
-
**Shared operation safety:**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
**Shared operation safety:** Every Legion issue workspace is a `jj workspace` of one shared
|
|
124
|
+
clone, so they all share one operation log: `jj undo`, `jj abandon`, and
|
|
125
|
+
`jj op restore|revert|abandon|undo` rewrite it for every tree at once (on 2026-09-12 one
|
|
126
|
+
worker's `jj undo` rewrote nine of another tree's commits). The extension refuses them in every
|
|
127
|
+
phase-worker pane before they run — a `bash` command in any position of a pipeline or `&&`
|
|
128
|
+
chain, with or without `-R`, judged on the whole argument list; `eval` code; and a `hub`
|
|
129
|
+
process start — from your own tool calls and from any `task` subagent you spawn (it runs in
|
|
130
|
+
your pane, against the same log), and a `bash` command whose quoted text merely mentions `jj`
|
|
131
|
+
with one of those words (a heredoc, an echo, a commit message) is refused too: write such text
|
|
132
|
+
with the `write` tool or say "operation-log rollback" instead. `jj restore <paths>`,
|
|
133
|
+
`jj op log`, and `jj op show` stay allowed. Recover forward only: a new commit
|
|
134
|
+
(`jj -R "$LEGION_WORKSPACE" new`) or `jj -R "$LEGION_WORKSPACE" restore <paths>` of files.
|
|
135
|
+
Anything else, stop and send the owning architect the `jj -R "$LEGION_WORKSPACE" log`
|
|
136
|
+
evidence; the architect decides, and an operator performs any operation-log restore with every
|
|
137
|
+
other tree paused.
|
|
127
138
|
|
|
128
139
|
## Phase work
|
|
129
140
|
|
|
@@ -138,9 +149,22 @@ Commit attribution is automatic: the extension exports a `JJ_CONFIG` overlay whe
|
|
|
138
149
|
session starts, so every jj commit you make carries an `Omp-Session: <this-session-id>`
|
|
139
150
|
trailer with no action from you. Do not add attribution trailers by hand.
|
|
140
151
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
Your pane's environment already supplies your phase's author and committer identity
|
|
153
|
+
(`JJ_USER`/`JJ_EMAIL` and the Git author/committer variables, set by the daemon when it opened
|
|
154
|
+
the pane; the daemon also re-authors the workspace's working copy for your role at each
|
|
155
|
+
assignment, since `jj split`/`jj describe` keep its author). Never set or override
|
|
156
|
+
`user.name`/`user.email` in any jj or Git scope — not `jj config set`, not `--config`, not
|
|
157
|
+
`git config`: `--config` outranks the pane environment and would put the wrong App back on your
|
|
158
|
+
commits, and the repository-scoped jj config is one file shared by every issue workspace of the
|
|
159
|
+
clone. Before a push, check
|
|
160
|
+
`jj -R "$LEGION_WORKSPACE" log -r 'main@origin..@' -T 'author.email() ++ " | " ++ committer.email() ++ " " ++ description.first_line() ++ "\n"'`
|
|
161
|
+
shows your role's App in both columns **on every commit you made** — not on the whole list:
|
|
162
|
+
earlier phases' commits are legitimately authored by their own role's App, and a conflict-forced
|
|
163
|
+
rebase legitimately sets the committer of every rebased commit, other roles' included, to the
|
|
164
|
+
rebaser. A wrong identity on your own commit is a pane-environment problem to report to the
|
|
165
|
+
architect, not something to pin (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`,
|
|
166
|
+
Hazard 1). Your session receives the credential capability it needs; invoke GitHub through the
|
|
167
|
+
credential helper:
|
|
144
168
|
|
|
145
169
|
```bash
|
|
146
170
|
legion gh -- <gh args…>
|
|
@@ -152,22 +176,29 @@ that execs `legion gh -- "$@"`, so `gh …` and `legion gh -- …` are the same
|
|
|
152
176
|
redeems a fresh token from your session's grant — identity is supplied per call, never stored.
|
|
153
177
|
Never run `gh auth login` or `gh auth setup-git`; there is no login state to create. The shim
|
|
154
178
|
refuses `pr merge` (and a raw `gh api …/merge`): no worker role merges a pull request — the merge
|
|
155
|
-
queue does, under its own authority.
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
179
|
+
queue does, under its own authority. It also refuses every GitHub-issue write — the `issue`
|
|
180
|
+
subcommand's `comment`, `create`, `edit`, `close`, `reopen`, `delete`, `pin`, `unpin`, `transfer`,
|
|
181
|
+
`lock`, `unlock`, and `develop`, and any raw `gh api` call to an `/issues` path whose method is not
|
|
182
|
+
GET (an explicit `-X`, or the POST that `-f`/`-F`/`--input` imply; pull-request conversation
|
|
183
|
+
comments live on that path too, so edit them with `gh pr comment`) — printing
|
|
184
|
+
`Legion issues live on Dispatch; use dispatch_message or dispatch_comment on <your LEGION_ISSUE>`:
|
|
185
|
+
Legion never reads or writes a GitHub issue (LEGION-78). `pr comment`, `pr review`,
|
|
186
|
+
`api …/pulls/…`, `api graphql`, and issue reads are unaffected. The credential reaches `legion`
|
|
187
|
+
through the file `$LEGION_GRANT_FILE` names, written before each of your bash commands by the
|
|
188
|
+
extension; never `cat`, `echo`, copy, or `export` it — `legion credential`, `legion gh`,
|
|
189
|
+
`jj git push`, and `legion handoff complete` read it themselves. The file is the pane's, not the
|
|
190
|
+
command's: a `task` subagent, an `eval` subprocess, or a background job in your pane reads the
|
|
191
|
+
grant your last bash command minted, so its `legion gh` or `jj git push` succeeds only within 60
|
|
192
|
+
seconds of that call and 403s afterwards — a timing artifact, not a broken credential; run
|
|
193
|
+
credentialed commands from your own bash calls.
|
|
163
194
|
|
|
164
195
|
## GitHub PR comment attribution
|
|
165
196
|
|
|
166
197
|
Append this exact structured footer to **every** pull-request comment and review that this
|
|
167
198
|
phase posts on GitHub. It preserves session provenance on the artifact itself so work stays
|
|
168
199
|
attributable to the session that produced it. Dispatch comments carry session provenance
|
|
169
|
-
natively through their own `actor`/`origin` fields; this footer is
|
|
170
|
-
|
|
200
|
+
natively through their own `actor`/`origin` fields; this footer is for GitHub PR artifacts and
|
|
201
|
+
for the retro's Dispatch message (`skills/legion-retro`):
|
|
171
202
|
|
|
172
203
|
```html
|
|
173
204
|
<!-- legion: {"session":"<session-id>","phase":"<phase>"} -->
|
|
@@ -220,11 +251,14 @@ PR opens, and every later phase keeps it current rather than replacing it:
|
|
|
220
251
|
```
|
|
221
252
|
## Verification
|
|
222
253
|
|
|
223
|
-
**CI:** `
|
|
254
|
+
**CI:** `Tests` run <run-id> — jobs lint, pr-title, typecheck, test all success at <head-sha>.
|
|
224
255
|
|
|
225
256
|
**Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
|
|
226
257
|
- Thread <id>: fixed in <commit-sha> — <one line>.
|
|
227
258
|
- Thread <id>: not a defect — <reason>.
|
|
259
|
+
`legion threads resolve --pr <n> --repo <owner>/<repo>` at <head-sha>:
|
|
260
|
+
resolved <thread URL>
|
|
261
|
+
left open <thread URL> — newest reply by <login> is not an acceptance
|
|
228
262
|
|
|
229
263
|
**Thermo:** thermonuclear-deep-review + thermonuclear-code-quality run once at <head-sha>:
|
|
230
264
|
<verdict>. (omitted entirely on a docs-only PR — no thermo pass runs)
|
|
@@ -238,8 +272,24 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
|
|
|
238
272
|
```
|
|
239
273
|
|
|
240
274
|
- **Threads are dispositioned individually, never resolved in bulk.** Every open review
|
|
241
|
-
thread gets its own line naming the fixing commit or the reason it isn't a defect
|
|
242
|
-
it
|
|
275
|
+
thread gets its own line naming the fixing commit or the reason it isn't a defect. The
|
|
276
|
+
reviewer answers each thread it opened with exactly one of `Accepted: fixed in <commit> — <one line>`,
|
|
277
|
+
`Accepted: not a defect — <reason>`, or `Still open: <what remains>`; nothing else is an
|
|
278
|
+
acceptance, and nobody replies after an `Accepted:` (any later reply that is not itself an
|
|
279
|
+
`Accepted:` — the opener's own follow-up included — leaves the thread open, since the command
|
|
280
|
+
reads only the newest comment). The review App can reply on a thread but can neither resolve it
|
|
281
|
+
nor push — GitHub grants both only to the pull request's author or an account with write (push)
|
|
282
|
+
access to the repository, and the review App is neither by design
|
|
283
|
+
(`packages/daemon/src/daemon/AGENTS.md`, GitHub Apps) — so the
|
|
284
|
+
**implementer** runs `legion threads resolve --pr <number> --repo <owner>/<repo>` before every
|
|
285
|
+
push that answers a review (the corrective push and the final `.legion/` deletion push) and
|
|
286
|
+
pastes its output into the `Threads` section. The command resolves each unresolved thread
|
|
287
|
+
whose newest comment is the opener's own `Accepted:` reply, one `resolveReviewThread` per
|
|
288
|
+
thread, prints `resolved <url>` or `left open <url> — newest reply by <login> is not an acceptance`,
|
|
289
|
+
and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one; report that
|
|
290
|
+
exit to the architect, which opens an action ask for a human to resolve the thread by hand —
|
|
291
|
+
never skip it silently. The merger runs the same command once more before publishing READY
|
|
292
|
+
and does not publish while any `left open` line remains.
|
|
243
293
|
- **Correctness fixes land in this PR; cleanup is one named fast-follow.** A finding that
|
|
244
294
|
changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
|
|
245
295
|
Findings about naming, duplication, or wording are batched into the single `Fast-follow`
|
|
@@ -249,7 +299,13 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
|
|
|
249
299
|
The implementer rebases the issue branch only when GitHub reports it `CONFLICTING` or the
|
|
250
300
|
controller asks because of a conflict — never to pick up `main` or to refresh CI. A single
|
|
251
301
|
failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
|
|
252
|
-
pushing a new commit.
|
|
302
|
+
pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
|
|
303
|
+
confirmation, not a new round (see *The unchanged-diff check* below). Before rebasing, record
|
|
304
|
+
the fingerprint at the current tip; after pushing the rebased branch, record it at the new
|
|
305
|
+
tip; post one PR comment (Legion footer):
|
|
306
|
+
`rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
|
|
307
|
+
Rebase the whole chain — `jj -R "$LEGION_WORKSPACE" rebase -s 'roots(main@origin..@)' -d main@origin` —
|
|
308
|
+
so the tester's and reviewer's commits move with yours.
|
|
253
309
|
- **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." The `Fast-follow:`
|
|
254
310
|
field names naming, duplication, or wording cleanup only; anything that changes behaviour,
|
|
255
311
|
hides an error, or breaks a gate lands in this PR.
|
|
@@ -275,25 +331,56 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
|
|
|
275
331
|
whole staging gate never ran. Environment or
|
|
276
332
|
secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
|
|
277
333
|
`.legion/test.json`, and only when the issue's acceptance criteria call for it — never
|
|
278
|
-
re-pasted into the PR body each round.
|
|
334
|
+
re-pasted into the PR body each round. After a conflict-forced rebase, compute the
|
|
335
|
+
fingerprint at the head your `E2E` line names and at the new head. Equal: re-run only the
|
|
336
|
+
bare gates — the repository's CI green at the new head and its smoke check — and change the
|
|
337
|
+
`E2E` line's head to the new SHA with
|
|
338
|
+
`rebase re-check <old-sha> → <new-sha>: fingerprint unchanged, bare gates only`; the
|
|
339
|
+
real-surface verification is not repeated. Different: a full test round.
|
|
279
340
|
- The reviewer verifies the `CI`, `Threads`, and `E2E` facts against GitHub directly —
|
|
280
341
|
never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
|
|
281
342
|
`task(agent="thermonuclear-code-quality")` once at that head and records the verdict.
|
|
282
343
|
Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round** —
|
|
283
344
|
`REQUEST_CHANGES` when any correctness finding stands, otherwise `COMMENT` while the head
|
|
284
|
-
still carries `.legion/`; `APPROVE` only for
|
|
285
|
-
the `.legion/` deletion alone,
|
|
345
|
+
still carries `.legion/`; `APPROVE` only for a head that carries no `.legion/` — the head
|
|
346
|
+
that differs from the reviewed one by the `.legion/` deletion alone, or, after a
|
|
347
|
+
conflict-forced rebase, the new head whose fingerprint equals the approved head's — always
|
|
348
|
+
named by SHA — carrying every inline comment in that single
|
|
286
349
|
call: `legion gh -- api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
|
|
287
350
|
with `commit_id`, `event` (`REQUEST_CHANGES`, `COMMENT`, or `APPROVE`), `body` (with the
|
|
288
351
|
Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
|
|
289
352
|
finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
|
|
290
353
|
Then return the issue to the architect; when clean, have the architect send the implementer
|
|
291
354
|
back to push the `.legion/` deletion (the review App cannot push), then review **that** head
|
|
292
|
-
and approve it by name.
|
|
355
|
+
and approve it by name. After a conflict-forced rebase, compute the fingerprint at the
|
|
356
|
+
`commit_id` of your last submitted review and at the new head. Equal and that review was
|
|
357
|
+
`APPROVE`: submit one more `APPROVE` naming the new head by SHA, its body naming both SHAs
|
|
358
|
+
and the fingerprint — a confirmation, not a round; no thermo pass, no thread pass. Equal and
|
|
359
|
+
that review was `COMMENT` or `REQUEST_CHANGES`: continue that round against the new head;
|
|
360
|
+
nothing restarts. Different: a new round — thermo again, one review.
|
|
361
|
+
When you re-review after a corrective push, answer every thread you opened in one of the
|
|
362
|
+
three forms above — `Accepted:` is the only reply the implementer's `legion threads resolve`
|
|
363
|
+
acts on — and approve only once every thread you opened carries your `Accepted:` reply and the
|
|
364
|
+
implementer's run has resolved it (verify `isResolved: true` with `gh api graphql`, never from
|
|
365
|
+
the PR body).
|
|
293
366
|
- Once a base is frozen for others to stack on, never rewrite it — fixes land as new
|
|
294
367
|
commits on top, and the `Chain` line records what is frozen.
|
|
295
|
-
-
|
|
296
|
-
|
|
368
|
+
- **Retro's commit does not void the reviewer's approval.** After the reviewer approves the
|
|
369
|
+
cleaned head, retro commits its learnings under `docs/solutions/` on top of it; that commit
|
|
370
|
+
stays, the approval stands, and the tree goes to the merger — never back to the tester or
|
|
371
|
+
reviewer. Anything else above the approved head does void it, and the merger tells the
|
|
372
|
+
architect the head must return to review instead of publishing. A conflict-forced rebase
|
|
373
|
+
after retro moves those documents with the branch; retro never re-runs.
|
|
374
|
+
- The merger runs `legion threads resolve --pr <n> --repo <owner>/<repo>` (it acts as the same
|
|
375
|
+
code-writing App as the implementer; resolving a thread changes no commit, so this run never
|
|
376
|
+
invalidates the approval), does not publish while any `left open` line remains or the command
|
|
377
|
+
exits 1 (report the thread to the architect instead), then
|
|
378
|
+
proves that rule with two commands and publishes. First
|
|
379
|
+
`cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R "$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`,
|
|
380
|
+
whose output is quoted in READY (an empty output is quoted as
|
|
381
|
+
`no file changes above the approved head`); then the same with `'~docs/solutions'` appended,
|
|
382
|
+
which must print nothing. Then it publishes `READY #<n> at <tip-sha>` naming the approved
|
|
383
|
+
head, the tip, and that summary, plus the PR body's gate facts, to the merge queue's role
|
|
297
384
|
(`notifications.role.pr-queue`) with `envoy_publish`. The merger never merges; the queue
|
|
298
385
|
merges under its own authority.
|
|
299
386
|
- **After the queue merges, the implementer verifies in production.** Sami, 2026-09-13,
|
|
@@ -306,6 +393,44 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
|
|
|
306
393
|
failed at 00:12Z on a resource staging never runs. If the slot fails on the change, the
|
|
307
394
|
implementer owns the fix and the next slot.
|
|
308
395
|
|
|
396
|
+
## The unchanged-diff check
|
|
397
|
+
|
|
398
|
+
The fingerprint every role compares after a conflict-forced rebase (every flag and the fileset
|
|
399
|
+
verified on jj 0.45.1):
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && \
|
|
403
|
+
jj -R "$LEGION_WORKSPACE" diff --from "fork_point(main@origin | <head-sha>)" --to <head-sha> \
|
|
404
|
+
--git --context 0 '~(.legion | docs/solutions)' \
|
|
405
|
+
| sed -e '/^@@/d' -e '/^index /d' | sha256sum
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- `<head-sha>` is a full commit SHA; a jj commit id is the git SHA GitHub shows.
|
|
409
|
+
- `fork_point(main@origin | <head-sha>)` is the base the branch was cut from *at that head*:
|
|
410
|
+
the old base for the pre-rebase head, the new base for the rebased one, so one command
|
|
411
|
+
serves both sides. On a stacked PR substitute its base branch for `main`
|
|
412
|
+
(`legion gh -- pr view <n> --json baseRefName`).
|
|
413
|
+
- A head the rebase hid is still addressable by its SHA in the shared workspace. A SHA the
|
|
414
|
+
workspace cannot resolve (`jj -R "$LEGION_WORKSPACE" log -r <sha>` errors) counts as a
|
|
415
|
+
changed diff — never as unchanged.
|
|
416
|
+
- `--context 0` drops context lines; the `sed` drops `@@` hunk headers (line positions move
|
|
417
|
+
on a rebase) and `index` lines (blob ids move when the base's copy of a file changed). What
|
|
418
|
+
is left is exactly the added and removed lines per file.
|
|
419
|
+
- The single fileset `'~(.legion | docs/solutions)'` leaves out the handoff ledger and retro's
|
|
420
|
+
learnings: process artifacts the rules above already exempt from re-review, which change
|
|
421
|
+
between one role's verified head and the next without changing the product. This is what lets
|
|
422
|
+
each role compare against *its own* last verified head instead of trusting another role's
|
|
423
|
+
numbers. It must be one expression: jj unions positional filesets, so two separate
|
|
424
|
+
`'~.legion' '~docs/solutions'` arguments select every file and exclude nothing. Once
|
|
425
|
+
`.legion/` is gone, jj warns `No matching entries for paths: .legion` on stderr; the hash is
|
|
426
|
+
unaffected.
|
|
427
|
+
|
|
428
|
+
Where each role gets its two heads: the implementer — the tip before and after its own rebase;
|
|
429
|
+
the tester — the head its `E2E` line names and the new head; the reviewer — the `commit_id` of
|
|
430
|
+
its last submitted review (`legion gh -- api repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login}'`)
|
|
431
|
+
and the new head; the merger never computes a fingerprint — it uses the `--summary` check
|
|
432
|
+
above.
|
|
433
|
+
|
|
309
434
|
## Completion gate: handoff write, verification, and persistence
|
|
310
435
|
|
|
311
436
|
Write the phase-specific handoff:
|
|
@@ -328,9 +453,12 @@ cd -- "$LEGION_WORKSPACE" && \
|
|
|
328
453
|
jj -R "$LEGION_WORKSPACE" split -m "<phase>: record handoff" .legion/<phase>.json
|
|
329
454
|
```
|
|
330
455
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
456
|
+
**Only the implementer pushes the issue branch.** It acts as the code-writing App
|
|
457
|
+
(`legion-implementer[bot]`, `appRoleForLegionRole` in `packages/daemon/src/daemon/github-apps.ts`),
|
|
458
|
+
the one App with `contents` permission (the merger acts as the same App but pushes nothing: it
|
|
459
|
+
verifies and publishes READY). If you are the implementer, advance the issue bookmark and push it
|
|
460
|
+
with the provisioned credential helper. `--bookmark` also publishes the locally provisioned
|
|
461
|
+
bookmark on its first push — a bookmark not yet tracking a remote one is tracked automatically:
|
|
334
462
|
|
|
335
463
|
```bash
|
|
336
464
|
cd -- "$LEGION_WORKSPACE" && \
|
|
@@ -338,10 +466,23 @@ cd -- "$LEGION_WORKSPACE" && \
|
|
|
338
466
|
jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
|
|
339
467
|
```
|
|
340
468
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
469
|
+
Every other role — planner, tester, reviewer, architects — acts as the review App
|
|
470
|
+
(`legion-reviewer[bot]`), which cannot push: the `split` above is your last step, and the commit
|
|
471
|
+
rides the implementer's next push (the corrective push after a review, or the final `.legion/`
|
|
472
|
+
deletion). A push from one of those roles is refused — over git it reads
|
|
473
|
+
`remote: Repository not found.`; the REST API's form of the same refusal is
|
|
474
|
+
`Resource not accessible by integration` — and that refusal is expected, not a failure to report
|
|
475
|
+
or retry.
|
|
476
|
+
|
|
477
|
+
Do not report phase completion until the write, existence check, and handoff commit succeed —
|
|
478
|
+
and, for the implementer, until the push has too. This is the committed copy the next phase
|
|
479
|
+
reads after revival. It is removed once, at the end of a clean review: the implementer pushes
|
|
480
|
+
that deletion at the reviewer's direction. No other phase removes it — and once it is gone
|
|
481
|
+
(`jj -R "$LEGION_WORKSPACE" file list -r @- .legion` prints nothing on stdout; jj warns on
|
|
482
|
+
stderr), this gate no longer applies: a later rebase, bare-gate re-check, confirmation, or retro
|
|
483
|
+
writes no `.legion/<phase>.json`, commits no handoff, and reports with `legion handoff complete`
|
|
484
|
+
alone (below). Recreating `.legion/` after its deletion changes the approved head and restarts
|
|
485
|
+
the review loop this rule exists to end.
|
|
345
486
|
|
|
346
487
|
## Completion: report to the architect, then stay
|
|
347
488
|
|