@sjawhar/pi-legion-envoy 0.44.1 → 1.1.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/README.md CHANGED
@@ -104,7 +104,9 @@ Configure the shared `envoy.json` with:
104
104
 
105
105
  The user file is `~/.config/opencode/envoy.json`; a
106
106
  `<cwd>/.opencode/envoy.json` file shallow-merges over it. `DISPATCH_URL` and
107
- `DISPATCH_TOKEN` override the file values for one process. Omitting
107
+ `DISPATCH_TOKEN` override the file values for one process; `DISPATCH_TOKEN_FILE`
108
+ (a path whose trimmed contents are the token — how the Legion daemon delivers it
109
+ to a pane) wins over both and never falls back when unreadable. Omitting
108
110
  `dispatch.serverUrl` while `dispatch.enabled` is true targets
109
111
  `http://localhost:8766`, the Go server's listen address. Invalid configuration,
110
112
  an invalid URL, or an empty token leaves the twelve tools unavailable and reports
package/dist/envoy.js CHANGED
@@ -30244,8 +30244,7 @@ var LegionDaemonApi = {
30244
30244
  roleTokens: record(string2(), string2()),
30245
30245
  controlSubject: nonEmptyString,
30246
30246
  gates: object({
30247
- design: _enum2(["root-issues", "off"]),
30248
- merge: _enum2(["human", "off"])
30247
+ design: _enum2(["root-issues", "off"])
30249
30248
  }).optional(),
30250
30249
  secret: nonEmptyString
30251
30250
  })
@@ -30254,14 +30253,6 @@ var LegionDaemonApi = {
30254
30253
  request: architectCapability.extend({ generation: number2().int() }),
30255
30254
  response: object({})
30256
30255
  },
