@sjawhar/opencode-legion-envoy 6.10.1 → 6.12.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.
@@ -1,13 +1,13 @@
1
1
  # The merge gate: review, retro, READY, and the production check
2
2
 
3
3
  Part of `skill://legion-worker`. Read it when you are the reviewer submitting a review or an
4
- approval, the implementer recording the production check, or the merger building READY. Every
5
- path it cites is in sjawhar/legion.
4
+ approval, the implementer disarming a withdrawn READY or recording the production check, or the
5
+ merger building READY and submitting the merge. Every path it cites is in sjawhar/legion.
6
6
 
7
7
  The order, in full: the tester's evidence green → the reviewer's approval of the head →
8
- retro → the merger's READY → the human merge → the implementer's production check. The approved
8
+ retro → the merger's READY → the merge → the implementer's production check. The approved
9
9
  head carries the issue's handoffs, `.legion/<issue>/`; retro's last commit removes them from the
10
- head a human merges (dispatch://LEGION-605), since a squash merge commits that head merged into
10
+ head that merges (dispatch://LEGION-605), since a squash merge commits that head merged into
11
11
  the default branch and nothing there reads a handoff, and READY refuses a head that still
12
12
  carries them. After the approval, only retro's commits leave it standing on their own: those that
13
13
  change only `docs/solutions/`, and that removal (*Retro*, below). A conflict-forced merge goes back
@@ -25,7 +25,7 @@ the head voids it.
25
25
  Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round** —
26
26
  `REQUEST_CHANGES` when any correctness finding stands, otherwise `APPROVE` of the head you
27
27
  reviewed — always named by SHA — carrying every inline comment in that single
28
- call: `legion gh -- api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
28
+ call: `gh api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
29
29
  with `commit_id`, `event` (`REQUEST_CHANGES` or `APPROVE`), `body` (with the
30
30
  Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
31
31
  finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
@@ -48,9 +48,10 @@ before you submit it, since an approval stands only on green checks and GitHub c
48
48
  once the head moves, and a verdict that settles red there makes the round's decision a request for
49
49
  changes naming the failing checks, unless only review workflows the project declares
50
50
  (`projects.<KEY>.review_workflows`) are red on their own findings: then you answer their threads,
51
- have them resolved with `legion threads resolve` and re-run the failed run, as your role prompt
52
- says, and approve once it passes. Any other red required workflow is a failing check like any
53
- other. A request for changes does not wait, since it stands whatever CI says and the issue leaves
51
+ name the ones you accepted to the implementer, who resolves them with its own `gh` as the pull
52
+ request's author, and re-run the failed run once they are resolved, as your role prompt says, and
53
+ approve once it passes. Any other red required workflow is a failing check like any other. A
54
+ request for changes does not wait, since it stands whatever CI says and the issue leaves
54
55
  reviewing with it. The verdict is of the checks and workflows the base branch requires, the set
55
56
  READY checks: red when one of them failed, and never red for a check the base branch does not
56
57
  require. A required check that was cancelled, or that the head's checks
@@ -73,37 +74,72 @@ review posted without a completion leaves the issue in reviewing until you finis
73
74
  - **Retro brings the PR body's path-derived content up to date before its push.** Whatever the
74
75
  repository's instructions derive from the pull request's changed paths (a checklist named for
75
76
  each class of path, read by a required check), retro recomputes for the whole diff at its head
76
- and writes into the live body before `legion push` (`skill://legion-retro`), since the merger
77
+ and writes into the live body before its push (`skill://legion-retro`), since the merger
77
78
  reports a stale body rather than rewriting it. A body edit changes no commit, so the approval
78
79
  stands.
79
80
 
80
81
  ## The merger
81
82
 
82
- - The merger runs `legion threads resolve --pr <n> --repo <owner>/<repo>` (it acts as the same
83
- code-writing App as the implementer; resolving a thread changes no commit, so this run never
84
- invalidates the approval), does not complete while any `left open` line remains or the command
85
- exits 1 (report the thread to the architect instead), then proves that rule with two commands.
86
- First `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R
83
+ - The merger resolves no review thread: the reviewer's approval already judged them, and READY
84
+ requires no thread state beyond it. It proves the head is the approved head plus retro's commits
85
+ with two commands. First `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R
87
86
  "$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`, whose output is quoted
88
87
  in READY; then the same with the one fileset `'~(docs/solutions | .legion/<issue>)'` appended,
89
88
  which must print nothing (jj unions separate path arguments, so two of them leave nothing out).
90
- READY itself refuses a head that still carries `.legion/<issue>/` (the merger tells the
91
- architect, which has it move the issue back to `retro`), and publishes a pull request a person
92
- already merged unread (`packages/daemon/internal/prompts/go/merger.md`). *The READY packet* is
89
+ *The READY packet* is
93
90
  `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)`
94
91
  (the shape `packages/daemon/internal/prompts/roles/merger.md` defines), then the PR body's
95
92
  `Outcome:` line and its `Not proven / risk:` value — every bullet under that label joined with
96
93
  `; ` on the one READY line, or `none` — quoted from the `## For the reviewer` block at that same
97
94
  head (or one line saying the body carries no brief — the packet still goes out), then the
98
- `--summary` output and the PR body's gate facts. The merger sends it as the `summary` of its
99
- `handoff_complete` with `ready: true`; the daemon posts it as a `dispatch message` on the issue,
100
- publishes it to the project's merge queue role when one is set, and says on the issue when that
101
- role has no live holder. The READY packet names both the implementer's and tester's `E2E` lines;
102
- a missing one is reported to the architect instead of completing. Legion never merges.
95
+ `--summary` output and the PR body's gate facts. The READY packet names both the implementer's
96
+ and tester's `E2E` lines; a missing one is reported to the architect instead of completing.
97
+ - READY is `handoff_complete` with `ready: true` and the packet as its `summary`; the merger runs
98
+ no pre-flight of its own, and its completion reports the commit the workspace stands on. READY's
99
+ checks run in the daemon, which reads the pull request's head on GitHub and refuses, before
100
+ recording anything: `READY_HEAD_CARRIES_HANDOFFS` while the head still carries `.legion/<issue>/`
101
+ (tell the architect, which has you move the issue back to `retro` with `request_backward_move`,
102
+ so the implementer's retro removes it; a refusal saying GitHub's read of the directory failed is
103
+ GitHub's failure, not the head's: complete again); `READY_HEAD_CONFLICTS` while the pull
104
+ request conflicts with its base, named — GitHub can neither merge it nor start pull_request CI
105
+ for it, so the implementer brings the base into the branch with a forward merge; and
106
+ `READY_CHECKS_NOT_GREEN` unless every check the base branch requires has succeeded at that head
107
+ and every workflow its rulesets require has a passing run there — no result for a check, or no
108
+ run of a workflow, means that head's push skipped CI when it should not have; a check still
109
+ running means wait; a failed one is a finding. Report the refusal to the architect; never
110
+ push a commit to make CI run. On success the daemon posts the packet as a `dispatch message` on
111
+ the issue, publishes it to the project's merge queue role when one is set, and says on the issue
112
+ when that role has no live holder; the answer's `note` says when READY was published on a pull
113
+ request a person already merged, whose head can no longer change, or on a base that requires no
114
+ check, which the packet itself also says.
115
+ - **The merger submits the merge the moment READY is accepted.** In the same turn, before anything
116
+ else, it runs `gh pr merge <n> -R <owner>/<repo> --auto --squash --match-head-commit <head>` with
117
+ its pane's plain `gh`: `<n>` the pull request number, `<head>` the full sha of the current head
118
+ READY named. A repository with a merge queue enqueues the pull request; one without arms
119
+ auto-merge; one whose rules are already satisfied merges at once. `--match-head-commit` has
120
+ GitHub refuse the request when the head has moved since READY read it. The repository's required
121
+ reviews and checks decide when it lands: the merger makes no merge by hand and bypasses no
122
+ rule. It then tells the architect with `envoy_publish` to its encoded role token what the
123
+ command answered — enqueued, auto-merge enabled, or merged — and the head. On any READY refusal
124
+ it submits nothing, since an armed request would merge through the gate the refusal names; a
125
+ READY the design gate refused is submitted later, on the architect's word, once the gate opens,
126
+ and that turn runs only `gh`. A `gh pr merge` GitHub refuses is reported to the architect with
127
+ its message; a person can still merge after READY. The packet's second line records the step,
128
+ `Merge: submitted on acceptance with gh pr merge <n> -R <owner>/<repo> --auto --squash --match-head-commit <head>`.
129
+ - **A withdrawn READY is disarmed by the implementer.** When READY is withdrawn (the head's CI
130
+ turned red, or it conflicts with its base, after READY, which sends the issue back to
131
+ `implementing`), a merge queue drops the pull request on the red check, but auto-merge stays
132
+ enabled across pushes, and `--match-head-commit` was checked only when it was enabled. So the
133
+ implementer, at the start of that implementing round, runs
134
+ `gh pr merge <n> -R <owner>/<repo> --disable-auto` before any push. A pull request still in a
135
+ merge queue is removed with `gh api graphql -f query='mutation($id:ID!){dequeuePullRequest(input:{id:$id}){mergeQueueEntry{id}}}' -F id=<pull request node id>`
136
+ (`--disable-auto` dequeues nothing). When nothing is armed — the merger's submission was refused,
137
+ or a queue already dropped it — the command exits 1 with "Can't disable auto-merge for this pull
138
+ request.": run it anyway and take that answer as nothing to disarm.
103
139
 
104
- ## After the human merge
140
+ ## After the merge
105
141
 
106
- - **After a human merges, the implementer verifies in production.**
142
+ - **After the merge, the implementer verifies in production.**
107
143
  The daemon starts the implementer again once the merge lands; the implementer watches the
108
144
  deploy slot that carries the merge to `production-apply` (or the equivalent publish step),
109
145
  drives the changed path in production through the user's own access path, and records the
@@ -26,15 +26,11 @@ later phase keeps it current rather than replacing it:
26
26
 
27
27
  **CI:** `Tests` run <run-id> — jobs lint, typecheck, test all success at <head-sha>; `PR Title` run <run-id> — job pr-title success at <head-sha>.
28
28
 
29
- **Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
30
- - Thread <id>: fixed in <commit-sha> — <one line>.
31
- - Thread <id>: not a defect — <reason>.
32
- `legion threads resolve --pr <n> --repo <owner>/<repo>`, run after the push that made <head-sha>:
33
- resolved <thread URL> — its opener's acceptance
34
- resolved <thread URL> — the Legion reviewer's acceptance of a bot's thread
35
- left open <thread URL> — newest reply by <login> is not an acceptance
36
- left open <thread URL> — newest reply by <login> is an unsubmitted draft in a pending review
37
- left open <thread URL> — newest reply by <login> is not its opener's or the Legion reviewer's acceptance
29
+ **Threads:** <n> resolved, 0 unresolved. Each answered and resolved individually by the implementer, the pull request's author, never in bulk (the reviewer names the bot threads it accepted; it resolves none itself):
30
+ - Thread <node id>: fixed in <commit-sha> — <one line>; resolved at <head-sha>.
31
+ - Thread <node id>: not a defect — <reason>; resolved at <head-sha>.
32
+ - Thread <node id>: accepted by the reviewer — <its reason, one line>; resolved at <head-sha>.
33
+ - Thread <node id>: still open — <what remains>; unresolved at <head-sha>.
38
34
 
39
35
  **Thermo:** `ce-simplify-code` once at <head-sha>: <0 applied | applied → new head <sha>>; thermonuclear pair at the reviewed head <sha>:
40
36
  <verdict>. (omitted entirely on a docs-only PR — there is no code for either pass, so neither runs)
@@ -61,7 +57,7 @@ ledger the reviewer and merger check against GitHub. The implementer writes `Out
61
57
  `Change`, `Proven by`, and `Size` when the pull request opens, and keeps them true after every
62
58
  push; `Outcome` is a sentence a user of the repository would recognise, never "fix bug" or a file
63
59
  name, and `Why` ends with the Dispatch key, never a URL. The reviewer writes `Look at first` and
64
- `Not proven / risk` at each round, into the live body (`legion gh -- api
60
+ `Not proven / risk` at each round, into the live body (`gh api
65
61
  repos/{owner}/{repo}/pulls/{number} --jq .body`, edit, then `--method PATCH ... -F body=@body.md`);
66
62
  `Not proven / risk` copies every claim recorded as unproven before READY — the tester's
67
63
  `failures`, the reviewer's own review, any proof-check comment already on the pull request —
@@ -88,19 +84,26 @@ and the tester's proof below are both this proof.
88
84
 
89
85
  - **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
90
86
  The proof is the one defined above. It goes into `.legion/<issue>/implement.json` as the required `proof`
91
- array (`handoff_write` for phase `implement` refuses a payload without one, or with a blank or
92
- whitespace-only field, and names the field), and into the PR body, because the reviewer and the
93
- merger verify facts on GitHub and never from a handoff.
87
+ array, and into the PR body, because the reviewer and the merger verify facts on GitHub and never
88
+ from a handoff. Nothing checks the handoff's shape when you complete — `handoff_complete` reports
89
+ the pushed commit carrying the file, and the daemon reads nothing in it — so an implement handoff
90
+ without a `proof` array, or with a blank or whitespace-only field in one, reaches the tester as it
91
+ is, and the tester records it as a failure against you.
94
92
  - **The tester verifies the implementer's proof and adds its own `E2E (tester)` line.** It re-runs
95
93
  the implementer's command or drives the same surface independently, and records the verdict in
96
- `.legion/<issue>/test.json` as `implementerProof` (`{verdict, how}`).
94
+ `.legion/<issue>/test.json` as `implementerProof` (`{verdict, how}`). The tester runs the
95
+ application, not CI again. Try to break the changed surface — wrong and boundary inputs, the
96
+ second and the concurrent run, the party it must refuse — judge whether the implementer's tests
97
+ would catch the ways it breaks, writing the red test for one that none would, and
98
+ read CI for everything static: lint, types and the unit and integration suites are CI's, cited
99
+ from the `CI` line, and no CI lane is rerun as evidence.
97
100
  A test handoff whose predecessor carried no proof is a test failure, not a gap for the tester to fill:
98
101
  record it in `failures` with `implementerProof.verdict: "rejected"`, complete the phase with
99
102
  `verdict: "fail"`, and the daemon returns the issue to the implementer — the agent that developed the change owns
100
- proving it (`handoff_write` for phase `test` refuses a rejected verdict, or `failed > 0`,
101
- with no recorded failure). Otherwise, add your own proof before completing — a proof as defined
102
- above — as the `E2E (tester)` line and the `proof` array `handoff_write` for
103
- phase `test` requires whenever you report no failure. A code path whose first execution is after merge — a
103
+ proving it (a rejected verdict, or `failed > 0`, always comes with the failure recorded in
104
+ `failures`). Otherwise, add your own proof before completing — a proof as defined
105
+ above — as the `E2E (tester)` line and the `proof` array a test handoff that reports no failure
106
+ carries. A code path whose first execution is after merge — a
104
107
  deploy workflow's inline step, a post-merge helper, a production-only resource — is untested
105
108
  until the implementer has executed it against a devN stack; if no surface can reach it, the
106
109
  tester names that missing surface as the blocker instead of passing the phase. Environment or
@@ -1,101 +1,77 @@
1
1
  # Review threads
2
2
 
3
- Part of `skill://legion-worker`. Read it when you reply to, accept, or resolve a review thread,
4
- or run `legion threads resolve`: the implementer after every push that answers a review, the
5
- merger before READY, and the reviewer, who answers threads on every re-review and runs the command
6
- only when a review workflow the project declares is red on a bot's findings.
7
- Every path it cites is in sjawhar/legion.
3
+ Part of `skill://legion-worker`. Read it when you reply on or resolve a review thread, list a pull
4
+ request's threads, or adjudicate a bot's finding: the implementer after every push that answers a
5
+ review, and the reviewer on every re-review. Every path it cites is in sjawhar/legion.
8
6
 
9
- - **Threads are dispositioned individually, never resolved in bulk.** Every open review
10
- thread gets its own line naming the fixing commit or the reason it isn't a defect. The
11
- reviewer answers each thread it opened, and each thread a bot opened that is none of Legion's
12
- role Apps, with exactly one of `Accepted: fixed in <commit> — <one line>`,
13
- `Accepted: not a defect — <reason>`, or `Still open: <what remains>`; nothing else is an
14
- acceptance, and nobody replies after an `Accepted:` (any later reply that is not itself an
15
- `Accepted:` — the opener's own follow-up included — leaves the thread open, because resolution
16
- considers only the newest comment). The review App can reply on a thread but cannot resolve it:
17
- GitHub grants resolving a review thread to the pull request's author, and the implementer opens
18
- every Legion pull request (`docs/site/src/content/docs/legion/running-legion.md`, "The two
19
- GitHub Apps"). In the reviewer's pane (`LEGION_ROLE=reviewer`), `legion threads resolve` asks
20
- the daemon instead (`POST /legion/v1/threads/resolve`), which resolves as the implementer's App
21
- only the threads a bot outside Legion's role Apps opened whose newest submitted comment is the
22
- reviewer's `Accepted:`, on the pull request of the reviewer's own issue, and prints the same
23
- lines; the reviewer never holds the implementer's token. A thread whose newest comment is a
24
- draft in the implement App's pending review (the implementer's or the merger's: GitHub shows it
25
- to that App alone, as which the daemon reads) is left open and never named: the command prints
26
- only how many there are (`<n> unresolved threads hold the implement App's pending draft and
27
- were left open`) and exits 1, as it does on a refusal. Report that count to the architect before
28
- you spend your one re-run of the failed workflow; the architect sends the issue back so the
29
- implementer submits or discards its pending review. Once the review is submitted, the
30
- implementer's reply is the thread's newest comment, which leaves it open: answer the thread
31
- again, then run the command again. Once the review is discarded, your `Accepted:` is the newest
32
- comment again, and running the command again closes the thread.
33
- When `LEGION_GRANT_FILE` or `LEGION_GRANT` is set, use `legion threads resolve --pr <number> --repo <owner>/<repo>`.
34
- When neither is set, add `--gh` to that command, which applies the fallback's rule below through
35
- your own `gh`; where no `legion` command is installed, use `gh api graphql` with the session's
36
- GitHub credential and the fallback below.
37
- In a Legion pane, the **implementer** runs the command after every push that answers a review
38
- and before its `handoff_complete`, and pastes its output, stamped with the head it just pushed, into
39
- the `Threads` section. The output is then recorded against the head the reviewer will read, and
40
- nothing reads thread state before the implementer's completion. The command resolves each
41
- unresolved thread whose newest submitted comment is the opener's own `Accepted:` reply. On a
42
- thread a bot account opened that is none of Legion's role Apps (the daemon names them, keyed by
43
- App role), the Legion reviewer's `Accepted:` also closes it. GitHub cannot tell a CI bot, which
44
- never accepts, from a person whose `gh` is routed to an App, so the reviewer adjudicates such a
45
- finding, and it may accept one an App-routed person raised. The subject of a finding never
46
- closes it: the implementer's `Fixed in <commit>: …` or `Declined: …` answers a thread and closes
47
- none. A thread either Legion App opened, a reviewer's finding included, still needs its opener's
48
- `Accepted:`. It makes one `resolveReviewThread` per
49
- thread, prints `resolved <url> — <whose acceptance>` (its opener's, or the Legion reviewer's on a
50
- bot's thread, so the ledger shows which) or `left open <url> — newest reply by <login> is …`
51
- naming why, and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one.
7
+ Three facts govern every thread, whoever opened it.
52
8
 
53
- Without a grant, page through `reviewThreads`, skip `isResolved: true`, and compare the opener
54
- with the newest comment. Query shape, inside `repository { pullRequest { … } }`:
9
+ - **Reply on each thread you answer, one disposition reply per thread.** Every open review thread
10
+ gets its own reply naming the fixing commit or the reason it isn't a defect: the implementer's
11
+ `Fixed in <commit>: <one line>` or `Declined: <reason>`; the reviewer's reasons in its own words —
12
+ the commit that fixed it, why it is not a defect, or what still stands. No word in a reply
13
+ resolves anything, and nothing reads a reply for a magic form: a thread is resolved only by the
14
+ resolution below, after its reply.
15
+ - **Resolve each thread you have answered, one thread per call, never in bulk, never a thread you
16
+ have not read.** GitHub grants `resolveReviewThread` to the pull request's author, and the
17
+ implementer opens every Legion pull request (`docs/site/src/content/docs/legion/running-legion.md`,
18
+ "The two GitHub Apps"), so the implementer resolves every thread, its own and the ones the
19
+ reviewer accepted:
20
+ - The **implementer**, after every push that answers a review and before its `handoff_complete`,
21
+ resolves each thread it answered with its own `gh`, one thread per call:
55
22
 
56
- ```graphql
57
- reviewThreads(first: 100, after: $after) {
58
- pageInfo { hasNextPage endCursor }
59
- nodes {
60
- id isResolved
61
- opener: comments(first: 1) { nodes { author { __typename login } } }
62
- newest: comments(last: 1) { nodes { author { __typename login } body state } }
63
- }
64
- }
65
- ```
23
+ ```bash
24
+ gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=<thread node id>
25
+ ```
66
26
 
67
- Resolve only when the newest comment is submitted, its `author` is the opener's account (the same
68
- `__typename` and `login`: a login alone is a string anyone may register), and its `body`, after
69
- removing leading spaces, tabs, CR, and LF, begins `Accepted:`. Without a
70
- grant nothing names Legion's own App logins, so this route closes a bot's thread only on its
71
- opener's `Accepted:`: leave one the Legion reviewer accepted for the implementer's or merger's
72
- run in a pane, or report it. For each thread to resolve:
27
+ A thread a bot opened (a CI bot's, or an App-routed person's) it answers the same way; the
28
+ reviewer adjudicates the finding. The bot threads the reviewer accepted, which the reviewer
29
+ names to the implementer by node id (below), the implementer resolves with the same call, one
30
+ per thread, as soon as the message reaches it or as part of answering that review, and records
31
+ in the PR body's `Threads` section too; never one the reviewer did not name. When GitHub refuses
32
+ a resolution, report the thread and GitHub's message to the architect with `envoy_publish`,
33
+ which opens a `dispatch ask` for a human to resolve it by hand; never skip it silently.
34
+ - The **reviewer** cannot resolve a thread as its own App: GitHub refuses the review App
35
+ `resolveReviewThread` on the implementer's pull request, so it replies, and names the threads
36
+ it accepted to the implementer, who resolves them. List the pull request's threads:
73
37
 
74
- ```graphql
75
- mutation($threadId: ID!) {
76
- resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
77
- }
78
- ```
38
+ ```bash
39
+ gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){pullRequest(number:$n){reviewThreads(first:100){nodes{id isResolved comments(last:1){nodes{author{login} body}}}}}}}' -F o=<owner> -F r=<repo> -F n=<number>
40
+ ```
79
41
 
80
- Re-read `reviewThreads` and confirm that thread's `isResolved` is true. In either route, report
81
- a refused resolution to the architect, which opens an ask for a human to resolve the thread by
82
- hand — never skip it silently. The merger runs the command once more before its READY
83
- completion and does not complete while any `left open` line remains. That run is where every accepted
84
- thread's resolution is guaranteed, since the merge queue's gate counts the unresolved threads at
85
- the head. Acceptances posted after the implementer's last run are resolved here.
42
+ Reply on each thread you answer, then tell the implementer the node ids of exactly the threads
43
+ you accepted — a bot's findings you adjudicated — never one you have not read: an
44
+ `envoy_publish` to the implementer's role topic (it is live on the issue and answers while you
45
+ wait; `skill://legion-worker` "Asking another role" has the topic), or your review body, which
46
+ it reads at its next round. The implementer resolves exactly those with its own `gh`. GitHub
47
+ cannot tell a CI bot from a person whose `gh` is routed to an App, so such a thread may be a
48
+ person's finding: accept it only when the finding itself is settled (a genuine Minor is settled
49
+ by being judged Minor), since the resolution says you judged it.
50
+ - **A reviewer who disagrees with a resolution replies and unresolves it.** Resolution is a
51
+ judgment, not a ledger entry: a thread the implementer resolved whose finding still stands gets
52
+ the reviewer's reply saying what stands and is unresolved, and a finding the reviewer cannot
53
+ accept becomes its own, with a `REQUEST_CHANGES` naming it.
86
54
 
