@sjawhar/opencode-legion-envoy 6.10.1 → 6.11.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.
@@ -22,7 +22,7 @@ with the `read` tool: a read by filesystem path stops at 300 lines.
22
22
  | When | Read |
23
23
  | --- | --- |
24
24
  | You write or edit the PR body, record a proof (an `E2E` line or a handoff `proof` array), verify another phase's proof, or run the simplify pass | `skill://legion-worker/references/pr-body.md` |
25
- | You reply to, accept, or resolve a review thread, or run `legion threads resolve` | `skill://legion-worker/references/review-threads.md` |
25
+ | You reply on or resolve a review thread, list a pull request's threads, or adjudicate a bot's finding | `skill://legion-worker/references/review-threads.md` |
26
26
  | GitHub reports a conflict, the pull request is retargeted, you compare heads after a conflict merge, or you would rewrite a pushed commit | `skill://legion-worker/references/conflicts-and-rewrites.md` |
27
27
  | You review or approve, build the READY packet, or check the merge in production | `skill://legion-worker/references/merge-gate.md` |
28
28
  | The issue renames a repository, package, or URL across the codebase | `skill://legion-worker/references/systematic-rename.md` |
@@ -60,10 +60,8 @@ only on this phase's artifact.
60
60
  You never start another Legion role: the daemon starts every phase worker itself, from its fixed
61
61
  workflow table. You may still use ordinary `task` subagents for your own phase work; none of them
62
62
  is a Legion role. A subagent runs in your pane as you, with every host tool but the `legion` tool
63
- (it is never offered one), so a `legion` command from its bash, a completion included, is yours at
64
- the daemon: give a subagent no completion step. Today such a command runs on the per-call grant
65
- your own last credentialed call wrote, within its 60 seconds (*Four facts about `gh`*, below);
66
- LEGION-631 (dispatch://LEGION-631) replaces that grant with the role's mounted token file.
63
+ (it is never offered one), so completing your phase is yours alone: give a subagent no completion
64
+ step.
67
65
  Escalate a product, scope, design, cross-phase, or lifecycle decision to the owning architect with
68
66
  `envoy_publish` to its role topic (`notifications.role.` followed by its encoded token, see
69
67
  above), carrying the verified facts and the decision needed. A `write` to `agent://` only reaches
@@ -90,7 +88,7 @@ implementer's own proof on a production-like surface at the head that merges, an
90
88
  head included (`skill://legion-worker/references/pr-body.md#what-a-proof-is`,
91
89
  `skill://legion-worker/references/pr-body.md#the-rules-every-phases-evidence-follows`); and the
92
90
  implementer's production check after the merge
93
- (`skill://legion-worker/references/merge-gate.md#after-the-human-merge`).
91
+ (`skill://legion-worker/references/merge-gate.md#after-the-merge`).
94
92
 
95
93
  ## Asking another role
96
94
 
@@ -118,25 +116,24 @@ Concurrent issues have disjoint workspaces; only the currently active phase muta
118
116
  one. After you complete and go idle, treat `$LEGION_WORKSPACE` as read-only in every later
119
117
  turn, including one an Envoy question starts: you are kept alive to answer questions, not to keep
120
118
  editing. Do not create new commits, run `jj -R "$LEGION_WORKSPACE" new`, or touch tracked files
121
- once your own handoff is committed (and, for the implementer, pushed) — a code change belongs to
119
+ once your own handoff is committed and pushed — a code change belongs to
122
120
  whichever phase is active now. The same holds once the Go daemon has taken your phase back without
123
121
  your completion: CI settling red while you tested, or reviewed a round you had not completed,
124
122
  moves the issue to `implementing` and interrupts your turn, and the implementer starts only once
125
123
  that turn has ended. Do not resume the interrupted work afterward.
126
124
 
127
125
  On every start, and especially after revival or re-creation, read the issue and then the
128
- committed predecessor handoffs in lifecycle order, with the `legion` tool's `handoff_read`:
129
-
130
- 1. `architect.json`
131
- 2. `plan.json`
132
- 3. `implement.json`
133
- 4. `test.json`
134
- 5. `review.json`
135
-
136
- Read only files that precede the assigned phase. Each was held to its phase's rules when it was
137
- written (`handoff_write`, in the completion gate below): fields the phase does not declare passed
138
- untouched and reach the next worker. The `legion` tool's `handoff_read` returns them; its
139
- description says where it reads each one from.
126
+ committed predecessor handoffs in lifecycle order, with the `read` tool from
127
+ `$LEGION_WORKSPACE/.legion/<issue>/`:
128
+
129
+ 1. `plan.json`
130
+ 2. `implement.json`
131
+ 3. `test.json`
132
+ 4. `review.json`
133
+
134
+ Read only files that precede the assigned phase. Each was checked against its phase's rules when
135
+ its phase completed (the completion gate below): fields the phase does not declare passed
136
+ untouched and reach the next worker.
140
137
  Write the phase-specific fields the next phase and the architect need, consistent with what
141
138
  predecessor phases already wrote. The durable copy lives in
142
139
  `$LEGION_WORKSPACE/.legion/<issue>/<phase>.json`. If a committed handoff conflicts with memory or a prior
@@ -146,32 +143,6 @@ If your task, prompt, or a message since your last turn begins `Your workspace w
146
143
  your phase's committed handoff and any `.legion/<issue>/workspace-recovered.json`, and reconcile
147
144
  before new work: anything unpushed is gone. That message stays in history; reconciled, it is done.
148
145
 
149
- ## jj Safety Rules
150
-
151
- - **Always `jj -R "$LEGION_WORKSPACE" new` to create isolated commits.** Never
152
- `jj -R "$LEGION_WORKSPACE" edit @-` to go back to a parent — this changes what `@` points
153
- to and makes `jj abandon` dangerous.
154
- - **Never `jj -R "$LEGION_WORKSPACE" abandon`.** If a mistake would require abandoning
155
- work, stop and send the owning architect the `jj -R "$LEGION_WORKSPACE" log` evidence.
156
- - **Before pushing, check ancestry:** `jj -R "$LEGION_WORKSPACE" log -r 'ancestors(@, 5)'`
157
- — verify only your issue's commits are in the chain, not unrelated work.
158
-
159
- **Shared operation safety:** Under tmux every issue workspace is a `jj workspace` of one shared
160
- clone and operation log; under Kubernetes each issue pod has its own, shared by that issue's roles
161
- alone. Either way `jj undo`, `jj abandon`, and `jj op restore|revert|abandon|undo` rewrite a log
162
- other agents work in. The extension refuses them in every phase-worker pane before they run — a
163
- `bash` command in any position of a pipeline or `&&` chain, with or without `-R`, judged on the
164
- whole argument list (a supervised service's start included); `eval` code; and stdin written to a
165
- service (a `write` to `proc://<id>`) — from your own tool calls and from any `task` subagent you
166
- spawn (it runs in your pane, against the same log), and a `bash` command whose quoted text merely
167
- mentions `jj` with one of those words (a heredoc, an echo, a commit message) is refused too: write
168
- such text with the `write` tool or say "operation-log rollback" instead. `jj restore <paths>`,
169
- `jj op log`, and `jj op show` stay allowed. Recover forward only: a new commit
170
- (`jj -R "$LEGION_WORKSPACE" new`) or `jj -R "$LEGION_WORKSPACE" restore <paths>` of files.
171
- Anything else, stop and send the owning architect the `jj -R "$LEGION_WORKSPACE" log`
172
- evidence; the architect decides, and an operator performs any operation-log restore with every
173
- other tree paused.
174
-
175
146
  ## Phase work
176
147
 
177
148
  Specifications written into Dispatch follow `skill://dispatch`'s [Writing a spec](skill://dispatch/SKILL.md#writing-a-spec), except that a phase worker writes no decision block: it sends an open product, scope or design decision to its architect, which writes the block.
@@ -184,6 +155,9 @@ An issue that renames a repository, package, or URL across the codebase also fol
184
155
  `skill://legion-worker/references/systematic-rename.md`; one that deletes code, a command, or
185
156
  documentation follows `skill://legion-worker/references/cleanup-deletion.md`.
186
157
 
158
+ Create isolated commits with `jj -R "$LEGION_WORKSPACE" new` and reviewable ones with
159
+ `jj -R "$LEGION_WORKSPACE" split -m "<message>" <paths…>`, each holding only your phase's paths.
160
+
187
161
  Commit attribution is automatic: the extension exports a `JJ_CONFIG` overlay when your
188
162
  session starts, so every jj commit you make carries an `Omp-Session: <this-session-id>`
189
163
  trailer with no action from you. Do not add attribution trailers by hand.
@@ -214,38 +188,28 @@ architect that log; do not accept it as a side effect. A wrong identity on your
214
188
  other App or none, is a pane-environment problem to report to the architect, not something to
215
189
  pin (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`,
216
190
  Hazard 1).
217
- Your session receives the credential capability it needs; invoke GitHub through the
218
- credential helper:
191
+ Your pane's `gh` and `git` already carry your role's GitHub identity; invoke GitHub with plain
192
+ `gh`:
219
193
 
220
194
  ```bash
221
- legion gh -- <gh args…>
195
+ gh <gh args…>
222
196
  ```
223
197
 
224
- Four facts about `gh` in a worker pane. The `gh` on your `PATH` is a shim
225
- (`<state_dir>/worker-bin/gh`, installed by the daemon at startup — `packages/daemon/internal/runtime/workerbin/workerbin.go`)
226
- that execs `legion gh -- "$@"`, so `gh …` and `legion gh -- …` are the same call, and each call
227
- redeems a fresh token from your session's grant — identity is supplied per call, never stored.
228
- Never run `gh auth login` or `gh auth setup-git`; there is no login state to create. The shim
229
- refuses `pr merge` (and a raw `gh api …/merge` or a GraphQL mutation) for every role: Legion never
230
- merges. It also refuses every GitHub-issue write — the `issue`
231
- subcommand's `comment`, `create`, `edit`, `close`, `reopen`, `delete`, `pin`, `unpin`, `transfer`,
232
- `lock`, `unlock`, and `develop`, and any raw `gh api` call to an `/issues` path whose method is not
233
- GET (an explicit `-X`, or the POST that `-f`/`-F`/`--input` imply; pull-request conversation
234
- comments live on that path too, so edit them with `gh pr comment`) — printing
235
- ``Legion issues live on Dispatch; use `dispatch message` or `dispatch comment` on <your LEGION_ISSUE>``:
236
- Legion never reads or writes a GitHub issue. `pr comment`, `pr review`,
237
- `api …/pulls/…`, `api graphql`, and issue reads are unaffected. The credential reaches `legion`
238
- through the file `$LEGION_GRANT_FILE` names, written by the extension before each of your bash
239
- commands, each `github` tool call, and each `read`/`grep` of a `pr://` or `issue://` URL (and by
240
- the `legion` tool before its `handoff_complete`); never `cat`, `echo`, copy, or
241
- `export` it — `legion credential`, `legion gh`, `jj git push`, and `handoff_complete` read it
242
- themselves. The file is the pane's, not the command's, and a grant lives 60 seconds: a `task`
243
- subagent, an `eval` subprocess, or a background job in your pane reads the grant your last such
244
- call wrote, and a `github` tool `run_watch` keeps polling `gh` on the one written when the call
245
- began, so each succeeds only within 60 seconds of that call and 403s afterwards — a timing
246
- artifact, not a broken credential. Run credentialed commands from your own bash calls, and watch a
247
- run that may outlast a minute with `gh run watch` in bash, which redeems once and then runs on the
248
- token it got.
198
+ How `gh` works in a worker pane. `GH_CONFIG_DIR` names a read-only directory holding gh's own
199
+ `hosts.yml` (your role's GitHub App installation token, user `x-access-token`) and `config.yml`,
200
+ which the daemon rewrites in place from its cached lease so it never expires under you; the `gh`
201
+ on your `PATH` is the ordinary binary and reads it on every call, `git` reads the same file
202
+ through the clone's `credential.helper` (`!gh auth git-credential`), and `GH_TOKEN`,
203
+ `GITHUB_TOKEN` and `GH_HOST` are set empty so nothing outranks it. Never run `gh auth login`,
204
+ `gh auth setup-git`, `gh config set` or `gh alias set`: there is no login state to create, and the
205
+ directory is read-only. One rule nothing enforces: **Legion's issues live on Dispatch, never on
206
+ GitHub issues** — no `gh issue` write and no non-GET `gh api` call to an `/issues` path
207
+ (pull-request conversation comments live on that path too; edit them with `gh pr comment`); use
208
+ `dispatch message` or `dispatch comment` on your `LEGION_ISSUE`. The `legion` tool's own daemon
209
+ calls carry a credential of their own, minted in-process; `gh` and `git` never see it, and you
210
+ never `cat`, `echo`, copy, or `export` a token. A `task` subagent, an `eval` subprocess, or a
211
+ background job in your pane inherits the same `GH_CONFIG_DIR` and so the same credential, for as
212
+ long as it runs.
249
213
 
250
214
  **Other credentials your pod may already carry.** Before reporting that a read is unreachable,
251
215
  check for them rather than assuming none exist: `AGENT_SECRETS_URL` and `AGENT_SECRETS_KEY_DIR`
@@ -273,7 +237,7 @@ for the retro's Dispatch message (`skill://legion-retro`):
273
237
  For example:
274
238
 
275
239
  ```bash
276
- legion gh -- pr comment <pr-number> \
240
+ gh pr comment <pr-number> \
277
241
  --body $'Verification complete.\n\n<!-- legion: {"session":"<session-id>","phase":"<phase>"} -->' \
278
242
  --repo <owner>/<repo>
279
243
  ```
@@ -300,13 +264,12 @@ The implementer opens the pull request. After its implementation commit and veri
300
264
  pushes the issue branch under this exact name with the one push procedure every role uses
301
265
  (*Every role pushes its own commits*, below).
302
266
 
303
- The provisioned issue workspace configures `credential.helper` with the daemon's absolute
304
- credential command, so `legion push` authenticates transparently through the same session
305
- capability. Never handle a token.
267
+ The shared clone's `credential.helper` is `!gh auth git-credential`, so `jj git push`
268
+ authenticates as your role's App from the same file your `gh` reads. Never handle a token.
306
269
 
307
- Then open the pull request with `legion gh -- pr create`. The PR body **must** contain the
270
+ Then open the pull request with `gh pr create`. The PR body **must** contain the
308
271
  line `Dispatch: <KEY>` — the daemon's fallback link from a PR to its Dispatch issue when the
309
- branch name alone is ambiguous. The credential helper and `legion gh` provide the GitHub
272
+ branch name alone is ambiguous. Your pane's `gh` and `git` provide the GitHub
310
273
  identity; never export, fetch, or replace a token. Other phases advance the existing branch
311
274
  rather than creating a replacement bookmark or PR.
312
275
 
@@ -321,23 +284,32 @@ never one. **Before you write or edit any line of the PR body or any `proof` arr
321
284
  `skill://legion-worker/references/pr-body.md`**: the template (the sole definition of the CI
322
285
  line), the full definition of a proof, what the tester verifies, and the simplify pass.
323
286
 
324
- - **Review threads** are disposed of one by one, never in bulk, and only an `Accepted:` from the
325
- thread's opener (or, on a bot's thread, from the Legion reviewer) closes one. The implementer
326
- runs `legion threads resolve` after every push that answers a review, before its completion,
327
- and the merger before READY: `skill://legion-worker/references/review-threads.md`.
287
+ - **The tester runs the application, not CI again.** It drives the changed surface for real and
288
+ tries to break it, judges whether the implementer's tests would catch what it broke, records what
289
+ it ran and saw, and must read CI for everything static — lint, types and the unit suites are
290
+ CI's work, rerun by no tester: `skill://legion-worker/references/pr-body.md`.
291
+ - **Review threads** are answered and resolved one by one, never in bulk, and never one you have
292
+ not read: one disposition reply per thread, then that thread's own resolution by the
293
+ implementer, the pull request's author — its `gh api graphql` mutation after every push that
294
+ answers a review, before its completion, for the threads it answered and for the bot threads the
295
+ reviewer names to it as accepted (the review App cannot resolve one):
296
+ `skill://legion-worker/references/review-threads.md`.
328
297
  - **No deferrals.** A finding that changes
329
298
  behaviour, hides an error, or breaks a gate is fixed in this pull request; naming, duplication,
330
299
  or wording cleanup is batched into the one `Fast-follow:` line instead of iterating per push.
331
300
  - **A red CI job** that failed on its own is re-run with
332
- `legion gh -- run rerun <run-id> --failed`, never by pushing a new commit or bringing in the base.
301
+ `gh run rerun <run-id> --failed`, never by pushing a new commit or bringing in the base.
333
302
  - **A conflict or a retarget** is the only reason to bring the base into the branch, always as a
334
303
  forward merge and never `jj rebase`; the unchanged-diff fingerprint each role compares
335
304
  afterwards is in the same reference: `skill://legion-worker/references/conflicts-and-rewrites.md`.
336
305
  - **The merge gate**, in order: the tester's evidence green → the reviewer's approval of the head
337
- → retro → the merger's READY → the human merge → the implementer's production check. Legion
338
- never merges. The reviewer's submissions,
339
- retro's commit, the merger's READY, and the production check follow
340
- `skill://legion-worker/references/merge-gate.md`.
306
+ → retro → the merger's READY → the merge → the implementer's production check. On READY's
307
+ acceptance the merger submits the pull request with
308
+ `gh pr merge <n> -R <owner>/<repo> --auto --squash --match-head-commit <head>`, and the
309
+ repository's required reviews and checks decide when it lands
310
+ (`skill://legion-worker/references/merge-gate.md`). The reviewer's submissions, retro's commit,
311
+ the merger's READY and its submission, the implementer's disarming of a withdrawn READY, and the
312
+ production check follow the same reference.
341
313
  - **No surface reaches the changed path** is a report to the architect, never a reason to
342
314
  complete the phase: `skill://legion-worker/references/pr-body.md`.
343
315
 
@@ -350,29 +322,17 @@ A planner picking up after the issue moves back from implementing starts fresh o
350
322
  tip — `jj -R "$LEGION_WORKSPACE" new legion/<KEY>@origin` — since the implementer's unpushed
351
323
  commits are no longer in the chain (*Every role pushes its own commits*, below).
352
324
 
353
- Write the phase-specific handoff: call the `legion` tool with `op: "handoff_write"`, `phase: "<p>"`,
354
- and `data`: a JSON object of the phase-specific fields only. It runs `legion handoff write` in
355
- `$LEGION_WORKSPACE` and returns its output.
356
-
357
- A handoff built from the one already on disk (a test handoff that accumulates review rounds can
358
- pass 128 KiB) can instead be piped from bash, so you never re-emit the whole payload:
359
- `cd -- "$LEGION_WORKSPACE" && bun -e 'const h = await Bun.file(".legion/<issue>/<phase>.json").json(); delete h.schemaVersion; delete h.phase; delete h.completed; <your edit to h>; console.log(JSON.stringify(h))' | legion handoff write --phase <phase>`.
360
- The program is single-quoted, so strings in your edit take double quotes. It is `bun` because the
361
- worker image a pod runs ships `bun` and not `jq`, and a devbox pane has the `bun` Legion builds
362
- with. With `--data` omitted, `legion handoff write` reads the JSON object from stdin. The CLI adds
363
- `schemaVersion`, `phase` and `completed` itself and refuses them in the data, hence the `delete`s.
364
-
365
- `handoff_write` validates the payload against the phase's schema before writing: an
366
- implement handoff without a well-formed `proof`, or a test handoff that reports no failure and
367
- carries no `proof` of its own, exits 1 naming the field and writes nothing. Each `proof` entry, in
368
- either phase, is an object of six non-empty strings: `criterion` (the acceptance line it proves),
369
- `surface`, `command`, `observed`, `headSha` (the commit it ran at) and `negativeControl`.
370
-
371
- Then verify the durable artifact exists:
372
-
373
- ```bash
374
- test -f "$LEGION_WORKSPACE/.legion/<issue>/<phase>.json"
375
- ```
325
+ Write the handoff, `$LEGION_WORKSPACE/.legion/<issue>/<phase>.json` (`plan`, `implement`, `test`
326
+ or `review`), with the ordinary `write` tool: a JSON object with `schemaVersion: 1`,
327
+ `phase: "<phase>"`, `issue: "<KEY>"`, `completed: "<RFC 3339 UTC time of writing>"`, and the
328
+ phase's own fields exactly as your role prompt spells them — the planner's `requiredSkills`,
329
+ `gapAnalysis`, `planReview` and `specDepartures` records; the implementer's `proof` array; the
330
+ tester's `implementerProof`, `failures` and, whenever it reports no failure, its own `proof`; the
331
+ reviewer's `verdict`, counts and `keyFindings`. Nothing stamps the first four for you. Each `proof`
332
+ entry, in either phase, is an object of six non-empty strings: `criterion` (the acceptance line it
333
+ proves), `surface`, `command`, `observed`, `headSha` (the commit it ran at) and `negativeControl`.
334
+ A handoff built from the one already on disk (a test handoff that accumulates review rounds) is
335
+ read with `read`, edited, and written whole with `write`, its `completed` set to now.
376
336
 
377
337
  Then commit that exact handoff file onto the issue branch:
378
338
 
@@ -381,63 +341,100 @@ cd -- "$LEGION_WORKSPACE" && \
381
341
  jj -R "$LEGION_WORKSPACE" split -m "<phase>: record handoff" .legion/<issue>/<phase>.json
382
342
  ```
383
343
 
344
+ Then push (below) and report completion: call the `legion` tool with `op: "handoff_complete"` and
345
+ `summary` (two sentences for the architect; the tester adds `verdict: "pass"` or `"fail"`, the
346
+ merger `ready: true`). **Push before you complete is a hard rule.** `handoff_complete` finds, with
347
+ your pane's jj, the newest commit on the issue branch that carries `.legion/<KEY>/<phase>.json`
348
+ and reports it to the daemon, and refuses — posting nothing — while:
349
+
350
+ - the file has a change still in the working copy: commit it, then complete again;
351
+ - the file is missing from the workspace: write it, then commit and push it;
352
+ - the file is not committed on this branch (only the base carries it): write and commit it;
353
+ - the carrying commit was authored by another App (every role of an issue shares the workspace):
354
+ run `jj -R "$LEGION_WORKSPACE" new`, then write and commit this phase's handoff again;
355
+ - the carrying commit is not yet on `legion/<KEY>@origin`: push the issue branch, then complete
356
+ again.
357
+
358
+ Nothing checks the file's shape: the fields your role prompt spells are what the next role reads.
359
+ The daemon reads no handoff file and no branch head; it refuses, before recording anything:
360
+
361
+ - `HANDOFF_NOT_NEW` — the commit is the one you reported for your previous phase: write and
362
+ commit this phase's handoff before completing.
363
+ - `READY_HEAD_CARRIES_HANDOFFS`, `READY_HEAD_CONFLICTS` and `READY_CHECKS_NOT_GREEN` — the
364
+ merger's READY, in `skill://legion-worker/references/merge-gate.md`.
365
+ - `GITHUB_READ_FAILED` — GitHub's failure, not the head's, reading READY's checks: complete again.
366
+
367
+ Such a refusal records nothing, so the corrected call at the same commit is applied. The answer
368
+ may carry a `note` (READY published on an already-merged pull request, or a base requiring no
369
+ check).
370
+
371
+ Retro's last commit removes `.legion/<issue>/` from the head that merges (`skill://legion-retro`);
372
+ a round after it, such as one a withdrawn READY sends back, writes and commits its own handoff
373
+ again, since `handoff_complete` reports the commit carrying the file. Retro, the merger and the
374
+ post-merge production check write no `.legion/<issue>/<phase>.json`, commit no handoff, and report
375
+ with `handoff_complete` alone, which reports the commit the workspace stands on.
376
+
384
377
  **Every role pushes its own commits.** After the handoff commit — and, for the tester, the red
385
- tests it wrote — push the issue branch with `legion push`, run from bash in your workspace:
378
+ tests it wrote — push the issue branch with the plain `jj` of your pane, from bash in your
379
+ workspace; `<KEY>` is your `LEGION_ISSUE`:
380
+
381
+ ```bash
382
+ cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> -r @- && jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
383
+ ```
384
+
385
+ A push whose commits touch only `.legion/` (a handoff-only push) ends its head's commit message
386
+ with `skip-checks: true` as the last line, preceded by two empty lines, so GitHub starts no
387
+ workflow run for it and the daemon carries the code head's verdict to it. GitHub honours the
388
+ trailer only in that shape — the last line, after two empty lines; one empty line is not enough,
389
+ and GitHub then starts every workflow as if the line were not there (a planner's handoff push
390
+ did exactly that in the first live run of this rule). jj keeps the two lines as written, where
391
+ `git commit`'s default cleanup would collapse them. Write the line with trailers off for the one
392
+ describe, since the pane's jj overlay would otherwise append `Omp-Session:` after it:
386
393
 
387
394
  ```bash
388
- cd -- "$LEGION_WORKSPACE" && legion push
395
+ jj -R "$LEGION_WORKSPACE" describe -r @- --config 'templates.commit_trailers=""' -m "$(jj -R "$LEGION_WORKSPACE" log -r @- --no-graph -T description)"$'\n\n\nskip-checks: true'
389
396
  ```
390
397
 
391
- It pushes `@-` through the provisioned credential helper, which authenticates as your role's App
392
- (`appauth.AppRoleFor` in `packages/daemon/internal/appauth/identity.go`). Your system prompt says
393
- which pushes skip CI and which commit-message keywords it refuses. While the head commit carries
398
+ Every other push — code, red tests, `docs/solutions/`, retro's removal of `.legion/<KEY>/`, a
399
+ forward merge, a rewrite — carries no such line and none of GitHub's bracket keywords
400
+ (`[skip ci]`, `[ci skip]`, `[no ci]`, `[skip actions]`, `[actions skip]`), which start no workflow
401
+ wherever they stand. The daemon classifies a push by the paths its commits touch, never by the
402
+ trailer: a wrong trailer stalls the issue (a trailered code push gets no verdict carried; a head
403
+ with no CI is refused at READY) and never passes it. While the head commit carries
394
404
  `skip-checks: true`, a pull-request body edit alone starts no GitHub workflow run, so a check that
395
405
  re-judges the body re-runs on that head only after a later push; on a code head the same edit
396
406
  re-runs it at once. While the pull request conflicts with its base (GitHub shows it
397
407
  `CONFLICTING`), no workflow triggered `on: pull_request` runs for any of its activity types, so
398
408
  a body edit re-judges nothing there either; a push cures both, but only once the pull request is
399
409
  mergeable, so the implementer forward-merges a conflicting one first (*Reintegrating the base* in
400
- `skill://legion-worker/references/conflicts-and-rewrites.md`). Its ancestry check refuses
401
- unless `@-` descends from `legion/<KEY>@origin` (or the branch is not on GitHub yet): your issue's
402
- roles share one clone (under tmux every issue does), so another role's push moves the remote branch
403
- here at once, and a push not descending from it would move it sideways onto your commit and
404
- drop theirs. A handoff commit never sits on an implementer's unpushed chain: when the issue moves
405
- back to planning, the implementer's unpushed commits stay off the bookmark until the implementer
406
- returns, and the planner writes its handoff on the remote tip.
407
-
408
- Before any rewrite of a commit you already pushed — a `jj squash --into` one, or any other
409
- rewrite — read *Rewriting pushed commits* in
410
- `skill://legion-worker/references/conflicts-and-rewrites.md`: it checks that no other tree's work
411
- is built on that commit and records the pushed tip `legion push` reads, the only case in which a
412
- push lets the remote branch move off its current tip.
413
-
414
- Before the push, check ancestry and identity as above: the chain carries every earlier phase's
415
- pushed commits, and pushing them with yours is expected. A refusal, and a push the remote rejects, is a
416
- report to the architect with the output, never a force-push. The merger makes no commit and
417
- pushes nothing.
418
-
419
- Do not report phase completion until the write, existence check, handoff commit, and push
420
- succeed. This is the committed copy the next phase reads after revival. Retro's last commit
421
- removes `.legion/<issue>/` from the head a human merges (`skill://legion-retro`); a round after
422
- it, such as one a withdrawn READY sends back, writes and commits its own handoff again, since
423
- `handoff_complete` refuses a handoff that commit removed. Retro and the post-merge production
424
- check write no `.legion/<issue>/<phase>.json`, commit no handoff, and report with
425
- `handoff_complete` alone (below).
410
+ `skill://legion-worker/references/conflicts-and-rewrites.md`).
411
+
412
+ Before the push, inspect `jj -R "$LEGION_WORKSPACE" log -r 'ancestors(@, 5)'` and run the
413
+ identity check above: the chain carries every earlier phase's pushed commits, and pushing them
414
+ with yours is expected. Your issue's roles share one clone (under tmux every issue does), so
415
+ another role's push moves `legion/<KEY>@origin` here at once, and `jj git push` refuses a remote
416
+ branch that moved: report the refusal to the architect with its output, never force. A handoff commit never
417
+ sits on an implementer's unpushed chain: when the issue moves back to planning, the implementer's
418
+ unpushed commits stay off the bookmark until the implementer returns, and the planner writes its
419
+ handoff on the remote tip. The one sanctioned non-fast-forward move is the rewrite of a commit you
420
+ already pushed — a `jj squash --into` one, or any other rewrite — and before it, read *Rewriting
421
+ pushed commits* in `skill://legion-worker/references/conflicts-and-rewrites.md`: its guard checks
422
+ that no other tree's work is built on that commit, and only after it clears is the bookmark moved
423
+ with `--allow-backwards`. The merger makes no commit and pushes nothing.
426
424
 
