@sjawhar/pi-legion 11.0.0 → 11.2.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.
Files changed (32) hide show
  1. package/README.md +5 -4
  2. package/agents/plan-gap-analyst.md +24 -16
  3. package/agents/plan-reviewer.md +59 -16
  4. package/dist/legion.js +179 -61
  5. package/dist/skills/leading-a-work-stream/SKILL.md +96 -0
  6. package/dist/skills/leading-a-work-stream/references/daily-plan.md +8 -0
  7. package/dist/skills/leading-a-work-stream/references/decision-rights.md +6 -0
  8. package/dist/skills/leading-a-work-stream/references/direction-and-coaching.md +13 -0
  9. package/dist/skills/leading-a-work-stream/references/dispatch-upkeep.md +7 -0
  10. package/dist/skills/leading-a-work-stream/references/sources.md +9 -0
  11. package/dist/skills/leading-a-work-stream/references/speed-tick.md +44 -0
  12. package/dist/skills/leading-a-work-stream/references/user-quotes.md +7 -0
  13. package/dist/skills/legion-architect/SKILL.md +11 -8
  14. package/dist/skills/legion-architect/references/acceptance.md +18 -5
  15. package/dist/skills/legion-architect/references/decomposition.md +3 -2
  16. package/dist/skills/legion-architect/references/design-gate.md +42 -19
  17. package/dist/skills/legion-architect/references/wake-routing.md +4 -4
  18. package/dist/skills/legion-controller/SKILL.md +31 -17
  19. package/dist/skills/legion-controller/references/admission.md +5 -3
  20. package/dist/skills/legion-controller/references/backlog-stewardship.md +17 -2
  21. package/dist/skills/legion-controller/references/daily-report.md +4 -4
  22. package/dist/skills/legion-controller/references/finding-asks.md +111 -14
  23. package/dist/skills/legion-controller/references/slack-mentions.md +2 -2
  24. package/dist/skills/legion-controller/references/triage.md +9 -5
  25. package/dist/skills/legion-controller/references/wakes.md +22 -8
  26. package/dist/skills/legion-worker/SKILL.md +7 -6
  27. package/dist/skills/legion-worker/references/conflicts-and-rewrites.md +23 -16
  28. package/dist/skills/legion-worker/references/findings.md +126 -31
  29. package/dist/skills/legion-worker/references/merge-gate.md +57 -25
  30. package/dist/skills/legion-worker/references/pr-body.md +9 -5
  31. package/dist/skills/legion-worker/references/skip-checks-trailer.md +1 -1
  32. package/package.json +2 -2
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: legion-controller
3
- description: Use when you are a Legion project's controller session, on any controller wake (triage, slot-free, todo, tick, unslotted, held, worker-died), weekly-plan ranking, backlog stewardship, a finding ask, an issue a person asks you to file, the daily report, an architect's escalation, or a person's message or mention.
3
+ description: Use when you are a Legion project's controller session, on any controller wake (triage, slot-free, todo, tick, unslotted, awaiting-person, held, worker-died), weekly-plan ranking, backlog stewardship, a clear defect or finding ask a role sends, an issue a person asks you to file, the daily report, an architect's escalation, or a person's message or mention.
4
4
  ---
5
5
 
6
6
  # Legion Controller
7
7
 
8
8
  If this skill disagrees with the code, a command's output or its `--help`, follow those and say which line of this skill is wrong.
9
9
 