87
- - **The reviewer, on a re-review.** When you re-review after a corrective push, answer every
88
- thread you opened, and every thread a bot opened that is none of Legion's role Apps, in one of
89
- the three forms above — `Accepted:` is the only reply `legion threads resolve` acts on. A bot's
90
- finding you cannot accept becomes your own: leave it `Still open:` and request changes.
91
- Approve once each of those threads has your own `Accepted:` as its newest submitted comment,
92
- whether or not GitHub shows the thread resolved yet, and every other unresolved thread its
93
- opener's (read the newest comments with `gh api graphql`, never from the PR body). Another
94
- opener's thread that a person resolved with GitHub's button, with no `Accepted:`, gates nothing:
95
- neither `legion threads resolve` nor the merge queue's gate counts a resolved thread. Resolution
96
- is the pull request author's App's, so your approval never waits on it, except when a review
97
- workflow the project declares (`projects.<KEY>.review_workflows`) is red on its findings: such a
98
- workflow passes on a re-run only once its threads are resolved, so you run `legion threads
99
- resolve` (the daemon resolves the bot threads you accepted) and re-run the failed run before you
100
- approve, as your role prompt says.
101
- The merger resolves accepted threads that remain open before its READY completion.
55
+ ## The PR body's `Threads` section
56
+
57
+ The implementer records each thread it answered in the PR body's `Threads` section
58
+ (`skill://legion-worker/references/pr-body.md`), stamped with the head it just pushed: the thread's
59
+ id, its disposition (`fixed in <commit-sha> — <one line>` or `not a defect — <reason>`), and the
60
+ head that answered it. The reviewer verifies thread state in `gh api graphql`, never from that
61
+ section: it is the implementer's record, and GitHub is the fact.
62
+
63
+ ## The reviewer, on a re-review
64
+
65
+ When you re-review after a corrective push, answer every thread you opened, and every thread a bot
66
+ opened that is none of Legion's role Apps, with one reply in your own words. A bot's finding you
67
+ cannot accept becomes your own: say what stands and request changes. Approve once each of those
68
+ threads carries your answer and no finding stands, read in `gh api graphql` — quote those answers
69
+ in the approval. Your approval never waits on a resolution: resolution is the pull request author's
70
+ App's, except when a review workflow the project declares (`projects.<KEY>.review_workflows`) is
71
+ red on its findings. Such a workflow starts again only on a push, so neither a reply nor a
72
+ resolution re-runs it, and it passes on a re-run only once its threads are resolved: answer its
73
+ threads, name the ones you accepted to the implementer with `envoy_publish` to its role topic, and
74
+ read the listing above until each shows `isResolved` true before you push your handoff, so the run
75
+ that push starts reads the threads as the implementer left them; then re-run the failed run once,
76
+ as your role prompt says, before you approve. The merger resolves nothing: READY requires no thread
77
+ state beyond what your approval already judged.