@sjawhar/pi-legion-envoy 5.19.0 → 5.20.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/legion.js +1 -1
- package/dist/skills/legion-architect/SKILL.md +2 -2
- package/dist/skills/legion-worker/SKILL.md +65 -365
- package/dist/skills/legion-worker/references/conflicts-and-rewrites.md +129 -0
- package/dist/skills/legion-worker/references/merge-gate.md +101 -0
- package/dist/skills/legion-worker/references/pr-body.md +125 -0
- package/dist/skills/legion-worker/references/review-threads.md +71 -0
- package/package.json +1 -1
- package/dist/skills/legion-worker/references/knowledge-injection.md +0 -98
- /package/dist/skills/legion-worker/{resources/strategies → references}/cleanup-deletion.md +0 -0
- /package/dist/skills/legion-worker/{resources/strategies → references}/systematic-rename.md +0 -0
package/dist/legion.js
CHANGED
|
@@ -33495,7 +33495,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
33495
33495
|
// package.json
|
|
33496
33496
|
var package_default = {
|
|
33497
33497
|
name: "@sjawhar/pi-legion-envoy",
|
|
33498
|
-
version: "5.
|
|
33498
|
+
version: "5.20.0",
|
|
33499
33499
|
type: "module",
|
|
33500
33500
|
omp: {
|
|
33501
33501
|
extensions: [
|
|
@@ -256,10 +256,10 @@ Preserve this order exactly:
|
|
|
256
256
|
|
|
257
257
|
What returns the tree to review: a changed diff — a commit above the approved head that
|
|
258
258
|
touches anything outside `docs/solutions/`, or a conflict-resolution merge whose fingerprint
|
|
259
|
-
(`skill://legion-worker`
|
|
259
|
+
(the unchanged-diff check, `skill://legion-worker/references/conflicts-and-rewrites.md`) differs from the approved head's. What does not: retro's
|
|
260
260
|
`docs/solutions/` commit, and a merge forced by a GitHub-reported conflict whose fingerprint
|
|
261
261
|
is unchanged. For that merge the order is: the implementer merges the bookmark forward with the
|
|
262
|
-
destination (
|
|
262
|
+
destination (the forward-merge procedure in `skill://legion-worker/references/conflicts-and-rewrites.md` — `jj new legion/<KEY> <destination>`,
|
|
263
263
|
never a rebase, since a rebase rewrites every descendant of the chain's fork point, including
|
|
264
264
|
another tree's branch stacked on it), pushes it with the ordinary push procedure (a genuine
|
|
265
265
|
fast-forward), and posts the before/after fingerprints; the tester re-runs the bare gates only;
|
|
@@ -14,6 +14,20 @@ copy the next phase can trust.
|
|
|
14
14
|
Every path this skill cites (`packages/...`, `docs/...`, `AGENTS.md`) is in sjawhar/legion, the
|
|
15
15
|
Legion repository, which need not be the repository you are working in.
|
|
16
16
|
|
|
17
|
+
## References
|
|
18
|
+
|
|
19
|
+
Each file below is part of this skill. Read it at the step beside it, through its `skill://` link
|
|
20
|
+
with the `read` tool: a read by filesystem path stops at 300 lines.
|
|
21
|
+
|
|
22
|
+
| When | Read |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| You write or edit the PR body, record a proof (an `E2E` line or a handoff `proof` array), verify another phase's proof, or run the simplify pass | `skill://legion-worker/references/pr-body.md` |
|
|
25
|
+
| You reply to, accept, or resolve a review thread, or run `legion threads resolve` | `skill://legion-worker/references/review-threads.md` |
|
|
26
|
+
| GitHub reports a conflict, the pull request is retargeted, you compare heads after a conflict merge, or you would rewrite a pushed commit | `skill://legion-worker/references/conflicts-and-rewrites.md` |
|
|
27
|
+
| You review or approve, push the `.legion/` deletion, publish READY, or check the merge in production | `skill://legion-worker/references/merge-gate.md` |
|
|
28
|
+
| The issue renames a repository, package, or URL across the codebase | `skill://legion-worker/references/systematic-rename.md` |
|
|
29
|
+
| The issue deletes code, a command, or documentation | `skill://legion-worker/references/cleanup-deletion.md` |
|
|
30
|
+
|
|
17
31
|
## Identity, scope, and role
|
|
18
32
|
|
|
19
33
|
The daemon spawns you as a separate `omp --mode rpc` process (behind `legion worker-shim`,
|
|
@@ -25,8 +39,8 @@ completes the boot handshake for you at session start — it registers with the
|
|
|
25
39
|
your role, and signals readiness. You never call `envoy_role_set` yourself.
|
|
26
40
|
|
|
27
41
|
Your role token is not the issue key spelled out literally. The daemon encodes it as
|
|
28
|
-
`legion-<project>-<
|
|
29
|
-
encodes to `legion-acme-
|
|
42
|
+
`legion-<project>-<key>-<role>` with the issue key lower-cased. For example, project `acme`, issue
|
|
43
|
+
`LEGION-41`, role `architect` encodes to `legion-acme-legion-41-architect`. Never hand-format one for another role: your own role
|
|
30
44
|
topic and the topic of the architect that owns your issue are stated at the end of your system
|
|
31
45
|
prompt (a "Legion addressing" line the daemon appends: on a child issue that is the child's
|
|
32
46
|
sub-architect when one is claimed, else the root's), a sibling role's topic is yours with the
|
|
@@ -153,6 +167,9 @@ Follow the repository's normal engineering workflow and the assigned issue's acc
|
|
|
153
167
|
criteria. Your phase's own charter and the predecessor handoffs you read define the phase
|
|
154
168
|
artifact and its completion evidence. Do not replace architect-owned decomposition, gate
|
|
155
169
|
discipline, scheduling, or human communication with labels or a local status model.
|
|
170
|
+
An issue that renames a repository, package, or URL across the codebase also follows
|
|
171
|
+
`skill://legion-worker/references/systematic-rename.md`; one that deletes code, a command, or
|
|
172
|
+
documentation follows `skill://legion-worker/references/cleanup-deletion.md`.
|
|
156
173
|
|
|
157
174
|
Commit attribution is automatic: the extension exports a `JJ_CONFIG` overlay when your
|
|
158
175
|
session starts, so every jj commit you make carries an `Omp-Session: <this-session-id>`
|
|
@@ -173,10 +190,11 @@ check
|
|
|
173
190
|
`jj -R "$LEGION_WORKSPACE" log -r 'main@origin..@' -T 'author.email() ++ " | " ++ committer.email() ++ " " ++ description.first_line() ++ "\n"'`
|
|
174
191
|
shows your role's App in both columns **on every commit you made** — not on the whole list:
|
|
175
192
|
earlier phases' commits are legitimately authored by their own role's App. Their *committer* is
|
|
176
|
-
a different matter, and no longer noise to accept. Resolving a conflict rewrites nothing
|
|
177
|
-
|
|
178
|
-
open to you (*Rewriting pushed commits*,
|
|
179
|
-
|
|
193
|
+
a different matter, and no longer noise to accept. Resolving a conflict rewrites nothing — it is a
|
|
194
|
+
forward merge (`skill://legion-worker/references/conflicts-and-rewrites.md`) — so it changes no
|
|
195
|
+
committer at all, and the one rewrite still open to you (*Rewriting pushed commits*, in the same
|
|
196
|
+
reference) resets the committer only of commits on your own chain that descend from the commit
|
|
197
|
+
you named, after its guard cleared. Another role's commit
|
|
180
198
|
carrying you as committer, which you did not rewrite that way, is evidence that something
|
|
181
199
|
rewrote commits it should not have — the observable symptom of LEGION-118. Stop and send the
|
|
182
200
|
architect that log; do not accept it as a side effect. A wrong identity on your own commit, the
|
|
@@ -261,325 +279,42 @@ branch name alone is ambiguous. The credential helper and `legion gh` provide th
|
|
|
261
279
|
identity; never export, fetch, or replace a token. Other phases advance the existing branch
|
|
262
280
|
rather than creating a replacement bookmark or PR.
|
|
263
281
|
|
|
264
|
-
## PR body and
|
|
265
|
-
|
|
266
|
-
The implementer writes the
|
|
267
|
-
later phase
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
-
|
|
276
|
-
|
|
277
|
-
`legion threads resolve
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
**Production:** <what was checked in production, how, what was observed> — merge commit <sha>.
|
|
295
|
-
(written by the implementer after the merge lands; `pending <what is missing>` until then)
|
|
296
|
-
|
|
297
|
-
**Fast-follow:** <one named cleanup item and where it will land>, or "none".
|
|
298
|
-
|
|
299
|
-
**Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
|
|
300
|
-
**Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, merge the bookmark onto the new base (`jj new legion/<KEY> <new base> -m "<message>"`) and push with `legion-worker`'s ordinary push procedure — a genuine fast-forward, never the procedure for rewritten commits — the new head runs Tests against the new merge result — and cite that run in the PR body.
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
**A proof** is the changed behaviour exercised on the surface a user reaches it through, recorded
|
|
304
|
-
as the exact command or run id, what was observed, the head SHA, and one negative control —
|
|
305
|
-
a deliberately broken input and the refusal or failure observed. The surface is
|
|
306
|
-
**production-like** — the repository's real-process test harness and fixtures, a sandbox
|
|
307
|
-
repository, a real browser, a devN stack, staging, or a local stack with real migrations, one that
|
|
308
|
-
has the resource the change touches — and each `E2E` line carries a **link** to that run,
|
|
309
|
-
screenshot, or e2e; human review does not replace user-facing verification, and a green unit suite
|
|
310
|
-
is not it. A unit or integration test is a regression lock, never proof of a criterion. Sami,
|
|
311
|
-
2026-09-13, verbatim: "They need to test everything in a production-like
|
|
312
|
-
environment before merging, and it is the agent that develops the feature that is responsible
|
|
313
|
-
for doing that. If there's anything blocking that, we need to fix it: if it's infrastructure, we
|
|
314
|
-
need to fix it; if it's tooling, we need to develop it; if it's skills, we need to fix the skills
|
|
315
|
-
... it should not require deploying to production to realize your feature doesn't work."
|
|
316
|
-
Evidence for the rule: in the week of 2026-09-08 three surfaces merged green and were wrong on
|
|
317
|
-
inspection (the Astrolabe IPI stack, Dispatch on ECS, the candidate flow), and on 2026-09-12 six
|
|
318
|
-
deploy slots died on code first executed after merge, including a production-only ECS bootstrap
|
|
319
|
-
the whole staging gate never ran. The implementer's proof and the tester's proof below are both
|
|
320
|
-
this proof.
|
|
321
|
-
|
|
322
|
-
- **Threads are dispositioned individually, never resolved in bulk.** Every open review
|
|
323
|
-
thread gets its own line naming the fixing commit or the reason it isn't a defect. The
|
|
324
|
-
reviewer answers each thread it opened, and each thread a bot opened that is none of Legion's
|
|
325
|
-
role Apps, with exactly one of `Accepted: fixed in <commit> — <one line>`,
|
|
326
|
-
`Accepted: not a defect — <reason>`, or `Still open: <what remains>`; nothing else is an
|
|
327
|
-
acceptance, and nobody replies after an `Accepted:` (any later reply that is not itself an
|
|
328
|
-
`Accepted:` — the opener's own follow-up included — leaves the thread open, because resolution
|
|
329
|
-
considers only the newest comment). The review App can reply on a thread but cannot resolve it:
|
|
330
|
-
GitHub grants resolving a review thread to the pull request's author, and the implementer opens
|
|
331
|
-
every Legion pull request (`packages/daemon/src/daemon/AGENTS.md`, GitHub Apps).
|
|
332
|
-
When `LEGION_GRANT_FILE` or `LEGION_GRANT` is set, use `legion threads resolve --pr <number> --repo <owner>/<repo>`; when neither is set, use `gh api graphql`
|
|
333
|
-
with the session's GitHub credential and the fallback below.
|
|
334
|
-
In a Legion pane, the **implementer** runs the command before every push that answers a review
|
|
335
|
-
(the corrective push and the final `.legion/` deletion push) and pastes its output into the
|
|
336
|
-
`Threads` section. The command resolves each unresolved thread whose newest submitted comment is
|
|
337
|
-
the opener's own `Accepted:` reply. On a thread a bot account opened that is none of Legion's
|
|
338
|
-
role Apps (the daemon names them, keyed by App role), the Legion reviewer's `Accepted:` also
|
|
339
|
-
closes it. GitHub cannot tell a CI bot, which never accepts, from a person whose `gh` is routed
|
|
340
|
-
to an App, so the reviewer adjudicates such a finding, and it may accept one an App-routed person
|
|
341
|
-
raised. The subject of a finding never closes it: the implementer's `Fixed in <commit>: …` or
|
|
342
|
-
`Declined: …` answers a thread and closes none. A thread either Legion App opened, a reviewer's
|
|
343
|
-
finding included, still needs its opener's `Accepted:`. It makes one `resolveReviewThread` per
|
|
344
|
-
thread, prints `resolved <url> — <whose acceptance>` (its opener's, or the Legion reviewer's on a
|
|
345
|
-
bot's thread, so the ledger shows which) or `left open <url> — newest reply by <login> is …`
|
|
346
|
-
naming why, and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one.
|
|
347
|
-
|
|
348
|
-
Without a grant, page through `reviewThreads`, skip `isResolved: true`, and compare the opener
|
|
349
|
-
with the newest comment. Query shape, inside `repository { pullRequest { … } }`:
|
|
350
|
-
|
|
351
|
-
```graphql
|
|
352
|
-
reviewThreads(first: 100, after: $after) {
|
|
353
|
-
pageInfo { hasNextPage endCursor }
|
|
354
|
-
nodes {
|
|
355
|
-
id isResolved
|
|
356
|
-
opener: comments(first: 1) { nodes { author { __typename login } } }
|
|
357
|
-
newest: comments(last: 1) { nodes { author { __typename login } body state } }
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Resolve only when the newest comment is submitted, its `author` is the opener's account (the same
|
|
363
|
-
`__typename` and `login`: a login alone is a string anyone may register), and its `body`, after
|
|
364
|
-
removing leading spaces, tabs, CR, and LF, begins `Accepted:`. Without a
|
|
365
|
-
grant nothing names Legion's own App logins, so this route closes a bot's thread only on its
|
|
366
|
-
opener's `Accepted:`: leave one the Legion reviewer accepted for the implementer's or merger's
|
|
367
|
-
run in a pane, or report it. For each thread to resolve:
|
|
368
|
-
|
|
369
|
-
```graphql
|
|
370
|
-
mutation($threadId: ID!) {
|
|
371
|
-
resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
Re-read `reviewThreads` and confirm that thread's `isResolved` is true. In either route, report
|
|
376
|
-
a refused resolution to the architect, which opens an ask for a human to resolve the thread by
|
|
377
|
-
hand — never skip it silently. The merger runs the command once more before publishing READY
|
|
378
|
-
and does not publish while any `left open` line remains.
|
|
379
|
-
- **Correctness fixes land in this PR; cleanup is one named fast-follow.** A finding that
|
|
380
|
-
changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
|
|
381
|
-
Findings about naming, duplication, or wording are batched into the single `Fast-follow`
|
|
382
|
-
line instead of iterating per push.
|
|
383
|
-
- **Reintegrate the base only on a real conflict, except after a base retarget — and with a
|
|
384
|
-
merge, never `jj rebase`.** Sami, 2026-09-11, verbatim:
|
|
385
|
-
"Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
|
|
386
|
-
The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
|
|
387
|
-
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never reintegrate the base to
|
|
388
|
-
pick up `main` or refresh CI. A single failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
|
|
389
|
-
pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
|
|
390
|
-
confirmation, not a new round (see *The unchanged-diff check* below); that name is the event's,
|
|
391
|
-
kept by the rules below and the learnings that cite it, and the operation it names is always
|
|
392
|
-
the merge here. Before merging, record
|
|
393
|
-
the fingerprint at the current tip; after pushing the merged branch, record it at the new
|
|
394
|
-
tip; post one PR comment (Legion footer):
|
|
395
|
-
`rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
|
|
396
|
-
Every issue workspace is a `jj workspace` of the same shared repository and operation log, and
|
|
397
|
-
jj always rebases every descendant of any commit it rewrites — a revset naming the root of your
|
|
398
|
-
own chain and rewriting it in place also rewrites whatever another tree has stacked on that root,
|
|
399
|
-
whichever selector chose it (`-s`, `-b`, and `-r` all rewrite descendants; `-r` only re-parents
|
|
400
|
-
them to fill the hole, which is worse). This is what happened in LEGION-118: one issue's own
|
|
401
|
-
conflict step moved a second issue's twelve commits and its bookmark onto a conflicted copy.
|
|
402
|
-
Resolve the conflict with a forward merge instead of a rewrite — merge the branch's own
|
|
403
|
-
bookmark with the destination in one new commit, so nothing existing is rewritten and nothing
|
|
404
|
-
built on your prior commits, in this tree or another, ever moves:
|
|
405
|
-
|
|
406
|
-
```bash
|
|
407
|
-
jj -R "$LEGION_WORKSPACE" new legion/<KEY> main@origin -m "merge: resolve conflict against main@origin"
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
Merge from the bookmark, never from `@`: a handoff split leaves `@` an empty, undescribed
|
|
411
|
-
commit above the described one the bookmark already names, and `jj git push` refuses to push
|
|
412
|
-
any commit without a description — merging from `@` drags that undescribed commit into the
|
|
413
|
-
ancestry and the push fails (`Won't push commit … since it has no description`); the bookmark
|
|
414
|
-
is always on a described, already-pushed commit. If the merge conflicts, resolve it in that
|
|
415
|
-
one commit — edit the markers directly; there is nothing to squash, since the merge is the
|
|
416
|
-
only new commit. Then `jj -R "$LEGION_WORKSPACE" new` to move off it, and push with the one
|
|
417
|
-
push procedure (*Every role pushes its own commits*, below): the merge descends from both the
|
|
418
|
-
bookmark's old position and the destination, so it is a genuine fast-forward and *Rewriting
|
|
419
|
-
pushed commits* never applies — nothing was rewritten, so there is no tip to record first.
|
|
420
|
-
- **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." The `Fast-follow:`
|
|
421
|
-
field names naming, duplication, or wording cleanup only; anything that changes behaviour,
|
|
422
|
-
hides an error, or breaks a gate lands in this PR.
|
|
423
|
-
- **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
|
|
424
|
-
The proof is the one defined above. It goes into `.legion/implement.json` as the required `proof`
|
|
425
|
-
array (`handoff_write` for phase `implement` refuses a payload without one, or with a blank or
|
|
426
|
-
whitespace-only field, and names the field), and into the PR body, because the reviewer and the
|
|
427
|
-
merger verify facts on GitHub and never from a handoff.
|
|
428
|
-
- **The tester verifies the implementer's proof and adds its own `E2E (tester)` line.** It re-runs
|
|
429
|
-
the implementer's command or drives the same surface independently, and records the verdict in
|
|
430
|
-
`.legion/test.json` as `implementerProof` (`{verdict, how}`).
|
|
431
|
-
A test handoff whose predecessor carried no proof is a test failure, not a gap for the tester to fill:
|
|
432
|
-
record it in `failures` with `implementerProof.verdict: "rejected"`, complete the phase, and let
|
|
433
|
-
the architect return the issue to the implementer — the agent that developed the change owns
|
|
434
|
-
proving it (`handoff_write` for phase `test` refuses a rejected verdict, or `failed > 0`,
|
|
435
|
-
with no recorded failure). Otherwise, add your own proof before completing — a proof as defined
|
|
436
|
-
above — as the `E2E (tester)` line and the `proof` array `handoff_write` for
|
|
437
|
-
phase `test` requires whenever you report no failure. A code path whose first execution is after merge — a
|
|
438
|
-
deploy workflow's inline step, a post-merge helper, a production-only resource — is untested
|
|
439
|
-
until the implementer has executed it against a devN stack; if no surface can reach it, the
|
|
440
|
-
tester names that missing surface as the blocker instead of passing the phase. Environment or
|
|
441
|
-
secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
|
|
442
|
-
`.legion/test.json`, and only when the issue's acceptance criteria call for it — never
|
|
443
|
-
re-pasted into the PR body each round. After a conflict-forced rebase, compute the
|
|
444
|
-
fingerprint at the head your `E2E` line names and at the new head. Equal: re-run only the
|
|
445
|
-
bare gates — the repository's CI green at the new head and its smoke check — and change the
|
|
446
|
-
`E2E` line's head to the new SHA with
|
|
447
|
-
`rebase re-check <old-sha> → <new-sha>: fingerprint unchanged, bare gates only`; the
|
|
448
|
-
real-surface verification is not repeated. Different: a full test round.
|
|
449
|
-
- **The implementer runs `skill://ce-simplify-code` once per pull request, after the last review round
|
|
450
|
-
closes and before the reviewer's final pass, when the diff touches runtime code; a docs-only
|
|
451
|
-
diff gets none.** It is scoped to the pull request's own diff, at the head where the last review
|
|
452
|
-
round closed: nothing applied leaves that head final; applied → the applied head is the final
|
|
453
|
-
head: CI runs on it, the pair runs once on it, and the E2E proof re-runs on it for the surface
|
|
454
|
-
the simplify diff touched (Sami, 2026-09-13: test on the real surface before merging, no
|
|
455
|
-
shortcuts — a refactor that "preserves behaviour" is a claim until it is executed). That cost is
|
|
456
|
-
why 0-applied is the expected outcome and a pass that applies is spent sparingly. At the applied
|
|
457
|
-
head the implementer re-cites the `CI` line and re-runs its own proof into `E2E (implementer)`,
|
|
458
|
-
and the tester re-runs its proof for the touched surface into `E2E (tester)`, before the
|
|
459
|
-
reviewer's final pass. Simplify is the last code change; the pair is the last review. Record it
|
|
460
|
-
in the `Thermo` line.
|
|
461
|
-
- The reviewer verifies the `CI`, `Threads`, and `E2E` facts against GitHub directly —
|
|
462
|
-
never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
|
|
463
|
-
`task(agent="thermonuclear-code-quality")` once at that head — the head the implementer's
|
|
464
|
-
simplify pass left final — and records the verdict.
|
|
465
|
-
Approval is refused while either `E2E (implementer)` or `E2E (tester)` is missing: `REQUEST_CHANGES` naming the missing line.
|
|
466
|
-
Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round** —
|
|
467
|
-
`REQUEST_CHANGES` when any correctness finding stands, otherwise `COMMENT` while the head
|
|
468
|
-
still carries `.legion/`; `APPROVE` only for a head that carries no `.legion/` — the head
|
|
469
|
-
that differs from the reviewed one by the `.legion/` deletion alone, or, after a
|
|
470
|
-
conflict-forced rebase, the new head whose fingerprint equals the approved head's — always
|
|
471
|
-
named by SHA — carrying every inline comment in that single
|
|
472
|
-
call: `legion gh -- api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
|
|
473
|
-
with `commit_id`, `event` (`REQUEST_CHANGES`, `COMMENT`, or `APPROVE`), `body` (with the
|
|
474
|
-
Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
|
|
475
|
-
finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
|
|
476
|
-
Then return the issue to the architect; when clean, have the architect send the implementer
|
|
477
|
-
back to push the `.legion/` deletion, then review **that** head and approve it by name. After a conflict-forced rebase, compute the fingerprint at the
|
|
478
|
-
`commit_id` of your last submitted review and at the new head. Equal and that review was
|
|
479
|
-
`APPROVE`: submit one more `APPROVE` naming the new head by SHA, its body naming both SHAs
|
|
480
|
-
and the fingerprint — a confirmation, not a round; no thermo pass, no thread pass. Equal and
|
|
481
|
-
that review was `COMMENT` or `REQUEST_CHANGES`: continue that round against the new head;
|
|
482
|
-
nothing restarts. Different: a new round — thermo again, one review.
|
|
483
|
-
When you re-review after a corrective push, answer every thread you opened in one of the
|
|
484
|
-
three forms above — `Accepted:` is the only reply the implementer's `legion threads resolve`
|
|
485
|
-
acts on — and approve only once every thread you opened carries your `Accepted:` reply and the
|
|
486
|
-
implementer's run has resolved it (verify `isResolved: true` with `gh api graphql`, never from
|
|
487
|
-
the PR body).
|
|
488
|
-
- Once a base is frozen for others to stack on, never rewrite it — fixes land as new
|
|
489
|
-
commits on top, and the `Chain` line records what is frozen.
|
|
490
|
-
- **Retro's commit does not void the reviewer's approval.** After the reviewer approves the
|
|
491
|
-
cleaned head, retro commits its learnings under `docs/solutions/` on top of it; that commit
|
|
492
|
-
stays, the approval stands, and the tree goes to the merger — never back to the tester or
|
|
493
|
-
reviewer. Anything else above the approved head does void it, and the merger tells the
|
|
494
|
-
architect the head must return to review instead of publishing. A conflict-forced rebase
|
|
495
|
-
after retro moves those documents with the branch; retro never re-runs.
|
|
496
|
-
- The merger runs `legion threads resolve --pr <n> --repo <owner>/<repo>` (it acts as the same
|
|
497
|
-
code-writing App as the implementer; resolving a thread changes no commit, so this run never
|
|
498
|
-
invalidates the approval), does not publish while any `left open` line remains or the command
|
|
499
|
-
exits 1 (report the thread to the architect instead), then proves that rule with two commands.
|
|
500
|
-
First `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R
|
|
501
|
-
"$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`, whose output is quoted
|
|
502
|
-
in READY (an empty output is quoted as `no file changes above the approved head`); then the same
|
|
503
|
-
with `'~docs/solutions'` appended, which must print nothing. The merger always posts
|
|
504
|
-
`READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)` (the shape
|
|
505
|
-
`packages/pi-envoy/roles/merger.md` defines), its summary, and the PR body's gate facts as a
|
|
506
|
-
`dispatch_message` on the issue. When the `Legion addressing` line names a merge queue, it also
|
|
507
|
-
publishes the same packet there with `envoy_publish`; a 404 means the Dispatch message remains
|
|
508
|
-
the durable notice and the merger stays idle. The READY packet names both the implementer's and
|
|
509
|
-
tester's `E2E` lines; a missing one is reported to the architect instead of published. Legion
|
|
510
|
-
never merges.
|
|
511
|
-
- **After a human merges, the implementer verifies in production.** Sami, 2026-09-13,
|
|
512
|
-
verbatim: "the agent that developed it should be responsible for testing in production."
|
|
513
|
-
The architect sends the implementer back once the merge lands; the implementer watches the
|
|
514
|
-
deploy slot that carries the merge to `production-apply` (or the equivalent publish step),
|
|
515
|
-
drives the changed path in production through the user's own access path, and records the
|
|
516
|
-
observation on the PR and the issue before the architect signs off. A staging pass is not
|
|
517
|
-
this: on 2026-09-12 a slot's entire staging gate passed at 00:02Z and its production-apply
|
|
518
|
-
failed at 00:12Z on a resource staging never runs. If the slot fails on the change, the
|
|
519
|
-
implementer owns the fix and the next slot.
|
|
520
|
-
The record has three places: the PR body's `Production:` line, one pull-request comment
|
|
521
|
-
carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
|
|
522
|
-
read GitHub, the architect reads the issue. When the deploy that carries the merge has not
|
|
523
|
-
happened (a shared profile still holding the previous plugin release, a daemon still running
|
|
524
|
-
the previous commit, a slot nobody has run), open a `dispatch_ask` naming the exact install or
|
|
525
|
-
restart step, with options for its outcomes, keep the `Production:` line at `pending <what is
|
|
526
|
-
missing>`, and complete the check once the human answers that it is done. Never record a
|
|
527
|
-
staging pass as the production check, and never let the architect sign off on a `pending` line.
|
|
528
|
-
|
|
529
|
-
## When no surface reaches the changed path
|
|
530
|
-
|
|
531
|
-
No surface reaches the changed path is a report to the architect, never a reason to complete the phase.
|
|
532
|
-
Say which surface is missing and what it would have to do — a rig that can spawn the role, a
|
|
533
|
-
sandbox that holds the resource, a credential, a command that does not exist yet — and send it to
|
|
534
|
-
the architect with `envoy_publish` to its role topic. The architect creates a child issue in this
|
|
535
|
-
tree to build it (infrastructure, tooling, or a skill) and resumes you once it lands. Sami,
|
|
536
|
-
2026-09-13, verbatim: "If there's anything blocking that, we need to fix it: if it's
|
|
537
|
-
infrastructure, we need to fix it; if it's tooling, we need to develop it; if it's skills, we need
|
|
538
|
-
to fix the skills." A code path whose first execution would be after the merge — a deploy
|
|
539
|
-
workflow's inline step, a post-merge helper, a production-only resource — is untested until you
|
|
540
|
-
have executed it somewhere production-like; completing with a unit-test-only handoff is the
|
|
541
|
-
failure this rule exists to stop.
|
|
542
|
-
|
|
543
|
-
## The unchanged-diff check
|
|
544
|
-
|
|
545
|
-
The fingerprint every role compares after a conflict-forced rebase (every flag and the fileset
|
|
546
|
-
verified on jj 0.45.1):
|
|
547
|
-
|
|
548
|
-
```bash
|
|
549
|
-
cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && \
|
|
550
|
-
jj -R "$LEGION_WORKSPACE" diff --from "fork_point(main@origin | <head-sha>)" --to <head-sha> \
|
|
551
|
-
--git --context 0 '~(.legion | docs/solutions)' \
|
|
552
|
-
| sed -e '/^@@/d' -e '/^index /d' | sha256sum
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
- `<head-sha>` is a full commit SHA; a jj commit id is the git SHA GitHub shows.
|
|
556
|
-
- `fork_point(main@origin | <head-sha>)` is the base the branch was cut from *at that head*:
|
|
557
|
-
the old base for the pre-rebase head, the new base for the rebased one, so one command
|
|
558
|
-
serves both sides. On a stacked PR substitute its base branch for `main`
|
|
559
|
-
(`legion gh -- pr view <n> --json baseRefName`).
|
|
560
|
-
- A head the rebase hid is still addressable by its SHA in the shared workspace. A SHA the
|
|
561
|
-
workspace cannot resolve (`jj -R "$LEGION_WORKSPACE" log -r <sha>` errors) counts as a
|
|
562
|
-
changed diff — never as unchanged.
|
|
563
|
-
- `--context 0` drops context lines; the `sed` drops `@@` hunk headers (line positions move
|
|
564
|
-
on a rebase) and `index` lines (blob ids move when the base's copy of a file changed). What
|
|
565
|
-
is left is exactly the added and removed lines per file.
|
|
566
|
-
- The single fileset `'~(.legion | docs/solutions)'` leaves out the handoff ledger and retro's
|
|
567
|
-
learnings: process artifacts the rules above already exempt from re-review, which change
|
|
568
|
-
between one role's verified head and the next without changing the product. This is what lets
|
|
569
|
-
each role compare against *its own* last verified head instead of trusting another role's
|
|
570
|
-
numbers. It must be one expression: jj unions positional filesets, so two separate
|
|
571
|
-
`'~.legion' '~docs/solutions'` arguments select every file and exclude nothing. Once
|
|
572
|
-
`.legion/` is gone, jj warns `No matching entries for paths: .legion` on stderr; the hash is
|
|
573
|
-
unaffected.
|
|
574
|
-
|
|
575
|
-
Where each role gets its two heads: the implementer — the tip before and after its own rebase;
|
|
576
|
-
the tester — the head its `E2E` line names and the new head; the reviewer — the `commit_id` of
|
|
577
|
-
its last submitted review (`legion gh -- api repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login}'`)
|
|
578
|
-
and the new head; the merger never computes a fingerprint — it uses the `--summary` check
|
|
579
|
-
above.
|
|
282
|
+
## PR body, review, and the merge gate
|
|
283
|
+
|
|
284
|
+
The implementer writes the pull request body in the READY format when it opens the pull request,
|
|
285
|
+
and every later phase edits its own lines of the live body rather than replacing it. Each proof
|
|
286
|
+
(the implementer's `E2E (implementer)` line and `proof` array, the tester's `E2E (tester)` line
|
|
287
|
+
and `proof` array) is the changed behaviour exercised on a production-like surface, recorded as
|
|
288
|
+
its command or run id, what was observed, the head SHA, and one negative control; a unit test is
|
|
289
|
+
never one. **Before you write or edit any line of the PR body or any `proof` array, read
|
|
290
|
+
`skill://legion-worker/references/pr-body.md`**: the template (the sole definition of the CI
|
|
291
|
+
line), the full definition of a proof, what the tester verifies, and the simplify pass.
|
|
292
|
+
|
|
293
|
+
- **Review threads** are disposed of one by one, never in bulk, and only an `Accepted:` from the
|
|
294
|
+
thread's opener (or, on a bot's thread, from the Legion reviewer) closes one. The implementer
|
|
295
|
+
runs `legion threads resolve` before every push that answers a review, and the merger before
|
|
296
|
+
READY: `skill://legion-worker/references/review-threads.md`.
|
|
297
|
+
- **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." A finding that changes
|
|
298
|
+
behaviour, hides an error, or breaks a gate is fixed in this pull request; naming, duplication,
|
|
299
|
+
or wording cleanup is batched into the one `Fast-follow:` line instead of iterating per push.
|
|
300
|
+
- **A red CI job** that failed on its own is re-run with
|
|
301
|
+
`legion gh -- run rerun <run-id> --failed`, never by pushing a new commit or bringing in the base.
|
|
302
|
+
- **A conflict or a retarget** is the only reason to bring the base into the branch, always as a
|
|
303
|
+
forward merge and never `jj rebase`; the unchanged-diff fingerprint each role compares
|
|
304
|
+
afterwards is in the same reference: `skill://legion-worker/references/conflicts-and-rewrites.md`.
|
|
305
|
+
- **The merge gate**, in order: the tester's evidence green → the implementer's `.legion/`
|
|
306
|
+
deletion push → the reviewer's approval of that head → retro → the merger's READY → the human
|
|
307
|
+
merge → the implementer's production check. Legion never merges. The reviewer's submissions,
|
|
308
|
+
retro's commit, the merger's READY, and the production check follow
|
|
309
|
+
`skill://legion-worker/references/merge-gate.md`.
|
|
310
|
+
- **No surface reaches the changed path** is a report to the architect, never a reason to
|
|
311
|
+
complete the phase: `skill://legion-worker/references/pr-body.md`.
|
|
580
312
|
|
|
581
313
|
## Completion gate: handoff write, verification, and persistence
|
|
582
314
|
|
|
315
|
+
The merger writes no handoff and pushes nothing, so this gate does not apply to it
|
|
316
|
+
(`packages/pi-envoy/roles/merger.md`).
|
|
317
|
+
|
|
583
318
|
Write the phase-specific handoff: call the `legion` tool with `op: "handoff_write"`, `phase: "<p>"`,
|
|
584
319
|
and `data`: a JSON object of the phase-specific fields only. It runs `legion handoff write` in
|
|
585
320
|
`$LEGION_WORKSPACE` and returns its output.
|
|
@@ -657,38 +392,11 @@ remote branch sideways onto your commit and drops theirs (jj 0.45.1:
|
|
|
657
392
|
`bookmark: legion/K [move sideways from <theirs> to <yours>]`). A clone that has not seen the other
|
|
658
393
|
push is refused by jj itself (`unexpectedly moved on the remote`).
|
|
659
394
|
|
|
660
|
-
|
|
661
|
-
rewrite
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
— which the rewrite leaves outside `::@-` — after a fetch and while your chain still descends
|
|
666
|
-
from it:
|
|
667
|
-
|
|
668
|
-
```bash
|
|
669
|
-
cd -- "$LEGION_WORKSPACE" && \
|
|
670
|
-
jj -R "$LEGION_WORKSPACE" git fetch && \
|
|
671
|
-
foreign=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
|
|
672
|
-
-r 'descendants(<the commit you are about to rewrite>) ~ ::@') && \
|
|
673
|
-
{ [ -z "$foreign" ] || { echo "not mine, and descends from the commit to rewrite: $foreign" >&2; false; }; } && \
|
|
674
|
-
behind=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
|
|
675
|
-
-r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin") ~ ::@-') && \
|
|
676
|
-
{ [ -z "$behind" ] || { echo "legion/<KEY>@origin is at $behind, which @- does not descend from" >&2; false; }; } && \
|
|
677
|
-
jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id' \
|
|
678
|
-
-r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin")' \
|
|
679
|
-
>"${TMPDIR:-/tmp}/legion-<KEY>-$LEGION_ROLE-rewritten-tip"
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
`descendants(<commit>) ~ ::@` is everything built on the commit you are about to rewrite that is
|
|
683
|
-
not on your own chain. Non-empty means the rewrite would move work that is not yours: do not
|
|
684
|
-
rewrite it. Put the change in a new commit on top instead, and report the listed commits to the
|
|
685
|
-
architect. On a two-workspace rig of this shape a `jj squash --into` a pushed commit reported
|
|
686
|
-
`Rebased 13 descendant commits` and moved a second issue's twelve commits and its bookmark; the
|
|
687
|
-
check above listed those thirteen and refused before anything moved.
|
|
688
|
-
|
|
689
|
-
Then rewrite, resolve, and push with the procedure above. It lets the remote branch sit on the
|
|
690
|
-
tip you recorded, which the rewrite replaced, and on nothing else: when another role pushed after
|
|
691
|
-
you recorded it, the push is refused. The push deletes the file.
|
|
395
|
+
Before any rewrite of a commit you already pushed — a `jj squash --into` one, or any other
|
|
396
|
+
rewrite — read *Rewriting pushed commits* in
|
|
397
|
+
`skill://legion-worker/references/conflicts-and-rewrites.md`: it checks that no other tree's work
|
|
398
|
+
is built on that commit and records the pushed tip `$tip_file` above reads, the only case in which
|
|
399
|
+
the push above lets the remote branch move off its current tip.
|
|
692
400
|
|
|
693
401
|
Before the push, check ancestry and identity as above: the chain carries every earlier phase's
|
|
694
402
|
commits, and pushing them with yours is expected. A refusal, and a push the remote rejects, is a
|
|
@@ -716,17 +424,9 @@ This publishes your phase's completion to the architect's role and clears the da
|
|
|
716
424
|
record of this issue's active phase. Do not add pipeline labels, run a controller loop, or
|
|
717
425
|
invent a different completion protocol — this is the whole contract.
|
|
718
426
|
|
|
719
|
-
A reviewer's phase ends with its completion, not with its review
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
before you submit it, since an approval stands only on green checks and GitHub can dismiss one
|
|
723
|
-
once the head moves, and a verdict that settles red there makes the round's decision a request for
|
|
724
|
-
changes naming the failing checks; a request for changes does not wait, since it stands whatever CI says and the
|
|
725
|
-
issue leaves reviewing with it. A review of a head the handoff push then replaces names a head
|
|
726
|
-
the pull request no longer has. A round that writes none (the final approval of the `.legion/`
|
|
727
|
-
deletion head) reviews the head as it is. The daemon moves the issue once both are in —
|
|
728
|
-
the decision GitHub reports and your completion, in either order — so a review posted without a
|
|
729
|
-
completion leaves the issue in reviewing until you finish.
|
|
427
|
+
A reviewer's phase ends with its completion, not with its review; the order of a review round
|
|
428
|
+
(the handoff push; for an approval, CI settled green at that head; the review of that head; then
|
|
429
|
+
the completion) is in `skill://legion-worker/references/merge-gate.md`.
|
|
730
430
|
|
|
731
431
|
**A refused completion is information, not a retry loop.** The daemon attributes your report to
|
|
732
432
|
the run whose task you took, and answers with what it found. What each answer carries, and what to
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Conflicts, retargets, fingerprints, and rewriting pushed commits
|
|
2
|
+
|
|
3
|
+
Part of `skill://legion-worker`. Read it when GitHub reports the pull request `CONFLICTING`, the
|
|
4
|
+
controller asks you to resolve a conflict, the pull request is retargeted to a new base, you
|
|
5
|
+
compare two heads after such a merge, or you are about to rewrite a commit you already pushed.
|
|
6
|
+
Every path it cites is in sjawhar/legion.
|
|
7
|
+
|
|
8
|
+
## Reintegrating the base
|
|
9
|
+
|
|
10
|
+
- **Reintegrate the base only on a real conflict, except after a base retarget — and with a
|
|
11
|
+
merge, never `jj rebase`.** Sami, 2026-09-11, verbatim:
|
|
12
|
+
"Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
|
|
13
|
+
The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
|
|
14
|
+
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never reintegrate the base to
|
|
15
|
+
pick up `main` or refresh CI (a single failed CI job is re-run on its own: *A red CI job* in
|
|
16
|
+
`skill://legion-worker`). A conflict-forced rebase that leaves the branch's diff unchanged is a
|
|
17
|
+
confirmation, not a new round (see *The unchanged-diff check* below); that name is the event's,
|
|
18
|
+
kept by the rules below and the learnings that cite it, and the operation it names is always
|
|
19
|
+
the merge here. Before merging, record
|
|
20
|
+
the fingerprint at the current tip; after pushing the merged branch, record it at the new
|
|
21
|
+
tip; post one PR comment (Legion footer):
|
|
22
|
+
`rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
|
|
23
|
+
Every issue workspace is a `jj workspace` of the same shared repository and operation log, and
|
|
24
|
+
jj always rebases every descendant of any commit it rewrites — a revset naming the root of your
|
|
25
|
+
own chain and rewriting it in place also rewrites whatever another tree has stacked on that root,
|
|
26
|
+
whichever selector chose it (`-s`, `-b`, and `-r` all rewrite descendants; `-r` only re-parents
|
|
27
|
+
them to fill the hole, which is worse). This is what happened in LEGION-118: one issue's own
|
|
28
|
+
conflict step moved a second issue's twelve commits and its bookmark onto a conflicted copy.
|
|
29
|
+
Resolve the conflict with a forward merge instead of a rewrite — merge the branch's own
|
|
30
|
+
bookmark with the destination in one new commit, so nothing existing is rewritten and nothing
|
|
31
|
+
built on your prior commits, in this tree or another, ever moves:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
jj -R "$LEGION_WORKSPACE" new legion/<KEY> main@origin -m "merge: resolve conflict against main@origin"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Merge from the bookmark, never from `@`: a handoff split leaves `@` an empty, undescribed
|
|
38
|
+
commit above the described one the bookmark already names, and `jj git push` refuses to push
|
|
39
|
+
any commit without a description — merging from `@` drags that undescribed commit into the
|
|
40
|
+
ancestry and the push fails (`Won't push commit … since it has no description`); the bookmark
|
|
41
|
+
is always on a described, already-pushed commit. If the merge conflicts, resolve it in that
|
|
42
|
+
one commit — edit the markers directly; there is nothing to squash, since the merge is the
|
|
43
|
+
only new commit. Then `jj -R "$LEGION_WORKSPACE" new` to move off it, and push with the one
|
|
44
|
+
push procedure (*Every role pushes its own commits* in `skill://legion-worker`): the merge descends from both the
|
|
45
|
+
bookmark's old position and the destination, so it is a genuine fast-forward and *Rewriting
|
|
46
|
+
pushed commits* never applies — nothing was rewritten, so there is no tip to record first.
|
|
47
|
+
- **After a retarget.** Retargeting a pull request to a new base does not re-run Tests. Merge the
|
|
48
|
+
bookmark onto the new base (`jj new legion/<KEY> <new base> -m "<message>"`) and push with the
|
|
49
|
+
ordinary push procedure — a genuine fast-forward, never the procedure for rewritten commits —
|
|
50
|
+
so the new head runs Tests against the new merge result, and cite that run in the PR body.
|
|
51
|
+
|
|
52
|
+
## The unchanged-diff check
|
|
53
|
+
|
|
54
|
+
The fingerprint every role compares after a conflict-forced rebase (every flag and the fileset
|
|
55
|
+
verified on jj 0.45.1):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && \
|
|
59
|
+
jj -R "$LEGION_WORKSPACE" diff --from "fork_point(main@origin | <head-sha>)" --to <head-sha> \
|
|
60
|
+
--git --context 0 '~(.legion | docs/solutions)' \
|
|
61
|
+
| sed -e '/^@@/d' -e '/^index /d' | sha256sum
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `<head-sha>` is a full commit SHA; a jj commit id is the git SHA GitHub shows.
|
|
65
|
+
- `fork_point(main@origin | <head-sha>)` is the base the branch was cut from *at that head*:
|
|
66
|
+
the old base for the pre-rebase head, the new base for the rebased one, so one command
|
|
67
|
+
serves both sides. On a stacked PR substitute its base branch for `main`
|
|
68
|
+
(`legion gh -- pr view <n> --json baseRefName`).
|
|
69
|
+
- A head the rebase hid is still addressable by its SHA in the shared workspace. A SHA the
|
|
70
|
+
workspace cannot resolve (`jj -R "$LEGION_WORKSPACE" log -r <sha>` errors) counts as a
|
|
71
|
+
changed diff — never as unchanged.
|
|
72
|
+
- `--context 0` drops context lines; the `sed` drops `@@` hunk headers (line positions move
|
|
73
|
+
on a rebase) and `index` lines (blob ids move when the base's copy of a file changed). What
|
|
74
|
+
is left is exactly the added and removed lines per file.
|
|
75
|
+
- The single fileset `'~(.legion | docs/solutions)'` leaves out the handoff ledger and retro's
|
|
76
|
+
learnings: process artifacts the merge gate already exempts from re-review
|
|
77
|
+
(`skill://legion-worker/references/merge-gate.md`), which change
|
|
78
|
+
between one role's verified head and the next without changing the product. This is what lets
|
|
79
|
+
each role compare against *its own* last verified head instead of trusting another role's
|
|
80
|
+
numbers. It must be one expression: jj unions positional filesets, so two separate
|
|
81
|
+
`'~.legion' '~docs/solutions'` arguments select every file and exclude nothing. Once
|
|
82
|
+
`.legion/` is gone, jj warns `No matching entries for paths: .legion` on stderr; the hash is
|
|
83
|
+
unaffected.
|
|
84
|
+
|
|
85
|
+
Where each role gets its two heads: the implementer — the tip before and after its own rebase;
|
|
86
|
+
the tester — the head its `E2E` line names and the new head; the reviewer — the `commit_id` of
|
|
87
|
+
its last submitted review (`legion gh -- api repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login}'`)
|
|
88
|
+
and the new head; the merger never computes a fingerprint — it uses the `--summary` check in
|
|
89
|
+
`skill://legion-worker/references/merge-gate.md`.
|
|
90
|
+
|
|
91
|
+
## Rewriting pushed commits
|
|
92
|
+
|
|
93
|
+
**Rewriting pushed commits** — a `jj squash --into` a commit already on GitHub, or any other
|
|
94
|
+
rewrite of a commit you already pushed — is the LEGION-118 hazard in a second shape: jj rebases
|
|
95
|
+
every descendant of any commit it rewrites, and in the one shared repository a descendant can be
|
|
96
|
+
another tree's branch stacked on your pushed commit, which then moves, with its bookmark, onto a
|
|
97
|
+
rewritten copy. So look for a descendant outside your own chain first, and record the pushed tip
|
|
98
|
+
— which the rewrite leaves outside `::@-` — after a fetch and while your chain still descends
|
|
99
|
+
from it:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cd -- "$LEGION_WORKSPACE" && \
|
|
103
|
+
jj -R "$LEGION_WORKSPACE" git fetch && \
|
|
104
|
+
foreign=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
|
|
105
|
+
-r 'descendants(<the commit you are about to rewrite>) ~ ::@') && \
|
|
106
|
+
{ [ -z "$foreign" ] || { echo "not mine, and descends from the commit to rewrite: $foreign" >&2; false; }; } && \
|
|
107
|
+
behind=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
|
|
108
|
+
-r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin") ~ ::@-') && \
|
|
109
|
+
{ [ -z "$behind" ] || { echo "legion/<KEY>@origin is at $behind, which @- does not descend from" >&2; false; }; } && \
|
|
110
|
+
jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id' \
|
|
111
|
+
-r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin")' \
|
|
112
|
+
>"${TMPDIR:-/tmp}/legion-<KEY>-$LEGION_ROLE-rewritten-tip"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`descendants(<commit>) ~ ::@` is everything built on the commit you are about to rewrite that is
|
|
116
|
+
not on your own chain. Non-empty means the rewrite would move work that is not yours: do not
|
|
117
|
+
rewrite it. Put the change in a new commit on top instead, and report the listed commits to the
|
|
118
|
+
architect. On a two-workspace rig of this shape a `jj squash --into` a pushed commit reported
|
|
119
|
+
`Rebased 13 descendant commits` and moved a second issue's twelve commits and its bookmark; the
|
|
120
|
+
check above listed those thirteen and refused before anything moved.
|
|
121
|
+
|
|
122
|
+
Then rewrite, resolve, and push with the one push procedure (*Every role pushes its own commits*
|
|
123
|
+
in `skill://legion-worker`). It lets the remote branch sit on the
|
|
124
|
+
tip you recorded, which the rewrite replaced, and on nothing else: when another role pushed after
|
|
125
|
+
you recorded it, the push is refused. The push deletes the file.
|
|
126
|
+
|
|
127
|
+
Once a base is frozen for others to stack on, never rewrite it: fixes land as new commits on top,
|
|
128
|
+
and the PR body's `Chain` line records what is frozen.
|
|
129
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# The merge gate: review, retro, READY, and the production check
|
|
2
|
+
|
|
3
|
+
Part of `skill://legion-worker`. Read it when you are the reviewer submitting a review or an
|
|
4
|
+
approval, the implementer pushing the `.legion/` deletion or recording the production check, or
|
|
5
|
+
the merger publishing READY. Every path it cites is in sjawhar/legion.
|
|
6
|
+
|
|
7
|
+
The order, in full: the tester's evidence green → the implementer's `.legion/` deletion push →
|
|
8
|
+
the reviewer's approval of that head → retro → the merger's READY → the human merge → the
|
|
9
|
+
implementer's production check. After the approval, only retro's `docs/solutions/` commit leaves
|
|
10
|
+
it standing on its own (*Retro*, below). A conflict-forced merge goes back to the reviewer for a
|
|
11
|
+
confirmation or a new round, as the fingerprint decides (*The reviewer*, below, and
|
|
12
|
+
`skill://legion-worker/references/conflicts-and-rewrites.md`), and any other change voids it.
|
|
13
|
+
|
|
14
|
+
## The reviewer
|
|
15
|
+
|
|
16
|
+
- The reviewer verifies the `CI`, `Threads`, and `E2E` facts against GitHub directly —
|
|
17
|
+
never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
|
|
18
|
+
`task(agent="thermonuclear-code-quality")` once at that head — the head the implementer's
|
|
19
|
+
simplify pass left final — and records the verdict.
|
|
20
|
+
Approval is refused while either `E2E (implementer)` or `E2E (tester)` is missing: `REQUEST_CHANGES` naming the missing line.
|
|
21
|
+
Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round** —
|
|
22
|
+
`REQUEST_CHANGES` when any correctness finding stands, otherwise `COMMENT` while the head
|
|
23
|
+
still carries `.legion/`; `APPROVE` only for a head that carries no `.legion/` — the head
|
|
24
|
+
that differs from the reviewed one by the `.legion/` deletion alone, or, after a
|
|
25
|
+
conflict-forced rebase, the new head whose fingerprint equals the approved head's — always
|
|
26
|
+
named by SHA — carrying every inline comment in that single
|
|
27
|
+
call: `legion gh -- api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
|
|
28
|
+
with `commit_id`, `event` (`REQUEST_CHANGES`, `COMMENT`, or `APPROVE`), `body` (with the
|
|
29
|
+
Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
|
|
30
|
+
finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
|
|
31
|
+
Then return the issue to the architect; when clean, have the architect send the implementer
|
|
32
|
+
back to push the `.legion/` deletion, then review **that** head and approve it by name. After a
|
|
33
|
+
conflict-forced rebase, compute the fingerprint (*The unchanged-diff check* in
|
|
34
|
+
`skill://legion-worker/references/conflicts-and-rewrites.md`) at the
|
|
35
|
+
`commit_id` of your last submitted review and at the new head. Equal and that review was
|
|
36
|
+
`APPROVE`: submit one more `APPROVE` naming the new head by SHA, its body naming both SHAs
|
|
37
|
+
and the fingerprint — a confirmation, not a round; no thermo pass, no thread pass. Equal and
|
|
38
|
+
that review was `COMMENT` or `REQUEST_CHANGES`: continue that round against the new head;
|
|
39
|
+
nothing restarts. Different: a new round — thermo again, one review.
|
|
40
|
+
- Answer every thread you opened, and every thread a bot opened that is none of Legion's role
|
|
41
|
+
Apps, as `skill://legion-worker/references/review-threads.md` says; the same reference says
|
|
42
|
+
when every thread is settled enough to approve.
|
|
43
|
+
|
|
44
|
+
A reviewer's phase ends with its completion, not with its review. A round that writes a handoff
|
|
45
|
+
takes this order: write, commit and push the handoff; submit the review of the head that push
|
|
46
|
+
made, by its SHA; then complete. An approval waits for the CI verdict to settle green at that head
|
|
47
|
+
before you submit it, since an approval stands only on green checks and GitHub can dismiss one
|
|
48
|
+
once the head moves, and a verdict that settles red there makes the round's decision a request for
|
|
49
|
+
changes naming the failing checks; a request for changes does not wait, since it stands whatever CI says and the
|
|
50
|
+
issue leaves reviewing with it. A review of a head the handoff push then replaces names a head
|
|
51
|
+
the pull request no longer has. A round that writes none (the final approval of the `.legion/`
|
|
52
|
+
deletion head) reviews the head as it is. The daemon moves the issue once both are in —
|
|
53
|
+
the decision GitHub reports and your completion, in either order — so a review posted without a
|
|
54
|
+
completion leaves the issue in reviewing until you finish.
|
|
55
|
+
|
|
56
|
+
## Retro
|
|
57
|
+
|
|
58
|
+
- **Retro's commit does not void the reviewer's approval.** After the reviewer approves the
|
|
59
|
+
cleaned head, retro commits its learnings under `docs/solutions/` on top of it; that commit
|
|
60
|
+
stays, the approval stands, and the tree goes to the merger — never back to the tester or
|
|
61
|
+
reviewer. Anything else above the approved head does void it, and the merger tells the
|
|
62
|
+
architect the head must return to review instead of publishing. A conflict-forced rebase
|
|
63
|
+
after retro moves those documents with the branch; retro never re-runs.
|
|
64
|
+
|
|
65
|
+
## The merger
|
|
66
|
+
|
|
67
|
+
- The merger runs `legion threads resolve --pr <n> --repo <owner>/<repo>` (it acts as the same
|
|
68
|
+
code-writing App as the implementer; resolving a thread changes no commit, so this run never
|
|
69
|
+
invalidates the approval), does not publish while any `left open` line remains or the command
|
|
70
|
+
exits 1 (report the thread to the architect instead), then proves that rule with two commands.
|
|
71
|
+
First `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R
|
|
72
|
+
"$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`, whose output is quoted
|
|
73
|
+
in READY (an empty output is quoted as `no file changes above the approved head`); then the same
|
|
74
|
+
with `'~docs/solutions'` appended, which must print nothing. The merger always posts
|
|
75
|
+
`READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)` (the shape
|
|
76
|
+
`packages/pi-envoy/roles/merger.md` defines), its summary, and the PR body's gate facts as a
|
|
77
|
+
`dispatch_message` on the issue. When the `Legion addressing` line names a merge queue, it also
|
|
78
|
+
publishes the same packet there with `envoy_publish`; a 404 means the Dispatch message remains
|
|
79
|
+
the durable notice and the merger stays idle. The READY packet names both the implementer's and
|
|
80
|
+
tester's `E2E` lines; a missing one is reported to the architect instead of published. Legion
|
|
81
|
+
never merges.
|
|
82
|
+
|
|
83
|
+
## After the human merge
|
|
84
|
+
|
|
85
|
+
- **After a human merges, the implementer verifies in production.** Sami, 2026-09-13,
|
|
86
|
+
verbatim: "the agent that developed it should be responsible for testing in production."
|
|
87
|
+
The architect sends the implementer back once the merge lands; the implementer watches the
|
|
88
|
+
deploy slot that carries the merge to `production-apply` (or the equivalent publish step),
|
|
89
|
+
drives the changed path in production through the user's own access path, and records the
|
|
90
|
+
observation on the PR and the issue before the architect signs off. A staging pass is not
|
|
91
|
+
this: on 2026-09-12 a slot's entire staging gate passed at 00:02Z and its production-apply
|
|
92
|
+
failed at 00:12Z on a resource staging never runs. If the slot fails on the change, the
|
|
93
|
+
implementer owns the fix and the next slot.
|
|
94
|
+
The record has three places: the PR body's `Production:` line, one pull-request comment
|
|
95
|
+
carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
|
|
96
|
+
read GitHub, the architect reads the issue. When the deploy that carries the merge has not
|
|
97
|
+
happened (a shared profile still holding the previous plugin release, a daemon still running
|
|
98
|
+
the previous commit, a slot nobody has run), open a `dispatch_ask` naming the exact install or
|
|
99
|
+
restart step, with options for its outcomes, keep the `Production:` line at `pending <what is
|
|
100
|
+
missing>`, and complete the check once the human answers that it is done. Never record a
|
|
101
|
+
staging pass as the production check, and never let the architect sign off on a `pending` line.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# PR body, proofs, and the simplify pass
|
|
2
|
+
|
|
3
|
+
Part of `skill://legion-worker`. Read it before you write or edit any line of the pull request
|
|
4
|
+
body, put a `proof` array in a handoff, verify another phase's proof, or run the simplify pass.
|
|
5
|
+
Every path it cites is in sjawhar/legion.
|
|
6
|
+
|
|
7
|
+
## The READY format
|
|
8
|
+
|
|
9
|
+
The implementer writes the PR body in the READY format from the moment the PR opens, and every
|
|
10
|
+
later phase keeps it current rather than replacing it:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
## Verification
|
|
14
|
+
|
|
15
|
+
**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>.
|
|
16
|
+
|
|
17
|
+
**Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
|
|
18
|
+
- Thread <id>: fixed in <commit-sha> — <one line>.
|
|
19
|
+
- Thread <id>: not a defect — <reason>.
|
|
20
|
+
`legion threads resolve --pr <n> --repo <owner>/<repo>` at <head-sha>:
|
|
21
|
+
resolved <thread URL> — its opener's acceptance
|
|
22
|
+
resolved <thread URL> — the Legion reviewer's acceptance of a bot's thread
|
|
23
|
+
left open <thread URL> — newest reply by <login> is not an acceptance
|
|
24
|
+
left open <thread URL> — newest reply by <login> is an unsubmitted draft in a pending review
|
|
25
|
+
left open <thread URL> — newest reply by <login> is not its opener's or the Legion reviewer's acceptance
|
|
26
|
+
|
|
27
|
+
**Thermo:** `ce-simplify-code` once at <head-sha>: <0 applied | applied → new head <sha>>; thermonuclear pair at the final head <sha>:
|
|
28
|
+
<verdict>. (omitted entirely on a docs-only PR — there is no code for either pass, so neither runs)
|
|
29
|
+
|
|
30
|
+
**E2E (implementer):** <surface> — ran `<command or run id>`, observed <result>, at head <sha>.
|
|
31
|
+
Negative control: <deliberately broken input> → <refusal or failure observed>.
|
|
32
|
+
|
|
33
|
+
**E2E (tester):** <surface> — ran `<command or run id>`, observed <result>, at head <sha>.
|
|
34
|
+
Negative control: <deliberately broken input> → <refusal or failure observed>.
|
|
35
|
+
Verified the implementer's proof by <re-running its command | driving the same surface independently>.
|
|
36
|
+
|
|
37
|
+
**Production:** <what was checked in production, how, what was observed> — merge commit <sha>.
|
|
38
|
+
(written by the implementer after the merge lands; `pending <what is missing>` until then)
|
|
39
|
+
|
|
40
|
+
**Fast-follow:** <one named cleanup item and where it will land>, or "none".
|
|
41
|
+
|
|
42
|
+
**Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What a proof is
|
|
46
|
+
|
|
47
|
+
**A proof** is the changed behaviour exercised on the surface a user reaches it through, recorded
|
|
48
|
+
as the exact command or run id, what was observed, the head SHA, and one negative control —
|
|
49
|
+
a deliberately broken input and the refusal or failure observed. The surface is
|
|
50
|
+
**production-like** — the repository's real-process test harness and fixtures, a sandbox
|
|
51
|
+
repository, a real browser, a devN stack, staging, or a local stack with real migrations, one that
|
|
52
|
+
has the resource the change touches — and each `E2E` line carries a **link** to that run,
|
|
53
|
+
screenshot, or e2e; human review does not replace user-facing verification, and a green unit suite
|
|
54
|
+
is not it. A unit or integration test is a regression lock, never proof of a criterion. Sami,
|
|
55
|
+
2026-09-13, verbatim: "They need to test everything in a production-like
|
|
56
|
+
environment before merging, and it is the agent that develops the feature that is responsible
|
|
57
|
+
for doing that. If there's anything blocking that, we need to fix it: if it's infrastructure, we
|
|
58
|
+
need to fix it; if it's tooling, we need to develop it; if it's skills, we need to fix the skills
|
|
59
|
+
... it should not require deploying to production to realize your feature doesn't work."
|
|
60
|
+
Evidence for the rule: in the week of 2026-09-08 three surfaces merged green and were wrong on
|
|
61
|
+
inspection (the Astrolabe IPI stack, Dispatch on ECS, the candidate flow), and on 2026-09-12 six
|
|
62
|
+
deploy slots died on code first executed after merge, including a production-only ECS bootstrap
|
|
63
|
+
the whole staging gate never ran. The implementer's proof and the tester's proof below are both
|
|
64
|
+
this proof.
|
|
65
|
+
|
|
66
|
+
## The rules every phase's evidence follows
|
|
67
|
+
|
|
68
|
+
- **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
|
|
69
|
+
The proof is the one defined above. It goes into `.legion/implement.json` as the required `proof`
|
|
70
|
+
array (`handoff_write` for phase `implement` refuses a payload without one, or with a blank or
|
|
71
|
+
whitespace-only field, and names the field), and into the PR body, because the reviewer and the
|
|
72
|
+
merger verify facts on GitHub and never from a handoff.
|
|
73
|
+
- **The tester verifies the implementer's proof and adds its own `E2E (tester)` line.** It re-runs
|
|
74
|
+
the implementer's command or drives the same surface independently, and records the verdict in
|
|
75
|
+
`.legion/test.json` as `implementerProof` (`{verdict, how}`).
|
|
76
|
+
A test handoff whose predecessor carried no proof is a test failure, not a gap for the tester to fill:
|
|
77
|
+
record it in `failures` with `implementerProof.verdict: "rejected"`, complete the phase, and let
|
|
78
|
+
the architect return the issue to the implementer — the agent that developed the change owns
|
|
79
|
+
proving it (`handoff_write` for phase `test` refuses a rejected verdict, or `failed > 0`,
|
|
80
|
+
with no recorded failure). Otherwise, add your own proof before completing — a proof as defined
|
|
81
|
+
above — as the `E2E (tester)` line and the `proof` array `handoff_write` for
|
|
82
|
+
phase `test` requires whenever you report no failure. A code path whose first execution is after merge — a
|
|
83
|
+
deploy workflow's inline step, a post-merge helper, a production-only resource — is untested
|
|
84
|
+
until the implementer has executed it against a devN stack; if no surface can reach it, the
|
|
85
|
+
tester names that missing surface as the blocker instead of passing the phase. Environment or
|
|
86
|
+
secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
|
|
87
|
+
`.legion/test.json`, and only when the issue's acceptance criteria call for it — never
|
|
88
|
+
re-pasted into the PR body each round. After a conflict-forced rebase, compute the
|
|
89
|
+
fingerprint (*The unchanged-diff check* in
|
|
90
|
+
`skill://legion-worker/references/conflicts-and-rewrites.md`) at the head your `E2E` line
|
|
91
|
+
names and at the new head. Equal: re-run only the
|
|
92
|
+
bare gates — the repository's CI green at the new head and its smoke check — and change the
|
|
93
|
+
`E2E` line's head to the new SHA with
|
|
94
|
+
`rebase re-check <old-sha> → <new-sha>: fingerprint unchanged, bare gates only`; the
|
|
95
|
+
real-surface verification is not repeated. Different: a full test round.
|
|
96
|
+
- **The implementer runs `skill://ce-simplify-code` once per pull request, after the last review round
|
|
97
|
+
closes and before the reviewer's final pass, when the diff touches runtime code; a docs-only
|
|
98
|
+
diff gets none.** It is scoped to the pull request's own diff, at the head where the last review
|
|
99
|
+
round closed: nothing applied leaves that head final; applied → the applied head is the final
|
|
100
|
+
head: CI runs on it, the pair runs once on it, and the E2E proof re-runs on it for the surface
|
|
101
|
+
the simplify diff touched (Sami, 2026-09-13: test on the real surface before merging, no
|
|
102
|
+
shortcuts — a refactor that "preserves behaviour" is a claim until it is executed). That cost is
|
|
103
|
+
why 0-applied is the expected outcome and a pass that applies is spent sparingly. At the applied
|
|
104
|
+
head the implementer re-cites the `CI` line and re-runs its own proof into `E2E (implementer)`,
|
|
105
|
+
and the tester re-runs its proof for the touched surface into `E2E (tester)`, before the
|
|
106
|
+
reviewer's final pass. Simplify is the last code change; the pair is the last review. Record it
|
|
107
|
+
in the `Thermo` line.
|
|
108
|
+
- **No deferrals** is the body's rule (*PR body, review, and the merge gate* in
|
|
109
|
+
`skill://legion-worker`): the `Fast-follow:` line holds naming, duplication, or wording cleanup
|
|
110
|
+
only. A base frozen for others to stack on is never rewritten (*Rewriting pushed commits* in
|
|
111
|
+
`skill://legion-worker/references/conflicts-and-rewrites.md`); the `Chain` line records it.
|
|
112
|
+
|
|
113
|
+
## When no surface reaches the changed path
|
|
114
|
+
|
|
115
|
+
No surface reaches the changed path is a report to the architect, never a reason to complete the phase.
|
|
116
|
+
Say which surface is missing and what it would have to do — a rig that can spawn the role, a
|
|
117
|
+
sandbox that holds the resource, a credential, a command that does not exist yet — and send it to
|
|
118
|
+
the architect with `envoy_publish` to its role topic. The architect creates a child issue in this
|
|
119
|
+
tree to build it (infrastructure, tooling, or a skill) and resumes you once it lands. Sami,
|
|
120
|
+
2026-09-13, verbatim: "If there's anything blocking that, we need to fix it: if it's
|
|
121
|
+
infrastructure, we need to fix it; if it's tooling, we need to develop it; if it's skills, we need
|
|
122
|
+
to fix the skills." A code path whose first execution would be after the merge — a deploy
|
|
123
|
+
workflow's inline step, a post-merge helper, a production-only resource — is untested until you
|
|
124
|
+
have executed it somewhere production-like; completing with a unit-test-only handoff is the
|
|
125
|
+
failure this rule exists to stop.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Review threads
|
|
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 before every push that answers a review, the
|
|
5
|
+
reviewer on every re-review, the merger before READY. Every path it cites is in sjawhar/legion.
|
|
6
|
+
|
|
7
|
+
- **Threads are dispositioned individually, never resolved in bulk.** Every open review
|
|
8
|
+
thread gets its own line naming the fixing commit or the reason it isn't a defect. The
|
|
9
|
+
reviewer answers each thread it opened, and each thread a bot opened that is none of Legion's
|
|
10
|
+
role Apps, with exactly one of `Accepted: fixed in <commit> — <one line>`,
|
|
11
|
+
`Accepted: not a defect — <reason>`, or `Still open: <what remains>`; nothing else is an
|
|
12
|
+
acceptance, and nobody replies after an `Accepted:` (any later reply that is not itself an
|
|
13
|
+
`Accepted:` — the opener's own follow-up included — leaves the thread open, because resolution
|
|
14
|
+
considers only the newest comment). The review App can reply on a thread but cannot resolve it:
|
|
15
|
+
GitHub grants resolving a review thread to the pull request's author, and the implementer opens
|
|
16
|
+
every Legion pull request (`packages/daemon/src/daemon/AGENTS.md`, GitHub Apps).
|
|
17
|
+
When `LEGION_GRANT_FILE` or `LEGION_GRANT` is set, use `legion threads resolve --pr <number> --repo <owner>/<repo>`.
|
|
18
|
+
When neither is set, add `--gh` to that command, which applies the fallback's rule below through
|
|
19
|
+
your own `gh`; where no `legion` command is installed, use `gh api graphql` with the session's
|
|
20
|
+
GitHub credential and the fallback below.
|
|
21
|
+
In a Legion pane, the **implementer** runs the command before every push that answers a review
|
|
22
|
+
(the corrective push and the final `.legion/` deletion push) and pastes its output into the
|
|
23
|
+
`Threads` section. The command resolves each unresolved thread whose newest submitted comment is
|
|
24
|
+
the opener's own `Accepted:` reply. On a thread a bot account opened that is none of Legion's
|
|
25
|
+
role Apps (the daemon names them, keyed by App role), the Legion reviewer's `Accepted:` also
|
|
26
|
+
closes it. GitHub cannot tell a CI bot, which never accepts, from a person whose `gh` is routed
|
|
27
|
+
to an App, so the reviewer adjudicates such a finding, and it may accept one an App-routed person
|
|
28
|
+
raised. The subject of a finding never closes it: the implementer's `Fixed in <commit>: …` or
|
|
29
|
+
`Declined: …` answers a thread and closes none. A thread either Legion App opened, a reviewer's
|
|
30
|
+
finding included, still needs its opener's `Accepted:`. It makes one `resolveReviewThread` per
|
|
31
|
+
thread, prints `resolved <url> — <whose acceptance>` (its opener's, or the Legion reviewer's on a
|
|
32
|
+
bot's thread, so the ledger shows which) or `left open <url> — newest reply by <login> is …`
|
|
33
|
+
naming why, and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one.
|
|
34
|
+
|
|
35
|
+
Without a grant, page through `reviewThreads`, skip `isResolved: true`, and compare the opener
|
|
36
|
+
with the newest comment. Query shape, inside `repository { pullRequest { … } }`:
|
|
37
|
+
|
|
38
|
+
```graphql
|
|
39
|
+
reviewThreads(first: 100, after: $after) {
|
|
40
|
+
pageInfo { hasNextPage endCursor }
|
|
41
|
+
nodes {
|
|
42
|
+
id isResolved
|
|
43
|
+
opener: comments(first: 1) { nodes { author { __typename login } } }
|
|
44
|
+
newest: comments(last: 1) { nodes { author { __typename login } body state } }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Resolve only when the newest comment is submitted, its `author` is the opener's account (the same
|
|
50
|
+
`__typename` and `login`: a login alone is a string anyone may register), and its `body`, after
|
|
51
|
+
removing leading spaces, tabs, CR, and LF, begins `Accepted:`. Without a
|
|
52
|
+
grant nothing names Legion's own App logins, so this route closes a bot's thread only on its
|
|
53
|
+
opener's `Accepted:`: leave one the Legion reviewer accepted for the implementer's or merger's
|
|
54
|
+
run in a pane, or report it. For each thread to resolve:
|
|
55
|
+
|
|
56
|
+
```graphql
|
|
57
|
+
mutation($threadId: ID!) {
|
|
58
|
+
resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Re-read `reviewThreads` and confirm that thread's `isResolved` is true. In either route, report
|
|
63
|
+
a refused resolution to the architect, which opens an ask for a human to resolve the thread by
|
|
64
|
+
hand — never skip it silently. The merger runs the command once more before publishing READY
|
|
65
|
+
and does not publish while any `left open` line remains.
|
|
66
|
+
|
|
67
|
+
- **The reviewer, on a re-review.** When you re-review after a corrective push, answer every
|
|
68
|
+
thread you opened in one of the three forms above — `Accepted:` is the only reply the
|
|
69
|
+
implementer's `legion threads resolve` acts on — and approve only once every thread you opened
|
|
70
|
+
carries your `Accepted:` reply and the implementer's run has resolved it (verify
|
|
71
|
+
`isResolved: true` with `gh api graphql`, never from the PR body).
|
package/package.json
CHANGED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# Knowledge Injection Algorithm
|
|
2
|
-
|
|
3
|
-
Canonical algorithm for injecting relevant learnings from `docs/solutions/` before phase-specific work begins. All worker workflows reference this file for the injection procedure; each workflow specifies its own keyword sources.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
Before starting main work, each phase checks the learnings index for applicable prior knowledge. This surfaces patterns, pitfalls, and institutional knowledge that previous workers documented.
|
|
8
|
-
|
|
9
|
-
**Injection must never block work.** If any step fails (missing index, invalid JSON, missing files, empty handoff data), skip silently and proceed with the phase's main work.
|
|
10
|
-
|
|
11
|
-
## Algorithm
|
|
12
|
-
|
|
13
|
-
### 1. Read the Index
|
|
14
|
-
|
|
15
|
-
Assemble the index by reading all per-entry JSON files in `docs/solutions/.index/`:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
# Read and merge all entry files in .index/ directory
|
|
19
|
-
for f in docs/solutions/.index/*.json; do
|
|
20
|
-
[ -f "$f" ] && cat "$f"
|
|
21
|
-
done
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Each file has the format `{ "version": 1, "entries": { "key": ["learning-path", ...] } }`. Merge all `entries` maps together, deduplicating learning paths per key.
|
|
25
|
-
|
|
26
|
-
If the `.index/` directory doesn't exist or contains no valid JSON files, skip injection entirely — proceed to the phase's main work.
|
|
27
|
-
|
|
28
|
-
### 2. Extract Keywords
|
|
29
|
-
|
|
30
|
-
Collect keywords from the phase-specific sources (defined in each workflow file). The extraction algorithm:
|
|
31
|
-
|
|
32
|
-
1. **Collect raw text** from the specified keyword sources (see the calling workflow's keyword source table)
|
|
33
|
-
2. **Tokenize**: split on whitespace, `/`, `-`, `_`, and camelCase boundaries
|
|
34
|
-
3. **Normalize**: lowercase all tokens
|
|
35
|
-
4. **Filter**: remove tokens < 3 chars and common stopwords (the, and, for, with, this, that, from, into, when, will, should, would, could, also, been, have, each, etc.)
|
|
36
|
-
5. **Deduplicate** tokens
|
|
37
|
-
6. **Extract full path segments**: e.g., `packages/daemon/src/state` — keep as-is for path matching in addition to individual tokens
|
|
38
|
-
|
|
39
|
-
Also look for references to:
|
|
40
|
-
- Source path segments (e.g., `packages/daemon/src/state/`, `serve-manager`)
|
|
41
|
-
- Module names (e.g., "daemon", "controller", "worker", "state")
|
|
42
|
-
- Component names (e.g., "serve-manager", "decision", "fetch")
|
|
43
|
-
- Feature areas (e.g., "skills", "linear", "github", "review", "retro")
|
|
44
|
-
- Integration concerns (e.g., "PR", "labels", "MCP")
|
|
45
|
-
- Domain concepts and error keywords from the context
|
|
46
|
-
|
|
47
|
-
### 3. Match Keywords Against Index
|
|
48
|
-
|
|
49
|
-
Use two matching modes against the keys in `.index`:
|
|
50
|
-
|
|
51
|
-
- **Path matching**: For each key that does NOT start with `tag:`, check if any extracted keyword appears as a substring of the key (case-insensitive). Collect all matched learning file paths.
|
|
52
|
-
- **Tag matching**: For each key that starts with `tag:`, extract the tag name (e.g., `tag:race-condition` → `race-condition`). Check if any extracted keyword matches the tag name (case-insensitive). Collect matched learning file paths.
|
|
53
|
-
|
|
54
|
-
### 4. Deduplicate and Rank
|
|
55
|
-
|
|
56
|
-
- Remove duplicates (same file matched via multiple keys)
|
|
57
|
-
- **Status filter**: For each candidate, read its YAML front matter `status` field. Exclude any file with `status: superseded`. If the file doesn't exist or has no front matter, include it (graceful degradation).
|
|
58
|
-
- **Primary rank: tag overlap** — For each remaining candidate, read its `tags` front matter field. Count how many of its tags appear in the extracted keywords (case-insensitive). Higher overlap = higher rank.
|
|
59
|
-
- **Secondary rank: key specificity** — Learnings matched via longer/more-specific keys rank higher (e.g., a match on `packages/daemon/src/state` outranks a match on `packages/daemon`)
|
|
60
|
-
- **Tertiary rank: match count** — Number of distinct key matches (more matches = more relevant)
|
|
61
|
-
- **Cap at 3 learnings maximum**
|
|
62
|
-
|
|
63
|
-
### 5. Read Matched Learnings
|
|
64
|
-
|
|
65
|
-
For each matched learning file (from `docs/solutions/<path>`):
|
|
66
|
-
|
|
67
|
-
1. Read YAML front matter: extract `title` and `tags` fields
|
|
68
|
-
2. Skip past front matter (`---` blocks) and headings, take the first paragraph of prose (typically the Problem or Overview section)
|
|
69
|
-
3. Prepend structured header: `[{title} | tags: {comma-separated tags}]`
|
|
70
|
-
4. Truncate entire output (header + prose) to **350 characters**
|
|
71
|
-
|
|
72
|
-
**If a matched file doesn't exist on disk:** Skip that entry silently (stale index entry from a file rename). Do not error.
|
|
73
|
-
|
|
74
|
-
### 6. Output Injected Learnings
|
|
75
|
-
|
|
76
|
-
Output the injected learnings visibly in the session before proceeding with the phase's main work:
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
## Relevant Learnings (from docs/solutions/)
|
|
80
|
-
|
|
81
|
-
1. [docs/solutions/<path>]: [{title} | tags: {tag1}, {tag2}] <prose excerpt> (350 chars max total)
|
|
82
|
-
2. [docs/solutions/<path>]: [{title} | tags: {tag1}, {tag2}] <prose excerpt>
|
|
83
|
-
3. [docs/solutions/<path>]: [{title} | tags: {tag1}, {tag2}] <prose excerpt>
|
|
84
|
-
|
|
85
|
-
(Review these for patterns and pitfalls relevant to this phase's work.)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**If no matches found:** Output "No relevant learnings found." and proceed. Do NOT add an empty section.
|
|
89
|
-
|
|
90
|
-
**Canonical identifiers:** All references to learnings use their `docs/solutions/` relative file path (e.g., `daemon/controller-lifecycle-separation.md`). These paths are the stable IDs used for injection, handoff tracking, and future aggregation. Never use titles or truncated text as identifiers.
|
|
91
|
-
|
|
92
|
-
## Fallback Behavior
|
|
93
|
-
|
|
94
|
-
When a keyword source is unavailable (missing handoff data, empty fields, missing phase data), silently fall back to the next available source as defined in the calling workflow's fallback rules. Never error on missing data.
|
|
95
|
-
|
|
96
|
-
## Integration with Handoffs
|
|
97
|
-
|
|
98
|
-
If the phase writes handoff data, include a `learningsInjected` field listing the `docs/solutions/` relative paths of all injected learnings. This enables downstream phases to see what knowledge was available and supports future aggregation.
|
|
File without changes
|
|
File without changes
|