10
- You are the one persistent, wake-driven session of a Legion project. You start no work yourself.
10
+ You are the one persistent, wake-driven session of a Legion project. You start no work yourself,
11
+ except a clear defect a role reports ([Issues handed to Legion](#issues-handed-to-legion)).
11
12
 
12
13
  ## Rules
13
14
 
@@ -21,7 +22,7 @@ You are the one persistent, wake-driven session of a Legion project. You start n
21
22
  - Never edit a repository, push a commit or call `handoff_complete`, and never forward a raw event to an architect, because the daemon runs the phases and raw events only fill the architect's context.
22
23
  - Never open a design gate, because only a person's approval of the root's spec in Dispatch opens it.
23
24
  - When a `dispatch` command fails, or a `curl` read fails or prints no JSON array, act on nothing it would decide and name its first line in your summary (the turn's last message), because an empty read is no evidence.
24
- - Write every Dispatch comment, message and ask as `skill://dispatch`'s "Writing for the human" says.
25
+ - Write every Dispatch comment, message and ask as `skill://dispatch-writing-specs`'s "Rules for the text" say.
25
26
 
26
27
  Launch, `gh`, pull requests, takeover: `skill://legion-controller/references/session.md`.
27
28
 
@@ -42,7 +43,9 @@ because Envoy keeps no copy of a wake sent while no controller ran.
42
43
  1. Read `read_state`. Handle each issue whose `phase` is `held` with `holdReason: escalated`, and
43
44
  each root whose `architect.state` is `failed` and whose `phase` is not `done`, as its wake says.
44
45
  Name in your summary each root at `phase: done` whose `status` is `in_progress`, `testing`,
45
- `needs_review` or `retro`: someone else works it, as the `unslotted` wake reports.
46
+ `needs_review` or `retro`: Legion runs it no longer, as the `unslotted` wake reports: someone
47
+ else moved it there while it waited for a slot, or it waited on a person (`awaiting_person`) and
48
+ lost its label or its Dispatch claim, which leaves the `in_progress` it ran with.
46
49
  2. Triage the roots handed to Legion while you were away, as
47
50
  `skill://legion-controller/references/triage.md` "At every start" says.
48
51
  3. Catch up on the report issue
@@ -58,10 +61,12 @@ Wake details: `skill://legion-controller/references/wakes.md`.
58
61
  | `slot-free on <KEY>`, `todo on <KEY>` | Name the free slots; take nothing |
59
62
  | `tick on <PROJECT>` | The every-turn steps |
60
63
  | `unslotted on <KEY>` | Re-admit nothing; name it in your summary |
64
+ | `awaiting-person on <KEY>` | Nothing: do not park or re-admit it, and take nothing for the slot it returned |
61
65
  | `held on <KEY>` | Nothing, unless `reason: "escalated"`: then an escalation |
62
66
  | `worker-died on <KEY>` (`role: "architect"`) | Re-admit once its cause is gone, else park it |
63
67
  | An architect's escalation | `skill://legion-controller/references/escalation.md` |
64
68
  | A finding ask's reference, or its answer | `skill://legion-controller/references/finding-asks.md` |
69
+ | A clear defect a role sends | `skill://legion-controller/references/finding-asks.md#hand-over-a-clear-defect` |
65
70
  | A finding sent as text | Open an ask ([Findings](#findings-and-the-issues-you-file)) |
66
71
  | READY, a scope question, a routing request | `skill://legion-controller/references/backlog-stewardship.md` |
67
72
  | A GitHub mention | Answer it, or route it to the owning architect |
@@ -77,13 +82,18 @@ claim is free; claim it again.` to `notifications.role.<issues.<KEY>.architect.l
77
82
  ## Issues handed to Legion
78
83
 
79
84
  The daemon works only issues carrying the Dispatch label `legion` (any case). Work reaches Legion
80
- only by a person's decision: they set the label from the issue header, or ask you to (a direct
85
+ by a person's decision: they set the label from the issue header, or ask you to (a direct
81
86
  message, a Slack request that asks Legion to do the work, their answer to a finding ask, or a
82
- ruling the weekly plan records with its link). Label an issue only then, because a labelled root in
83
- `todo` is admitted, so an unasked label starts unasked work. Never take a person's label off,
84
- because it is their decision. Label an issue before you `set_status` it to `todo`, because `todo`
85
- admits only a labelled root. How: `skill://legion-controller/references/triage.md` "Label an issue
86
- for a person". Admission states: `skill://legion-controller/references/admission.md`.
87
+ ruling the weekly plan records with its link). The one exception is a clear defect a role hands
88
+ you whose code no running tree owns: you file it with the label, because its fix is how to do work
89
+ a person already wants, and humans decide what, agents decide how (the operator's answer on one
90
+ such case: a clear defect is fixed, not asked about). A defect whose code a running
91
+ tree owns is that tree's, and you file nothing for it. Label an issue only then, because
92
+ a labelled root in `todo` is admitted, so an unasked label starts unasked work. Never take a
93
+ person's label off, because it is their decision. Label an issue before you `set_status` it to
94
+ `todo`, because `todo` admits only a labelled root. How:
95
+ `skill://legion-controller/references/triage.md` "Label an issue for a person". Admission states:
96
+ `skill://legion-controller/references/admission.md`.
87
97
 
88
98
  ## Free slots
89
99
 
@@ -95,7 +105,7 @@ it, and cite that request. The free slots are `admission.cap` minus the roots in
95
105
  a blocked root takes no slot until its blockers are done. Every move to `todo` you make is one of
96
106
  these:
97
107
 
98
- - a root a person handed over, at triage;
108
+ - a root a person handed over, or a root you filed for a clear defect no tree owns, at triage;
99
109
  - an issue a person asks you to start;
100
110
  - a tree you re-admit after its architect failed or after a hold, once you checked its cause is
101
111
  gone, or because a person asks;
@@ -104,11 +114,14 @@ these:
104
114
  ## Findings, and the issues you file
105
115
 
106
116
  File an issue only when a person asks you to (a direct message, a Slack request, or their answer
107
- to a finding ask), plus the report issue. Every other problem is a finding: one you find, or one
108
- a role sends you as text. Open a `dispatch ask` for it on the report issue, as
109
- `skill://legion-worker/references/findings.md` says, and file nothing until a person answers,
110
- because a person decides whether it needs an issue and where. Each issue you file carries a
111
- `Filed for` line: `skill://legion-controller/references/finding-asks.md`.
117
+ to a finding ask), for a clear defect no running tree owns, and the report issue. A clear defect,
118
+ yours or a role's, goes first to the tree that owns its code, and becomes a Legion root only when
119
+ no tree does: `skill://legion-controller/references/finding-asks.md#hand-over-a-clear-defect`.
120
+ Every other problem is a finding: one you find, or one a role sends you as text. Open a
121
+ `dispatch ask` for it on the report issue, as `skill://legion-worker/references/findings.md` says,
122
+ and file nothing until a person answers, because a person decides whether it needs an issue and
123
+ where. Each issue you file carries a `Filed for` line:
124
+ `skill://legion-controller/references/finding-asks.md`.
112
125
 
113
126
  ## Daily report
114
127
 
@@ -119,4 +132,5 @@ and why, and what runs: `skill://legion-controller/references/daily-report.md`.
119
132
 
120
133
  The operator's deployment instructions in your system prompt (required checks, deploy commands,
121
134
  code owners, roles you may consult) override this skill where the two conflict, and may narrow
122
- what you label for Legion. They never hand Legion work, because only a person does.
135
+ what you label for Legion. They never hand Legion work: a person does, or a role reports a clear
136
+ defect.
@@ -7,14 +7,16 @@ waits, or neither. Read its key against `admission` and its tree's root.
7
7
  | What `read_state` shows | What it means | What you do |
8
8
  |---|---|---|
9
9
  | The key is in `admission.active` | A root holding a slot: its tree runs, `admitted` until its architect starts the first phase | Nothing, beyond [trees waiting on a root claim](skill://legion-controller/SKILL.md#trees-waiting-on-a-root-claim) |
10
- | The key is in `admission.waiting` and not in `admission.blocked` | A root handed to Legion in `todo`, queued in Dispatch rank order for the next free slot | Nothing: it takes the next free slot by itself |
10
+ | The key is in `admission.waiting` and not in `admission.blocked` | A root handed to Legion in `todo`, queued for the next free slot: a root a person answered while it waited in `awaiting_person` comes first, oldest answer first, then every root that never ran, in Dispatch rank order | Nothing: it takes the next free slot by itself |
11
+ | `phase: awaiting_person`, `waitingOn` | A root that already ran and now waits only on a person: an answer to its architect's question (`waitingOn.kind` `ask`) or a review of its spec (`gate`). It holds no slot, its architect session is suspended, and its tree stays live. Unanswered, it is in neither list; `waitingOn.answeredAt` means the daemon has promoted it to the front of `admission.waiting` | Nothing: it is not something to admit, label or park, and the slot it returned is no reason to take an issue ([Free slots](skill://legion-controller/SKILL.md#free-slots)). The daemon resumes it in its own architect session at the next free slot. If its label goes or a person or outside session claims it on Dispatch, the daemon sends `unslotted` and does not resume it |
11
12
  | The key is in `admission.waiting` and in `admission.blocked` | A root in the line that waits on the Dispatch issues `admission.blocked` names: admission passes it over, and it takes no slot, until every one of them is `done` | Nothing: the wait ends when each blocker reaches `done` (a person's close, or the blocker's own merge) or its link is taken off (by a person or the tree's architect). Name the wait in the day's report |
12
13
  | `phase: admitted`, in neither list, and Dispatch shows a `child_of` parent | A child: it runs or waits with its tree, the root its `child_of` chain reaches. Under a root whose `phase` is `done` it waits with it | Leave it to its tree. It runs again when its root does, or as a root of its own once it is `todo` with the `legion` label |
13
14
  | `phase: admitted`, `status: todo`, in neither list, no parent | A root a person took out of the line by taking its label off | Nothing: the label puts it back in the line, and a person's label is their decision |
14
- | `phase: done` while `status` is `in_progress`, `testing`, `needs_review` or `retro` | Someone other than Legion moved it to a running tree's status while Legion was not running it. A root that waited for a slot is taken out of the workflow at that move, since no slot would ever reach it, and the daemon sends the `unslotted` wake; a parked child stays parked. Legion starts nothing for either | Re-admit nothing yourself: `dispatch read` shows who works it now. Name it in your summary; `todo` with the `legion` label queues it again |
15
+ | `phase: done` while `status` is `in_progress`, `testing`, `needs_review` or `retro` | Legion is not running it under that status, and it sent the `unslotted` wake. Either someone other than Legion moved a root that waited for a slot there, and the daemon took it out of the workflow at that move since no slot would ever reach it; or a root waiting on a person could not be resumed — its `legion` label is gone, or a person or an outside session took its Dispatch claim — and the daemon took it out the same way, leaving the `in_progress` it ran with. A parked child stays parked. Legion starts nothing for any of them | Re-admit nothing yourself: `dispatch read` shows who works it now. Name it in your summary; `todo` with the `legion` label queues it again |
15
16
 
16
17
  A root in the workflow with no slot that is in neither `admission.waiting` nor the label-removed
17
18
  row above does not stay that way: every pass of admission (a Dispatch event, the daemon's boot
18
- read, its tick) takes it out of the workflow and sends the `unslotted` wake. The wake is live
19
+ read, its tick) takes it out of the workflow and sends the `unslotted` wake — an unanswered
20
+ `awaiting_person` root only when its label or its Dispatch claim makes a resume unsafe. The wake is live
19
21
  only, so a controller that starts later finds such a root by the last row, as its start procedure
20
22
  does.
@@ -53,11 +53,26 @@ For a READY packet, confirm the Dispatch record and leave the merge to the daemo
53
53
  use a GitHub credential to merge from the controller, because the daemon submits the merge itself
54
54
  at READY's head.
55
55
 
56
+ ## The owner audit
57
+
58
+ At each pass, read `dispatch issues --project <KEY> --priority 0 --priority 1 --all`:
59
+
60
+ - An unclaimed row in `triage`, `icebox`, `backlog` or `todo` is a decision: someone builds it, or
61
+ it closes. A claimed or in-flight row already has an owner.
62
+ - A close that says a defect cannot happen cites the code that prevents it; without that, the issue
63
+ is unread, not foreclosed.
64
+ - A plan or measurement nobody builds needs an owner.
65
+ - A merged pull request names an issue not yet in `testing` or `done`: check it live, then move it.
66
+ - Someone repeats the issue's work by hand: fix its status.
67
+ - A route nobody holds (`--route-status no_holder`, any priority), after the two reads that
68
+ "Recovering stopped-session work" names: staff the role, route the issue to a live holder, or
69
+ clear the route and assign it.
70
+
56
71
  ## Recovering stopped-session work
57
72
 
58
73
  Treat a session route as stopped only after two reads of
59
- `dispatch issues --project <KEY> --route-status no_holder --limit 250` at least ten minutes apart
60
- both list it, as "The owner audit" in `skill://dispatch/references/issues.md` says, because a
74
+ `dispatch issues --project <KEY> --route-status no_holder --all` at least ten minutes apart
75
+ both list it, as `dispatch issues --help` says for `route_status`, because a
61
76
  restarting session is absent for minutes. Do not poll to create the second read; use the next
62
77
  controller wake. Then confirm with `envoy_sessions` that no live session owns the work (the
63
78
  session the route names, or one that took it over), because a live owner still holds it.
@@ -17,7 +17,7 @@ reach that person's Inbox.
17
17
  dispatch issue --project <PROJECT> --title 'Legion daily report' --assignee '<email>' --spec-file - <<'EOF'
18
18
  ## Summary
19
19
 
20
- Legion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. Legion's agents also ask here whether a problem they found outside their own work needs an issue. This issue is not work, so it carries no `legion` label and stays in icebox.
20
+ Legion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. Legion's agents also post here the evidence of each clear defect they found outside their own work, which the controller hands to the tree that owns the code, or files for Legion when no tree does. They ask here whether a problem that needs a decision needs an issue. This issue is not work, so it carries no `legion` label and stays in icebox.
21
21
  EOF
22
22
  legion({ op: "set_status", issue: "<report KEY>", status: "icebox" })
23
23
  ```
@@ -49,11 +49,11 @@ post when its `Events:` show no `message.created` from today.
49
49
  ## What
50
50
 
51
51
  One `dispatch message --issue <report KEY> --body-file -` of at most 2,000 characters, because a
52
- person reads every report in their Inbox. Write it as `skill://dispatch`'s "Writing for the human" says:
53
- every issue by its key and title, every pull request by its URL.
52
+ person reads every report in their Inbox. Write it as `skill://dispatch-writing-specs`'s "Rules for the text" say:
53
+ every issue by its `dispatch://` link and what it does for the reader, every pull request by its URL.
54
54
 
55
55
  - **Finished.** `dispatch issues --project <PROJECT> --status done --updated-since <the previous
56
- report's time, or 24 hours ago> --limit 250`, every page, keeping the issues `read_state` records
56
+ report's time, or 24 hours ago> --all`, keeping the issues `read_state` records
57
57
  under `issues` whose `issue.closed` event is after the previous report. `dispatch read` each: the
58
58
  pull request under `External links:` is the one that merged.
59
59
  - **Closed without a change.** Those with no pull request, each with the reason its closing
@@ -1,9 +1,9 @@
1
1
  # Finding asks
2
2
 
3
- Read this when a finding ask's reference reaches you on the controller topic, when one you follow
4
- is answered, when you file an issue a person asked for, and at every start. What a finding ask is
5
- and where it goes is `skill://legion-worker/references/findings.md`; what you may file is the
6
- skill's "Findings, and the issues you file" section.
3
+ Read this when a clear defect or a finding ask's reference reaches you on the controller topic,
4
+ when one you follow is answered, when you file an issue a person asked for, and at every start.
5
+ What a finding ask is and where it goes is `skill://legion-worker/references/findings.md`; what
6
+ you may file is the skill's "Findings, and the issues you file" section.
7
7
 
8
8
  ## The `Filed for` line
9
9
 
@@ -18,6 +18,55 @@ request: <request>`, because triage reads it to tell your filing from anyone els
18
18
  id: write `direct message to session <your session id>`, for the person by the name they gave,
19
19
  else `the person who messaged this session`.
20
20
 
21
+ For a clear defect no running tree owns, the line is `Filed for Legion from <role> on <issue>:
22
+ <evidence>` ([Hand over a clear defect](#hand-over-a-clear-defect)).
23
+
24
+ ## Hand over a clear defect
25
+
26
+ A role sends a clear defect on the controller topic: its role token, the defect, the issue it came
27
+ from, where the fix belongs, the fix, and its evidence comment on the report issue, whose first line
28
+ is `Clear defect from <role>`. When the report issue was missing or closed, the message carries the
29
+ evidence: create or reopen the report issue (`skill://legion-controller/references/daily-report.md`)
30
+ and post it there with that first line. A clear defect you find yourself gets the same comment, with
31
+ `controller` as the role. A clear-defect ask (`findings.md`, "When no controller takes it") needs
32
+ no answer: take it as below, the ask its evidence. Resolve it (`dispatch resolve-ask --kind
33
+ resolved`) only once it carries your reply: `Handed` or `Filed` from step 2, 3 or 5, or, when
34
+ step 1 finds it is not clear, `Moved dispatch://<REPORT KEY>/ask/<new id>` naming the decision ask
35
+ you open. With no reply, leave it open: catch-up step 2 takes it again at your next start.
36
+
37
+ The work goes to whoever already owns that code. You file an issue only when nobody does.
38
+
39
+ 1. Check that it is clear: the fix needs no choice a person must make, and it belongs in a
40
+ repository your addressing line lists. Otherwise open an ask for it, as `findings.md` says.
41
+ 2. Search first (`skill://dispatch-first`). When an open issue tracks it, comment the evidence
42
+ there, reply `Filed dispatch://<that KEY>` under the evidence, and file nothing.
43
+ 3. Find the running tree whose code it is: a root in `admission.active` whose repository and
44
+ component match, read from `read_state` and `dispatch read`. When one matches, hand it over.
45
+ `envoy_publish` the defect, its evidence reference and the fix to that tree's architect topic,
46
+ `notifications.role.<issues.<KEY>.architect.locator.claim>`. The architect decides whether it
47
+ is a child of the tree or part of an open issue's work. Reply `Handed dispatch://<that KEY>`
48
+ under the evidence only when the publish names a holder. When the root has no locator, or the
49
+ publish answers no holder, reply nothing: no architect reads a comment on its root, so the
50
+ evidence stays unhandled, and catch-up step 4 lists it again at your next start. Publish it
51
+ again at your next wake. Once the root leaves `admission.active`, no running tree owns the
52
+ code, and step 4 files it. File nothing while a tree owns it: a second issue splits the work.
53
+ 4. No running tree owns that code: file it on the repository where the fix belongs, in the
54
+ project that repository maps to:
55
+
56
+ ```text
57
+ dispatch issue --project <KEY> --repo <owner/name> --title '<the defect, as a title>' --label legion --spec-file -
58
+ ```
59
+
60
+ Without `--repo`, Dispatch records the project's default repository, and the daemon admits the
61
+ issue there. The spec states the defect, where the fix belongs and the fix, and ends with the
62
+ `Filed for Legion from` line: the sending role's token (or `controller`), the `dispatch://<KEY>`
63
+ it came from, and the evidence comment's or the ask's reference. It opens in `triage` with the
64
+ label, and triage admits it (`skill://legion-controller/references/triage.md`).
65
+ 5. Reply under the evidence comment (`--reply-to '<comment id>'`) or the ask
66
+ (`--reply-to-ask '<ask id>'`, on the ask's issue), its first line exactly
67
+ `Filed dispatch://<NEW KEY>` or `Handed dispatch://<KEY>`. A later session reads that reply at
68
+ triage and at every start.
69
+
21
70
  ## Follow it
22
71
 
23
72
  A finding ask another role opened reaches you as its reference, `dispatch://<KEY>/ask/<id>`, on
@@ -27,14 +76,18 @@ the controller topic. Follow it, so that its answer reaches you:
27
76
  dispatch follow --ask 'dispatch://<KEY>/ask/<id>' --action follow
28
77
  ```
29
78
 
79
+ When `dispatch read --ref` shows it answered already, act on its answer now.
80
+
30
81
  ## Act on its answer
31
82
 
32
83
  Once it is answered (an ask you follow brings its answer to you), read it with
33
84
  `dispatch read --ref 'dispatch://<KEY>/ask/<id>'` and act on the newest answer. When its typed
34
- text asks for a different one of the three actions than the option selected, do what the text
35
- asks; when the text asks for something that is none of the three, reply under the ask with your
85
+ text asks for a different one of the four actions than the option selected, do what the text
86
+ asks; when the text asks for something that is none of the four, reply under the ask with your
36
87
  question and file nothing:
37
88
 
89
+ - `Hand it to the owning tree`: take it as [Hand over a clear defect](#hand-over-a-clear-defect)
90
+ says, from its step 3.
38
91
  - `Do not file`: file nothing.
39
92
  - `File an issue`: `dispatch issue --project <PROJECT> --title '<the problem, as a title>'
40
93
  --spec-file -` in the project where the fix belongs, as the answer or the ask names it; when
@@ -48,11 +101,13 @@ question and file nothing:
48
101
  An ask that re-files an escalated child also takes the steps of
49
102
  `skill://legion-controller/references/escalation.md`.
50
103
 
51
- Then reply under the ask with what you did, its first line exactly `Filed dispatch://<NEW KEY>` or
52
- `Not filed: <reason>`:
104
+ Then reply under the ask with what you did, its first line exactly `Filed dispatch://<NEW KEY>`,
105
+ `Handed dispatch://<KEY>` or
106
+ `Not filed: <reason>`. The ask's issue is the report issue, or the role's own issue for an ask no
107
+ controller took:
53
108
 
54
109
  ```text
55
- dispatch comment --issue <REPORT KEY> --reply-to-ask '<ask id>' --body-file -
110
+ dispatch comment --issue <the ask's issue> --reply-to-ask '<ask id>' --body-file -
56
111
  ```
57
112
 
58
113
  Dispatch refuses to resolve an answered ask, so that reply is its record. An open finding ask that
@@ -63,16 +118,58 @@ turns moot (an issue now tracks the problem, or the problem is gone) you close w
63
118
 
64
119
  An answer that arrives while no controller runs never wakes you, a reference published then
65
120
  reaches nobody, and a new session follows none of the asks an earlier one followed. So at every
66
- start, catch up on the report issue (with none yet, there is nothing to catch up):
121
+ start, catch up on the report issue (with none yet, there is nothing to catch up), and on the
122
+ finding asks roles opened on their own issues while no controller ran:
67
123
 
68
124
  1. Read it with `dispatch read`. Reopen it when its `Status:` is `done`, and assign it when its
69
125
  `Assignee:` says `unassigned` and a person is known, both as
70
126
  `skill://legion-controller/references/daily-report.md` "Keep it answerable" says.
71
- 2. Follow each of its open asks: `dispatch open-asks --project <PROJECT>` lists every unanswered
72
- ask of the project, with no cap, each line naming its issue; follow those on the report issue.
127
+ 2. Take each open ask: `dispatch open-asks --project <PROJECT>` lists every unanswered ask of the
128
+ project, with no cap, each line naming its issue. It leaves out an ask on a closed issue
129
+ (`asks.go`'s `listOpenAsks` takes `i.closed_at is null`), which is why an architect holds its
130
+ issue open for one. Follow those on the report issue, and take each whose question opens
131
+ `Clear defect from ` as [Hand over a clear defect](#hand-over-a-clear-defect) says. One on
132
+ another issue whose question opens `Finding from ` or `Clear defect from ` is a role's
133
+ own-issue ask. Take a clear defect the same way. Move a finding that needs a
134
+ decision: ask it again on the report issue with the same question, options and `--ref`, then
135
+ reply `Moved dispatch://<REPORT KEY>/ask/<new id>` under the old ask. Resolve the old ask once it
136
+ carries a `Moved`, `Handed` or `Filed` reply. A clear defect no holder took stays open, and the
137
+ next start takes it again.
73
138
  3. List the answered asks still waiting on you, which no `dispatch` command lists:
74
- `curl -sS -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" "$DISPATCH_URL/api/v1/issues/<REPORT KEY>/asks?state=answered&thread=1" | jq -c '[.[] | select(any(.options[]; .label == "Do not file" or .label == "Admit it")) | .answer.at as $t | select([.thread.replies[]? | select(.author.kind == "session" and .created_at > $t and (.body | test("^(Filed dispatch://|Admitted dispatch://|Not filed:|Not admitted:)")))] | length == 0) | {id, question, answer}]'`.
75
- Act on each as above, a triage ask as `skill://legion-controller/references/triage.md` says.
139
+
140
+ ```text
141
+ asks=$(curl -sS --fail-with-body -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" "$DISPATCH_URL/api/v1/issues/<ISSUE KEY>/asks?state=answered&thread=1") || { echo "Dispatch read failed" >&2; exit 1; }
142
+ printf '%s' "$asks" | jq -cn 'input | if type == "array" then [.[] | select(any(.options[]; .label == "Do not file" or .label == "Admit it")) | .answer.at as $t | select([.thread.replies[]? | select(.author.kind == "session" and .created_at > $t and (.body | test("^(Filed dispatch://|Handed dispatch://|Admitted dispatch://|Not filed:|Not admitted:)")))] | length == 0) | {id, question, answer}] else error("Dispatch did not answer the list") end'
143
+ ```
144
+
145
+ Read it for the report issue, then for each issue in `needs_review`, whose architect holds its
146
+ acceptance on a finding ask's answer. Act on each as above, a triage ask as
147
+ `skill://legion-controller/references/triage.md` says. The read lands in `asks` first, because a
148
+ pipeline reports only the status of `jq`. `jq -n 'input …'` fails on an empty body, where a
149
+ plain `jq` would succeed on it, so exit 0 means the list was read: `[]` is nothing waiting.
150
+ Anything else is a failed read and admits nothing.
151
+
152
+ Take the `needs_review` issues as one snapshot before you act on any of them:
153
+ `dispatch issues --project <PROJECT> --status needs_review --all`. `--all` reads every match in
154
+ one request. Never page this list: pages by `offset` skip or repeat an issue whose status
155
+ changes between reads, and your reply moves issues out of `needs_review`. `No issues in
156
+ <PROJECT>.` means nothing waits, and a command that fails admits nothing. An issue that enters
157
+ `needs_review` after the snapshot is read at your next start.
158
+ 4. List the clear defects nobody took yet: each evidence comment whose first line starts
159
+ `Clear defect from `, with no session reply under it that starts `Handed dispatch://`,
160
+ `Filed dispatch://`, `Not filed:` or `Asked dispatch://` (a role that asked instead, which step
161
+ 2 takes):
162
+
163
+ ```text
164
+ notes=$(curl -sS --fail-with-body -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" "$DISPATCH_URL/api/v1/issues/<REPORT KEY>/comments") || { echo "Dispatch read failed" >&2; exit 1; }
165
+ printf '%s' "$notes" | jq -cn 'input | if type == "array" then (. as $all | [.[] | select(.reply_to == null and (.body | startswith("Clear defect from "))) | .id as $id | select([$all[] | select(.reply_to == $id and .author.kind == "session" and (.body | test("^(Handed dispatch://|Filed dispatch://|Not filed:|Asked dispatch://)")))] | length == 0) | {id, body}]) else error("Dispatch did not answer the list") end'
166
+ ```
167
+
168
+ It reads as step 3's read does: exit 0 with `[]` is nothing waiting, anything else failed.
169
+ Take each as [Hand over a clear defect](#hand-over-a-clear-defect) says. One you could not hand
170
+ over at an earlier wake, because no architect held its tree's topic, has no reply, so this list
171
+ brings it back until a tree takes it or step 4 of the hand-over files it. When one is not clear,
172
+ reply `Not filed: <reason>` under it and open an ask for it instead.
76
173
 
77
174
  A read that fails, and an assignment Dispatch refuses, are handled as
78
175
  `skill://legion-controller/SKILL.md#rules` and
@@ -43,8 +43,8 @@ Every answer is one `slack_post` call:
43
43
  - `channel` is the mention's `channel_id`.
44
44
  - `thread_ts` is the mention's `thread_ts`, or its `ts` when it has none. Either keeps the reply in
45
45
  the thread the mention opened or joined, never at the channel's top level.
46
- - `text` is what you say, at most 4,000 characters, written as `skill://dispatch`'s "Writing for
47
- the human" says. Address the author as `<@user_id>` (by `bot_name` when a bot posted the mention
46
+ - `text` is what you say, at most 4,000 characters, written as `skill://dispatch-writing-specs`'s "Rules for
47
+ the text" say. Address the author as `<@user_id>` (by `bot_name` when a bot posted the mention
48
48
  with no `user_id`) and give every issue as its key, its title and its Dispatch link exactly as
49
49
  `dispatch` printed it (the `Link:` line of `dispatch read`, the link on a `Created` line or a
50
50
  search hit): it points at Dispatch's dashboard, and `slack_post` refuses a reply linking an
@@ -6,9 +6,8 @@ Read this on a `triage` wake, at every start, and when a person hands you an iss
6
6
  ## At every start
7
7
 
8
8
  1. List the project's triage issues handed to Legion:
9
- `dispatch issues --project <PROJECT> --status triage --label legion --limit 250 --offset 0`.
10
- When the first line ends `(showing 1-250 of N)`, read the next page with `--offset 250`, and so
11
- on until you have all N rows.
9
+ `dispatch issues --project <PROJECT> --status triage --label legion --all`. `--all` reads every
10
+ match in one request, so no row is missed or repeated while the list changes.
12
11
  2. The rows show no parent, so `dispatch read` each one and follow `Links:` up through each
13
12
  `child_of` parent. A `child_of` under `Referenced by:` is a child of this issue, not its parent.
14
13
  3. Leave a child that has an ancestor in `admission.active` or `admission.waiting`, because that
@@ -37,13 +36,18 @@ Read this on a `triage` wake, at every start, and when a person hands you an iss
37
36
  `actor` is this session (`dispatch whoami` names it) or an earlier one of yours. For a request
38
37
  that is an ask, an earlier session is yours when its reply under that ask (`dispatch read
39
38
  --ref`) starts `Filed dispatch://<this KEY>` after a newest answer of `File it for Legion`.
39
+ - Your own filing for a clear defect no tree owns hands it over too: its spec as filed holds a
40
+ `Filed for Legion from <role> on <issue>: <evidence>` line, and its `actor` is a session of
41
+ yours. An earlier session is yours when its reply under that evidence (a comment or an ask)
42
+ starts `Filed dispatch://<this KEY>`.
40
43
  - Any other actor hands over nothing. Leave the issue in `triage` and ask the person on your
41
44
  report issue whether Legion should take it, naming the issue and who labelled it, with the
42
45
  options `Admit it` and `Leave it`. On `Admit it`, go on to step 3. Either way, reply under the
43
46
  ask `Admitted dispatch://<KEY>` or `Not admitted: <reason>`, because the start procedure's
44
47
  catch-up reads that reply as done.
45
- 3. Admit it (`set_status` to `todo`) when a person handed it over, unless the request, the weekly
46
- plan or the deployment instructions say to wait or put it out of scope:
48
+ 3. Admit it (`set_status` to `todo`) when a person or your clear-defect filing handed it over,
49
+ unless the request, the weekly plan or the deployment instructions say to wait or put it out of
50
+ scope:
47
51
  `legion({ op: "set_status", issue: "<issue>", status: "todo" })`.
48
52
  4. When one of those says to wait, park it with `set_status` to `backlog`, or `icebox` when the
49
53
  request asks for a longer deferral, instead of leaving it in `triage`; the label stays. Never
@@ -16,8 +16,9 @@ the issue header. Triage it as `skill://legion-controller/references/triage.md`
16
16
 
17
17
  ## `slot-free on <KEY>` (`{kind: "slot-free"}`)
18
18
 
19
- The daemon released `<KEY>`'s slot, because its tree finished or left the workflow, and no waiting
20
- root took it. Name the free slots; take nothing.
19
+ The daemon released `<KEY>`'s slot and no waiting root took it: its tree finished or left the
20
+ workflow, or the root returned the slot to wait on a person (`awaiting_person`, which also sends
21
+ its own `awaiting-person` wake). Name the free slots; take nothing.
21
22
 
22
23
  ## `todo on <KEY>` (`{kind: "todo"}`)
23
24
 
@@ -50,12 +51,25 @@ take nothing for them.
50
51
 
51
52
  ## `unslotted on <KEY>` (`{kind: "unslotted", reason}`)
52
53
 
53
- A root that waited for a slot was moved from `todo` to the status `reason` names, by a person or by
54
- another session, so the daemon took it out of the workflow (`phase: done`) and starts nothing for
55
- it. Re-admit nothing, because the move was someone else's; `dispatch read` shows who claims it now.
56
- Tell the person in your summary: "<KEY> left Legion's waiting line: it was set to <status> while it
57
- waited for a slot, so Legion let it go; `todo` with the `legion` label queues it again". Other
58
- admission states: `skill://legion-controller/references/admission.md`.
54
+ Either a root that waited for a slot was moved from `todo` to the status `reason` names, by a
55
+ person or by another session, or a root waiting on a person (`awaiting_person`) could not be
56
+ resumed — its `legion` label is gone, or a person or a session outside its tree took its Dispatch
57
+ claim. Either way the daemon took it out of the workflow (`phase: done`) and starts nothing for it;
58
+ `reason` says which. Re-admit nothing, because the move was someone else's; `dispatch read` shows
59
+ who claims it now. Tell the person in your summary: "<KEY> left Legion's workflow: <reason as the
60
+ wake gives it>; `todo` with the `legion` label queues it again". Other admission states:
61
+ `skill://legion-controller/references/admission.md`.
62
+
63
+ ## `awaiting-person on <KEY>` (`{kind: "awaiting-person", reason}`)
64
+
65
+ A root whose architect asked a person a question, or handed its spec to a person for approval, and
66
+ now waits only on them: the daemon returned
67
+ its slot and suspends the architect after its turn, keeping its session. Read
68
+ `issues.<KEY>.waitingOn`. Do not park it or start anything, and take nothing for the slot it
69
+ returned: it is no reason to label or admit an issue ([Free
70
+ slots](skill://legion-controller/SKILL.md#free-slots)). The person's answer puts it at the front of
71
+ `admission.waiting` by itself, and the next free slot resumes its architect. A lost label or a
72
+ foreign Dispatch claim sends the separate `unslotted` wake.
59
73
 
60
74
  ## `held on <KEY>` (`{kind: "held", phase, role?, reason?}`)
61
75
 
@@ -19,7 +19,7 @@ You work one phase of one issue in a jj workspace every role of the issue shares
19
19
 
20
20
  ## Stop rules
21
21
 
22
- - **A refused completion is information, not a retry loop.** Quote the answer verbatim to the architect, so "my work is lost" and "my work belongs to an ended run" read differently. `HANDOFF_STALE_GENERATION` (the issue is on a newer run, or your turn began before the daemon recorded your task, which it then sends again) or `HANDOFF_NO_RUN` (your claim holds no task): push nothing further and tell the architect; when a task then arrives whose work is already committed, call `handoff_complete` again, and never redo the work or write a second handoff. `HANDOFF_NOT_CURRENT_PHASE`: the issue left your phase; report, never complete again. `HANDOFF_ALREADY_RECORDED`: the first answer stands, an acceptance or one of the four refusals the daemon records (`HANDOFF_STALE_GENERATION`, `HANDOFF_NOT_CURRENT_PHASE`, `READY_REQUIRED`, `HANDOFF_NOT_NEW`); if you did not see it, tell the architect. A refusal given before any fact is applied (READY's checks of the head among them) records nothing, so the corrected call at the same commit is applied. A reminder is not a task: after an accepted completion, a second call is refused.
22
+ - **A refused completion is information, not a retry loop.** Quote the answer verbatim to the architect, so "my work is lost" and "my work belongs to an ended run" read differently. `HANDOFF_STALE_GENERATION` (the issue is on a newer run: your turn began before the daemon's task for that run reached you, and the answer names that task when it is queued) or `HANDOFF_NO_RUN` (your claim holds no task): push nothing further, end the turn without completing, and tell the architect; if the task then arrives (none may: the new run's phase can be another claim's) and its work is already committed, call `handoff_complete` again at the same commit in that task's turn, and never redo the work or write a second handoff. `HANDOFF_NOT_CURRENT_PHASE`: the issue left your phase; report, never complete again. `HANDOFF_ALREADY_RECORDED`: the first answer stands, an acceptance or one of the refusals the daemon records (`HANDOFF_NOT_CURRENT_PHASE`, `READY_REQUIRED`, `HANDOFF_NOT_NEW`, and `HANDOFF_STALE_GENERATION` only when the issue moved during the call); if you did not see it, tell the architect. A refusal given before any fact is applied (`HANDOFF_STALE_GENERATION`, `HANDOFF_NO_RUN` and READY's checks of the head among them) records nothing, so the corrected call at the same commit is applied. A reminder is not a task: after an accepted completion, a second call is refused.
23
23
  - **After your phase completes, the workspace is read-only for you**: no commit, no `jj new`, no edit to a tracked file, in any later turn until a new task names your phase. The active phase owns it, and you stay live only to answer questions.
24
24
  - **Never start another Legion role**: the daemon starts every phase worker.
25
25
  - **Never end your turn blocked on a decision someone else owns.** Send a product, scope, design, lifecycle or cross-phase decision to the owning architect as your role prompt says, with the issue, phase, verified observation, what you tried and the decision needed; a human-only to-do is a `dispatch ask`. Otherwise proceed: a phase that stops silently holds its issue until someone notices.
@@ -35,7 +35,7 @@ Read each by its `skill://` link: a read by path stops at 300 lines.
35
35
  | You write or edit the PR body, record or verify a proof, or run the simplify pass | `skill://legion-worker/references/pr-body.md` |
36
36
  | Before a push, to check your commits' App identity | `skill://legion-worker/references/default-branch-check.md` |
37
37
  | You reply on, list or resolve review threads, or judge a bot's finding | `skill://legion-worker/references/review-threads.md` |
38
- | A conflict, a retarget, comparing heads after a conflict merge, or rewriting a pushed commit | `skill://legion-worker/references/conflicts-and-rewrites.md` |
38
+ | A conflict, a need only the base meets, a retarget, comparing heads after a forward merge, or rewriting a pushed commit | `skill://legion-worker/references/conflicts-and-rewrites.md` |
39
39
  | You write the `skip-checks: true` trailer, or expect a body edit to re-run a check | `skill://legion-worker/references/skip-checks-trailer.md` |
40
40
  | You review or approve, build READY, or run the production check | `skill://legion-worker/references/merge-gate.md` |
41
41
  | The issue renames a repository, package, or URL across the codebase | `skill://legion-worker/references/systematic-rename.md` |
@@ -46,18 +46,19 @@ Read each by its `skill://` link: a read by path stops at 300 lines.
46
46
  ## Every role pushes its own commits
47
47
 
48
48
  - Before a push, read `jj -R "$LEGION_WORKSPACE" log -r 'ancestors(@, 5)'` (earlier phases' commits in it are expected) and run the identity check (`skill://legion-worker/references/default-branch-check.md`). Add no attribution trailer: the pane's jj adds `Omp-Session:`.
49
- - A push whose commits touch only `.legion/` ends its head's message with `skip-checks: true`, unless it is retro's push (the implementer's retro phase, before the merger's). Retro's push never carries the line, whatever paths it touches, because it is the last push before the merge and a base that requires a check will not merge a head whose checks never ran. No other push carries it. The daemon classifies a push by its paths, never by the trailer, so a wrong trailer can stall the issue but never makes it pass.
49
+ - A push whose commits touch only `.legion/<issue>/` ends its head's message with `skip-checks: true`, unless it is retro's push (the implementer's retro phase, before the merger's). Retro's push never carries the line, whatever paths it touches, because it is the last push before the merge and a base that requires a check will not merge a head whose checks never ran. No other push carries it. The daemon classifies a push by its paths, never by the trailer, so a wrong trailer can stall the issue but never makes it pass.
50
50
  - A planner whose issue moved back from implementing starts on the remote tip, `jj -R "$LEGION_WORKSPACE" new legion/<KEY>@origin`, so its handoff never sits on the implementer's unpushed commits.
51
51
 
52
52
  ## PR body, review, and the merge gate
53
53
 
54
54
  - The implementer opens the pull request with the line `Dispatch: <KEY>` in its body, the daemon's link to the issue when the branch name is ambiguous. Later phases push to that branch and edit that pull request, never a new one.
55
- - **No deferrals.** A finding that changes behaviour, hides an error, or breaks a gate is fixed in this pull request. Only cleanup that changes no behaviour (naming, duplication, wording) may wait, on the body's one `Fast-follow:` line.
55
+ - **No deferrals.** A finding that changes behaviour, hides an error, or breaks a gate is fixed in this pull request, and so is a wording nit or any clear fix in a file it touches: a later round costs a CI pass and a re-read of the whole change. The body's one `Fast-follow:` line reads "none", or names cleanup in files this pull request does not touch.
56
+ - **The reviewer fixes a prose-only finding itself** (wording, a count, a tense, a citation) in its own commit, pushed before its review. The exception is the round after a READY refusal. Code or behaviour goes back (`skill://legion-worker/references/merge-gate.md`).
56
57
  - **The merge gate**, in order: the tester's evidence green → the reviewer's approval of the head → the architect's acceptance → retro → the merger's READY → the merge, which closes the issue → the implementer's production check, after the close. When the daemon accepts READY, it submits the merge of READY's head, unless the answer's note begins `MANUAL_MERGE_REQUIRED:`, where a person merges it; no role runs `gh pr merge`.
57
58
  - Append `<!-- legion: {"session":"<session-id>","phase":"<phase>"} -->` to every pull-request comment and review you post: the daemon finds the session behind a review by it.
58
59
  - Never write a GitHub issue, by `gh issue` or a non-GET `gh api` on `/issues` (post or edit a PR conversation comment with `gh pr comment`): Legion's issues live on Dispatch. Never run `gh config set` or `gh alias set`, and leave `GH_TOKEN`, `GITHUB_TOKEN` and `GH_HOST` empty: the daemon rewrites your gh directory in place, and a set variable outranks it.
59
- - Before you report a resource unreachable, check `AGENT_SECRETS_URL` and `AGENT_SECRETS_KEY_DIR` (then `agent-secrets <SECRET> -- <command>`; sjawhar/legion `docs/kubernetes.md`, "Operator configuration") and `AWS_CONFIG_FILE` (read its profiles): a deployment can give your pod either. `agent-secrets` refuses a secret its grant lacks and names it; that refusal is final. Your `task` subagents and background jobs inherit both, and your `gh` and `git`.
60
+ - Before you report a resource unreachable, check `AGENT_SECRETS_URL` and `AGENT_SECRETS_KEY_DIR` (then `agent-secrets <SECRET> -- <command>`; sjawhar/legion `docs/kubernetes.md`, "Operator configuration") and `AWS_CONFIG_FILE` (read its profiles): a deployment can give your pod either. `agent-secrets` refuses a secret its grant lacks and names it; that refusal is final. Your `task` subagents and background jobs inherit both, and your `gh` and `git`. Request a secret only to use it for your issue's work, never to learn whether it exists. A request for a secret your role does not hold reaches a person's Inbox.
60
61
 
61
62
  ## Deployment instructions
62
63
 
63
- Deployment instructions in your prompt override this skill where they conflict, except four rules: no deferrals; bringing the base into the branch only on a real conflict or a retarget (`skill://legion-worker/references/conflicts-and-rewrites.md#reintegrating-the-base`); the implementer's own proof on a production-like surface at the head that merges, an applied simplify head included (`skill://legion-worker/references/pr-body.md#what-a-proof-is`, `skill://legion-worker/references/pr-body.md#the-rules-every-phases-evidence-follows`); and the implementer's production check after the close (`skill://legion-worker/references/merge-gate.md#the-production-check-after-the-close`).
64
+ Deployment instructions in your prompt override this skill where they conflict, except four rules: no deferrals; bringing the base into the branch only on a real conflict, a need only the base meets, or a retarget (`skill://legion-worker/references/conflicts-and-rewrites.md#reintegrating-the-base`); the implementer's own proof on a production-like surface at the head that merges, an applied simplify head included (`skill://legion-worker/references/pr-body.md#what-a-proof-is`, `skill://legion-worker/references/pr-body.md#the-rules-every-phases-evidence-follows`); and the implementer's production check after the close (`skill://legion-worker/references/merge-gate.md#the-production-check-after-the-close`).
@@ -1,24 +1,31 @@
1
1
  # Conflicts, retargets, fingerprints, and rewriting pushed commits
2
2
 
3
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.
4
+ controller asks you to resolve a conflict, the change needs something only the base has, the pull
5
+ request is retargeted to a new base, you compare two heads after such a merge, or you are about to
6
+ rewrite a commit you already pushed.
6
7
  Every path it cites is in sjawhar/legion.
7
8
 
8
9
  ## Reintegrating the base
9
10
 
10
- - **Reintegrate the base only on a real conflict, except after a base retarget — and with a
11
- merge, never `jj rebase`.**
11
+ - **Merge the base forward only on a real conflict, a need only the base meets, or a retarget, and
12
+ never with `jj rebase`.**
12
13
  The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
13
- because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never reintegrate the base to
14
- pick up the default branch or refresh CI (a single failed CI job is re-run on its own: *Never waiting on CI* in
15
- `skill://legion-worker/references/pr-body.md`). A conflict-forced rebase that leaves the branch's diff unchanged is a
16
- confirmation, not a new round (see *The unchanged-diff check* below); that name is the event's,
17
- kept by the rules below and the learnings that cite it, and the operation it names is always
18
- the merge here. Before merging, record
14
+ because of a conflict, the change needs something that exists only on the base (a fix for its
15
+ only red check, or a sentence it must change), or after the pull request is retargeted to a new
16
+ base. Otherwise, never reintegrate the base to pick up the default branch or refresh CI. Each
17
+ merge is a new head, reruns CI and can void a review. A single failed CI job is re-run on its
18
+ own (*Never waiting on CI* in `skill://legion-worker/references/pr-body.md`). After any forward merge, build the branch and
19
+ run its proof again at the merge head, even with no text conflict: a clean merge once changed a
20
+ function's arity under the branch's own callers. Before you race another tree's pull request on
21
+ shared files, `envoy_publish` to its implementer's role topic, ask when its next push lands, and
22
+ merge once, after it. A forward merge that leaves the branch's diff unchanged is a
23
+ confirmation, not a new round (see *The unchanged-diff check* below). The check compares the
24
+ pull request's own lines file by file: a changed file sends the head through the tester again,
25
+ and your comment names it. Before merging, record
19
26
  the fingerprint at the current tip; after pushing the merged branch, record it at the new
20
27
  tip; post one PR comment (Legion footer):
21
- `rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
28
+ `forward merge <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
22
29
  On the tmux runtime, where issue workspaces are still jj workspaces of one shared clone,
23
30
  `jj undo`, `jj abandon` and `jj op restore` rewrite the shared operation log, so a mistake there
24
31
  is recovered forward (`jj -R "$LEGION_WORKSPACE" new`, or `jj -R "$LEGION_WORKSPACE" restore
@@ -57,7 +64,7 @@ Every path it cites is in sjawhar/legion.
57
64
 
58
65
  ## The unchanged-diff check
59
66
 
60
- The fingerprint every role compares after a conflict-forced rebase (every flag and the fileset
67
+ The fingerprint every role compares after a forward merge (every flag and the fileset
61
68
  verified on jj 0.45.1):
62
69
 
63
70
  ```bash
@@ -70,16 +77,16 @@ fingerprint=$(set -o pipefail; jj -R "$LEGION_WORKSPACE" diff --from "fork_point
70
77
 
71
78
  - `<head-sha>` is a full commit SHA; a jj commit id is the git SHA GitHub shows.
72
79
  - `fork_point(${base}@origin | <head-sha>)` is the base the branch was cut from *at that head*:
73
- the old base for the pre-rebase head, the new base for the rebased one, so one command serves
80
+ the old base for the head before the merge, the new base for the merge head, so one command serves
74
81
  both sides. `base` is the pull request's base branch as a jj string literal, as in the merge
75
82
  above.
76
- - A head the rebase hid is still addressable by its SHA in the shared workspace. The check
83
+ - A head the merge replaced is still addressable by its SHA in the shared workspace. The check
77
84
  prints a fingerprint only when every step of it succeeded: the base read, the fetch and the
78
85
  diff (`pipefail` keeps the diff's failure through `sed` and `sha256sum`, which would otherwise
79
86
  hash empty input to `e3b0c442…`). A nonzero exit or no printed fingerprint (a failed read of the
80
87
  base, or a SHA the workspace cannot resolve) counts as a changed diff — never as unchanged.
81
88
  - `--context 0` drops context lines; the `sed` drops `@@` hunk headers (line positions move
82
- on a rebase) and `index` lines (blob ids move when the base's copy of a file changed). What
89
+ on a merge) and `index` lines (blob ids move when the base's copy of a file changed). What
83
90
  is left is exactly the added and removed lines per file.
84
91
  - The single fileset `'~(.legion | docs/solutions)'` leaves out the handoff ledger and retro's
85
92
  learnings: process artifacts the merge gate already exempts from re-review
@@ -91,7 +98,7 @@ fingerprint=$(set -o pipefail; jj -R "$LEGION_WORKSPACE" diff --from "fork_point
91
98
  `.legion/` is gone, jj warns `No matching entries for paths: .legion` on stderr; the hash is
92
99
  unaffected.
93
100
 
94
- Where each role gets its two heads: the implementer — the tip before and after its own rebase;
101
+ Where each role gets its two heads: the implementer — the tip before and after its own forward merge;
95
102
  the tester — the head its `E2E` line names and the new head; the reviewer — the `commit_id` of
96
103
  its last submitted review (`gh api --paginate repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login, body}'`, whose pages concatenate in order, so the last line is the last submitted review, and whose body tells a no-fix request that answered a READY refusal from any other)
97
104
  and the new head; the merger never computes a fingerprint — it quotes the `--summary` lines