@sjawhar/opencode-legion-envoy 9.0.0 → 9.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 (43) hide show
  1. package/dist/dispatch.js +162 -41
  2. package/dist/src/server.js +20 -17
  3. package/package.json +1 -1
  4. package/skills/AGENTS.md +7 -6
  5. package/skills/dispatch/SKILL.md +54 -169
  6. package/skills/dispatch-first/SKILL.md +23 -17
  7. package/skills/dispatch-writing-specs/SKILL.md +76 -0
  8. package/skills/leading-a-work-stream/SKILL.md +96 -0
  9. package/skills/leading-a-work-stream/references/daily-plan.md +8 -0
  10. package/skills/leading-a-work-stream/references/decision-rights.md +6 -0
  11. package/skills/leading-a-work-stream/references/direction-and-coaching.md +13 -0
  12. package/skills/leading-a-work-stream/references/dispatch-upkeep.md +7 -0
  13. package/skills/leading-a-work-stream/references/sources.md +9 -0
  14. package/skills/leading-a-work-stream/references/speed-tick.md +44 -0
  15. package/skills/leading-a-work-stream/references/user-quotes.md +7 -0
  16. package/skills/legion-architect/SKILL.md +11 -8
  17. package/skills/legion-architect/references/acceptance.md +18 -5
  18. package/skills/legion-architect/references/decomposition.md +3 -2
  19. package/skills/legion-architect/references/design-gate.md +42 -19
  20. package/skills/legion-architect/references/wake-routing.md +4 -4
  21. package/skills/legion-controller/SKILL.md +31 -17
  22. package/skills/legion-controller/references/admission.md +5 -3
  23. package/skills/legion-controller/references/backlog-stewardship.md +17 -2
  24. package/skills/legion-controller/references/daily-report.md +4 -4
  25. package/skills/legion-controller/references/finding-asks.md +111 -14
  26. package/skills/legion-controller/references/slack-mentions.md +2 -2
  27. package/skills/legion-controller/references/triage.md +9 -5
  28. package/skills/legion-controller/references/wakes.md +22 -8
  29. package/skills/legion-worker/SKILL.md +7 -6
  30. package/skills/legion-worker/references/conflicts-and-rewrites.md +23 -16
  31. package/skills/legion-worker/references/findings.md +126 -31
  32. package/skills/legion-worker/references/merge-gate.md +57 -25
  33. package/skills/legion-worker/references/pr-body.md +9 -5
  34. package/skills/legion-worker/references/skip-checks-trailer.md +1 -1
  35. package/skills/dispatch/references/api.md +0 -21
  36. package/skills/dispatch/references/asks.md +0 -43
  37. package/skills/dispatch/references/document-edits.md +0 -59
  38. package/skills/dispatch/references/documents.md +0 -79
  39. package/skills/dispatch/references/examples.md +0 -85
  40. package/skills/dispatch/references/issues.md +0 -66
  41. package/skills/dispatch/references/messages.md +0 -31
  42. package/skills/dispatch/references/reading.md +0 -32
  43. package/skills/dispatch-brainstorming/SKILL.md +0 -56
@@ -3,12 +3,27 @@
3
3
  Read this when you find a problem the issue you work does not cover: a defect in another
4
4
  component, repository or tool, a gap in Legion itself, or a production check's `fail`. It holds for
5
5
  every Legion role (each phase worker, each architect, the controller) and for each of their
6
- subagents, and it replaces the filing step of `skill://dispatch-first` and `skill://dispatch`.
6
+ subagents, and it replaces the filing step of `skill://dispatch-first` and `skill://dispatch`. The
7
+ controller fixes nothing itself: it hands a finding to the tree that owns the code, as
8
+ `skill://legion-controller`, "Findings, and the issues you file", says.
7
9
 
8
- ## Ask a person; never file
10
+ ## Fix it where you are, else hand it over, and ask only for a decision
9
11
 
10
- A finding outside your issue is a `dispatch ask`, never a `dispatch issue`: a person decides
11
- whether it needs an issue, where, and whether Legion works it.
12
+ A finding outside your issue never becomes a `dispatch issue` you file. Take the first of these
13
+ three steps that fits:
14
+
15
+ 1. **Fix it here.** A defect with a clear fix in code your tree touches or depends on goes into
16
+ your tree's own pull request, while you are in that code. The fix needs no choice a person must
17
+ make, and the evidence shows the defect. Say in your handoff and in the pull request body what
18
+ you fixed beside the issue's own work. A fix deferred to another issue is a fix nobody makes.
19
+ 2. **Hand it over.** Any other clear defect goes to the controller with its evidence, as [Send a
20
+ finding to the controller](#send-a-finding-to-the-controller) says. The controller gives it to
21
+ the running tree whose code it is. Only when no tree owns that code does it file a Legion root.
22
+ 3. **Ask.** A finding that needs a person's authority, taste or risk appetite is a `dispatch ask`
23
+ with one recommendation, as [Where it goes](#where-it-goes) says: whether the work is wanted at
24
+ all, which of several designs, who owns it, or a change of scope, cost or risk.
25
+
26
+ A fix you are not sure of is not step 1: hand it over, or ask when a person must decide.
12
27
 
13
28
  - A problem inside your issue is not such a finding: fix it in your change, since nothing needed
14
29
  for correctness is deferred.
@@ -17,21 +32,89 @@ whether it needs an issue, where, and whether Legion works it.
17
32
  rules*), which may file it as a child of the tree. Anything else, in the same
18
33
  repository or not, is a finding.
19
34
  - Search first (`skill://dispatch-first`): when an open issue already tracks the problem, comment
20
- the evidence on that issue and ask nothing. When an open ask on the report issue already asks
21
- about it (`dispatch open-asks --project <PROJECT>` lists them), add your evidence under that ask
22
- (`dispatch comment --issue <REPORT KEY> --reply-to-ask '<ask id>' --body-file -`, the evidence on
23
- stdin) and open no second one.
24
-
25
- Three kinds of issue are still filed: an architect's children of its own tree (`dispatch issue
26
- --parent`, `skill://legion-architect`); an issue a person asked the controller to file, in a
27
- direct message, in a Slack request, or in their answer to one of these asks; and the report issue
28
- itself, which the controller creates once when the project has none
29
- (`skill://legion-controller`, "Findings, and the issues you file" and "Daily report").
35
+ the evidence on that issue and send or ask nothing. When an open ask on the report issue already
36
+ asks about it (`dispatch open-asks --project <PROJECT>` lists them), add your evidence under that
37
+ ask (`dispatch comment --issue <REPORT KEY> --reply-to-ask '<ask id>' --body-file -`, the
38
+ evidence on stdin) and open no second one.
39
+
40
+ These issues are filed:
41
+
42
+ - an architect's children of its own tree (`dispatch issue --parent`, `skill://legion-architect`)
43
+ - a Legion root the controller files for a clear defect no running tree owns
44
+ - an issue a person asked the controller to file, in a direct message, in a Slack request, or in
45
+ their answer to an ask
46
+ - the report issue itself, which the controller creates once when the project has none
47
+
48
+ `skill://legion-controller`, "Findings, and the issues you file", and
49
+ `skill://legion-controller/references/daily-report.md` say how.
50
+
51
+ ## Send a finding to the controller
52
+
53
+ Post the evidence first as a `dispatch comment` on the report issue ([Where it goes](#where-it-goes)
54
+ says which), its first line exactly `Clear defect from <your role token>`. It prints
55
+ `Posted comment <id>`. Then send the controller one message with
56
+ `envoy_publish` to the controller topic your `Legion addressing` line names, with
57
+ `expects_reply: none`. It holds:
58
+
59
+ - your role token, from your `Legion addressing` line
60
+ - the defect, as its reader recognises it, and the issue it came from (`dispatch://<KEY>`)
61
+ - where the fix belongs: the repository and the component
62
+ - the fix
63
+ - the evidence comment, `dispatch://<REPORT KEY>/comment/<id>`
64
+
65
+ The controller hands it to the running tree whose code it is, and files a Legion root only when no
66
+ tree owns that code. Either way it replies under your evidence comment with where the work went.
67
+ You file nothing, and you wait for nothing. When the report issue is missing or closed, put the
68
+ evidence in the message itself. A production check's `Production:` line then names the defect as
69
+ sent to the controller.
70
+
71
+ ### When no controller takes it
72
+
73
+ The controller topic is a live wake: a message published while no controller runs reaches nobody,
74
+ and no controller reads it later. `envoy_publish`'s own answer tells you whether one took it. A
75
+ publish to a role answers with its live `holder`. With no live controller it fails instead, with
76
+ `no holder for role legion-<project>-controller` or a holder that lapsed, and the listener delivers
77
+ nothing (`packages/envoy/cmd/listener`, the test "unheld role returns 404 without publishing").
78
+ Then make the finding an ask, as [What the ask says](#what-the-ask-says) spells it, on the first
79
+ of these that holds, so a person sees it and the controller finds it when it returns:
80
+
81
+ 1. **The report issue is open:** ask there, the question opening `Clear defect from <your role
82
+ token>` and `--ref` naming your evidence comment. Then reply `Asked dispatch://<REPORT
83
+ KEY>/ask/<id>` under that comment, so the controller takes the ask and not the comment. The
84
+ report issue's assignee sees the ask, and the controller's catch-up takes it.
85
+ 2. **The report issue is missing or closed:** ask on your own issue, the evidence posted there as
86
+ a comment first. Then publish the ask's reference to your tree's architect (`skill://legion-worker`,
87
+ *Stop rules*). An architect holds its own. The architect holds your issue's
88
+ acceptance until a controller has taken the ask (`skill://legion-architect`, "Acceptance and the
89
+ merge"). That hold is what keeps the ask readable: an open ask on a closed issue drops out of
90
+ the controller's own listing (`asks.go`'s `listOpenAsks` takes `i.closed_at is null`), so an
91
+ issue that closed over one hides it for good.
92
+ 3. **Your own issue is closed too** (Dispatch refuses the ask `409 ISSUE_CLOSED`, as on a
93
+ production check): put the report issue back in reach yourself, with the `dispatch` commands
94
+ below, and ask there. Never `legion({op: "set_status"})`, which the controller's report steps
95
+ (`skill://legion-controller/references/daily-report.md`) use: that op is the controller's
96
+ alone, and your session is refused `set_status is not
97
+ available to a phase-worker session` (`packages/pi-legion/src/tools.ts`'s `OPERATIONS`). No
98
+ other issue can hold an answerable ask, so this is the one case a role writes the report issue.
99
+ - It exists and is closed: `dispatch issue-update --issue <REPORT KEY> --status icebox`. Send
100
+ the status by itself. A closed issue takes a reopening status alone, and refuses it beside
101
+ an assignee or any other field (`api/issue_patch.go`, the `before.ClosedAt` branch). This is
102
+ the one lifecycle status your own prompt lets you write. The report issue is in no tree, so
103
+ the daemon re-asserts nothing on it: `workflow`'s `sessionStatusWrite` acts only for a
104
+ session that claims the issue's tree, or for an admitted root.
105
+ - A search finds none: create it as "Where it goes" names it, `dispatch issue --project
106
+ <PROJECT> --title 'Legion daily report' --spec-file -`, with the Summary
107
+ `skill://legion-controller/references/daily-report.md` gives, then park it with
108
+ `dispatch issue-update --issue <NEW KEY> --status icebox`. It carries no `legion` label.
109
+ - Then post the evidence there and ask there, as [What the ask says](#what-the-ask-says)
110
+ spells it. Say in your summary that you reopened or created it, so the controller knows.
111
+
112
+ Name the ask in your summary, and in a production check's `Production:` line.
30
113
 
31
114
  ## Where it goes
32
115
 
33
- On the controller's report issue, always: the issue the deployment instructions name for Legion's
34
- reports, else the project's `Legion daily report`, which
116
+ An ask goes on the controller's report issue whenever it is open: the issue the deployment
117
+ instructions name for Legion's reports, else the project's `Legion daily report`, which
35
118
  `dispatch search --query '"Legion daily report"' --project <PROJECT>` finds. It stays open in
36
119
  `icebox`, so the ask stays answerable whatever happens to the issue it came from; once an issue
37
120
  closes, Dispatch takes its open asks out of the Inbox and refuses their answers
@@ -42,49 +125,61 @@ When the project has no report issue yet, or a person closed it (`dispatch read`
42
125
  done`, and Dispatch refuses the ask with `409 ISSUE_CLOSED`), do not create or reopen one yourself
43
126
  (the controller does): send the finding to the controller as text, with `envoy_publish` to the
44
127
  controller topic your `Legion addressing` line names (`expects_reply: none`), holding the problem,
45
- the issue it came from and the evidence. The controller creates or reopens the report issue and
46
- opens the ask. A production check's `Production:` line then names the finding as sent to the
47
- controller.
128
+ the issue it came from, the evidence and the decision it needs. The controller creates or reopens
129
+ the report issue and opens the ask. When that publish fails because no controller holds the role,
130
+ take step 2 or 3 of [When no controller takes it](#when-no-controller-takes-it). A production
131
+ check's `Production:` line then names the finding as sent to the controller, or names the ask.
48
132
 
49
- ## What it says
133
+ ## What the ask says
50
134
 
51
135
  The question stands alone, since the ask view shows no comment. It holds at most 800 characters,
52
136
  the `Ref: <reference>` line `--ref` appends included (about 75 characters for a comment's
53
- reference). It carries:
137
+ reference). Its first line names who asks: `Clear defect from <your role token>` for a clear
138
+ defect no controller took, else `Finding from <your role token>`. The architect's acceptance and the
139
+ controller's catch-up find a finding ask by that line. Then it carries:
54
140
 
55
141
  - the problem, as its reader recognises it, and the issue it came from (`dispatch://<KEY>`);
56
142
  - where the fix would belong: the repository or layer, and a plain statement that it is not
57
143
  Legion's when it is not. A problem is not Legion's because you work in Legion: name the
58
144
  component that misbehaved;
59
- - your recommendation and its reason.
145
+ - the decision a person must make, and your recommendation and its reason. A clear defect no
146
+ controller took carries the fix instead, and its options ask where the work goes, with
147
+ `Hand it to the owning tree` first and recommended.
60
148
 
61
149
  The evidence (the commands, their output and its time, the file and line, the pull request) does
62
- not fit. Post it first as a `dispatch comment` on the report issue, which prints
63
- `Posted comment <id>`, and cite that comment with `--ref`, the one reference an ask takes:
150
+ not fit. Post it first as a `dispatch comment` on the issue the ask goes on, which prints `Posted
151
+ comment <id>`, and cite that comment with `--ref`, the one reference an ask takes. That issue,
152
+ `<ASK KEY>` below, is the report issue, except as [When no controller takes
153
+ it](#when-no-controller-takes-it) says:
64
154
 
65
155
  ```bash
66
- dispatch comment --issue <REPORT KEY> --body-file - <<'EOF'
156
+ dispatch comment --issue <ASK KEY> --body-file - <<'EOF'
67
157
  <the evidence: commands, output with its time, file:line, links>
68
158
  EOF
69
- # Only when your Legion addressing line lists the repository where the fix belongs, keep the
70
- # `File it for Legion` option; otherwise drop that line.
71
- dispatch ask --issue <REPORT KEY> --ref 'dispatch://<REPORT KEY>/comment/<id>' --question-file - \
159
+ # Keep `File it for Legion` only when your Legion addressing line lists the repository where the
160
+ # fix belongs. For a clear defect no controller took, replace the `File an issue` option below with
161
+ # 'Hand it to the owning tree: <the tree, when you know it>', the first option and recommended.
162
+ dispatch ask --issue <ASK KEY> --ref 'dispatch://<ASK KEY>/comment/<id>' --question-file - \
72
163
  --option 'File an issue: <what it would track, and who would own it>' \
73
164
  --option 'File it for Legion: Legion works it once it is filed' \
74
165
  --option 'Do not file: <what stays as it is>' <<'EOF'
166
+ Finding from <your role token>
75
167
  <the problem and dispatch://<KEY> it came from; where the fix belongs, and that it is not Legion's when it is not; your recommendation and why>
76
168
  EOF
77
169
  ```
78
170
 
79
171
  Legion refuses to record an issue on a repository the project does not map, hence that condition.
80
172
 
81
- ## Tell the controller
173
+ ## Tell the controller about the ask
82
174
 
83
175
  The person answers in Dispatch, and the answer reaches only the ask's followers. So once the ask is
84
176
  open, `envoy_publish` its reference, `dispatch://<REPORT KEY>/ask/<id>` (the id from the
85
177
  `Asked <id> on …` line), to the controller topic your `Legion addressing` line names, with
86
178
  `expects_reply: none`. The controller follows the ask and files what the person's answer asks for;
87
179
  you file nothing, whatever the answer says. The controller itself skips this step, since it follows
88
- each ask it opens.
180
+ each ask it opens. When that publish names no holder, do nothing more for an ask on the report
181
+ issue: the controller's catch-up follows it at its next start.
89
182
 
90
- The ask blocks nothing of your own issue: its work goes on.
183
+ An ask on the report issue blocks nothing of your own issue: its work goes on. One on your own
184
+ issue holds its acceptance, as step 2 of [When no controller takes
185
+ it](#when-no-controller-takes-it) says.
@@ -23,10 +23,10 @@ it, and any other change to the head voids it.
23
23
  `task(agent="thermonuclear-code-quality")` once at that head — the head the round reviews —
24
24
  and records the verdict. The CI it reads is the checks that stand for the code under review:
25
25
  those GitHub ran for the newest push of the branch that started `pull_request` workflows, when
26
- every commit above that push's head changes only `.legion/` (GitHub starts no workflow run for a
27
- head whose commit message carries `skip-checks: true`, which every handoff-only push before
28
- retro does; retro's push carries no such line, so its head does get GitHub's workflow runs of
29
- its own). With `<o>/<r>`, `<n>`, `<base>`, `<headRefName>` and `<headRefOid>` from
26
+ every commit above that push's head changes only `.legion/<issue>/` (GitHub starts no workflow
27
+ run for a head whose commit message carries `skip-checks: true`, which every handoff-only push
28
+ before retro does; retro's push carries no such line, so its head does get GitHub's workflow runs
29
+ of its own). With `<o>/<r>`, `<n>`, `<base>`, `<headRefName>` and `<headRefOid>` from
30
30
  `gh pr view <n> --json baseRefName,headRefOid,headRefName`, four reads. The required set is the
31
31
  base branch's rulesets' required checks and its branch protection's, when it has any. Both
32
32
  branch endpoints take the base as a URL path, each segment escaped and each `/` kept (a raw `#`
@@ -46,8 +46,9 @@ it, and any other change to the head voids it.
46
46
  newest first). That commit's check-runs
47
47
  (`gh api repos/<o>/<r>/commits/<ci head>/check-runs --paginate --jq '.check_runs[] | [.name, .status, .conclusion] | @tsv'`):
48
48
  every required name present, `completed` and `success` (GitHub counts `neutral` and `skipped`
49
- as passing for a required check). And the compare proving only `.legion/` changed since
50
- (`gh api repos/<o>/<r>/compare/<ci head>...<headRefOid> --jq '{handoffsOnly: ([.files[].filename] | all(startswith(".legion/"))), files: (.files | length)}'`
49
+ as passing for a required check). And the compare proving only `.legion/<issue>/` changed since,
50
+ `<issue>` being your `LEGION_ISSUE` (another issue's `.legion/` is code to the daemon and READY)
51
+ (`gh api repos/<o>/<r>/compare/<ci head>...<headRefOid> --jq '{handoffsOnly: ([.files[] | .filename, (.previous_filename // empty)] | all(startswith(".legion/<issue>/"))), files: (.files | length)}'`
51
52
  prints `handoffsOnly` `true`; `false` means code was pushed since CI ran, so wait for the new
52
53
  push's runs and read the CI head again). GitHub's compare lists at most 300 files for the whole
53
54
  comparison, so `files` at 300 is a comparison GitHub cut at its cap and `handoffsOnly` decides
@@ -67,7 +68,7 @@ it, and any other change to the head voids it.
67
68
  Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
68
69
  finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
69
70
  A `COMMENT` decides nothing. After a
70
- conflict-forced rebase, compute the fingerprint (*The unchanged-diff check* in
71
+ forward merge, compute the fingerprint (*The unchanged-diff check* in
71
72
  `skill://legion-worker/references/conflicts-and-rewrites.md`) at the
72
73
  `commit_id` of your last submitted review and at the new head. Equal and that review was
73
74
  `APPROVE`: submit one more `APPROVE` naming the new head by SHA, its body naming both SHAs
@@ -75,37 +76,63 @@ it, and any other change to the head voids it.
75
76
  that review was `COMMENT` or `REQUEST_CHANGES`: continue that round against the new head;
76
77
  nothing restarts. Different, or either run printed no fingerprint: a new round — thermo again,
77
78
  one review. A return whose task's `Reason:` names a READY refusal is never a confirmation, even
78
- after a rebase, and a no-fix `REQUEST_CHANGES` that answered one is never continued: the round
79
+ after a forward merge, and a no-fix `REQUEST_CHANGES` that answered one is never continued: the round
79
80
  after it reviews the refused change in full from the last `APPROVE` (*Retro's commits do not
80
81
  void the reviewer's approval* below).
81
82
  - Answer every thread you opened, and every thread a bot opened that is none of Legion's role
82
83
  Apps, as `skill://legion-worker/references/review-threads.md` says; the same reference says
83
84
  when every thread is settled enough to approve, and resolving one never gates your approval.
85
+ - **Fix a prose-only finding yourself** — wording, a count, a tense, a citation — in your own
86
+ commit on the pull request, and list it in your review. Send the work back only for a finding
87
+ that touches code or behaviour. Push the fix **before** you submit the round's review, and
88
+ review the head that push made. A change pushed above the head you approved leaves the approval
89
+ standing for nothing: the round goes stuck and your architect is told (`ApprovalStands` in
90
+ `packages/daemon/internal/classify/decisions.go`, `stuckApprovedOtherCode` in
91
+ `packages/daemon/internal/workflow/review.go`, and the cases "the approval, then a code push"
92
+ and "a push since the approved head changed code, then an approval of the head" in
93
+ `packages/daemon/internal/workflow/review_test.go`). That push carries no `skip-checks: true`
94
+ line, so GitHub runs the branch's workflows on it. Wait for that head's required checks to be
95
+ green, and approve only then. The daemon sends nothing back for a red at a head the review App
96
+ pushed (`PlannedRed` in `classify/decisions.go`), so your own read is the only one. A red you
97
+ approve over reaches nobody: the tree runs on to the merge, where GitHub holds the pull request
98
+ under the base branch's required checks until someone reads them. A red there is a
99
+ `REQUEST_CHANGES` when the code is at fault, and your own commit to fix when your prose broke
100
+ it. The architect reads your prose commits at its acceptance (*The architect's acceptance*,
101
+ below), and that read is no new round. A round after a READY refusal is the one exception. Its
102
+ approval needs the tester's `E2E` line to name the head with only `.legion/<issue>/` changed since. So
103
+ there you fix no prose yourself, and you name each prose finding in a `REQUEST_CHANGES`.
84
104
 
85
105
  A reviewer's phase ends with its completion, not with its review. Every round writes a handoff
86
106
  and takes this order: read CI as above; write, commit and push the handoff; submit the review of
87
107
  the head that push made, by its SHA; then complete. A red at the code is a `REQUEST_CHANGES`
88
108
  naming the check, never an approval. A request for changes does not wait, since it stands
89
- whatever CI says and the issue leaves reviewing with it. The approval waits on nothing at the
90
- handoff commit either: it ends the round whatever CI says there, since the round reads no CI
109
+ whatever CI says and the issue leaves reviewing with it. The approval waits on nothing at a
110
+ handoff-only commit either: it ends the round whatever CI says there, since the round reads no CI
91
111
  verdict, and a red the daemon reads later — at that commit or any above it — is the code's red
92
112
  and resumes the implementer from wherever the issue is (reviewing, retro or merging) without the
93
- reviewer. A review of a head the handoff push then replaces names a head the pull request no
94
- longer has. The daemon moves the issue once both are in — the decision GitHub reports and your
95
- completion, in either order — so a review posted without a completion leaves the issue in
113
+ reviewer. A head your own push changed beyond `.legion/<issue>/` is the exception. The daemon marks it a
114
+ planned red and sends nothing back for it, so wait for its checks yourself (*Fix a prose-only
115
+ finding yourself*, above). A review of a head the handoff push then replaces names a head the pull
116
+ request no longer has. The daemon moves the issue once both are in — the decision GitHub reports
117
+ and your completion, in either order — so a review posted without a completion leaves the issue in
96
118
  reviewing until you finish.
97
119
 
98
120
  ## The architect's acceptance
99
121
 
100
122
  - Between the reviewer's approval and retro the issue sits in `accepting` (Dispatch status
101
123
  `needs_review`), where no worker is started: the tree's architect reads the evidence (the
102
- tester's handoff, the PR body's `E2E (implementer)` and `E2E (tester)` lines, the reviewer's
103
- approval of the head by SHA, and for a parent that every child is done) and accepts with the
124
+ tester's handoff, the PR body's `E2E (implementer)` and `E2E (tester)` lines and the reviewer's
125
+ approval of the head by SHA; children still running do not hold it) and accepts with the
104
126
  `legion` tool's `sign_off`, which moves the issue to retro, or sends it back to `implementing`
105
127
  or `testing` with `request_backward_move` and a reason, which reaches the started worker's task
106
- as its `Reason:` line and counts a review round. The acceptance asks no `Production:` line of
107
- anyone: that line is written after the close (*The production check after the close*, below).
108
- CI red at the head in `accepting` returns the issue to `implementing` as it does in review.
128
+ as its `Reason:` line and counts a review round. The architect also reads the reviewer's own
129
+ prose commits here, with the rest of the evidence, and that read is no new round. It does not
130
+ accept while the issue holds a finding ask no controller has taken (`findings.md`, "When no
131
+ controller takes it"). The acceptance asks no `Production:` line of anyone: that line is written
132
+ after the close (*The production check after the close*, below).
133
+ CI red at the head in `accepting` returns the issue to `implementing` as it does in review. A
134
+ head the review App pushed beyond `.legion/<issue>/` is the exception: the daemon marks it a planned red
135
+ and acts on it nowhere, and GitHub holds the merge under the base branch's own rules.
109
136
 
110
137
  ## Retro
111
138
 
@@ -246,11 +273,13 @@ reviewing until you finish.
246
273
  previous release, a daemon still running the previous commit, a deploy nobody has run): the
247
274
  summary names what is missing, and the `Production:` line stays at
248
275
  `pending deploy of <merge sha>`.
249
- - `fail`, for a defect: open a `dispatch ask` for it on the controller's report issue, as
250
- `skill://legion-worker/references/findings.md` says, publish its reference to the controller
251
- topic, and file no issue. It names this issue, the pull request, the merge commit, what you
252
- drove and what failed. Name the ask (`dispatch://<REPORT KEY>/ask/<id>`) in the `Production:`
253
- line and in the summary.
276
+ - `fail`, for a defect: file no issue, and take the path
277
+ `skill://legion-worker/references/findings.md` gives it. A defect with a clear fix goes to the
278
+ controller topic, with its evidence comment on the controller's report issue. A defect that
279
+ needs a decision is a `dispatch ask` on the report issue, and you publish its reference to the
280
+ controller topic. Either names this issue, the pull request, the merge commit, what you drove
281
+ and what failed. Name the message or the ask (`dispatch://<REPORT KEY>/ask/<id>`) in the
282
+ `Production:` line and in the summary.
254
283
  - **Nothing waits in the session for a deploy.** A `pending` ends the look. Where no wake is
255
284
  queued yet, the daemon suspends your claim with its session kept, and the issue's Sandbox goes
256
285
  with its pod and disk; where a wake arrived while the look was open, the next look starts at once
@@ -278,8 +307,11 @@ reviewing until you finish.
278
307
  run because production did not carry the merge within the bound.
279
308
  - **The record lives on the pull request alone.** Dispatch refuses a message, a comment or an ask
280
309
  on a closed issue (`ISSUE_CLOSED`), so the deployment instructions' "on the pull request and the
281
- issue" is the pull request for a closed issue, and a defect reaches Dispatch as the ask above.
282
- The architect accepted the issue before the merge and is asked nothing after it. `pass` or
310
+ issue" is the pull request for a closed issue. A defect reaches Dispatch by the two paths above:
311
+ a clear defect as the message to the controller, which gives it to the tree that owns that code,
312
+ and any other as the ask on the
313
+ controller's report issue. The architect accepted the issue before the merge and is asked
314
+ nothing after it. `pass` or
283
315
  `fail` closes the check: your claim is retired and the issue's Sandbox goes with its pod and
284
316
  disk, for a root as for a child. Neither ending retires or suspends anything of an issue back in
285
317
  the workflow (a root reopened inside its linger): its claim and pod are the new run's.
@@ -43,7 +43,7 @@ Verified the implementer's proof by <re-running its command | driving the same s
43
43
  **Production:** <what was checked in production, how, what was observed> — merge commit <sha>; woken by <event>.
44
44
  (written by the implementer from the production check after the merge closes the issue, naming what woke that look; `pending deploy of <sha>` until then)
45
45
 
46
- **Fast-follow:** <one named cleanup item and where it will land>, or "none".
46
+ **Fast-follow:** "none", or one named cleanup item in a file this pull request does not touch, and where it will land.
47
47
 
48
48
  **Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
49
49
  ```
@@ -124,13 +124,13 @@ and the tester's proof below are both this proof.
124
124
  tester names that missing surface as the blocker instead of passing the phase. Environment or
125
125
  secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
126
126
  `.legion/<issue>/test.json`, and only when the issue's acceptance criteria call for it — never
127
- re-pasted into the PR body each round. After a conflict-forced rebase, compute the
127
+ re-pasted into the PR body each round. After a forward merge, compute the
128
128
  fingerprint (*The unchanged-diff check* in
129
129
  `skill://legion-worker/references/conflicts-and-rewrites.md`) at the head your `E2E` line
130
130
  names and at the new head. Equal: re-run only the
131
131
  bare gates — the repository's CI green at the new head and its smoke check — and change the
132
132
  `E2E` line's head to the new SHA with
133
- `rebase re-check <old-sha> → <new-sha>: fingerprint unchanged, bare gates only`; the
133
+ `forward-merge re-check <old-sha> → <new-sha>: fingerprint unchanged, bare gates only`. The
134
134
  real-surface verification is not repeated. Different: a full test round.
135
135
  - **The implementer runs `skill://ce-simplify-code` once per pull request, in its first
136
136
  implementing round, after its own proof and before it completes that round, when the diff
@@ -144,8 +144,12 @@ and the tester's proof below are both this proof.
144
144
  That cost is why 0-applied is the expected outcome and a pass that applies is spent sparingly.
145
145
  The reviewer's pair runs at the head each round reviews. Record both in the `Thermo` line.
146
146
  - **No deferrals** is the body's rule (*PR body, review, and the merge gate* in
147
- `skill://legion-worker`): the `Fast-follow:` line holds naming, duplication, or wording cleanup
148
- only. A base frozen for others to stack on is never rewritten (*Rewriting pushed commits* in
147
+ `skill://legion-worker`): a wording nit, and any clear fix in a file this pull request touches,
148
+ is made in the running round, because a later round costs a CI pass and a re-read of the whole
149
+ change (the operator's answer on that case: fix it while you are there, and defer no fix to
150
+ another issue). The `Fast-follow:` line reads
151
+ `none`, or names only cleanup in files this pull request does not touch.
152
+ A base frozen for others to stack on is never rewritten (*Rewriting pushed commits* in
149
153
  `skill://legion-worker/references/conflicts-and-rewrites.md`); the `Chain` line records it.
150
154
  - **GitHub refuses a pull request body over 65,536 characters.** Each verification round links its
151
155
  evidence (the run, the comment) rather than inlining it once the body passes about 48,000
@@ -8,7 +8,7 @@ sjawhar/legion.
8
8
 
9
9
  ## The shape GitHub honours
10
10
 
11
- A push whose commits touch only `.legion/` (a handoff-only push) ends its head's commit message
11
+ A push whose commits touch only `.legion/<issue>/` (a handoff-only push) ends its head's commit message
12
12
  with `skip-checks: true` as the last line, preceded by two empty lines, so GitHub starts no
13
13
  workflow run for it and the daemon carries the code head's verdict to it — except retro's push,
14
14
  which never carries the line whatever paths it touches, for the reason the next section gives.
@@ -1,21 +0,0 @@
1
- # Authentication and the HTTP API
2
-
3
- Read this to set up a Dispatch token, or to call a route no `dispatch` command covers.
4
-
5
- ## Agent authentication
6
-
7
- - Use a personal token. A human mints it in Dispatch **Settings → Agent tokens**. Supply it as `DISPATCH_TOKEN` or as `dispatch.token` in the session's `envoy.json`. The server records that human as the owner of your writes.
8
- - Which `envoy.json`: the session's own directory under an Oh My Pi profile other than `default`, a Claude Code `CLAUDE_CONFIG_DIR` or an OpenCode `OPENCODE_CONFIG_DIR`. Otherwise the file `ENVOY_CONFIG_FILE` names, else `~/.config/legion/envoy.json` (under `$XDG_CONFIG_HOME` when set).
9
- - The plugin passes its choice in `ENVOY_CONFIG_FILE` with `DISPATCH_HOST`. In a Claude Code Bash that lacks both, `CLAUDE_CODE_CHILD_SESSION=1` makes the command read `CLAUDE_CONFIG_DIR`'s file. A Legion pane reads the `DISPATCH_TOKEN_FILE` path the daemon sets.
10
- - A `dispatch.token` written `!<command>` is a command that prints the token.
11
- - A checkout's `.envoy/envoy.json` sets `dispatch.serverUrl` and `dispatch.token` only together. Its `envoy` section is not read. A file's token goes only to its own server: a `DISPATCH_URL` for another server needs `DISPATCH_TOKEN` or `DISPATCH_TOKEN_FILE`.
12
- - Never configure `DISPATCH_AGENT_TOKEN` (the shared devbox fallback) for one agent.
13
- - Never set the sign-in callback origin in an agent's `envoy.json`. It is `DISPATCH_SERVER_URL` in the deployment `compose/.env`: the exact URL humans type. `<DISPATCH_SERVER_URL>/auth/callback` is registered on the sign-in client (`DISPATCH_SIGNIN_CLIENT_ID`).
14
-
15
- ## Finding a route
16
-
17
- - `GET /api/v1` (no credential) lists every route as `{method, path, auth, description}`. `auth` is `public`, `any`, `human` (a bearer gets `403 HUMAN_ONLY`) or `bearer`.
18
- - `404 NOT_FOUND` with "GET /api/v1 lists every route" means a wrong path. Read the index, do not guess.
19
- - Branch on an error's `code`, never its text.
20
- - `GET /api/v1/issues` returns every match unpaged, or one page with `limit` (1–250) or `offset`: `{issues, total, limit, offset}`. Only the unpaged array is exact: a page walk can repeat or skip an issue that changes. `cursor` is `400 INVALID_QUERY`. `dispatch issues` pages for you.
21
- - Event logs page with `after` or `before` and `limit`. `GET /api/v1/search` takes `limit` up to 50 with no next page. Other routes do not page.
@@ -1,43 +0,0 @@
1
- # Asks after they open: who answers, editing, resolving, replying, and following
2
-
3
- Read this once an ask is open. Whether to ask, and how to write it, is "Before you ask" in `skill://dispatch`.
4
-
5
- ## Who answers an ask
6
-
7
- - An ask goes to the issue's assignee's Inbox: the `Assignee:` that `dispatch read` prints decides whose, not the project's default. On an unassigned issue it is in nobody's default Inbox or `Needs you` count. A project's default assignee fills in only when an issue is created with nobody named and nothing earlier in the chain below names someone; an issue created with an explicit nobody, set to nobody later, or created before the default was set stays unassigned.
8
- - `dispatch whoami` prints whom you act for: your token owner's email, or no owner (a service token shows as `<namespace>/<name>`, as your writes do). An issue you create goes to your owner, else inherits its parent's assignee, else goes to the project's default assignee when a person set one on the Settings page, else stays unassigned.
9
- - On an unassigned issue where the answer matters, assign it to your owner, or name in the question who should answer. Assign with `PATCH /api/v1/issues/{key}` and `{"assignee": "<email>"}`. An email nobody signed in with gets `ASSIGNEE_NOT_ALLOWED`.
10
- - Never reassign an issue a human holds to get a faster answer. That is the human's call.
11
-
12
- ## Handing a human a to-do, editing, resolving, and replying
13
-
14
- - A to-do follows the question-and-options contract of "Writing for the human". For a human-only action, what it changes and risks constrains the answer. No label is special. If an outcome needs a reason, say so in its description.
15
-
16
- ```bash
17
- dispatch ask --issue DSP-42 \
18
- --question 'The fix is merged but cannot run in production: the signing certificate it needs expired, and only its owner can renew it in the registrar console. Renewing replaces the certificate every client checks. How should we proceed? Recommendation: renew it now because the fix cannot run without it.' \
19
- --option 'Renew the certificate: Lets the fix run but replaces the certificate every client checks.' \
20
- --option 'Leave it expired: Changes nothing now but leaves the fix unable to run.'
21
- ```
22
-
23
- - Correct an open ask in place with `dispatch edit-ask`, not a second ask, while the same decision stays open. The human then reconfirms. You cannot edit an answered or resolved ask. If the decision of an open ask changed, retract it and open a new one. If the decision of an answered ask changed, open a new one.
24
- - `edit-ask` on a block ask writes the block too, so it mints a version and closes a spec's design gate until approved. Change anything else about the block with `dispatch doc-edit`. A retry with unchanged fields is safe.
25
- - A human can change an answer. A new `ask.answered` carries `previous_answer`. Act on the newest.
26
- - When the selected option and the typed text disagree, the text is the answer. Act on it. To clarify, open a new ask.
27
- - Retract a moot or superseded ask, or resolve one whose answer you found:
28
-
29
- ```bash
30
- dispatch resolve-ask --ask <id> --kind retracted --reason 'A newer ask supersedes this question.'
31
- ```
32
-
33
- - Use `retracted` for obsolete, `resolved` for found. Always give a reason: the question stays in its card. Resolution never records a human decision. An answered ask cannot be resolved.
34
- - `doc-edit` refuses to delete an open ask's block. A block deleted another way (an upload, the browser) closes its ask. Restoring it reopens the ask.
35
- - Reply under an ask with `dispatch comment --reply-to-ask <id>`. Close your review comment with `dispatch resolve-comment` ([Documents](skill://dispatch/references/documents.md#comments-and-suggestions)).
36
- - A human reply while the ask is open (`ask_state: open`) is a request for clarification, not an answer. Answer in the thread with `--reply-to-ask`, or reword with `edit-ask`. Do not open a second ask.
37
- - Each reply sets whose turn it is. Your default reply (`--turn human`) puts the ask in the human's `Waiting on you`. For any progress or working-on-it note, use `--turn agent`. A human reply hands the turn to you. Reads show it as `waiting_on`.
38
-
39
- ## Following
40
-
41
- - You follow an ask you opened or replied to, or one a human added you to. Its answer, edits, resolution and replies reach your agent topic directly.
42
- - Leave or rejoin: `dispatch follow --ask <id> --action unfollow` (or `follow`). `--ask` takes a full id, a `dispatch://KEY/ask/<id>`, or an 8+ hex prefix unique among your open asks. Removal arrives as `ask.follower_removed`, an add as `ask.follower_added`.
43
- - No write subscribes you to an owner. Replies to your comments and new comments on your documents also reach you. For everything else, subscribe: `envoy_subscribe notifications.dispatch.issue.<KEY>.>` or `envoy_subscribe notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The topic carries every event. `notify` controls only wake and routed delivery. A human can unsubscribe you (`subscription.removed`).
@@ -1,59 +0,0 @@
1
- # Editing a document
2
-
3
- Edit every Dispatch document (an issue's spec, a project document) in place with `dispatch doc-edit`. Never upload it again. `dispatch doc-edit --help` has the full rules for each operation: markers, task boxes, hard breaks, anchors and preconditions.
4
-
5
- ```bash
6
- dispatch doc-edit --issue <KEY> --artifact spec --summary '<names the version>' --ops-json-file - <<'EOF'
7
- [{"op": "replace", "find": "old sentence", "with": "new sentence"}]
8
- EOF
9
- ```
10
-
11
- - Ops: `replace`, `delete`, `insert`, `retype`, `move`, `delete_row`, `delete_column`. Keys: `op`, `find`, `with`, `occurrence`, `markdown`, `after`, `before`, `block`, `index`, `type`, `attributes`. An unknown key is refused before sending. A misspelled `with` would turn the `replace` into a delete.
12
- - The output names the version, `unchanged_ops`, `lost_ops`, and a `Document token:`.
13
- - Use `--summary` to name a version that records a decision.
14
-
15
- ## Targeting text
16
-
17
- - Quote text as rendered: inline code without backticks, bold without asterisks, link text without syntax, a cell by its text, code without fences. One quote stays in one textblock. Split changes that span blocks.
18
- - A miss names the three nearest blocks. A `heading:` anchor must match the whole heading. Use zero-based `occurrence` for a repeat. Re-read before you retry a missing or ambiguous target.
19
- - `dispatch doc-read --blocks` lists each live block's id, type, token, anchor count and opening words, and each table row and cell index. Only live ids address an edit.
20
-
21
- ## Rewriting paragraphs
22
-
23
- - **One `replace` per paragraph, then a read-back.** A `with` of two paragraphs is refused. Each `replace` keeps the block id and the anchors outside the text you change. An anchor on changed text loses its quote but keeps its pin.
24
- - More new paragraphs than old: add the rest as **one** `insert` holding them all, `after` the last rewritten paragraph as it now reads. Two inserts at one anchor come out reversed. An insert lands after the top-level block that holds the quote.
25
- - Fewer: `delete` each leftover paragraph by its whole text or `block` id.
26
- - `replace` keeps a block's kind. When the new text adds a non-paragraph block (heading, list, table, quote, rule, fence or typed block) beside paragraphs, `insert` it beside the paragraph you replaced. Only when no paragraph of the new text stays, `delete` the old block, then `insert` the new one, anchored on the block before or after it.
27
- - An empty `with` deletes the matched text on purpose and leaves an empty block. Read the passage and its neighbours back with `dispatch doc-read`. A grep for your new words is not a check.
28
- - Dispatch refuses block HTML (`<div>`, `<!-- -->`, a lone `<br>` paragraph) in `with`. Inline HTML stays.
29
- - To add a rule, `insert` `***`. A `---` in `with` is stored as `\---`.
30
-
31
- ## Blocks and ids
32
-
33
- - A block loses its id only when deleted: by `delete`, by `delete_row` / `delete_column` (its cells), or by a `move` that empties its container. Its anchors go with it. `replace`, `insert`, `move` and `retype` keep ids.
34
- - `delete` of a block's whole text deletes the block. A partial match keeps it. A bullet with a nested list hoists the list. A bullet with other content needs `delete {block:"<item id>"}`.
35
- - Dispatch refuses a `delete` or `retype` that takes out an open ask's block, delete-then-insert included. Reword with `replace`, relocate with `move`, or use `dispatch edit-ask`. Moving an open block out of a container and deleting the container takes two calls.
36
- - `retype` turns a paragraph or typed block into a typed block in place, keeping its id. Use it when a paragraph is the question that should become a decision.
37
- - An ask block has two ids. The block id (`:::ask{#<id> …}`) goes bare in `block` for `move` / `delete` (`block:<id>` only in `before` / `after`). `dispatch read --ref dispatch://KEY/ask/<id>` takes either id, whole or as a unique prefix of 8 or more hex characters, and prints the ask id as `Ask id:`. `dispatch open-asks` lists ask ids. `edit-ask`, `resolve-ask`, `follow` and `--reply-to-ask` take only the ask id.
38
-
39
- ## Tables
40
-
41
- - `delete_row` / `delete_column` take the table `block` and a zero-based `index`. Row 0 is the header. The last body row and a row's last column cannot go. Open asks or unresolved comments on the cells block it.
42
- - Inserting bare body rows (no header or delimiter) at a cell quote extends that table. A wider row is `TABLE_WIDTH`. A full table with a header pasted there becomes a second table.
43
-
44
- ## Stale and concurrent edits
45
-
46
- - Guard with `--precondition-json`: `{"document": "<token>"}` or `{"blocks": [{"id": "<id>", "token": "<token>"}]}`. Use a document token for `insert` and `move`. A block guard must cover every block the batch changes.
47
- - Chain guarded edits by passing each output's token to the next. Read once, not once per edit.
48
- - `409 PRECONDITION_FAILED` or `409 EDIT_LOST_TO_CONCURRENT_CHANGE`: nothing written. Re-read and decide again. Non-empty `lost_ops` on success: a browser deleted that text after the write. Re-read before you build on it. A result that could not check survival: re-read.
49
- - A batch that changes nothing mints no version, even with `--summary`. `POST /api/v1/artifacts/<id>/versions` names the current state as a version on purpose.
50
-
51
- ## Edge cases
52
-
53
- - Inside a code block, `with` is the code's literal text. Trailing line breaks, and whitespace-only lines in a list item's code, do not survive a read.
54
- - A CR LF or lone CR in any text you write is stored as a line feed.
55
- - Replacing a non-paragraph block writes its marker as text. A backtick fence becomes an inline code span. A tilde fence stays literal.
56
- - An anchor that straddles the rewritten text keeps its mark on the words you left, with a shorter quote.
57
- - Emptying a task item's first paragraph while another block follows it is `INVALID_OP`.
58
- - A later op in one batch that names a block an earlier `delete {block}` took out fails as `INVALID_OP`. So does a move anchored inside the moved block, or a delete that leaves a typed block without its required body.
59
- - An op that only takes text out is never reported lost.