427
425
  ## Completion: report to the architect, then stay
428
426
 
429
- Report completion to the architect with the `legion` tool's `handoff_complete` and a two-sentence
430
- `summary`. This publishes your phase's completion to the architect's role and clears the daemon's
431
- record of this issue's active phase. A shell `legion handoff complete`, should one run, completes
432
- the phase at the daemon just as well and is not refused, but only the tool call tells the
433
- extension's phase stall: after a shell completion the reminder arrives when the turn settles and
434
- again after each later Envoy delivery's turn, until a tool `handoff_complete` next succeeds (in
435
- practice the next phase's). A completion made on that reminder is refused and closes nothing —
436
- `HANDOFF_NOT_CURRENT_PHASE` once the phase has moved, `HANDOFF_ALREADY_RECORDED` where a
437
- completion moves nothing (the implementer before its pull request exists; the production check) —
438
- so answer the reminder with a WAITING line saying the phase was completed from the shell. Do not
439
- add pipeline labels, run a controller loop, or invent a different completion protocol — this is the
440
- whole contract.
427
+ Report completion to the architect: call the `legion` tool with `op: "handoff_complete"` and
428
+ `summary`: two sentences for the architect. The tool is your only way to the daemon
429
+ (`handoff_complete`; `read_record`, your issue's record, which is what "re-read your issue
430
+ record" means; and `request_backward_move`), and nothing of yours runs `legion` from bash. The
431
+ tool call is what the extension's phase stall records, and a turn that ends with the phase still
432
+ open gets one reminder; do not complete again on that reminder when your first completion was
433
+ accepted — it stands, and a second is refused because the issue has already left your phase.
434
+
435
+ This publishes your phase's completion to the architect's role and clears the daemon's
436
+ record of this issue's active phase. Do not add pipeline labels, run a controller loop, or
437
+ invent a different completion protocol — this is the whole contract.
441
438
 
442
439
  A reviewer's phase ends with its completion, not with its review; the order of a review round
443
440
  (the handoff push; for an approval, CI settled green at that head; the review of that head; then
@@ -19,11 +19,13 @@ Every path it cites is in sjawhar/legion.
19
19
  the fingerprint at the current tip; after pushing the merged branch, record it at the new
20
20
  tip; post one PR comment (Legion footer):
21
21
  `rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
22
- Every issue workspace is a `jj workspace` of the same shared repository and operation log, and
23
- jj always rebases every descendant of any commit it rewrites — a revset naming the root of your
24
- own chain and rewriting it in place also rewrites whatever another tree has stacked on that root,
25
- whichever selector chose it (`-s`, `-b`, and `-r` all rewrite descendants; `-r` only re-parents
26
- them to fill the hole, which is worse).
22
+ On the tmux runtime, where issue workspaces are still jj workspaces of one shared clone,
23
+ `jj undo`, `jj abandon` and `jj op restore` rewrite the shared operation log, so a mistake there
24
+ is recovered forward (`jj -R "$LEGION_WORKSPACE" new`, or `jj -R "$LEGION_WORKSPACE" restore
25
+ <paths>` of files). In that shared repository jj always rebases every descendant of any commit it
26
+ rewrites — a revset naming the root of your own chain and rewriting it in place also rewrites
27
+ whatever another tree has stacked on that root, whichever selector chose it (`-s`, `-b`, and
28
+ `-r` all rewrite descendants; `-r` only re-parents them to fill the hole, which is worse).
27
29
  Resolve the conflict with a forward merge instead of a rewrite — merge the branch's own
28
30
  bookmark with the destination in one new commit, so nothing existing is rewritten and nothing
29
31
  built on your prior commits, in this tree or another, ever moves:
@@ -38,14 +40,14 @@ Every path it cites is in sjawhar/legion.
38
40
  ancestry and the push fails (`Won't push commit … since it has no description`); the bookmark
39
41
  is always on a described, already-pushed commit. If the merge conflicts, resolve it in that
40
42
  one commit — edit the markers directly; there is nothing to squash, since the merge is the
41
- only new commit. Then `jj -R "$LEGION_WORKSPACE" new` to move off it, and push with the one
42
- push procedure (*Every role pushes its own commits* in `skill://legion-worker`): the merge descends from both the
43
- bookmark's old position and the destination, so it is a genuine fast-forward and *Rewriting
44
- pushed commits* never applies — nothing was rewritten, so there is no tip to record first.
43
+ only new commit. Then `jj -R "$LEGION_WORKSPACE" new` to move off it, and push it (*Every role
44
+ pushes its own commits* in `skill://legion-worker`; a merge is a code push, so its head carries
45
+ no `skip-checks: true` line): the merge descends from both the bookmark's old position and the
46
+ destination, so it is a genuine fast-forward and *Rewriting pushed commits* never applies.
45
47
  - **After a retarget.** Retargeting a pull request to a new base does not re-run Tests. Merge the
46
- bookmark onto the new base (`jj new legion/<KEY> <new base> -m "<message>"`) and push with the
47
- ordinary push procedure — a genuine fast-forward, never the procedure for rewritten commits —
48
- so the new head runs Tests against the new merge result, and cite that run in the PR body.
48
+ bookmark onto the new base (`jj new legion/<KEY> <new base> -m "<message>"`) and push it the
49
+ ordinary way — a genuine fast-forward, never `--allow-backwards` — so the new head runs Tests
50
+ against the new merge result, and cite that run in the PR body.
49
51
 
50
52
  ## The unchanged-diff check
51
53
 
@@ -63,7 +65,7 @@ cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && \
63
65
  - `fork_point(main@origin | <head-sha>)` is the base the branch was cut from *at that head*:
64
66
  the old base for the pre-rebase head, the new base for the rebased one, so one command
65
67
  serves both sides. On a stacked PR substitute its base branch for `main`
66
- (`legion gh -- pr view <n> --json baseRefName`).
68
+ (`gh pr view <n> --json baseRefName`).
67
69
  - A head the rebase hid is still addressable by its SHA in the shared workspace. A SHA the
68
70
  workspace cannot resolve (`jj -R "$LEGION_WORKSPACE" log -r <sha>` errors) counts as a
69
71
  changed diff — never as unchanged.
@@ -82,7 +84,7 @@ cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && \
82
84
 
83
85
  Where each role gets its two heads: the implementer — the tip before and after its own rebase;
84
86
  the tester — the head its `E2E` line names and the new head; the reviewer — the `commit_id` of
85
- its last submitted review (`legion gh -- api repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login}'`)
87
+ its last submitted review (`gh api repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login}'`)
86
88
  and the new head; the merger never computes a fingerprint — it uses the `--summary` check in
87
89
  `skill://legion-worker/references/merge-gate.md`.
88
90
 
@@ -90,25 +92,17 @@ and the new head; the merger never computes a fingerprint — it uses the `--sum
90
92
 
91
93
  **Rewriting pushed commits** — a `jj squash --into` a commit already on GitHub, or any other
92
94
  rewrite of a commit you already pushed — is the hazard *Reintegrating the base* describes, in a
93
- second shape: jj rebases
94
- every descendant of any commit it rewrites, and in the one shared repository a descendant can be
95
- another tree's branch stacked on your pushed commit, which then moves, with its bookmark, onto a
96
- rewritten copy. So look for a descendant outside your own chain first, and record the pushed tip
97
- — which the rewrite leaves outside `::@-` — after a fetch and while your chain still descends
98
- from it:
95
+ second shape: jj rebases every descendant of any commit it rewrites, and in the one shared
96
+ repository a descendant can be another tree's branch stacked on your pushed commit, which then
97
+ moves, with its bookmark, onto a rewritten copy. So look for a descendant outside your own chain
98
+ first, after a fetch and while your chain still descends from the pushed tip:
99
99
 
100
100
  ```bash
101
101
  cd -- "$LEGION_WORKSPACE" && \
102
102
  jj -R "$LEGION_WORKSPACE" git fetch && \
103
103
  foreign=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
104
104
  -r 'descendants(<the commit you are about to rewrite>) ~ ::@') && \
105
- { [ -z "$foreign" ] || { echo "not mine, and descends from the commit to rewrite: $foreign" >&2; false; }; } && \
106
- behind=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
107
- -r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin") ~ ::@-') && \
108
- { [ -z "$behind" ] || { echo "legion/<KEY>@origin is at $behind, which @- does not descend from" >&2; false; }; } && \
109
- jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id' \
110
- -r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin")' \
111
- >"${TMPDIR:-/tmp}/legion-<KEY>-$LEGION_ROLE-rewritten-tip"
105
+ { [ -z "$foreign" ] || { echo "not mine, and descends from the commit to rewrite: $foreign" >&2; false; }; }
112
106
  ```
113
107
 
114
108
  `descendants(<commit>) ~ ::@` is everything built on the commit you are about to rewrite that is
@@ -116,10 +110,18 @@ not on your own chain. Non-empty means the rewrite would move work that is not y
116
110
  rewrite it. Put the change in a new commit on top instead, and report the listed commits to the
117
111
  architect.
118
112
 
119
- Then rewrite, resolve, and push with the one push procedure (*Every role pushes its own commits*
120
- in `skill://legion-worker`). It lets the remote branch sit on the
121
- tip you recorded, which the rewrite replaced, and on nothing else: when another role pushed after
122
- you recorded it, the push is refused. The push deletes the file.
113
+ Once the guard has cleared, rewrite, resolve, and push once, moving the bookmark backwards onto
114
+ the rewritten head — the one sanctioned non-fast-forward move, and the only place
115
+ `--allow-backwards` is ever used:
116
+
117
+ ```bash
118
+ cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> -r @- --allow-backwards && jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
119
+ ```
120
+
121
+ A rewrite head is a code push and never carries the `skip-checks: true` line: when the rewrite
122
+ carried one onto it (a squash of a trailered handoff commit into a code commit), describe the head
123
+ again without that line before you push. `jj git push` still refuses a remote branch another role
124
+ moved since your fetch: report the refusal to the architect with its output, never force.
123
125
 
124
126
  Once a base is frozen for others to stack on, never rewrite it: fixes land as new commits on top,
125
127
  and the PR body's `Chain` line records what is frozen.