30257
- MergeGate: {
30258
- request: architectCapability.extend({ pr: number2().int().positive() }),
30259
- response: object({
30260
- approved: boolean2(),
30261
- pr: number2().int().positive(),
30262
- headSha: nonEmptyString
30263
- })
30264
- },
30265
30256
  ProcessExit: {
30266
30257
  request: architectCapability.extend({ generation: number2().int() }),
30267
30258
  response: object({})
@@ -31198,7 +31189,7 @@ ${envelope.payload ?? ""}`;
31198
31189
  }
31199
31190
 
31200
31191
  // ../envoy-client/src/dispatch-config.ts
31201
- import { readFileSync } from "fs";
31192
+ import { readFileSync as readFileSync2 } from "fs";
31202
31193
  import { homedir } from "os";
31203
31194
  import * as path from "path";
31204
31195
 
@@ -31207,6 +31198,21 @@ function messageFor(error) {
31207
31198
  return error instanceof Error ? error.message : String(error);
31208
31199
  }
31209
31200
 
31201
+ // ../envoy-client/src/secret-file.ts
31202
+ import { readFileSync } from "fs";
31203
+ function readSecretFile(variable, filePath) {
31204
+ let contents;
31205
+ try {
31206
+ contents = readFileSync(filePath, "utf8");
31207
+ } catch (error) {
31208
+ throw new Error(`${variable} names ${filePath}, which could not be read: ${messageFor(error)}`);
31209
+ }
31210
+ const value = contents.trim();
31211
+ if (value.length === 0)
31212
+ throw new Error(`${variable} names ${filePath}, which is empty`);
31213
+ return value;
31214
+ }
31215
+
31210
31216
  // ../envoy-client/src/dispatch-config.ts
31211
31217
  var DEFAULT_SERVER_URL = "http://localhost:8766";
31212
31218
  function normalizeDispatchUrl(url2) {
@@ -31243,7 +31249,7 @@ function describeSchemaIssue(filePath, error) {
31243
31249
  function readEnvoyFile(filePath) {
31244
31250
  let raw;
31245
31251
  try {
31246
- raw = readFileSync(filePath, "utf-8");
31252
+ raw = readFileSync2(filePath, "utf-8");
31247
31253
  } catch {
31248
31254
  return { kind: "absent" };
31249
31255
  }
@@ -31276,8 +31282,22 @@ function resolveDispatchConfig(env, options = {}) {
31276
31282
  };
31277
31283
  const rawUrl = explicitUrl !== undefined ? { value: explicitUrl, source: "DISPATCH_URL" } : merged.enabled === true ? { value: merged.serverUrl ?? DEFAULT_SERVER_URL, source: "dispatch.serverUrl" } : null;
31278
31284
  const url2 = rawUrl ? parsedDispatchUrl(rawUrl.value, rawUrl.source) : { url: null, error: null };
31279
- const token = env.DISPATCH_TOKEN ?? merged.token ?? null;
31280
- const tokenSource = env.DISPATCH_TOKEN === undefined ? "dispatch.token" : "DISPATCH_TOKEN";
31285
+ let token;
31286
+ let tokenSource;
31287
+ if (env.DISPATCH_TOKEN_FILE !== undefined) {
31288
+ tokenSource = "DISPATCH_TOKEN_FILE";
31289
+ try {
31290
+ token = readSecretFile(tokenSource, env.DISPATCH_TOKEN_FILE);
31291
+ } catch (error) {
31292
+ return { enabled: false, url: url2.url, token: null, error: messageFor(error) };
31293
+ }
31294
+ } else if (env.DISPATCH_TOKEN !== undefined) {
31295
+ tokenSource = "DISPATCH_TOKEN";
31296
+ token = env.DISPATCH_TOKEN;
31297
+ } else {
31298
+ tokenSource = "dispatch.token";
31299
+ token = merged.token ?? null;
31300
+ }
31281
31301
  if (url2.error !== null)
31282
31302
  return { enabled: false, url: null, token, error: url2.error };
31283
31303
  if (url2.url === null)
package/dist/legion.js CHANGED
@@ -30243,8 +30243,7 @@ var LegionDaemonApi = {
30243
30243
  roleTokens: record(string2(), string2()),
30244
30244
  controlSubject: nonEmptyString,
30245
30245
  gates: object({
30246
- design: _enum2(["root-issues", "off"]),
30247
- merge: _enum2(["human", "off"])
30246
+ design: _enum2(["root-issues", "off"])
30248
30247
  }).optional(),
30249
30248
  secret: nonEmptyString
30250
30249
  })
@@ -30253,14 +30252,6 @@ var LegionDaemonApi = {
30253
30252
  request: architectCapability.extend({ generation: number2().int() }),
30254
30253
  response: object({})
30255
30254
  },
30256
- MergeGate: {
30257
- request: architectCapability.extend({ pr: number2().int().positive() }),
30258
- response: object({
30259
- approved: boolean2(),
30260
- pr: number2().int().positive(),
30261
- headSha: nonEmptyString
30262
- })
30263
- },
30264
30255
  ProcessExit: {
30265
30256
  request: architectCapability.extend({ generation: number2().int() }),
30266
30257
  response: object({})
@@ -30441,6 +30432,21 @@ function messageFor(error) {
30441
30432
  var import_nats2 = __toESM(require_mod4(), 1);
30442
30433
  import { logger as logger2 } from "@oh-my-pi/pi-utils";
30443
30434
 
30435
+ // ../envoy-client/src/secret-file.ts
30436
+ import { readFileSync } from "fs";
30437
+ function readSecretFile(variable, filePath) {
30438
+ let contents;
30439
+ try {
30440
+ contents = readFileSync(filePath, "utf8");
30441
+ } catch (error) {
30442
+ throw new Error(`${variable} names ${filePath}, which could not be read: ${messageFor(error)}`);
30443
+ }
30444
+ const value = contents.trim();
30445
+ if (value.length === 0)
30446
+ throw new Error(`${variable} names ${filePath}, which is empty`);
30447
+ return value;
30448
+ }
30449
+
30444
30450
  // src/legion/classify.ts
30445
30451
  function classifySession(env) {
30446
30452
  if (env.LEGION_CONTROLLER !== undefined && env.LEGION_TREE !== undefined) {
@@ -30466,10 +30472,20 @@ function requiredEnvironment(env, key) {
30466
30472
  throw new Error(`${key} is required for Legion`);
30467
30473
  return value;
30468
30474
  }
30475
+ function requiredSecret(env, key) {
30476
+ const fileKey = `${key}_FILE`;
30477
+ const file = env[fileKey];
30478
+ if (file !== undefined)
30479
+ return readSecretFile(fileKey, file);
30480
+ return requiredEnvironment(env, key);
30481
+ }
30469
30482
  function requiredControllerCapability(env) {
30483
+ if (env.LEGION_CONTROLLER_SECRET_FILE !== undefined) {
30484
+ return readSecretFile("LEGION_CONTROLLER_SECRET_FILE", env.LEGION_CONTROLLER_SECRET_FILE);
30485
+ }
30470
30486
  const secret = env.LEGION_CONTROLLER_SECRET;
30471
30487
  if (!secret) {
30472
- throw new Error("LEGION_CONTROLLER_SECRET is required to claim the controller. " + "Launch OMP with LEGION_CONTROLLER_SECRET in its environment before running " + "/legion-claim-controller.");
30488
+ throw new Error("LEGION_CONTROLLER_SECRET or LEGION_CONTROLLER_SECRET_FILE is required to claim the controller. " + "Launch OMP with one of them in its environment before running /legion-claim-controller.");
30473
30489
  }
30474
30490
  return secret;
30475
30491
  }
@@ -30593,7 +30609,6 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery) {
30593
30609
  workerStarted: (input) => post("/legion/v1/worker/started", input, LegionDaemonApi.WorkerStarted.response),
30594
30610
  workerReady: (input) => noContent("/legion/v1/worker/ready", input, LegionDaemonApi.WorkerReady.response),
30595
30611
  spawnWorker: (input) => post("/legion/v1/worker/spawn", input, LegionDaemonApi.SpawnWorker.response),
30596
- mergeGate: (input) => post("/legion/v1/merge-gate", input, LegionDaemonApi.MergeGate.response),
30597
30612
  releaseWave: (input) => post("/legion/v1/waves/release", input, LegionDaemonApi.WaveRelease.response),
30598
30613
  provisioningCredential: (input) => post("/legion/v1/provisioning-credential", input, LegionDaemonApi.ProvisioningCredential.response),
30599
30614
  escalate: (input) => noContent("/legion/v1/escalate", input, LegionDaemonApi.Escalate.response),
@@ -30712,20 +30727,12 @@ var LEGION_OP_FIELDS = {
30712
30727
  register_gate: ["issue", "askId"],
30713
30728
  release_wave: ["issues"],
30714
30729
  escalate: ["kind", "context"],
30715
- merge_gate: ["pr"],
30716
30730
  spawn_worker: ["issue", "role", "task"]
30717
30731
  };
30718
30732
  function legionToolSchema(pi) {
30719
30733
  const z = pi.zod;
30720
30734
  return z.object({
30721
- op: z.enum([
30722
- "set_status",
30723
- "register_gate",
30724
- "release_wave",
30725
- "escalate",
30726
- "merge_gate",
30727
- "spawn_worker"
30728
- ]),
30735
+ op: z.enum(["set_status", "register_gate", "release_wave", "escalate", "spawn_worker"]),
30729
30736
  issue: z.string().optional(),
30730
30737
  status: z.enum(LIFECYCLE_STATUSES2).optional(),
30731
30738
  askId: z.string().optional(),
@@ -30733,7 +30740,6 @@ function legionToolSchema(pi) {
30733
30740
  context: z.unknown().optional(),
30734
30741
  issues: z.array(z.string()).optional(),
30735
30742
  rationale: z.string().optional(),
30736
- pr: z.number().optional(),
30737
30743
  role: z.enum(LEGION_ROLES).optional(),
30738
30744
  task: z.string().optional()
30739
30745
  });
@@ -30757,13 +30763,6 @@ function createLegionTool(deps) {
30757
30763
  throw new Error(`${String(parameters.op)} requires ${name}`);
30758
30764
  return value;
30759
30765
  };
30760
- const numberInput = (name) => {
30761
- const value = parameters[name];
30762
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
30763
- throw new Error(`${String(parameters.op)} requires a positive integer ${name}`);
30764
- }
30765
- return value;
30766
- };
30767
30766
  const op = String(parameters.op);
30768
30767
  const allowedFields = LEGION_OP_FIELDS[op];
30769
30768
  if (allowedFields) {
@@ -30774,13 +30773,6 @@ function createLegionTool(deps) {
30774
30773
  }
30775
30774
  }
30776
30775
  switch (parameters.op) {
30777
- case "merge_gate":
30778
- return jsonSuccess(await daemon.mergeGate({
30779
- tree: architect.tree,
30780
- pr: numberInput("pr"),
30781
- sessionId,
30782
- secret: architect.secret
30783
- }));
30784
30776
  case "set_status": {
30785
30777
  const status = parameters.status;
30786
30778
  if (typeof status !== "string" || !isLifecycleStatus(status)) {
@@ -31645,7 +31637,7 @@ ${envelope.payload ?? ""}`;
31645
31637
  }
31646
31638
 
31647
31639
  // ../envoy-client/src/dispatch-config.ts
31648
- import { readFileSync } from "fs";
31640
+ import { readFileSync as readFileSync2 } from "fs";
31649
31641
  import { homedir } from "os";
31650
31642
  import * as path3 from "path";
31651
31643
  var DEFAULT_SERVER_URL = "http://localhost:8766";
@@ -31683,7 +31675,7 @@ function describeSchemaIssue(filePath, error) {
31683
31675
  function readEnvoyFile(filePath) {
31684
31676
  let raw;
31685
31677
  try {
31686
- raw = readFileSync(filePath, "utf-8");
31678
+ raw = readFileSync2(filePath, "utf-8");
31687
31679
  } catch {
31688
31680
  return { kind: "absent" };
31689
31681
  }
@@ -31716,8 +31708,22 @@ function resolveDispatchConfig(env, options = {}) {
31716
31708
  };
31717
31709
  const rawUrl = explicitUrl !== undefined ? { value: explicitUrl, source: "DISPATCH_URL" } : merged.enabled === true ? { value: merged.serverUrl ?? DEFAULT_SERVER_URL, source: "dispatch.serverUrl" } : null;
31718
31710
  const url2 = rawUrl ? parsedDispatchUrl(rawUrl.value, rawUrl.source) : { url: null, error: null };
31719
- const token = env.DISPATCH_TOKEN ?? merged.token ?? null;
31720
- const tokenSource = env.DISPATCH_TOKEN === undefined ? "dispatch.token" : "DISPATCH_TOKEN";
31711
+ let token;
31712
+ let tokenSource;
31713
+ if (env.DISPATCH_TOKEN_FILE !== undefined) {
31714
+ tokenSource = "DISPATCH_TOKEN_FILE";
31715
+ try {
31716
+ token = readSecretFile(tokenSource, env.DISPATCH_TOKEN_FILE);
31717
+ } catch (error) {
31718
+ return { enabled: false, url: url2.url, token: null, error: messageFor(error) };
31719
+ }
31720
+ } else if (env.DISPATCH_TOKEN !== undefined) {
31721
+ tokenSource = "DISPATCH_TOKEN";
31722
+ token = env.DISPATCH_TOKEN;
31723
+ } else {
31724
+ tokenSource = "dispatch.token";
31725
+ token = merged.token ?? null;
31726
+ }
31721
31727
  if (url2.error !== null)
31722
31728
  return { enabled: false, url: null, token, error: url2.error };
31723
31729
  if (url2.url === null)
@@ -34018,7 +34024,7 @@ function legionExtension(pi) {
34018
34024
  return createLegionDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"), fetch, {
34019
34025
  recoveryToken: (sessionId) => {
34020
34026
  if (capability !== undefined && sessionId === capability.sessionID) {
34021
- return requiredEnvironment(process.env, "LEGION_BOOT_TOKEN");
34027
+ return requiredSecret(process.env, "LEGION_BOOT_TOKEN");
34022
34028
  }
34023
34029
  throw new Error(`Legion session ${sessionId} has no persisted recovery token`);
34024
34030
  },
@@ -34101,7 +34107,7 @@ function legionExtension(pi) {
34101
34107
  return;
34102
34108
  if (bootstrap)
34103
34109
  return bootstrap;
34104
- const bootToken = requiredEnvironment(process.env, "LEGION_BOOT_TOKEN");
34110
+ const bootToken = requiredSecret(process.env, "LEGION_BOOT_TOKEN");
34105
34111
  bootstrap = (async () => {
34106
34112
  const { sessionFile, agentId } = await persistedTranscript(context);
34107
34113
  const started = await (async () => {
@@ -34168,7 +34174,7 @@ function legionExtension(pi) {
34168
34174
  return;
34169
34175
  if (bootstrap)
34170
34176
  return bootstrap;
34171
- const bootToken = requiredEnvironment(process.env, "LEGION_BOOT_TOKEN");
34177
+ const bootToken = requiredSecret(process.env, "LEGION_BOOT_TOKEN");
34172
34178
  const workspace = requiredEnvironment(process.env, "LEGION_WORKSPACE");
34173
34179
  bootstrap = (async () => {
34174
34180
  const { sessionFile, agentId } = await persistedTranscript(context);
@@ -34,6 +34,8 @@ directly for a durable human question; replies come back to the worker's own ses
34
34
  ## Durable artifacts
35
35
 
36
36
  Phase handoffs are committed in lifecycle order: architect, plan, implement, test, and review.
37
- The reviewer removes `.legion/` as its final commit; retro records its learning in
37
+ 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
38
40
  `docs/solutions/` and writes no handoff. GitHub comments and reviews carry the required Legion
39
41
  footer so the daemon can attribute artifacts to their worker session.
@@ -164,28 +164,30 @@ Wait for the messaged implementer to report its durable retro result. Retro outp
164
164
  `docs/solutions/` plus an issue comment; it must not create a `.legion` file or change
165
165
  the reviewer-approved head after cleanup.
166
166
 
167
- ## 6. Architect sign-off and final merge gate
167
+ ## 6. Architect sign-off and merge
168
168
 
169
169
  Sign off only when scope is fully met, integration evidence is current, corrective work
170
170
  is complete, review is clean, retro completed, and no necessary work was silently
171
171
  deferred. Make the sign-off comment explicit about that evidence.
172
172
 
173
- When the config-armed final merge gate applies, preserve this order exactly:
173
+ Preserve this order exactly:
174
174
 
175
175
  1. tester green and review cycles complete;
176
- 2. reviewer pushes the `.legion/` deletion as its final commit and approves that head;
177
- 3. retro completes without dirtying the branch;
178
- 4. enter the Sami-approval step by calling
179
- `legion({ op: "merge_gate", pr: <pull request number> })`. The daemon performs one
180
- current GitHub review read against the pinned head. If it returns `approved: true`, the
181
- approval already satisfies the gate and you immediately continue to the merger; do not
182
- wait for a new wake. If it returns `approved: false`, request or retain Sami approval
183
- and park for a later `pr-ready` wake. Do not poll or retry this check;
184
- 5. The merger verifies the approved head and publishes `READY #<n> at <sha>` to
185
- `notifications.role.pr-queue`; it never merges. The merge queue approves and merges under its own authority.
186
-
187
- If anything changes the approved head, return to review; do not let the merger publish
188
- `READY` for an obsolete approval.
176
+ 2. on a clean review, `spawn_worker` the implementer once more to push only the `.legion/`
177
+ deletion (the review App holds no `contents` permission), then the reviewer approves that
178
+ head. The deletion must land before that approval, which is head-pinned. An implementer
179
+ completion always writes the issue's status as `testing`; this one is not a test round,
180
+ so on its `phase-complete` wake call `legion({ op: "set_status", issue, status: "retro" })`
181
+ before messaging the reviewer to approve;
182
+ 3. retro completes without dirtying the branch beyond `docs/solutions/`;
183
+ 4. the merger verifies the current head is the reviewer-approved head plus only the retro
184
+ commits and publishes `READY #<n> at <sha>` to `notifications.role.pr-queue`; it never
185
+ merges. The merge queue merges under its own authority and the repository's own rules
186
+ (branch protection, CODEOWNERS); whether a human must approve first is that repository's
187
+ setting, not Legion's, and you never ask for or wait on such an approval.
188
+
189
+ If anything else changes the head, return to review; do not let the merger publish `READY`
190
+ for an obsolete approval.
189
191
 
190
192
  ## 7. Close
191
193
 
@@ -193,7 +195,7 @@ After the merge result and sign-off are recorded, post the sign-off and close th
193
195
  through the Legion write surface:
194
196
 
195
197
  ```text
196
- dispatch_comment({ issue: "LEGION-40", body: "<sign-off: scope, integration evidence, review, retro, Sami approval, and merge>" })
198
+ dispatch_comment({ issue: "LEGION-40", body: "<sign-off: scope, integration evidence, review, retro, and merge>" })
197
199
  legion({ op: "set_status", issue: "LEGION-40", status: "done" })
198
200
  ```
199
201
 
@@ -213,7 +215,7 @@ corresponding lifecycle procedure.
213
215
  | `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). |
214
216
  | `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`. |
215
217
  | `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. |
216
- | `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/Sami/merger order only for that current head. |
218
+ | `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
217
219
  | `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. |
218
220
  | `pr-blocked` | 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. |
219
221
  | `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. |
@@ -18,8 +18,9 @@ until that PR merges, this skill's contract is not yet runnable on `main`.
18
18
  The Legion extension claims `legion-<project>-controller` and registers controller readiness
19
19
  with the daemon during session startup. Do not handle a wake unless that startup succeeded.
20
20
 
21
- For an interactive takeover, start OMP with `LEGION_CONTROLLER_SECRET` and
22
- `LEGION_DAEMON_URL` in its environment, then run:
21
+ For an interactive takeover, start OMP with `LEGION_CONTROLLER_SECRET` (or
22
+ `LEGION_CONTROLLER_SECRET_FILE`, a path to a file holding it) and `LEGION_DAEMON_URL` in its
23
+ environment, then run:
23
24
 
24
25
  ```text
25
26
  /legion-claim-controller
@@ -15,14 +15,15 @@ Follow this ordering exactly. It keeps the reviewed branch clean while preservin
15
15
  retrospective's durable output.
16
16
 
17
17
  1. Tester green and all code-review cycles finish.
18
- 2. The reviewer removes `.legion/`, pushes that deletion as its final commit, then approves.
18
+ 2. The implementer pushes the `.legion/` deletion at the reviewer's direction, and the reviewer
19
+ approves that head.
19
20
  3. Run this retro: commit durable learnings to `docs/solutions/` and post the issue comment.
20
21
  Retro writes **no `.legion` file**, so it never re-dirties the cleaned handoff tree.
21
- 4. Sami approves the final reviewed head.
22
- 5. The merger verifies the approved head, publishes `READY`, and pushes nothing; the merge queue merges.
22
+ 4. The merger verifies the approved head, publishes `READY`, and pushes nothing; the merge queue
23
+ merges under the repository's own rules.
23
24
 
24
- Do not start retro before step 2, skip it because the change seems mechanical, or merge before
25
- steps 3 and 4. The design gate is not a substitute for this final merge gate.
25
+ Do not start retro before step 2, skip it because the change seems mechanical, or publish `READY`
26
+ before step 3. The design gate is not a substitute for review and retro.
26
27
 
27
28
  ## Two perspectives
28
29
 
@@ -84,4 +85,4 @@ Before returning, verify all of the following:
84
85
  - The learning documents and issue comment both exist.
85
86
  - No `.legion` file was created or modified by retro.
86
87
  - The fresh-eyes analysis was considered alongside the implementer's context.
87
- - Sami's approval and the merger remain subsequent steps, not work performed by retro.
88
+ - The merger remains a subsequent step, not work performed by retro.
@@ -19,8 +19,9 @@ until that PR merges, this skill's contract is not yet runnable on `main`.
19
19
 
20
20
  The daemon spawns you as a separate `omp --mode rpc` process (behind `legion worker-shim`,
21
21
  in a tmux pane) with `LEGION_TREE`, `LEGION_ISSUE`, `LEGION_ROLE`, `LEGION_GENERATION`,
22
- `LEGION_BOOT_TOKEN`, `LEGION_DAEMON_URL`, `LEGION_STATE_DIR`, and `LEGION_WORKSPACE` in your
23
- environment (`LEGION_PROJECT` is also supplied, but nothing reads it). The extension
22
+ `LEGION_BOOT_TOKEN_FILE` (a 0600 file under `$LEGION_STATE_DIR/secrets` holding your boot token;
23
+ the extension reads it for you), `LEGION_DAEMON_URL`, `LEGION_STATE_DIR`, and `LEGION_WORKSPACE`
24
+ in your environment (`LEGION_PROJECT` is also supplied, but nothing reads it). The extension
24
25
  completes the boot handshake for you at session start — it registers with the daemon, claims
25
26
  your role, and signals readiness. You never call `envoy_role_set` yourself.
26
27
 
@@ -219,8 +220,9 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
219
220
  never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
220
221
  `task(agent="thermonuclear-code-quality")` once at that head and records the verdict.
221
222
  Skip the `Thermo` line entirely on a docs-only PR. Post every correctness finding as a
222
- PR review comment and return the issue to the architect; when clean, delete `.legion/`,
223
- push, review **that** head, and approve it by name.
223
+ PR review comment and return the issue to the architect; when clean, have the architect send
224
+ the implementer back to push the `.legion/` deletion (the review App cannot push), then
225
+ review **that** head and approve it by name.
224
226
  - Once a base is frozen for others to stack on, never rewrite it — fixes land as new
225
227
  commits on top, and the `Chain` line records what is frozen.
226
228
  - The merger confirms the approved head still equals the current head, then publishes
@@ -262,8 +264,8 @@ cd -- "$LEGION_WORKSPACE" && \
262
264
 
263
265
  Do not report phase completion until the write, existence check, and handoff commit
264
266
  succeed; when an issue branch exists, its push is also required. This is the committed
265
- copy the next phase reads after revival. The reviewer later removes `.legion/` as its final
266
- commit; phase workers do not remove it.
267
+ copy the next phase reads after revival. It is removed once, at the end of a clean review: the
268
+ implementer pushes that deletion at the reviewer's direction. No other phase removes it.
267
269
 
268
270
  ## Completion: report to the architect, then stay
269
271
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "0.44.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [