@sjawhar/pi-legion-envoy 1.22.1 → 1.23.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
@@ -30279,8 +30279,9 @@ var stateTree = strictObject({
30279
30279
  locator: stateTreeLocator.optional()
30280
30280
  });
30281
30281
  var stateGate = strictObject({
30282
- designAskId: nonEmptyString.optional(),
30283
- designApproved: nonEmptyString.optional()
30282
+ artifactId: nonEmptyString,
30283
+ latestVersion: number2().int().positive(),
30284
+ approvedVersion: number2().int().positive().optional()
30284
30285
  });
30285
30286
  var stateRole = strictObject({
30286
30287
  role: nonEmptyString,
@@ -30422,7 +30423,11 @@ var LegionDaemonApi = {
30422
30423
  response: object({})
30423
30424
  },
30424
30425
  GatesRegister: {
30425
- request: architectCapability.extend({ issue: nonEmptyString, askId: nonEmptyString }),
30426
+ request: architectCapability.extend({
30427
+ issue: nonEmptyString,
30428
+ artifactId: uuid2(),
30429
+ version: number2().int().positive()
30430
+ }),
30426
30431
  response: object({})
30427
30432
  },
30428
30433
  Grant: {
@@ -32629,7 +32634,7 @@ ${trailer.join(`
32629
32634
  const result = await client.requestApproval(resolved.artifact.id, { actor });
32630
32635
  if (result.ask === null) {
32631
32636
  return {
32632
- text: `${resolved.artifact.name} is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. An edit after approval makes it stale, so request again only for a new version.`,
32637
+ text: `${resolved.artifact.name} (document id ${resolved.artifact.id}) is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. An edit after approval makes it stale, so request again only for a new version.`,
32633
32638
  details: {
32634
32639
  ...resolved.owner.kind === "project" ? documentResultDetails(resolved.artifact) : { issue: resolved.issue?.key },
32635
32640
  artifact: resolved.artifact.id,
@@ -32639,7 +32644,7 @@ ${trailer.join(`
32639
32644
  }
32640
32645
  const details = await askResultDetails(client, result.ask, resolved);
32641
32646
  return {
32642
- text: `Approval requested for ${resolved.artifact.name} at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
32647
+ text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
32643
32648
  details: { ...details, artifact: resolved.artifact.id, version: result.version }
32644
32649
  };
32645
32650
  }
package/dist/legion.js CHANGED
@@ -30279,8 +30279,9 @@ var stateTree = strictObject({
30279
30279
  locator: stateTreeLocator.optional()
30280
30280
  });
30281
30281
  var stateGate = strictObject({
30282
- designAskId: nonEmptyString.optional(),
30283
- designApproved: nonEmptyString.optional()
30282
+ artifactId: nonEmptyString,
30283
+ latestVersion: number2().int().positive(),
30284
+ approvedVersion: number2().int().positive().optional()
30284
30285
  });
30285
30286
  var stateRole = strictObject({
30286
30287
  role: nonEmptyString,
@@ -30422,7 +30423,11 @@ var LegionDaemonApi = {
30422
30423
  response: object({})
30423
30424
  },
30424
30425
  GatesRegister: {
30425
- request: architectCapability.extend({ issue: nonEmptyString, askId: nonEmptyString }),
30426
+ request: architectCapability.extend({
30427
+ issue: nonEmptyString,
30428
+ artifactId: uuid2(),
30429
+ version: number2().int().positive()
30430
+ }),
30426
30431
  response: object({})
30427
30432
  },
30428
30433
  Grant: {
@@ -30796,9 +30801,12 @@ var LIFECYCLE_STATUSES2 = [
30796
30801
  function isLifecycleStatus(value) {
30797
30802
  return LIFECYCLE_STATUSES2.includes(value);
30798
30803
  }
30804
+ function isDispatchArtifactId(value) {
30805
+ return LegionDaemonApi.GatesRegister.request.shape.artifactId.safeParse(value).success;
30806
+ }
30799
30807
  var LEGION_OP_FIELDS = {
30800
30808
  set_status: ["issue", "status"],
30801
- register_gate: ["issue", "askId"],
30809
+ register_gate: ["issue", "artifactId", "version"],
30802
30810
  release_wave: ["issues"],
30803
30811
  escalate: ["kind", "context"],
30804
30812
  spawn_worker: ["issue", "role", "task"]
@@ -30809,7 +30817,8 @@ function legionToolSchema(pi) {
30809
30817
  op: z.enum(["set_status", "register_gate", "release_wave", "escalate", "spawn_worker"]),
30810
30818
  issue: z.string().optional(),
30811
30819
  status: z.enum(LIFECYCLE_STATUSES2).optional(),
30812
- askId: z.string().optional(),
30820
+ artifactId: z.string().optional(),
30821
+ version: z.number().optional(),
30813
30822
  kind: z.enum(["re-file", "capacity", "cross-tree"]).optional(),
30814
30823
  context: z.unknown().optional(),
30815
30824
  issues: z.array(z.string()).optional(),
@@ -30823,7 +30832,7 @@ function createLegionTool(deps) {
30823
30832
  return {
30824
30833
  name: "legion",
30825
30834
  label: "legion",
30826
- description: "Perform a Legion lifecycle write through the Legion daemon. " + `spawn_worker's response "status" means: "spawned" \u2014 a fresh pane just opened and is ` + 'running now; "resumed" \u2014 an existing worker was prompted directly over its live socket, ' + "or (if its boot has not confirmed yet) its task was recorded to deliver once that boot " + 'completes; "queued" \u2014 the running-worker cap is full, the task was recorded and this ' + 'role will start on its own once a slot frees. Never re-spawn a role after "resumed" or ' + '"queued" \u2014 wait for the worker-started notification instead.',
30835
+ description: "Perform a Legion lifecycle write through the Legion daemon. " + "register_gate records the root spec document a human must approve: `artifactId` is the " + "document id (a UUID) and `version` the version number, both copied from the `artifact` and " + "`version` fields of dispatch_request_approval's result \u2014 never the slug or file name you " + "passed to that tool. " + `spawn_worker's response "status" means: "spawned" \u2014 a fresh pane just opened and is ` + 'running now; "resumed" \u2014 an existing worker was prompted directly over its live socket, ' + "or (if its boot has not confirmed yet) its task was recorded to deliver once that boot " + 'completes; "queued" \u2014 the running-worker cap is full, the task was recorded and this ' + 'role will start on its own once a slot frees. Never re-spawn a role after "resumed" or ' + '"queued" \u2014 wait for the worker-started notification instead.',
30827
30836
  defaultInactive: true,
30828
30837
  parameters: legionToolSchema(pi),
30829
30838
  execute: async (_id, parameters, _signal, _onUpdate, context) => {
@@ -30861,15 +30870,25 @@ function createLegionTool(deps) {
30861
30870
  });
30862
30871
  return jsonSuccess({});
30863
30872
  }
30864
- case "register_gate":
30873
+ case "register_gate": {
30874
+ const version = parameters.version;
30875
+ if (typeof version !== "number" || !Number.isSafeInteger(version) || version <= 0) {
30876
+ throw new Error("register_gate requires a positive integer version");
30877
+ }
30878
+ const artifactId = stringInput("artifactId");
30879
+ if (!isDispatchArtifactId(artifactId)) {
30880
+ throw new Error(`register_gate requires artifactId to be the document id (a UUID) from dispatch_request_approval's result, not "${artifactId}"`);
30881
+ }
30865
30882
  await daemon.gatesRegister({
30866
30883
  tree: architect.tree,
30867
30884
  sessionId,
30868
30885
  secret: architect.secret,
30869
30886
  issue: stringInput("issue"),
30870
- askId: stringInput("askId")
30887
+ artifactId,
30888
+ version
30871
30889
  });
30872
30890
  return jsonSuccess({});
30891
+ }
30873
30892
  case "release_wave": {
30874
30893
  const issues = parameters.issues;
30875
30894
  if (!Array.isArray(issues) || !issues.every((issue) => typeof issue === "string")) {
@@ -33045,7 +33064,7 @@ ${trailer.join(`
33045
33064
  const result = await client.requestApproval(resolved.artifact.id, { actor });
33046
33065
  if (result.ask === null) {
33047
33066
  return {
33048
- text: `${resolved.artifact.name} is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. An edit after approval makes it stale, so request again only for a new version.`,
33067
+ text: `${resolved.artifact.name} (document id ${resolved.artifact.id}) is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. An edit after approval makes it stale, so request again only for a new version.`,
33049
33068
  details: {
33050
33069
  ...resolved.owner.kind === "project" ? documentResultDetails(resolved.artifact) : { issue: resolved.issue?.key },
33051
33070
  artifact: resolved.artifact.id,
@@ -33055,7 +33074,7 @@ ${trailer.join(`
33055
33074
  }
33056
33075
  const details = await askResultDetails(client, result.ask, resolved);
33057
33076
  return {
33058
- text: `Approval requested for ${resolved.artifact.name} at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
33077
+ text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
33059
33078
  details: { ...details, artifact: resolved.artifact.id, version: result.version }
33060
33079
  };
33061
33080
  }
@@ -33,7 +33,8 @@ separate coordinator to finish necessary work.
33
33
  hold (your own, or one `spawn_worker` returned) or compute another with the
34
34
  `roleToken` helper from `@legion/contracts` exactly the way the daemon does.
35
35
  - There is no label vocabulary. Dispatch status replaces the board, and the design gate
36
- is a `dispatch_ask` answered `Approve`, not a label. Never attempt to apply a label.
36
+ is a human approving the root spec document at a version in Dispatch, requested with
37
+ `dispatch_request_approval` — not a label and not an ask. Never attempt to apply a label.
37
38
  - Deferring necessary work is failure. The sole valid deferral is a new child issue you
38
39
  create and continue to own. Re-file a genuinely independent child through the
39
40
  controller rather than treating it as an abandoned dependency.
@@ -87,34 +88,51 @@ adoption/decomposition and waves, acceptance criteria, and the integration test
87
88
  "spec" artifact beside it (`dispatch_artifact` with the primary document's name replaces the
88
89
  human's document; do not do that). Both readers described in
89
90
  [Writing for the human](../dispatch/SKILL.md#writing-for-the-human) must be able to follow it.
90
- When the config-armed root design gate applies, run this exact sequence **before any
91
- Legion-role spawn**, including a sub-architect:
91
+ The design gate runs only when the "Design gate policy" line at the end of your system prompt
92
+ says `gates.design: root-issues`. When it says `gates.design: off`, write the spec and continue
93
+ to section 2 with no approval step at all: do not request approval, do not register a gate, and
94
+ do not wait for `design-approved`. A sub-architect on a child issue has no policy line and never
95
+ runs the gate either: the root approval covers the tree. When the gate is armed, run this exact
96
+ sequence **before any Legion-role spawn**, including a sub-architect:
92
97
 
93
98
  ```text
94
99
  dispatch_doc_edit({ issue: "<root issue>", ... }) // extend the primary document in place
95
- askId = dispatch_ask({
100
+ result = dispatch_request_approval({ issue: "<root issue>" }) // the primary document by default
101
+ legion({
102
+ op: "register_gate",
96
103
  issue: "<root issue>",
97
- question: "<what is true today, in one sentence> <what will be true when this lands, in one sentence> <how: one issue or N child issues, and what the first step is> I recommend Approve because <one reason>.",
98
- options: [
99
- { label: "Approve", description: "Work starts as described; the first worker is spawned now." },
100
- { label: "Hold", description: "Nothing starts; reply on the issue with what should change first." },
101
- ]
104
+ artifactId: result.details.artifact, // the document id, a UUID such as 4e0aca36-77b3-43bd-96cf-d58890ae64e4
105
+ version: result.details.version, // the version number the human is asked to approve
102
106
  })
103
- legion({ op: "register_gate", issue: "<root issue>", askId })
104
107
  ```
105
108
 
106
- Both options are required: a question with only `Approve` is not a decision. The whole ask is
107
- read on a phone by someone who has not read the code: no file paths, line numbers, document
108
- versions, or role tokens in it. Sami, 2026-09-12, on a gate ask that broke this rule: "I have no
109
- idea what the fuck you're talking about."
110
-
111
- Then park. Do not release a wave or spawn a Legion role until a later delivered wake
112
- shows `design-approved` on the root. On a deployment whose design gate is off
113
- (`gates.design: off` in its `legion.yaml`), the daemon satisfies the gate as you register it
114
- and `design-approved` arrives immediately — proceed. The daemon then closes the ask on Dispatch
115
- (you will see `ask.resolved` for it); that is expected and needs nothing from you. Approval
116
- covers the entire tree: later waves, re-scopes, and integration-failure children do not repeat
117
- this sequence.
109
+ `dispatch_request_approval` opens a system question on the document with the fixed options
110
+ `Approve` and `Request changes`; a human answers it from the Inbox or approves from the
111
+ document's own header. Never open a `dispatch_ask` with an `Approve` option yourself: an
112
+ ordinary question is not a gate and the daemon ignores its answer. Copy `artifactId` and
113
+ `version` from the result of `dispatch_request_approval` — its text reads "Approval requested for
114
+ spec.md (document id <UUID>) at version <N>" and its `details.artifact` / `details.version` carry
115
+ the same two values. The document id is never the slug or file name you passed in (`spec`,
116
+ `spec.md`): the daemon recognizes the document's approval events by that id, and both the
117
+ `legion` tool and the daemon refuse a value that is not a UUID. Calling
118
+ `dispatch_request_approval` again while a request is open returns the same open request, so it
119
+ is safe to repeat. If its text instead reads "spec.md (document id <UUID>) is already approved at
120
+ version <N>" — a human approved from the document header before you asked — still call
121
+ `register_gate` with that id and version: the daemon reads the approval from Dispatch as it
122
+ registers, opens the gate, and delivers `design-approved` at once. The same read covers a human
123
+ who answers the question between your `dispatch_request_approval` and `register_gate` calls, so
124
+ an approval is never lost to timing; you never approve anything yourself.
125
+
126
+ Then park. Do not release a wave or spawn a Legion role until a later delivered wake shows
127
+ `design-approved` on the root. On `design-changes-requested`, revise the spec (a new version of
128
+ the primary document), call `dispatch_request_approval` again — it re-opens the request at the
129
+ new version — and stay parked. Approval is pinned to the spec version: editing the root spec
130
+ after approval closes the gate again with no wake (you made the edit, or the `artifact.version`
131
+ event on your issue tells you), so call `dispatch_request_approval` again, and release no new
132
+ wave and spawn no new role until the next `design-approved` arrives — work already in flight
133
+ continues. Later waves, re-scopes, and integration-failure children that leave the root spec
134
+ untouched need no new approval, and a child issue's spec is never gated: the root approval covers
135
+ the tree.
118
136
 
119
137
  ## 2. Children in flight
120
138
 
@@ -191,7 +209,7 @@ legion({
191
209
  ```
192
210
 
193
211
  Wait for the implementer to report its durable retro result. Retro output is
194
- `docs/solutions/` plus an issue comment; it must not create a `.legion` file or change
212
+ `docs/solutions/` plus an issue comment; it must not create a `.legion` file or rewrite
195
213
  the reviewer-approved head after cleanup.
196
214
 
197
215
  ## 6. Architect sign-off and merge
@@ -206,19 +224,31 @@ Preserve this order exactly:
206
224
  2. on a clean review, `spawn_worker` the implementer once more to push only the `.legion/`
207
225
  deletion (the review App holds no `contents` permission), then the reviewer approves that
208
226
  head. The deletion must land before that approval, which is head-pinned. An implementer
209
- completion always writes the issue's status as `testing`; this one is not a test round,
210
- so on its `phase-complete` wake call `legion({ op: "set_status", issue, status: "retro" })`
211
- before `spawn_worker` on the reviewer to approve that head (a finished reviewer may already
212
- be retired; `spawn_worker` resumes it);
213
- 3. retro completes without dirtying the branch beyond `docs/solutions/`;
214
- 4. the merger verifies the current head is the reviewer-approved head plus only the retro
215
- commits and publishes `READY #<n> at <sha>` to `notifications.role.pr-queue`; it never
227
+ completion advances the status only from `in_progress` to `testing`; this push, like retro
228
+ later, leaves the status where it is, so you set nothing by hand — on its `phase-complete`
229
+ wake, `spawn_worker` the reviewer to approve that head (a finished reviewer may already be
230
+ retired; `spawn_worker` resumes it);
231
+ 3. retro commits its learnings under `docs/solutions/` on top of the approved head; that
232
+ commit does not void the approval and never returns the tree to the tester or reviewer;
233
+ 4. the merger verifies the current head is the reviewer-approved head plus only commits that
234
+ change `docs/solutions/` (`jj diff --from <approved-sha> --to <tip-sha> --summary`, quoted in READY)
235
+ and publishes `READY #<n> at <sha>` to `notifications.role.pr-queue`; it never
216
236
  merges. The merge queue merges under its own authority and the repository's own rules
217
237
  (branch protection, CODEOWNERS); whether a human must approve first is that repository's
218
238
  setting, not Legion's, and you never ask for or wait on such an approval.
219
239
 
220
- If anything else changes the head, return to review; do not let the merger publish `READY`
221
- for an obsolete approval.
240
+ What returns the tree to review: a changed diff — a commit above the approved head that
241
+ touches anything outside `docs/solutions/`, or a rebase whose fingerprint (the `legion-worker`
242
+ skill's unchanged-diff check) differs from the approved head's. What does not: retro's
243
+ `docs/solutions/` commit, and a rebase forced by a GitHub-reported conflict whose fingerprint
244
+ is unchanged. For that rebase the order is: the implementer rebases and posts the before/after
245
+ fingerprints; the tester re-runs the bare gates only; the reviewer confirms and approves the new
246
+ head by SHA (or continues its round if it had not approved); the merger republishes READY.
247
+ Retro does not re-run. A rebase happens only when GitHub reports `CONFLICTING`
248
+ (`legion gh -- pr view <n> --json mergeable,mergeStateStatus`); read that on every end-game
249
+ wake — `pr-ready`, `pr-review`, `phase-complete`, `catchup-overseer` — because a `CONFLICTING`
250
+ PR gets no CI and no wake announces it, and send the implementer to rebase the moment you see
251
+ it. Do not let the merger publish `READY` for an obsolete approval.
222
252
 
223
253
  If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
224
254
  to resolve, open an action ask (`dispatch_ask` with `kind: "action"`) that names the thread's URL
@@ -249,16 +279,18 @@ corresponding lifecycle procedure.
249
279
  | `child-closed` | Read the child completion and remaining open children. Re-scope or close obsolete open work; release an appropriate next wave, or await `children-complete`. |
250
280
  | `children-complete` | Execute steps 3–4: parent integration verification; failures become a new child wave, success advances to review and retro. |
251
281
  | `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. |
252
- | `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 has already returned the issue's Dispatch status to `in_progress` for this) 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). |
282
+ | `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. |
283
+ | `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. |
284
+ | `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. |
253
285
  | `worker-queued` | Payload `{type:"worker-queued", issue, role}`. The deployment's worker cap is full; this role's spawn is queued. Do not respawn or retry — wait for `worker-started`. |
254
286
  | `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. |
255
287
  | `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
256
- | `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; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. |
288
+ | `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. |
257
289
  | `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. |
258
290
  | `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. |
259
291
  | `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. |
260
292
  | `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. |
261
- | `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. 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. |
293
+ | `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. |
262
294
  | `worker-died` | Payload `{type:"worker-died", issue, role}`. The daemon probed and retried this role's worker through `MAX_LAUNCH_FAILURES` attempts and could not confirm a boot — never a raw-event replay or a silent revive. Reassess the work and `spawn_worker` again for the role (it resumes the same agent via `--resume` if a session file survived) or reassign it if the failure looks environmental, not agent-specific. |
263
295
  | `reopened` | Reopen the root lifecycle: inspect the reason and current artifacts, reassess scope and children, and resume at the first applicable numbered step. |
264
296
 
@@ -129,7 +129,11 @@ on stale entries until their source artifact explains the anomaly.
129
129
  ## Mentions
130
130
 
131
131
  Read the mention and its artifact. Answer it when it asks the controller for triage or
132
- human-facing information. Otherwise resolve the authoritative owning architect role and
133
- route the verified context with `envoy_publish`. Do not route raw event traffic or invent a
134
- role token from a partial issue reference.
132
+ human-facing information. A human asking how to let a root proceed past its design gate
133
+ approves the root issue's spec document in Dispatch — the `Approve` control in the document's
134
+ header, or the approval question the architect's request opened in the Inbox. The controller
135
+ never opens a gate and there is no operator command for it; a project that does not want the
136
+ gate at all runs `gates.design: off` in its `legion.yaml`. Otherwise resolve the authoritative
137
+ owning architect role and route the verified context with `envoy_publish`. Do not route raw
138
+ event traffic or invent a role token from a partial issue reference.
135
139
 
@@ -19,13 +19,20 @@ retrospective's durable output.
19
19
  approves that head.
20
20
  3. Run this retro: commit durable learnings to `docs/solutions/` and post the issue comment.
21
21
  Retro writes **no `.legion` file**, so it never re-dirties the cleaned handoff tree.
22
- 4. The merger verifies the approved head, publishes `READY`, and pushes nothing; the merge queue
23
- merges under the repository's own rules.
22
+ 4. The merger verifies the tip is the approved head plus commits that change only
23
+ `docs/solutions/` — `jj diff --from <approved-sha> --to <tip-sha> --summary`, quoted in READY —
24
+ publishes `READY`, and pushes nothing; the merge queue merges under the repository's own
25
+ rules.
24
26
  5. After the merge lands, the implementer — not the reviewer, the merger, or the queue — verifies
25
27
  the change in production and records it on the PR and the issue (Sami, 2026-09-13, verbatim:
26
28
  "the agent that developed it should be responsible for testing in production"). The
27
29
  architect's sign-off waits for that record.
28
30
 
31
+ Retro's commit sits above the reviewer's approved head and the approval stands: a commit that
32
+ changes only `docs/solutions/` does not void it, and the tree goes from retro to the merger —
33
+ never back to the tester or reviewer. A conflict-forced rebase after retro moves these documents
34
+ with the branch; retro does not re-run.
35
+
29
36
  Do not start retro before step 2, skip it because the change seems mechanical, or publish `READY`
30
37
  before step 3. The design gate is not a substitute for review and retro.
31
38
 
@@ -83,13 +90,16 @@ legion gh -- issue comment <issue-number> \
83
90
  ```
84
91
 
85
92
  The issue comment and `docs/solutions/` commit are the only retro outputs. Never write a
86
- handoff, phase artifact, local feedback log, or completion label.
93
+ handoff, phase artifact, local feedback log, or completion label; `.legion/` was deleted before
94
+ retro and nothing recreates it. Report completion with `legion handoff complete` alone (its
95
+ summary: two sentences for the architect) — no `legion handoff write`.
87
96
 
88
97
  ## Completion check
89
98
 
90
99
  Before returning, verify all of the following:
91
100
 
92
- - The reviewer cleanup commit remains below the retro documentation commit.
101
+ - The reviewer cleanup commit remains below the retro documentation commit, and the reviewer's
102
+ approval of that cleanup head stands: the merger accepts the approved head plus this commit.
93
103
  - The learning documents and issue comment both exist.
94
104
  - No `.legion` file was created or modified by retro.
95
105
  - The fresh-eyes analysis was considered alongside the implementer's context.
@@ -220,7 +220,7 @@ PR opens, and every later phase keeps it current rather than replacing it:
220
220
  ```
221
221
  ## Verification
222
222
 
223
- **CI:** `pr-checks-result` run <run-id> — success at <head-sha>.
223
+ **CI:** `Tests` run <run-id> — jobs lint, pr-title, typecheck, test all success at <head-sha>.
224
224
 
225
225
  **Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
226
226
  - Thread <id>: fixed in <commit-sha> — <one line>.
@@ -268,7 +268,13 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
268
268
  The implementer rebases the issue branch only when GitHub reports it `CONFLICTING` or the
269
269
  controller asks because of a conflict — never to pick up `main` or to refresh CI. A single
270
270
  failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
271
- pushing a new commit.
271
+ pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
272
+ confirmation, not a new round (see *The unchanged-diff check* below). Before rebasing, record
273
+ the fingerprint at the current tip; after pushing the rebased branch, record it at the new
274
+ tip; post one PR comment (Legion footer):
275
+ `rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
276
+ Rebase the whole chain — `jj -R "$LEGION_WORKSPACE" rebase -s 'roots(main@origin..@)' -d main@origin` —
277
+ so the tester's and reviewer's commits move with yours.
272
278
  - **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." The `Fast-follow:`
273
279
  field names naming, duplication, or wording cleanup only; anything that changes behaviour,
274
280
  hides an error, or breaks a gate lands in this PR.
@@ -294,21 +300,33 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
294
300
  whole staging gate never ran. Environment or
295
301
  secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
296
302
  `.legion/test.json`, and only when the issue's acceptance criteria call for it — never
297
- re-pasted into the PR body each round.
303
+ re-pasted into the PR body each round. After a conflict-forced rebase, compute the
304
+ fingerprint at the head your `E2E` line names and at the new head. Equal: re-run only the
305
+ bare gates — the repository's CI green at the new head and its smoke check — and change the
306
+ `E2E` line's head to the new SHA with
307
+ `rebase re-check <old-sha> → <new-sha>: fingerprint unchanged, bare gates only`; the
308
+ real-surface verification is not repeated. Different: a full test round.
298
309
  - The reviewer verifies the `CI`, `Threads`, and `E2E` facts against GitHub directly —
299
310
  never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
300
311
  `task(agent="thermonuclear-code-quality")` once at that head and records the verdict.
301
312
  Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round** —
302
313
  `REQUEST_CHANGES` when any correctness finding stands, otherwise `COMMENT` while the head
303
- still carries `.legion/`; `APPROVE` only for the head that differs from the reviewed one by
304
- the `.legion/` deletion alone, named by SHA — carrying every inline comment in that single
314
+ still carries `.legion/`; `APPROVE` only for a head that carries no `.legion/` — the head
315
+ that differs from the reviewed one by the `.legion/` deletion alone, or, after a
316
+ conflict-forced rebase, the new head whose fingerprint equals the approved head's — always
317
+ named by SHA — carrying every inline comment in that single
305
318
  call: `legion gh -- api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
306
319
  with `commit_id`, `event` (`REQUEST_CHANGES`, `COMMENT`, or `APPROVE`), `body` (with the
307
320
  Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
308
321
  finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
309
322
  Then return the issue to the architect; when clean, have the architect send the implementer
310
323
  back to push the `.legion/` deletion (the review App cannot push), then review **that** head
311
- and approve it by name.
324
+ and approve it by name. After a conflict-forced rebase, compute the fingerprint at the
325
+ `commit_id` of your last submitted review and at the new head. Equal and that review was
326
+ `APPROVE`: submit one more `APPROVE` naming the new head by SHA, its body naming both SHAs
327
+ and the fingerprint — a confirmation, not a round; no thermo pass, no thread pass. Equal and
328
+ that review was `COMMENT` or `REQUEST_CHANGES`: continue that round against the new head;
329
+ nothing restarts. Different: a new round — thermo again, one review.
312
330
  When you re-review after a corrective push, answer every thread you opened in one of the
313
331
  three forms above — `Accepted:` is the only reply the implementer's `legion threads resolve`
314
332
  acts on — and approve only once every thread you opened carries your `Accepted:` reply and the
@@ -316,13 +334,24 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
316
334
  the PR body).
317
335
  - Once a base is frozen for others to stack on, never rewrite it — fixes land as new
318
336
  commits on top, and the `Chain` line records what is frozen.
337
+ - **Retro's commit does not void the reviewer's approval.** After the reviewer approves the
338
+ cleaned head, retro commits its learnings under `docs/solutions/` on top of it; that commit
339
+ stays, the approval stands, and the tree goes to the merger — never back to the tester or
340
+ reviewer. Anything else above the approved head does void it, and the merger tells the
341
+ architect the head must return to review instead of publishing. A conflict-forced rebase
342
+ after retro moves those documents with the branch; retro never re-runs.
319
343
  - The merger runs `legion threads resolve --pr <n> --repo <owner>/<repo>` (it acts as the same
320
344
  code-writing App as the implementer; resolving a thread changes no commit, so this run never
321
345
  invalidates the approval), does not publish while any `left open` line remains or the command
322
- exits 1 (report the thread to the architect instead), confirms the approved head still equals
323
- the current head, then publishes `READY #<n> at <sha>` plus the PR body's gate facts to the
324
- merge queue's role (`notifications.role.pr-queue`) with `envoy_publish`. The merger never
325
- merges; the queue merges under its own authority.
346
+ exits 1 (report the thread to the architect instead), then
347
+ proves that rule with two commands and publishes. First
348
+ `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R "$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`,
349
+ whose output is quoted in READY (an empty output is quoted as
350
+ `no file changes above the approved head`); then the same with `'~docs/solutions'` appended,
351
+ which must print nothing. Then it publishes `READY #<n> at <tip-sha>` naming the approved
352
+ head, the tip, and that summary, plus the PR body's gate facts, to the merge queue's role
353
+ (`notifications.role.pr-queue`) with `envoy_publish`. The merger never merges; the queue
354
+ merges under its own authority.
326
355
  - **After the queue merges, the implementer verifies in production.** Sami, 2026-09-13,
327
356
  verbatim: "the agent that developed it should be responsible for testing in production."
328
357
  The architect sends the implementer back once the merge lands; the implementer watches the
@@ -333,6 +362,44 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
333
362
  failed at 00:12Z on a resource staging never runs. If the slot fails on the change, the
334
363
  implementer owns the fix and the next slot.
335
364
 
365
+ ## The unchanged-diff check
366
+
367
+ The fingerprint every role compares after a conflict-forced rebase (every flag and the fileset
368
+ verified on jj 0.45.1):
369
+
370
+ ```bash
371
+ cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && \
372
+ jj -R "$LEGION_WORKSPACE" diff --from "fork_point(main@origin | <head-sha>)" --to <head-sha> \
373
+ --git --context 0 '~(.legion | docs/solutions)' \
374
+ | sed -e '/^@@/d' -e '/^index /d' | sha256sum
375
+ ```
376
+
377
+ - `<head-sha>` is a full commit SHA; a jj commit id is the git SHA GitHub shows.
378
+ - `fork_point(main@origin | <head-sha>)` is the base the branch was cut from *at that head*:
379
+ the old base for the pre-rebase head, the new base for the rebased one, so one command
380
+ serves both sides. On a stacked PR substitute its base branch for `main`
381
+ (`legion gh -- pr view <n> --json baseRefName`).
382
+ - A head the rebase hid is still addressable by its SHA in the shared workspace. A SHA the
383
+ workspace cannot resolve (`jj -R "$LEGION_WORKSPACE" log -r <sha>` errors) counts as a
384
+ changed diff — never as unchanged.
385
+ - `--context 0` drops context lines; the `sed` drops `@@` hunk headers (line positions move
386
+ on a rebase) and `index` lines (blob ids move when the base's copy of a file changed). What
387
+ is left is exactly the added and removed lines per file.
388
+ - The single fileset `'~(.legion | docs/solutions)'` leaves out the handoff ledger and retro's
389
+ learnings: process artifacts the rules above already exempt from re-review, which change
390
+ between one role's verified head and the next without changing the product. This is what lets
391
+ each role compare against *its own* last verified head instead of trusting another role's
392
+ numbers. It must be one expression: jj unions positional filesets, so two separate
393
+ `'~.legion' '~docs/solutions'` arguments select every file and exclude nothing. Once
394
+ `.legion/` is gone, jj warns `No matching entries for paths: .legion` on stderr; the hash is
395
+ unaffected.
396
+
397
+ Where each role gets its two heads: the implementer — the tip before and after its own rebase;
398
+ the tester — the head its `E2E` line names and the new head; the reviewer — the `commit_id` of
399
+ its last submitted review (`legion gh -- api repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login}'`)
400
+ and the new head; the merger never computes a fingerprint — it uses the `--summary` check
401
+ above.
402
+
336
403
  ## Completion gate: handoff write, verification, and persistence
337
404
 
338
405
  Write the phase-specific handoff:
@@ -368,7 +435,13 @@ cd -- "$LEGION_WORKSPACE" && \
368
435
  Do not report phase completion until the write, existence check, and handoff commit
369
436
  succeed; when an issue branch exists, its push is also required. This is the committed
370
437
  copy the next phase reads after revival. It is removed once, at the end of a clean review: the
371
- implementer pushes that deletion at the reviewer's direction. No other phase removes it.
438
+ implementer pushes that deletion at the reviewer's direction. No other phase removes it — and
439
+ once it is gone (`jj -R "$LEGION_WORKSPACE" file list -r @- .legion` prints nothing on stdout;
440
+ jj warns on stderr), this
441
+ gate no longer applies: a later rebase, bare-gate re-check, confirmation, or retro writes no
442
+ `.legion/<phase>.json`, commits no handoff, and reports with `legion handoff complete` alone
443
+ (below). Recreating `.legion/` after its deletion changes the approved head and restarts the
444
+ review loop this rule exists to end.
372
445
 
373
446
  ## Completion: report to the architect, then stay
374
447
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.22.1",
3
+ "version": "1.23.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "legion": {
15
- "daemonApiVersion": 1
15
+ "daemonApiVersion": 2
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",