@sjawhar/pi-legion-envoy 1.24.3 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/envoy.js CHANGED
@@ -29972,7 +29972,7 @@ var dispatchToolSpecs = [
29972
29972
  },
29973
29973
  {
29974
29974
  name: "dispatch_doc_edit",
29975
- description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29975
+ description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + "For replace, delete, and quote insert anchors, find text exactly as rendered: omit Markdown markers such as backticks or asterisks. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29976
29976
  arguments: (z) => ({
29977
29977
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29978
29978
  project: z.string().describe("Project key owning the document.").optional(),
@@ -30103,6 +30103,15 @@ var HANDOFF_SCHEMA_VERSION = 1;
30103
30103
  var HANDOFF_PHASES = ["architect", "plan", "implement", "test", "review"];
30104
30104
  var isoTimestamp = string2().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
30105
30105
  var handoffPhase = _enum2(HANDOFF_PHASES);
30106
+ var nonEmpty = string2().min(1);
30107
+ var proofSchema = object({
30108
+ criterion: nonEmpty,
30109
+ surface: nonEmpty,
30110
+ command: nonEmpty,
30111
+ observed: nonEmpty,
30112
+ headSha: nonEmpty,
30113
+ negativeControl: nonEmpty
30114
+ }).passthrough();
30106
30115
  var routingHintsSchema = object({
30107
30116
  skipArchitect: boolean2().optional(),
30108
30117
  complexity: _enum2(["trivial", "small", "medium", "large"]).optional(),
@@ -30122,7 +30131,7 @@ var architectSchema = baseHandoffSchema.extend({
30122
30131
  subIssues: array(string2()).optional(),
30123
30132
  routingHints: routingHintsSchema,
30124
30133
  concerns: array(string2()).optional()
30125
- });
30134
+ }).passthrough();
30126
30135
  var requiredSkillsSchema = object({
30127
30136
  implement: array(string2()).optional(),
30128
30137
  test: array(string2()).optional(),
@@ -30136,24 +30145,30 @@ var planSchema = baseHandoffSchema.extend({
30136
30145
  concerns: array(string2()).optional(),
30137
30146
  workflowRecommendation: string2().optional(),
30138
30147
  requiredSkills: requiredSkillsSchema
30139
- });
30148
+ }).passthrough();
30140
30149
  var implementSchema = baseHandoffSchema.extend({
30141
30150
  phase: literal("implement"),
30142
30151
  filesChanged: array(string2()).optional(),
30152
+ proof: array(proofSchema).min(1),
30143
30153
  trickyParts: array(string2()).optional(),
30144
30154
  deviations: array(string2()).optional(),
30145
30155
  openQuestions: array(string2()).optional(),
30146
30156
  subPlanningNeeded: boolean2().optional(),
30147
30157
  discoveredComplexity: array(string2()).optional(),
30148
30158
  suggestedSubWorkers: number2().optional()
30149
- });
30159
+ }).passthrough();
30150
30160
  var testSchema = baseHandoffSchema.extend({
30151
30161
  phase: literal("test"),
30152
30162
  passed: number2().optional(),
30153
30163
  failed: number2().optional(),
30154
30164
  failures: array(object({ criterion: string2(), evidence: string2() }).passthrough()).optional(),
30165
+ implementerProof: object({ verdict: _enum2(["verified", "rejected"]), how: nonEmpty }).passthrough(),
30166
+ proof: array(proofSchema).min(1).optional(),
30155
30167
  documentationFeedback: string2().optional(),
30156
30168
  observations: array(string2()).optional()
30169
+ }).passthrough().refine((handoff) => (handoff.failures?.length ?? 0) > 0 || (handoff.failed ?? 0) > 0 || (handoff.proof?.length ?? 0) > 0, {
30170
+ path: ["proof"],
30171
+ message: "a passing test handoff needs the tester's own production-like proof"
30157
30172
  });
30158
30173
  var reviewSchema = baseHandoffSchema.extend({
30159
30174
  phase: literal("review"),
@@ -30162,13 +30177,13 @@ var reviewSchema = baseHandoffSchema.extend({
30162
30177
  minor: number2().optional(),
30163
30178
  verdict: _enum2(["approved", "changes_requested"]).optional(),
30164
30179
  keyFindings: array(object({ severity: string2(), file: string2(), description: string2() }).passthrough()).optional()
30165
- });
30180
+ }).passthrough();
30166
30181
  var phaseHandoffSchema = discriminatedUnion("phase", [
30167
- architectSchema.passthrough(),
30168
- planSchema.passthrough(),
30169
- implementSchema.passthrough(),
30170
- testSchema.passthrough(),
30171
- reviewSchema.passthrough()
30182
+ architectSchema,
30183
+ planSchema,
30184
+ implementSchema,
30185
+ testSchema,
30186
+ reviewSchema
30172
30187
  ]);
30173
30188
  var handoffMessageSchema = object({
30174
30189
  from: handoffPhase,
package/dist/legion.js CHANGED
@@ -29972,7 +29972,7 @@ var dispatchToolSpecs = [
29972
29972
  },
29973
29973
  {
29974
29974
  name: "dispatch_doc_edit",
29975
- description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29975
+ description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + "For replace, delete, and quote insert anchors, find text exactly as rendered: omit Markdown markers such as backticks or asterisks. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29976
29976
  arguments: (z) => ({
29977
29977
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29978
29978
  project: z.string().describe("Project key owning the document.").optional(),
@@ -30103,6 +30103,15 @@ var HANDOFF_SCHEMA_VERSION = 1;
30103
30103
  var HANDOFF_PHASES = ["architect", "plan", "implement", "test", "review"];
30104
30104
  var isoTimestamp = string2().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
30105
30105
  var handoffPhase = _enum2(HANDOFF_PHASES);
30106
+ var nonEmpty = string2().min(1);
30107
+ var proofSchema = object({
30108
+ criterion: nonEmpty,
30109
+ surface: nonEmpty,
30110
+ command: nonEmpty,
30111
+ observed: nonEmpty,
30112
+ headSha: nonEmpty,
30113
+ negativeControl: nonEmpty
30114
+ }).passthrough();
30106
30115
  var routingHintsSchema = object({
30107
30116
  skipArchitect: boolean2().optional(),
30108
30117
  complexity: _enum2(["trivial", "small", "medium", "large"]).optional(),
@@ -30122,7 +30131,7 @@ var architectSchema = baseHandoffSchema.extend({
30122
30131
  subIssues: array(string2()).optional(),
30123
30132
  routingHints: routingHintsSchema,
30124
30133
  concerns: array(string2()).optional()
30125
- });
30134
+ }).passthrough();
30126
30135
  var requiredSkillsSchema = object({
30127
30136
  implement: array(string2()).optional(),
30128
30137
  test: array(string2()).optional(),
@@ -30136,24 +30145,30 @@ var planSchema = baseHandoffSchema.extend({
30136
30145
  concerns: array(string2()).optional(),
30137
30146
  workflowRecommendation: string2().optional(),
30138
30147
  requiredSkills: requiredSkillsSchema
30139
- });
30148
+ }).passthrough();
30140
30149
  var implementSchema = baseHandoffSchema.extend({
30141
30150
  phase: literal("implement"),
30142
30151
  filesChanged: array(string2()).optional(),
30152
+ proof: array(proofSchema).min(1),
30143
30153
  trickyParts: array(string2()).optional(),
30144
30154
  deviations: array(string2()).optional(),
30145
30155
  openQuestions: array(string2()).optional(),
30146
30156
  subPlanningNeeded: boolean2().optional(),
30147
30157
  discoveredComplexity: array(string2()).optional(),
30148
30158
  suggestedSubWorkers: number2().optional()
30149
- });
30159
+ }).passthrough();
30150
30160
  var testSchema = baseHandoffSchema.extend({
30151
30161
  phase: literal("test"),
30152
30162
  passed: number2().optional(),
30153
30163
  failed: number2().optional(),
30154
30164
  failures: array(object({ criterion: string2(), evidence: string2() }).passthrough()).optional(),
30165
+ implementerProof: object({ verdict: _enum2(["verified", "rejected"]), how: nonEmpty }).passthrough(),
30166
+ proof: array(proofSchema).min(1).optional(),
30155
30167
  documentationFeedback: string2().optional(),
30156
30168
  observations: array(string2()).optional()
30169
+ }).passthrough().refine((handoff) => (handoff.failures?.length ?? 0) > 0 || (handoff.failed ?? 0) > 0 || (handoff.proof?.length ?? 0) > 0, {
30170
+ path: ["proof"],
30171
+ message: "a passing test handoff needs the tester's own production-like proof"
30157
30172
  });
30158
30173
  var reviewSchema = baseHandoffSchema.extend({
30159
30174
  phase: literal("review"),
@@ -30162,13 +30177,13 @@ var reviewSchema = baseHandoffSchema.extend({
30162
30177
  minor: number2().optional(),
30163
30178
  verdict: _enum2(["approved", "changes_requested"]).optional(),
30164
30179
  keyFindings: array(object({ severity: string2(), file: string2(), description: string2() }).passthrough()).optional()
30165
- });
30180
+ }).passthrough();
30166
30181
  var phaseHandoffSchema = discriminatedUnion("phase", [
30167
- architectSchema.passthrough(),
30168
- planSchema.passthrough(),
30169
- implementSchema.passthrough(),
30170
- testSchema.passthrough(),
30171
- reviewSchema.passthrough()
30182
+ architectSchema,
30183
+ planSchema,
30184
+ implementSchema,
30185
+ testSchema,
30186
+ reviewSchema
30172
30187
  ]);
30173
30188
  var handoffMessageSchema = object({
30174
30189
  from: handoffPhase,
@@ -34,8 +34,14 @@ durable human question; replies come back to the worker's own session.
34
34
  ## Durable artifacts
35
35
 
36
36
  Phase handoffs are committed in lifecycle order: architect, plan, implement, test, and review.
37
+ Only the implementer pushes them: it and the merger act as the code-writing GitHub App, while
38
+ the planner, tester, reviewer, and architects act as the review App (`appRoleForLegionRole`,
39
+ `packages/daemon/src/daemon/github-apps.ts`), which holds no `contents` permission — their
40
+ handoff commits stay on the shared workspace's issue branch and ride the implementer's next push.
37
41
  A clean review ends with the `.legion/` deletion pushed by the implementer at the reviewer's
38
- direction (the review App holds no `contents` permission and cannot push), which the reviewer
39
- then approves; retro records its learning in
42
+ direction, which the reviewer then approves; retro records its learning in
40
43
  `docs/solutions/` and writes no handoff. GitHub comments and reviews carry the required Legion
41
44
  footer so the daemon can attribute artifacts to their worker session.
45
+ The implement handoff carries the implementer's own production-like proof and the test handoff the
46
+ tester's verdict on it plus the tester's own; `legion handoff write` refuses a payload the phase's
47
+ schema rejects and names the field. Retro's message goes to the Dispatch issue (`dispatch_message`), never a GitHub issue.
@@ -243,9 +243,10 @@ type EditOp = {
243
243
  };
244
244
  ```
245
245
 
246
- Target `replace` and `delete` by the document's plain text: inline-code and link text match without Markdown syntax, and a table-cell
247
- anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay within one textblock; split changes
248
- that span separate blocks into separate operations.
246
+ Target `replace`, `delete`, and quote insert anchors by a block's text as rendered: write inline
247
+ code without backticks, bold without asterisks, and link text without link syntax. A table-cell
248
+ anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay
249
+ within one textblock; split changes that span separate blocks into separate operations.
249
250
 
250
251
  `replace` requires `find` and `with`; `delete` requires `find`; `insert` requires `markdown` and exactly one of `after` or `before`. An
251
252
  insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Ordinary inserts create a sibling block before or after the quote or
@@ -286,6 +287,11 @@ Should we ship the migration?
286
287
  :::
287
288
  ```
288
289
 
290
+ When a human answers a decision written as an ask block, the answer lives on that ask. Use
291
+ `dispatch_resolve_ask` when the decision is resolved without a human response, or preserve the
292
+ human's answer; never rewrite the question into its answer or blank its options. An edit that leaves
293
+ an ask block without a question or with a blank option is rejected with `INVALID_ASK_BLOCK`.
294
+
289
295
  ## Comments and suggestions
290
296
 
291
297
  Add feedback with:
@@ -222,7 +222,9 @@ file or rewrite the reviewer-approved head after cleanup.
222
222
 
223
223
  Sign off only when scope is fully met, integration evidence is current, corrective work
224
224
  is complete, review is clean, retro completed, and no necessary work was silently
225
- deferred. Make the sign-off comment explicit about that evidence.
225
+ deferred. Make the sign-off comment explicit about that evidence. Sign-off also requires the
226
+ implementer's production report: a `Production:` line that names what was driven, how, what was
227
+ observed, and the merge commit — never a `pending` one, and never a staging pass.
226
228
 
227
229
  Preserve this order exactly:
228
230
 
@@ -242,6 +244,11 @@ Preserve this order exactly:
242
244
  merges. The merge queue merges under its own authority and the repository's own rules
243
245
  (branch protection, CODEOWNERS); whether a human must approve first is that repository's
244
246
  setting, not Legion's, and you never ask for or wait on such an approval.
247
+ 5. the merge queue merges; you then `spawn_worker` the **implementer** once more with the
248
+ production-check task. It drives the changed path in production through the user's own access
249
+ path and records what it saw on the pull request and on this issue. Close only after the implementer's production report exists.
250
+ A defect it finds is a corrective child issue of this tree, not a note on a closed one; a deploy
251
+ the implementer cannot perform is its action ask, and the issue waits for it.
245
252
 
246
253
  What returns the tree to review: a changed diff — a commit above the approved head that
247
254
  touches anything outside `docs/solutions/`, or a rebase whose fingerprint (the `legion-worker`
@@ -264,11 +271,11 @@ and the implementer's and merger's runs of the command close every accepted one.
264
271
 
265
272
  ## 7. Close
266
273
 
267
- After the merge result and sign-off are recorded, post the sign-off and close this issue
268
- through the Legion write surface:
274
+ After the merge result, the implementer's production report, and sign-off are recorded, post the
275
+ sign-off and close this issue through the Legion write surface:
269
276
 
270
277
  ```text
271
- dispatch_comment({ issue: "LEGION-40", body: "<sign-off: scope, integration evidence, review, retro, and merge>" })
278
+ dispatch_comment({ issue: "LEGION-40", body: "<sign-off: scope, integration evidence, review, retro, merge, and the implementer's production report>" })
272
279
  legion({ op: "set_status", issue: "LEGION-40", status: "done" })
273
280
  ```
274
281
 
@@ -289,13 +296,13 @@ corresponding lifecycle procedure.
289
296
  | `child-reopened` | Treat the completion edge as reset. Reassess the reopened child and return the tree to children-in-flight; do not continue an already-started end-game. |
290
297
  | `design-approved` | Payload `{type:"design-approved"}`. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
291
298
  | `design-changes-requested` | Payload `{type:"design-changes-requested", version, reason, author?}`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec, call `dispatch_request_approval` again, and stay parked; the gate is closed. |
292
- | `phase-complete` | Payload `{type:"phase-complete", issue, role, summary}`. May arrive live or via `catchup-overseer`'s `phaseCompletions`. Read the committed handoff for that phase, then spawn the next phase's owner, or `spawn_worker` on the same role again to resume it with corrections if the handoff shows unresolved gaps. A `reviewer` completion whose GitHub review is `CHANGES_REQUESTED` (the daemon returns the issue's Dispatch status to `in_progress` for this, on the reviewer's completion and again when you spawn the corrective implementer unless the daemon already knows the issue is `in_progress`) means `spawn_worker` the **implementer** again with the review findings — thread URLs and blocking items — as its task, then route back through tester and reviewer in order; never `spawn_worker` the reviewer directly off this wake and never proceed to retro on this verdict. A reviewer completion with an `APPROVED` review proceeds to retro (step 5). A `reviewer` completion after a conflict-forced rebase whose review body names an unchanged fingerprint is a confirmation, not a round: if retro already completed, `spawn_worker` the merger; otherwise resume the step you were on. |
299
+ | `phase-complete` | Payload `{type:"phase-complete", issue, role, summary}`. May arrive live or via `catchup-overseer`'s `phaseCompletions`. Read the committed handoff for that phase, then spawn the next phase's owner, or `spawn_worker` on the same role again to resume it with corrections if the handoff shows unresolved gaps. A `reviewer` completion whose GitHub review is `CHANGES_REQUESTED` (the daemon returns the issue's Dispatch status to `in_progress` for this, on the reviewer's completion and again when you spawn the corrective implementer unless the daemon already knows the issue is `in_progress`) means `spawn_worker` the **implementer** again with the review findings — thread URLs and blocking items — as its task, then route back through tester and reviewer in order; never `spawn_worker` the reviewer directly off this wake and never proceed to retro on this verdict. A reviewer completion with an `APPROVED` review proceeds to retro (step 5). A `reviewer` completion after a conflict-forced rebase whose review body names an unchanged fingerprint is a confirmation, not a round: if retro already completed, `spawn_worker` the merger; otherwise resume the step you were on. An `implementer` completion that follows the merge is its production report: read the record on the pull request and the issue, then run step 7 — the issue is already at `retro`, the daemon writes no status for this completion, and you set `done` yourself. A `tester` completion whose handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof, goes back to the **implementer** with that finding — never forward to the reviewer, and never by supplying the proof from another role. A worker that reports no surface reaches the changed path gets a child issue in this tree (infrastructure, tooling, or a skill) and a resume once it lands; that report is never a reason to advance the phase. |
293
300
  | `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. |
294
301
  | `worker-started` | Payload `{type:"worker-started", issue, role}`. A previously queued role has been promoted and is now running. Treat it exactly as a normal spawn: resume tracking that role's live session. |
295
302
  | `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
296
303
  | `pr-review` | Payload `{type:"pr-review", state, author, body}`. Delivered to whichever role is currently active for the issue, falling back to you when no worker phase is active. Follows the same verdict rule as a reviewer's `phase-complete`: `state: "changes_requested"` sends the implementer back in with the review findings, then tester, then reviewer — never the reviewer again and never retro; that `spawn_worker` returns the issue to `in_progress` on its own (the daemon writes it for a corrective implementer whenever the PR's latest recorded review is changes requested, a human's after approval included), so you set nothing by hand; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. `state: "approved"` on a rebased head whose body names an unchanged fingerprint is that confirmation: proceed to retro if it has not run, otherwise to the merger — never to a second retro or test round. |
297
304
  | `pr-blocked` | Payload `{type:"pr-blocked", pr, attempts}`. `attempts` counts heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count; a push the daemon cannot classify (a listener without `changed_paths`, a list capped at 100, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. Assign a focused implementer or corrective child, then return it through testing and review; do not treat the blocked PR as final. |
298
- | `pr-merged` | Payload `{type:"pr-merged", pr, mergeCommitSha}`. The merge queue landed the PR. This is your cue for step 7: post the sign-off comment naming that merge commit and set the issue `done`. Nothing else follows a merge. |
305
+ | `pr-merged` | Payload `{type:"pr-merged", pr, mergeCommitSha}`. The merge queue landed the PR. `spawn_worker` the **implementer** with the production-check task naming that merge commit (it resumes the same agent; a retired role has no live holder, so never `envoy_publish` for this). Its `phase-complete` is what brings you to step 7: verify the record on the pull request and this issue first, then sign off naming it and set the issue `done`. A merge is not the close. |
299
306
  | `pr-closed-unmerged` | Decide from current scope whether to reopen the work, send a fresh implementer, or cancel it with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
300
307
  | `issue-comment` | Interpret the comment in the issue's design context. Answer it, adjust the plan, or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
301
308
  | `catchup-overseer` | Verify its gates, child counts, and PR verdicts against current artifacts, then resume the applicable numbered lifecycle step. It is a current-state snapshot, not a raw-event replay. `gates[LEGION_TREE].open` is the design gate's current state: `true` means the root spec is approved at its current version and you may spawn; `false` (or no `open` key, meaning no gate is registered) means the sequence in section 1 still applies. For each entry in its `phaseCompletions` (`{issue, role, summary, at}`, phases that completed while you were not live), handle it exactly as a `phase-complete` wake. Then compare `childCounts[LEGION_ISSUE].open` (the children not `done`) with `legion state` and Dispatch: any **open** released child — `todo` through `retro` — with no architect role claim gets `spawn_worker` for its architect, a `child-adopted` or `child-status` wake you missed while not live; a `done` child gets nothing, whether or not a lingering legacy tree of its own still shows in `legion state`. |
@@ -28,6 +28,10 @@ retrospective's durable output.
28
28
  the change in production and records it on the PR and the issue (Sami, 2026-09-13, verbatim:
29
29
  "the agent that developed it should be responsible for testing in production"). The
30
30
  architect's sign-off waits for that record.
31
+ The record is the pull request's `Production:` line, one pull-request comment, and a
32
+ `dispatch_message` on the issue, each naming what was driven, how, what was observed, and the
33
+ merge commit. A defect the production check finds becomes a corrective child issue of the same tree,
34
+ owned by the architect and implemented by the same implementer; the parent stays open until it lands.
31
35
 
32
36
  Retro's commit sits above the reviewer's approved head and the approval stands: a commit that
33
37
  changes only `docs/solutions/` does not void it, and the tree goes from retro to the merger —
@@ -40,10 +44,12 @@ before step 3. The design gate is not a substitute for review and retro.
40
44
  ## Two perspectives
41
45
 
42
46
  1. Re-read the issue, its acceptance criteria, the PR, test evidence, and review evidence.
43
- Confirm the PR's `E2E` line links a pre-merge proof on a production-like surface (a devN
44
- stack, staging, or a local stack with real migrations). If it links only a unit suite or
45
- nothing, the retro's first durable learning is that gap, and the issue goes back to the
46
- tester before `READY`.
47
+ Confirm the PR carries both proofs: the implementer's own `E2E (implementer)` line and the tester's `E2E (tester)` line,
48
+ each naming a production-like surface (a scratch daemon, a smoke rig, a sandbox repository, a devN
49
+ stack, staging, or a local stack with real migrations), a command or run id, an observation, a head
50
+ SHA, and a negative control. If either is missing, or links only a unit suite, the retro's first
51
+ durable learning is that gap and the issue goes back — to the implementer for its own proof, to the
52
+ tester for the tester's — before `READY`.
47
53
  Do not rebase or create a new branch; work on the existing issue branch.
48
54
  2. Spawn one fresh-eyes subagent. Give it the issue and PR, ask it to inspect the diff and
49
55
  return concrete reusable learnings, and require it to return analysis rather than edit files.
@@ -75,8 +81,9 @@ related_issues:
75
81
  Commit the documentation on the existing issue branch, advance its existing bookmark, and push
76
82
  that branch. Do not create a replacement branch or bookmark. Then post one Dispatch message on
77
83
  the issue — `issue` is your `LEGION_ISSUE`; Legion issues live on Dispatch, never on a GitHub
78
- issue, and the `gh` shim refuses every GitHub-issue write — naming the documents and the
79
- one-to-three most useful takeaways. The message must carry this revived implementer's structured
84
+ issue, and the `gh` shim refuses every GitHub-issue write — naming the documents, the
85
+ one-to-three most useful takeaways, the two proofs you read, and the production check that
86
+ follows the merge. The message must carry this revived implementer's structured
80
87
  attribution footer with `phase` set to `retro`; the body is capped at 2,000 characters:
81
88
 
82
89
  ```ts
@@ -90,6 +97,10 @@ dispatch_message({
90
97
  **Key takeaways:**
91
98
  - <reusable lesson>
92
99
 
100
+ **Proofs read:** implementer <surface/command>, tester <surface/command>.
101
+
102
+ **Production check:** <what the implementer will drive after the merge, or the action ask it opened>
103
+
93
104
  <!-- legion: {"session":"<session-id>","phase":"retro"} -->`,
94
105
  })
95
106
  ```
@@ -105,7 +116,8 @@ Before returning, verify all of the following:
105
116
 
106
117
  - The reviewer cleanup commit remains below the retro documentation commit, and the reviewer's
107
118
  approval of that cleanup head stands: the merger accepts the approved head plus this commit.
108
- - The learning documents and the Dispatch message both exist.
119
+ - The learning documents and the Dispatch message both exist (never a GitHub issue comment).
120
+ - Both proofs were read, and any gap in either is recorded as a learning.
109
121
  - No `.legion` file was created or modified by retro.
110
122
  - The fresh-eyes analysis was considered alongside the implementer's context.
111
123
  - The merger remains a subsequent step, not work performed by retro.
@@ -89,7 +89,7 @@ Concurrent issues have disjoint workspaces; only the currently active phase muta
89
89
  one. After you complete and go idle, treat `$LEGION_WORKSPACE` as read-only: you are kept
90
90
  alive to answer questions, not to keep editing. Do not create new commits, run
91
91
  `jj -R "$LEGION_WORKSPACE" new`, or touch tracked files once your own handoff is committed
92
- and pushed — a code change belongs to whichever phase is active now.
92
+ (and, for the implementer, pushed) — a code change belongs to whichever phase is active now.
93
93
 
94
94
  On every start, and especially after revival or re-creation, read the issue and then the
95
95
  committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legion/`:
@@ -263,9 +263,16 @@ left open <thread URL> — newest reply by <login> is not an acceptance
263
263
  **Thermo:** thermonuclear-deep-review + thermonuclear-code-quality run once at <head-sha>:
264
264
  <verdict>. (omitted entirely on a docs-only PR — no thermo pass runs)
265
265
 
266
- **E2E:** <surface> — ran `<command or run id>`, observed <result>, at head <sha>.
266
+ **E2E (implementer):** <surface> — ran `<command or run id>`, observed <result>, at head <sha>.
267
267
  Negative control: <deliberately broken input> → <refusal or failure observed>.
268
268
 
269
+ **E2E (tester):** <surface> — ran `<command or run id>`, observed <result>, at head <sha>.
270
+ Negative control: <deliberately broken input> → <refusal or failure observed>.
271
+ Verified the implementer's proof by <re-running its command | driving the same surface independently>.
272
+
273
+ **Production:** <what was checked in production, how, what was observed> — merge commit <sha>.
274
+ (written by the implementer after the merge lands; `pending <what is missing>` until then)
275
+
269
276
  **Fast-follow:** <one named cleanup item and where it will land>, or "none".
270
277
 
271
278
  **Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
@@ -309,12 +316,26 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
309
316
  - **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." The `Fast-follow:`
310
317
  field names naming, duplication, or wording cleanup only; anything that changes behaviour,
311
318
  hides an error, or breaks a gate lands in this PR.
312
- - The tester fills in the `E2E` section: the real surface a user reaches the criterion
319
+ - **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
320
+ The proof is the changed behaviour exercised on the surface a user reaches it through — a
321
+ scratch daemon, a smoke rig, a sandbox repository, a real browser, a devN stack, a local stack
322
+ with real migrations — with the exact command or run id, what was observed, the head SHA, and
323
+ one negative control. The same proof goes into `.legion/implement.json` as its required `proof`
324
+ array (`legion handoff write --phase implement` refuses a payload without one and names the
325
+ field), and into the PR body, because the reviewer and the merger verify facts on GitHub and
326
+ never from a handoff. A unit or integration test is a regression lock, never proof of a
327
+ criterion.
328
+ - **The tester verifies the implementer's proof and adds its own `E2E (tester)` line.** It re-runs
329
+ the implementer's command or drives the same surface independently, records the verdict in
330
+ `.legion/test.json` as `implementerProof` (`{verdict, how}`), and records its own proof beside
331
+ it. A test handoff whose predecessor carried no proof is a test failure, not a gap for the tester to fill:
332
+ record it in `failures` with `implementerProof.verdict: "rejected"`, complete the phase, and let
333
+ the architect return the issue to the implementer — the agent that developed the change owns
334
+ proving it. The tester's own proof names the real surface a user reaches the criterion
313
335
  through, the exact command or run id, what was observed, the head SHA, and one negative
314
- control — a deliberately broken input and the refusal or failure it produced. A unit or
315
- integration test is a regression lock, never proof of a criterion. The surface is
336
+ control — a deliberately broken input and the refusal or failure it produced. The surface is
316
337
  **production-like** — a devN stack, staging, or a local stack with real migrations, one that
317
- has the resource the change touches — and the `E2E` line carries a **link** to that run,
338
+ has the resource the change touches — and each `E2E` line carries a **link** to that run,
318
339
  screenshot, or e2e; the merge queue does not approve a user-facing change without it, and a
319
340
  green unit suite is not it. Sami, 2026-09-13, verbatim: "They need to test everything in a
320
341
  production-like environment before merging, and it is the agent that develops the feature
@@ -340,6 +361,7 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
340
361
  - The reviewer verifies the `CI`, `Threads`, and `E2E` facts against GitHub directly —
341
362
  never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
342
363
  `task(agent="thermonuclear-code-quality")` once at that head and records the verdict.
364
+ Approval is refused while either `E2E (implementer)` or `E2E (tester)` is missing: `REQUEST_CHANGES` naming the missing line.
343
365
  Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round** —
344
366
  `REQUEST_CHANGES` when any correctness finding stands, otherwise `COMMENT` while the head
345
367
  still carries `.legion/`; `APPROVE` only for a head that carries no `.legion/` — the head
@@ -381,8 +403,9 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
381
403
  `no file changes above the approved head`); then the same with `'~docs/solutions'` appended,
382
404
  which must print nothing. Then it publishes `READY #<n> at <tip-sha>` naming the approved
383
405
  head, the tip, and that summary, plus the PR body's gate facts, to the merge queue's role
384
- (`notifications.role.pr-queue`) with `envoy_publish`. The merger never merges; the queue
385
- merges under its own authority.
406
+ (`notifications.role.pr-queue`) with `envoy_publish`. The READY packet names both the
407
+ implementer's and the tester's `E2E` lines; a missing one is reported to the architect instead
408
+ of published. The merger never merges; the queue merges under its own authority.
386
409
  - **After the queue merges, the implementer verifies in production.** Sami, 2026-09-13,
387
410
  verbatim: "the agent that developed it should be responsible for testing in production."
388
411
  The architect sends the implementer back once the merge lands; the implementer watches the
@@ -392,6 +415,29 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
392
415
  this: on 2026-09-12 a slot's entire staging gate passed at 00:02Z and its production-apply
393
416
  failed at 00:12Z on a resource staging never runs. If the slot fails on the change, the
394
417
  implementer owns the fix and the next slot.
418
+ The record has three places: the PR body's `Production:` line, one pull-request comment
419
+ carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
420
+ read GitHub, the architect reads the issue. When the deploy that carries the merge has not
421
+ happened (a shared profile still holding the previous plugin release, a daemon still running
422
+ the previous commit, a slot nobody has run), open an action ask — `dispatch_ask` with
423
+ `kind: "action"` — naming the exact install or restart step, keep the `Production:` line at
424
+ `pending <what is missing>`, and complete the check once the human answers Done. Never record
425
+ a staging pass as the production check, and never let the architect sign off on a `pending`
426
+ line.
427
+
428
+ ## When no surface reaches the changed path
429
+
430
+ No surface reaches the changed path is a report to the architect, never a reason to complete the phase.
431
+ Say which surface is missing and what it would have to do — a rig that can spawn the role, a
432
+ sandbox that holds the resource, a credential, a command that does not exist yet — and send it to
433
+ the architect with `envoy_publish` to its role topic. The architect creates a child issue in this
434
+ tree to build it (infrastructure, tooling, or a skill) and resumes you once it lands. Sami,
435
+ 2026-09-13, verbatim: "If there's anything blocking that, we need to fix it: if it's
436
+ infrastructure, we need to fix it; if it's tooling, we need to develop it; if it's skills, we need
437
+ to fix the skills." A code path whose first execution would be after the merge — a deploy
438
+ workflow's inline step, a post-merge helper, a production-only resource — is untested until you
439
+ have executed it somewhere production-like; completing with a unit-test-only handoff is the
440
+ failure this rule exists to stop.
395
441
 
396
442
  ## The unchanged-diff check
397
443
 
@@ -440,6 +486,10 @@ cd -- "$LEGION_WORKSPACE" && \
440
486
  legion handoff write --phase <p> --data '<JSON object of phase-specific fields only>'
441
487
  ```
442
488
 
489
+ `legion handoff write` validates the payload against the phase's schema before writing: an
490
+ implement handoff without a well-formed `proof`, or a test handoff that reports no failure and
491
+ carries no `proof` of its own, exits 1 naming the field and writes nothing.
492
+
443
493
  Then verify the durable artifact exists:
444
494
 
445
495
  ```bash
@@ -453,9 +503,12 @@ cd -- "$LEGION_WORKSPACE" && \
453
503
  jj -R "$LEGION_WORKSPACE" split -m "<phase>: record handoff" .legion/<phase>.json
454
504
  ```
455
505
 
456
- If the issue bookmark exists locally, advance it and push it with the provisioned
457
- credential helper. `--bookmark` also publishes the locally provisioned bookmark on its
458
- first push — a bookmark not yet tracking a remote one is tracked automatically:
506
+ **Only the implementer pushes the issue branch.** It acts as the code-writing App
507
+ (`legion-implementer[bot]`, `appRoleForLegionRole` in `packages/daemon/src/daemon/github-apps.ts`),
508
+ the one App with `contents` permission (the merger acts as the same App but pushes nothing: it
509
+ verifies and publishes READY). If you are the implementer, advance the issue bookmark and push it
510
+ with the provisioned credential helper. `--bookmark` also publishes the locally provisioned
511
+ bookmark on its first push — a bookmark not yet tracking a remote one is tracked automatically:
459
512
 
460
513
  ```bash
461
514
  cd -- "$LEGION_WORKSPACE" && \
@@ -463,16 +516,23 @@ cd -- "$LEGION_WORKSPACE" && \
463
516
  jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
464
517
  ```
465
518
 
466
- Do not report phase completion until the write, existence check, and handoff commit
467
- succeed; when an issue branch exists, its push is also required. This is the committed
468
- copy the next phase reads after revival. It is removed once, at the end of a clean review: the
469
- implementer pushes that deletion at the reviewer's direction. No other phase removes it — and
470
- once it is gone (`jj -R "$LEGION_WORKSPACE" file list -r @- .legion` prints nothing on stdout;
471
- jj warns on stderr), this
472
- gate no longer applies: a later rebase, bare-gate re-check, confirmation, or retro writes no
473
- `.legion/<phase>.json`, commits no handoff, and reports with `legion handoff complete` alone
474
- (below). Recreating `.legion/` after its deletion changes the approved head and restarts the
475
- review loop this rule exists to end.
519
+ Every other role — planner, tester, reviewer, architects — acts as the review App
520
+ (`legion-reviewer[bot]`), which cannot push: the `split` above is your last step, and the commit
521
+ rides the implementer's next push (the corrective push after a review, or the final `.legion/`
522
+ deletion). A push from one of those roles is refused — over git it reads
523
+ `remote: Repository not found.`; the REST API's form of the same refusal is
524
+ `Resource not accessible by integration` — and that refusal is expected, not a failure to report
525
+ or retry.
526
+
527
+ Do not report phase completion until the write, existence check, and handoff commit succeed —
528
+ and, for the implementer, until the push has too. This is the committed copy the next phase
529
+ reads after revival. It is removed once, at the end of a clean review: the implementer pushes
530
+ that deletion at the reviewer's direction. No other phase removes it — and once it is gone
531
+ (`jj -R "$LEGION_WORKSPACE" file list -r @- .legion` prints nothing on stdout; jj warns on
532
+ stderr), this gate no longer applies: a later rebase, bare-gate re-check, confirmation, or retro
533
+ writes no `.legion/<phase>.json`, commits no handoff, and reports with `legion handoff complete`
534
+ alone (below). Recreating `.legion/` after its deletion changes the approved head and restarts
535
+ the review loop this rule exists to end.
476
536
 
477
537
  ## Completion: report to the architect, then stay
478
538
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.24.3",
3
+ "version": "1.25.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [