@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 +3 -1
- package/dist/envoy.js +34 -14
- package/dist/legion.js +49 -43
- package/dist/skills/AGENTS.md +3 -1
- package/dist/skills/legion-architect/SKILL.md +19 -17
- package/dist/skills/legion-controller/SKILL.md +3 -2
- package/dist/skills/legion-retro/SKILL.md +7 -6
- package/dist/skills/legion-worker/SKILL.md +8 -6
- package/package.json +1 -1
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
|
|
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 =
|
|
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
|
-
|
|
31280
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
31720
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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);
|
package/dist/skills/AGENTS.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
173
|
+
Preserve this order exactly:
|
|
174
174
|
|
|
175
175
|
1. tester green and review cycles complete;
|
|
176
|
-
2.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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,
|
|
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/
|
|
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`
|
|
22
|
-
`LEGION_DAEMON_URL` in its
|
|
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
|
|
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.
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
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
|
-
-
|
|
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
|
-
`
|
|
23
|
-
|
|
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,
|
|
223
|
-
push
|
|
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.
|
|
266
|
-
|
|
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
|
|