@sjawhar/pi-legion-envoy 1.67.0 → 1.68.1
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 +0 -6
- package/dist/legion.js +162 -97
- package/dist/skills/legion-controller/SKILL.md +18 -15
- package/dist/skills/legion-worker/SKILL.md +11 -3
- package/package.json +2 -2
package/dist/envoy.js
CHANGED
|
@@ -30502,12 +30502,6 @@ var phaseHandoffSchema = exports_external.discriminatedUnion("phase", [
|
|
|
30502
30502
|
testSchema,
|
|
30503
30503
|
reviewSchema
|
|
30504
30504
|
]);
|
|
30505
|
-
var handoffMessageSchema = exports_external.object({
|
|
30506
|
-
from: handoffPhase,
|
|
30507
|
-
to: handoffPhase,
|
|
30508
|
-
body: exports_external.string(),
|
|
30509
|
-
timestamp: isoTimestamp
|
|
30510
|
-
});
|
|
30511
30505
|
// ../contracts/src/subject.ts
|
|
30512
30506
|
var AGENT_TOPIC_PREFIX = "notifications.agent.";
|
|
30513
30507
|
var ROLE_TOPIC_PREFIX = "notifications.role.";
|
package/dist/legion.js
CHANGED
|
@@ -16142,7 +16142,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
16142
16142
|
// package.json
|
|
16143
16143
|
var package_default = {
|
|
16144
16144
|
name: "@sjawhar/pi-legion-envoy",
|
|
16145
|
-
version: "1.
|
|
16145
|
+
version: "1.68.1",
|
|
16146
16146
|
type: "module",
|
|
16147
16147
|
omp: {
|
|
16148
16148
|
extensions: [
|
|
@@ -16155,7 +16155,7 @@ var package_default = {
|
|
|
16155
16155
|
},
|
|
16156
16156
|
legion: {
|
|
16157
16157
|
daemonApiVersion: 8,
|
|
16158
|
-
goDaemonApiVersion:
|
|
16158
|
+
goDaemonApiVersion: 4
|
|
16159
16159
|
},
|
|
16160
16160
|
repository: {
|
|
16161
16161
|
type: "git",
|
|
@@ -30574,12 +30574,6 @@ var phaseHandoffSchema = exports_external.discriminatedUnion("phase", [
|
|
|
30574
30574
|
testSchema,
|
|
30575
30575
|
reviewSchema
|
|
30576
30576
|
]);
|
|
30577
|
-
var handoffMessageSchema = exports_external.object({
|
|
30578
|
-
from: handoffPhase,
|
|
30579
|
-
to: handoffPhase,
|
|
30580
|
-
body: exports_external.string(),
|
|
30581
|
-
timestamp: isoTimestamp
|
|
30582
|
-
});
|
|
30583
30577
|
// ../contracts/src/subject.ts
|
|
30584
30578
|
var AGENT_TOPIC_PREFIX = "notifications.agent.";
|
|
30585
30579
|
var ROLE_TOPIC_PREFIX = "notifications.role.";
|
|
@@ -31238,42 +31232,47 @@ function onEnvoyRoleRegained(listener) {
|
|
|
31238
31232
|
}
|
|
31239
31233
|
|
|
31240
31234
|
// src/legion/controller-session.ts
|
|
31241
|
-
function
|
|
31235
|
+
function typescriptControllerDaemon(rerunReadyAfterRegain, pluginVersion) {
|
|
31236
|
+
let daemon;
|
|
31237
|
+
const client = () => {
|
|
31238
|
+
daemon ??= createLegionDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"));
|
|
31239
|
+
return daemon;
|
|
31240
|
+
};
|
|
31241
|
+
return {
|
|
31242
|
+
claim: async ({ sessionID, capability, ompSessionFile, context }) => {
|
|
31243
|
+
const daemon2 = client();
|
|
31244
|
+
const { project } = await daemon2.state();
|
|
31245
|
+
const token = controllerToken(project);
|
|
31246
|
+
await claimEnvoyRole(sessionID, token, "setInterval" in context ? context : undefined);
|
|
31247
|
+
const ready = {
|
|
31248
|
+
secret: capability,
|
|
31249
|
+
sessionId: sessionID,
|
|
31250
|
+
...ompSessionFile === undefined ? {} : { ompSessionFile },
|
|
31251
|
+
pluginVersion
|
|
31252
|
+
};
|
|
31253
|
+
await daemon2.controllerReady(ready);
|
|
31254
|
+
onEnvoyRoleRegained(async (role, reason) => {
|
|
31255
|
+
if (role !== token)
|
|
31256
|
+
return;
|
|
31257
|
+
await rerunReadyAfterRegain("controller/ready", role, reason, () => daemon2.controllerReady(ready));
|
|
31258
|
+
});
|
|
31259
|
+
return () => daemon2.grant({ sessionId: sessionID, secret: capability });
|
|
31260
|
+
}
|
|
31261
|
+
};
|
|
31262
|
+
}
|
|
31263
|
+
function createControllerSession(persistedTranscript, checkSubagentSession, daemon) {
|
|
31242
31264
|
let controllerSessionID;
|
|
31243
31265
|
let controllerCapability;
|
|
31244
|
-
let
|
|
31245
|
-
const controllerDaemonClient = () => {
|
|
31246
|
-
controllerDaemon ??= createLegionDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"));
|
|
31247
|
-
return controllerDaemon;
|
|
31248
|
-
};
|
|
31266
|
+
let mintControllerGrant;
|
|
31249
31267
|
let controllerTranscript;
|
|
31250
31268
|
const claim = async (context) => {
|
|
31251
31269
|
const sessionID = context.sessionManager.getSessionId();
|
|
31252
|
-
const
|
|
31253
|
-
|
|
31254
|
-
controllerCapability = secret;
|
|
31255
|
-
const { project } = await daemon.state();
|
|
31256
|
-
const token = controllerToken(project);
|
|
31257
|
-
await claimEnvoyRole(sessionID, token, "setInterval" in context ? context : undefined);
|
|
31270
|
+
const capability = controllerCapability ?? requiredControllerCapability(process.env);
|
|
31271
|
+
controllerCapability = capability;
|
|
31258
31272
|
const ompSessionFile = classifySession(process.env).kind === "controller" ? (await persistedTranscript(context)).sessionFile : undefined;
|
|
31259
|
-
await daemon.
|
|
31260
|
-
secret,
|
|
31261
|
-
sessionId: sessionID,
|
|
31262
|
-
...ompSessionFile === undefined ? {} : { ompSessionFile },
|
|
31263
|
-
pluginVersion
|
|
31264
|
-
});
|
|
31273
|
+
mintControllerGrant = await daemon().claim({ sessionID, capability, ompSessionFile, context });
|
|
31265
31274
|
controllerSessionID = sessionID;
|
|
31266
31275
|
controllerTranscript = ompSessionFile;
|
|
31267
|
-
onEnvoyRoleRegained(async (role, reason) => {
|
|
31268
|
-
if (role !== token)
|
|
31269
|
-
return;
|
|
31270
|
-
await rerunReadyAfterRegain("controller/ready", role, reason, () => daemon.controllerReady({
|
|
31271
|
-
secret,
|
|
31272
|
-
sessionId: sessionID,
|
|
31273
|
-
...controllerTranscript === undefined ? {} : { ompSessionFile: controllerTranscript },
|
|
31274
|
-
pluginVersion
|
|
31275
|
-
}));
|
|
31276
|
-
});
|
|
31277
31276
|
};
|
|
31278
31277
|
const handleSessionStart = async (context) => {
|
|
31279
31278
|
if (classifySession(process.env).kind !== "controller")
|
|
@@ -31297,13 +31296,12 @@ function createControllerSession(persistedTranscript, checkSubagentSession, reru
|
|
|
31297
31296
|
context.ui.notify(`legion: re-claiming the controller for this session failed (${messageFor(error48)}). Until a re-claim succeeds, controller wakes and merges will not reach this session and its shell commands run without a Legion grant, so legion gh is unavailable.`, "warning");
|
|
31298
31297
|
}
|
|
31299
31298
|
};
|
|
31300
|
-
const isClaimedSession = (sessionID) => controllerSessionID === sessionID &&
|
|
31299
|
+
const isClaimedSession = (sessionID) => controllerSessionID === sessionID && mintControllerGrant !== undefined;
|
|
31301
31300
|
const mintGrant = async (sessionID) => {
|
|
31302
|
-
if (controllerSessionID !== sessionID ||
|
|
31301
|
+
if (controllerSessionID !== sessionID || mintControllerGrant === undefined) {
|
|
31303
31302
|
throw new Error("Controller session is not registered; cannot mint its grant");
|
|
31304
31303
|
}
|
|
31305
|
-
|
|
31306
|
-
return controllerDaemonClient().grant({ sessionId: sessionID, secret });
|
|
31304
|
+
return mintControllerGrant();
|
|
31307
31305
|
};
|
|
31308
31306
|
return { claim, handleSessionStart, reclaimAfterSessionChange, isClaimedSession, mintGrant };
|
|
31309
31307
|
}
|
|
@@ -31436,11 +31434,18 @@ var legionGoAdmission = exports_external.strictObject({
|
|
|
31436
31434
|
active: exports_external.array(nonEmptyString2),
|
|
31437
31435
|
waiting: exports_external.array(nonEmptyString2)
|
|
31438
31436
|
});
|
|
31437
|
+
var legionGoControllerLocator = exports_external.strictObject({
|
|
31438
|
+
runtime: exports_external.enum(["tmux", "kubernetes"]),
|
|
31439
|
+
external: exports_external.literal(true),
|
|
31440
|
+
sessionId: nonEmptyString2,
|
|
31441
|
+
registeredAt: timestamp
|
|
31442
|
+
});
|
|
31439
31443
|
var LegionGoStateResponse = exports_external.strictObject({
|
|
31440
31444
|
daemon: goDaemonInfo,
|
|
31441
31445
|
admission: legionGoAdmission,
|
|
31442
31446
|
issues: exports_external.record(exports_external.string(), legionGoIssue),
|
|
31443
|
-
pendingStatusWrites: exports_external.array(legionGoPendingStatusWrite)
|
|
31447
|
+
pendingStatusWrites: exports_external.array(legionGoPendingStatusWrite),
|
|
31448
|
+
controllerLocator: legionGoControllerLocator.optional()
|
|
31444
31449
|
});
|
|
31445
31450
|
var LegionGoRegisterResponse = exports_external.strictObject({
|
|
31446
31451
|
claimToken: nonEmptyString2,
|
|
@@ -31450,6 +31455,15 @@ var LegionGoRegisterResponse = exports_external.strictObject({
|
|
|
31450
31455
|
generation: exports_external.number().int().positive(),
|
|
31451
31456
|
secret: nonEmptyString2
|
|
31452
31457
|
});
|
|
31458
|
+
var LegionGoControllerRegisterResponse = exports_external.strictObject({
|
|
31459
|
+
claimToken: nonEmptyString2,
|
|
31460
|
+
role: exports_external.literal("controller"),
|
|
31461
|
+
generation: exports_external.number().int().positive(),
|
|
31462
|
+
secret: nonEmptyString2
|
|
31463
|
+
});
|
|
31464
|
+
var LegionGoControllerSecretResponse = exports_external.strictObject({
|
|
31465
|
+
secret: nonEmptyString2
|
|
31466
|
+
});
|
|
31453
31467
|
var LegionGoErrorResponse = exports_external.union([
|
|
31454
31468
|
exports_external.strictObject({ error: nonEmptyString2 }),
|
|
31455
31469
|
exports_external.strictObject({ code: nonEmptyString2, error: nonEmptyString2 })
|
|
@@ -31504,6 +31518,10 @@ var LegionGoGrantRequest = exports_external.strictObject({
|
|
|
31504
31518
|
tree: nonEmptyString2,
|
|
31505
31519
|
issue: nonEmptyString2
|
|
31506
31520
|
});
|
|
31521
|
+
var LegionGoControllerGrantRequest = exports_external.strictObject({
|
|
31522
|
+
sessionId: nonEmptyString2,
|
|
31523
|
+
secret: nonEmptyString2
|
|
31524
|
+
});
|
|
31507
31525
|
var LegionGoGrantCredentialRequest = exports_external.strictObject({
|
|
31508
31526
|
grantId: nonEmptyString2
|
|
31509
31527
|
});
|
|
@@ -31624,9 +31642,11 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
31624
31642
|
return {
|
|
31625
31643
|
state: () => read("GET", "/legion/v1/state", LegionGoStateResponse),
|
|
31626
31644
|
register: (input) => read("POST", "/legion/v1/claims/register", LegionGoRegisterResponse, input),
|
|
31645
|
+
registerController: (input) => read("POST", "/legion/v1/claims/register", LegionGoControllerRegisterResponse, input),
|
|
31627
31646
|
ready: (input) => acknowledge("/legion/v1/claims/ready", input),
|
|
31628
31647
|
exit: (input) => acknowledge("/legion/v1/claims/exit", input),
|
|
31629
31648
|
grant: (input) => post("/legion/v1/grants", LegionGoGrantRequest, LegionGoGrantResponse, input),
|
|
31649
|
+
controllerGrant: (input) => post("/legion/v1/grants", LegionGoControllerGrantRequest, LegionGoGrantResponse, input),
|
|
31630
31650
|
githubToken: (input) => post("/legion/v1/gh-token", LegionGoGrantCredentialRequest, LegionGoGitHubTokenResponse, input),
|
|
31631
31651
|
gitCredential: (input) => post("/legion/v1/git-credential", LegionGoGrantCredentialRequest, LegionGoGitCredentialResponse, input),
|
|
31632
31652
|
provisioningCredential: (input) => post("/legion/v1/provisioning-credential", LegionGoGrantCredentialRequest, LegionGoGitHubTokenResponse, input),
|
|
@@ -31726,13 +31746,31 @@ async function callGoReadyWithRetry(label, call) {
|
|
|
31726
31746
|
}
|
|
31727
31747
|
}
|
|
31728
31748
|
}
|
|
31749
|
+
function goControllerDaemon(daemon, transcript) {
|
|
31750
|
+
return {
|
|
31751
|
+
claim: async ({ sessionID, capability, context }) => {
|
|
31752
|
+
const { sessionFile, agentId } = await transcript(context);
|
|
31753
|
+
const client = daemon();
|
|
31754
|
+
const registration = await client.registerController({
|
|
31755
|
+
bootToken: capability,
|
|
31756
|
+
sessionId: sessionID,
|
|
31757
|
+
ompSessionFile: sessionFile,
|
|
31758
|
+
agentId,
|
|
31759
|
+
pluginContract: package_default.legion.goDaemonApiVersion
|
|
31760
|
+
}).catch((error48) => {
|
|
31761
|
+
if (error48 instanceof LegionGoDaemonApiError) {
|
|
31762
|
+
console.error(`[legion] claims/register for the controller failed (${error48.status}): ${error48.detail}`);
|
|
31763
|
+
}
|
|
31764
|
+
throw error48;
|
|
31765
|
+
});
|
|
31766
|
+
await claimEnvoyRole(sessionID, registration.claimToken, "setInterval" in context ? context : undefined);
|
|
31767
|
+
return () => client.controllerGrant({ sessionId: sessionID, secret: registration.secret });
|
|
31768
|
+
}
|
|
31769
|
+
};
|
|
31770
|
+
}
|
|
31729
31771
|
async function bootstrapGoClaim(context, state) {
|
|
31730
|
-
|
|
31731
|
-
if (classification.kind === "not-legion")
|
|
31772
|
+
if (classifySession(process.env).kind === "not-legion")
|
|
31732
31773
|
return;
|
|
31733
|
-
if (classification.kind === "controller") {
|
|
31734
|
-
throw new Error("LEGION_DAEMON_API=go names the Go daemon, which launches no controller session");
|
|
31735
|
-
}
|
|
31736
31774
|
const sessionID = context.sessionManager.getSessionId();
|
|
31737
31775
|
const capability = state.capability();
|
|
31738
31776
|
if (capability !== undefined && capability.sessionID !== sessionID)
|
|
@@ -31811,7 +31849,6 @@ import { execFile as execFile2 } from "child_process";
|
|
|
31811
31849
|
var HANDOFF_OPERATION_FIELDS = {
|
|
31812
31850
|
handoff_write: ["phase", "data"],
|
|
31813
31851
|
handoff_read: ["phase"],
|
|
31814
|
-
handoff_message: ["sender", "recipient", "body"],
|
|
31815
31852
|
handoff_complete: ["summary", "verdict", "ready", "phase"]
|
|
31816
31853
|
};
|
|
31817
31854
|
var HANDOFF_OPERATIONS = Object.keys(HANDOFF_OPERATION_FIELDS);
|
|
@@ -31822,15 +31859,12 @@ function handoffSchemaFields(z2) {
|
|
|
31822
31859
|
return {
|
|
31823
31860
|
phase: z2.enum(HANDOFF_PHASES).optional(),
|
|
31824
31861
|
data: z2.unknown().optional(),
|
|
31825
|
-
sender: z2.enum(HANDOFF_PHASES).optional(),
|
|
31826
|
-
recipient: z2.enum(HANDOFF_PHASES).optional(),
|
|
31827
|
-
body: z2.string().optional(),
|
|
31828
31862
|
summary: z2.string().optional(),
|
|
31829
31863
|
verdict: z2.enum(["pass", "fail"]).optional(),
|
|
31830
31864
|
ready: z2.boolean().optional()
|
|
31831
31865
|
};
|
|
31832
31866
|
}
|
|
31833
|
-
var HANDOFF_DESCRIPTION = "Handoff actions (phase workers and sub-architects): handoff_write writes this phase's handoff " + "(`phase`, and `data`: the phase-specific fields as a JSON object) to `.legion/<phase>.json` in " + "the issue workspace; handoff_read returns the handoffs (every phase, or `phase`); " + "
|
|
31867
|
+
var HANDOFF_DESCRIPTION = "Handoff actions (phase workers and sub-architects): handoff_write writes this phase's handoff " + "(`phase`, and `data`: the phase-specific fields as a JSON object) to `.legion/<phase>.json` in " + "the issue workspace; handoff_read returns the handoffs (every phase, or `phase`); " + "handoff_complete reports this phase complete to the daemon (`summary`: two sentences for the " + "architect; `verdict` pass|fail when your role's instructions require one; `ready: true` for " + "the merger's READY; no `phase` is needed, and one other than your own is refused). Each " + "returns the command's output; a failed action changed nothing. What a later phase needs goes " + "in your handoff; a question for another live role goes to its role topic with envoy_publish.";
|
|
31834
31868
|
var ROLE_HANDOFF_PHASE = {
|
|
31835
31869
|
architect: "architect",
|
|
31836
31870
|
planner: "plan",
|
|
@@ -31864,21 +31898,12 @@ function commandArguments(operation, parameters) {
|
|
|
31864
31898
|
throw new Error("handoff_write requires data: the phase's handoff fields as a JSON object");
|
|
31865
31899
|
}
|
|
31866
31900
|
const phase = required2(parameters, operation, "phase");
|
|
31867
|
-
return ["handoff", "write", "--phase", phase,
|
|
31901
|
+
return { args: ["handoff", "write", "--phase", phase], stdin: JSON.stringify(data) };
|
|
31868
31902
|
}
|
|
31869
31903
|
case "handoff_read":
|
|
31870
|
-
return
|
|
31871
|
-
|
|
31872
|
-
|
|
31873
|
-
"handoff",
|
|
31874
|
-
"message",
|
|
31875
|
-
"--from",
|
|
31876
|
-
required2(parameters, operation, "sender"),
|
|
31877
|
-
"--to",
|
|
31878
|
-
required2(parameters, operation, "recipient"),
|
|
31879
|
-
"--body",
|
|
31880
|
-
required2(parameters, operation, "body")
|
|
31881
|
-
];
|
|
31904
|
+
return {
|
|
31905
|
+
args: parameters.phase === undefined ? ["handoff", "read"] : ["handoff", "read", "--phase", required2(parameters, operation, "phase")]
|
|
31906
|
+
};
|
|
31882
31907
|
case "handoff_complete": {
|
|
31883
31908
|
const command = [
|
|
31884
31909
|
"handoff",
|
|
@@ -31908,13 +31933,13 @@ function commandArguments(operation, parameters) {
|
|
|
31908
31933
|
}
|
|
31909
31934
|
if (ready === true)
|
|
31910
31935
|
command.push("--ready");
|
|
31911
|
-
return command;
|
|
31936
|
+
return { args: command };
|
|
31912
31937
|
}
|
|
31913
31938
|
}
|
|
31914
31939
|
}
|
|
31915
|
-
function runLegion(args, cwd, signal) {
|
|
31940
|
+
function runLegion(args, stdin, cwd, signal) {
|
|
31916
31941
|
const { promise: promise2, resolve, reject } = Promise.withResolvers();
|
|
31917
|
-
execFile2("legion", args, { cwd, env: process.env, signal, timeout: COMMAND_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, (error48, stdout, stderr) => {
|
|
31942
|
+
const child = execFile2("legion", args, { cwd, env: process.env, signal, timeout: COMMAND_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, (error48, stdout, stderr) => {
|
|
31918
31943
|
const output = [stdout, stderr].map((text) => text.trimEnd()).filter((text) => text !== "").join(`
|
|
31919
31944
|
`);
|
|
31920
31945
|
if (error48 === null) {
|
|
@@ -31925,13 +31950,18 @@ function runLegion(args, cwd, signal) {
|
|
|
31925
31950
|
reject(new Error(`legion ${args.slice(0, 2).join(" ")} did not run: ${messageFor(error48)}`));
|
|
31926
31951
|
}
|
|
31927
31952
|
});
|
|
31953
|
+
child.stdin?.on("error", (error48) => {
|
|
31954
|
+
if (error48.code !== "EPIPE")
|
|
31955
|
+
reject(error48);
|
|
31956
|
+
});
|
|
31957
|
+
child.stdin?.end(stdin ?? "");
|
|
31928
31958
|
return promise2;
|
|
31929
31959
|
}
|
|
31930
31960
|
async function runHandoffAction(input) {
|
|
31931
|
-
const args = commandArguments(input.operation, input.parameters);
|
|
31961
|
+
const { args, stdin } = commandArguments(input.operation, input.parameters);
|
|
31932
31962
|
if (input.operation === "handoff_complete")
|
|
31933
31963
|
await writeMintedGrant(input.mintGrant);
|
|
31934
|
-
const { exitCode, output } = await runLegion(args, requiredEnvironment(process.env, "LEGION_WORKSPACE"), input.signal);
|
|
31964
|
+
const { exitCode, output } = await runLegion(args, stdin, requiredEnvironment(process.env, "LEGION_WORKSPACE"), input.signal);
|
|
31935
31965
|
const text = output === "" ? `legion ${args.slice(0, 2).join(" ")} exited ${exitCode}` : output;
|
|
31936
31966
|
const details = { operation: input.operation, exitCode };
|
|
31937
31967
|
if (exitCode !== 0)
|
|
@@ -32467,7 +32497,7 @@ function jjLogRewriteMention(text) {
|
|
|
32467
32497
|
if (!JJ_MENTION.test(text))
|
|
32468
32498
|
return;
|
|
32469
32499
|
const match = JJ_LOG_REWRITE_MENTION.exec(text);
|
|
32470
|
-
return match === null ? undefined : match[0].replace(/\W+/g, " ")
|
|
32500
|
+
return match === null ? undefined : `jj ${match[0].replace(/\W+/g, " ")}`;
|
|
32471
32501
|
}
|
|
32472
32502
|
function splitShellCommands(command) {
|
|
32473
32503
|
const commands = [];
|
|
@@ -32530,29 +32560,60 @@ function splitShellCommands(command) {
|
|
|
32530
32560
|
endCommand();
|
|
32531
32561
|
return commands;
|
|
32532
32562
|
}
|
|
32533
|
-
function jjLogRewriteInvocation(
|
|
32534
|
-
const
|
|
32563
|
+
function jjLogRewriteInvocation(words) {
|
|
32564
|
+
const jj = words.findIndex((word) => word === "jj" || word.endsWith("/jj"));
|
|
32565
|
+
if (jj === -1)
|
|
32566
|
+
return;
|
|
32567
|
+
const args = words.slice(jj + 1);
|
|
32568
|
+
const rewritesLog = args.some((arg) => JJ_LOG_REWRITE_WORDS.includes(arg)) || args.some((arg) => JJ_OP_WORDS.includes(arg)) && args.some((arg) => JJ_OP_LOG_REWRITE_WORDS.includes(arg));
|
|
32569
|
+
return rewritesLog ? words.slice(jj).join(" ") : undefined;
|
|
32570
|
+
}
|
|
32571
|
+
var JJ_LOG_REWRITE = {
|
|
32572
|
+
mention: jjLogRewriteMention,
|
|
32573
|
+
invocation: jjLogRewriteInvocation,
|
|
32574
|
+
refusal: (attempt) => `refused \`${attempt}\`: jj undo, jj abandon, and jj op restore/revert/abandon/undo ` + "rewrite the jj operation log, which every Legion issue workspace shares (each is a jj " + "workspace of one clone), so they rewrite other trees' commits too. Recover forward with " + "a new commit or `jj restore <paths>` of files; anything else, stop and send the owning " + 'architect the `jj -R "$LEGION_WORKSPACE" log` evidence.'
|
|
32575
|
+
};
|
|
32576
|
+
var LEGION_HANDOFF_COMPLETE_MENTION = /\blegion[\s"'`,[\]]+handoff[\s"'`,[\]]+complete\b/;
|
|
32577
|
+
var LEGION_HANDOFF_COMPLETE = {
|
|
32578
|
+
mention: (text) => LEGION_HANDOFF_COMPLETE_MENTION.test(text) ? "legion handoff complete" : undefined,
|
|
32579
|
+
invocation: (words) => {
|
|
32580
|
+
const legion = words.findIndex((word, index) => (word === "legion" || word.endsWith("/legion")) && words[index + 1] === "handoff" && words[index + 2] === "complete");
|
|
32581
|
+
return legion === -1 ? undefined : words.slice(legion).join(" ");
|
|
32582
|
+
},
|
|
32583
|
+
refusal: (attempt) => `refused \`${attempt}\`: a phase is completed with the \`legion\` tool's ` + "`handoff_complete`, never a shell command: the tool call is what records the phase " + "complete. A root architect or a `task` subagent has no handoff actions: it leaves the " + "completion to the worker. Text that only names the command (a commit message, a PR body) " + "reads as the command: pass it in a file."
|
|
32584
|
+
};
|
|
32585
|
+
var PANE_RULES = {
|
|
32586
|
+
"phase-worker": [JJ_LOG_REWRITE, LEGION_HANDOFF_COMPLETE],
|
|
32587
|
+
"root-architect": [LEGION_HANDOFF_COMPLETE]
|
|
32588
|
+
};
|
|
32589
|
+
function refusedCommand(command, commands, rule) {
|
|
32535
32590
|
if (commands === undefined) {
|
|
32536
|
-
return
|
|
32591
|
+
return rule.mention(command) === undefined ? undefined : command.trim();
|
|
32537
32592
|
}
|
|
32538
32593
|
for (const words of commands) {
|
|
32539
|
-
const mentioned = words.find((word) =>
|
|
32594
|
+
const mentioned = words.find((word) => rule.mention(word) !== undefined);
|
|
32540
32595
|
if (mentioned !== undefined)
|
|
32541
32596
|
return mentioned;
|
|
32542
|
-
const
|
|
32543
|
-
if (
|
|
32544
|
-
|
|
32545
|
-
const args = words.slice(jj + 1);
|
|
32546
|
-
const rewritesLog = args.some((arg) => JJ_LOG_REWRITE_WORDS.includes(arg)) || args.some((arg) => JJ_OP_WORDS.includes(arg)) && args.some((arg) => JJ_OP_LOG_REWRITE_WORDS.includes(arg));
|
|
32547
|
-
if (rewritesLog)
|
|
32548
|
-
return words.slice(jj).join(" ");
|
|
32597
|
+
const invocation = rule.invocation(words);
|
|
32598
|
+
if (invocation !== undefined)
|
|
32599
|
+
return invocation;
|
|
32549
32600
|
}
|
|
32550
32601
|
return;
|
|
32551
32602
|
}
|
|
32552
|
-
function
|
|
32603
|
+
function paneRuleRefusal(toolCall, rules) {
|
|
32604
|
+
if (rules.length === 0)
|
|
32605
|
+
return;
|
|
32553
32606
|
const { toolName, input } = toolCall;
|
|
32554
32607
|
if (toolName === "bash") {
|
|
32555
|
-
|
|
32608
|
+
if (typeof input.command !== "string")
|
|
32609
|
+
return;
|
|
32610
|
+
const commands = splitShellCommands(input.command);
|
|
32611
|
+
for (const rule of rules) {
|
|
32612
|
+
const attempt = refusedCommand(input.command, commands, rule);
|
|
32613
|
+
if (attempt !== undefined)
|
|
32614
|
+
return rule.refusal(attempt);
|
|
32615
|
+
}
|
|
32616
|
+
return;
|
|
32556
32617
|
}
|
|
32557
32618
|
let text;
|
|
32558
32619
|
if (toolName === "eval" && typeof input.code === "string")
|
|
@@ -32561,8 +32622,12 @@ function jjLogRewriteAttempt(toolCall) {
|
|
|
32561
32622
|
text = [input.application, ...Array.isArray(input.args) ? input.args : [], input.text].filter((part) => typeof part === "string").join(" ");
|
|
32562
32623
|
} else
|
|
32563
32624
|
return;
|
|
32564
|
-
const
|
|
32565
|
-
|
|
32625
|
+
for (const rule of rules) {
|
|
32626
|
+
const mention = rule.mention(text);
|
|
32627
|
+
if (mention !== undefined)
|
|
32628
|
+
return rule.refusal(`${toolName}: ${mention}`);
|
|
32629
|
+
}
|
|
32630
|
+
return;
|
|
32566
32631
|
}
|
|
32567
32632
|
var LEGION_LOADED_MARKER = Symbol.for("legion.pi-envoy.legion-loaded");
|
|
32568
32633
|
var CODE_MUTATION_TOOLS = ["edit", "write", "apply_patch"];
|
|
@@ -32581,7 +32646,7 @@ function legionExtension(pi) {
|
|
|
32581
32646
|
let capability;
|
|
32582
32647
|
let bootstrap;
|
|
32583
32648
|
const checkSubagentSession = subagentSessionCheck();
|
|
32584
|
-
let
|
|
32649
|
+
let paneRules;
|
|
32585
32650
|
let phaseStall = "closed";
|
|
32586
32651
|
const phaseWorkerSession = (context) => capability !== undefined && capability.sessionID === context.sessionManager.getSessionId() && capability.role !== "architect";
|
|
32587
32652
|
const advancePhaseStall = (input) => {
|
|
@@ -32626,11 +32691,13 @@ function legionExtension(pi) {
|
|
|
32626
32691
|
console.error(`[legion] ${endpoint} after role ${role} was ${reason} failed; the daemon's held work stays undelivered until the next regain or boot: ${messageFor(error48)}`);
|
|
32627
32692
|
}
|
|
32628
32693
|
};
|
|
32694
|
+
const typescriptController = typescriptControllerDaemon(rerunReadyAfterRegain, package_default.version);
|
|
32695
|
+
const goController = goControllerDaemon(goRoleDaemon, persistedTranscript);
|
|
32629
32696
|
const controllerSession = createControllerSession(async (context) => {
|
|
32630
32697
|
const persisted = await persistedTranscript(context);
|
|
32631
32698
|
recordBootstrappedSession(persisted.sessionFile);
|
|
32632
32699
|
return persisted;
|
|
32633
|
-
}, checkSubagentSession,
|
|
32700
|
+
}, checkSubagentSession, () => process.env.LEGION_DAEMON_API === "go" ? goController : typescriptController);
|
|
32634
32701
|
const reclaimArchitect = async () => {
|
|
32635
32702
|
if (capability === undefined || capability.kind !== "root-architect") {
|
|
32636
32703
|
throw new Error("Legion root architect is not available for reclamation");
|
|
@@ -32828,6 +32895,10 @@ function legionExtension(pi) {
|
|
|
32828
32895
|
return;
|
|
32829
32896
|
phaseStall = restorePhaseStall(context.sessionManager.getBranch?.() ?? []);
|
|
32830
32897
|
if (process.env.LEGION_DAEMON_API === "go") {
|
|
32898
|
+
if (classifySession(process.env).kind === "controller") {
|
|
32899
|
+
await controllerSession.handleSessionStart(context);
|
|
32900
|
+
return;
|
|
32901
|
+
}
|
|
32831
32902
|
await bootstrapGoClaim(context, {
|
|
32832
32903
|
capability: () => capability,
|
|
32833
32904
|
setCapability: (next) => {
|
|
@@ -32870,16 +32941,10 @@ function legionExtension(pi) {
|
|
|
32870
32941
|
toolCallId: toolCall.toolCallId,
|
|
32871
32942
|
toolName: toolCall.toolName
|
|
32872
32943
|
});
|
|
32873
|
-
|
|
32874
|
-
|
|
32875
|
-
|
|
32876
|
-
|
|
32877
|
-
return {
|
|
32878
|
-
block: true,
|
|
32879
|
-
reason: `refused \`${jjAttempt}\`: jj undo, jj abandon, and jj op restore/revert/abandon/undo ` + "rewrite the jj operation log, which every Legion issue workspace shares (each is a jj " + "workspace of one clone), so they rewrite other trees' commits too. Recover forward with " + "a new commit or `jj restore <paths>` of files; anything else, stop and send the owning " + 'architect the `jj -R "$LEGION_WORKSPACE" log` evidence.'
|
|
32880
|
-
};
|
|
32881
|
-
}
|
|
32882
|
-
}
|
|
32944
|
+
paneRules ??= PANE_RULES[classifySession(process.env).kind] ?? [];
|
|
32945
|
+
const refusal = paneRuleRefusal(toolCall, paneRules);
|
|
32946
|
+
if (refusal !== undefined)
|
|
32947
|
+
return { block: true, reason: refusal };
|
|
32883
32948
|
if (await checkSubagentSession(context))
|
|
32884
32949
|
return;
|
|
32885
32950
|
const sessionID = context.sessionManager.getSessionId();
|
|
@@ -59,23 +59,26 @@ command run `/legion-claim-controller` again.
|
|
|
59
59
|
This handshake lets the daemon redeliver held controller work. It does not turn the controller
|
|
60
60
|
into a state holder: daemon state and the Dispatch project remain authoritative.
|
|
61
61
|
|
|
62
|
-
### Started by the operator
|
|
63
|
-
|
|
64
|
-
When the daemon
|
|
65
|
-
|
|
66
|
-
[--daemon-url <
|
|
67
|
-
|
|
68
|
-
to a 0600 file under `LEGION_STATE_DIR`
|
|
69
|
-
beside the `gh` shim and the `legion`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
### Started by the operator
|
|
63
|
+
|
|
64
|
+
When the daemon cannot open a terminal for you — the TypeScript daemon under `runtime: kubernetes`,
|
|
65
|
+
and the Go daemon under either runtime, since it launches no controller — nobody launched your
|
|
66
|
+
pane: the operator ran `legion controller start --config controller.yaml [--daemon-url <url>]` on
|
|
67
|
+
their own machine, and you are that foreground OMP session. The command fetched a fresh controller
|
|
68
|
+
secret from the daemon with the operator's token, wrote it to a 0600 file under `LEGION_STATE_DIR`
|
|
69
|
+
(`~/.local/state/legion/<project>-controller` by default) beside the `gh` shim and the `legion`
|
|
70
|
+
launcher, and started you with `LEGION_CONTROLLER=1` and the same environment a tmux controller pane
|
|
71
|
+
carries, so nothing changes in how you handle wakes. Under the TypeScript daemon the extension
|
|
72
|
+
claims the role and calls `/controller/ready` exactly as under tmux; under the Go daemon
|
|
73
|
+
(`LEGION_DAEMON_API=go` in your environment) it registers on `/legion/v1/claims/register` with the
|
|
74
|
+
secret, then claims the role. The daemon records you as `controllerLocator: {runtime, external:
|
|
75
|
+
true, sessionId, registeredAt}`, `runtime` being the daemon's own (`kubernetes`, or `tmux` under the
|
|
76
|
+
Go daemon). The TypeScript daemon reads your liveness from the Envoy role registry (the holder of
|
|
74
77
|
`legion-<project>-controller` and its `last_seen`), not from a pane: keep the session running.
|
|
75
78
|
Exiting it leaves the project without a controller until the operator runs the command again —
|
|
76
|
-
the daemon logs `controller not registered; run legion controller start` once per
|
|
77
|
-
interval and launches nothing itself. `legion state` and `legion status <KEY>
|
|
78
|
-
over `LEGION_DAEMON_URL
|
|
79
|
+
the TypeScript daemon logs `controller not registered; run legion controller start` once per
|
|
80
|
+
boot-timeout interval and launches nothing itself. `legion state` and `legion status <KEY>
|
|
81
|
+
<status>` work here over `LEGION_DAEMON_URL`. A second `legion controller start` replaces you: it
|
|
79
82
|
mints a new secret, so your grants stop working and the role moves to the new session.
|
|
80
83
|
|
|
81
84
|
## Deployment instructions
|
|
@@ -507,6 +507,14 @@ Write the phase-specific handoff: call the `legion` tool with `op: "handoff_writ
|
|
|
507
507
|
and `data`: a JSON object of the phase-specific fields only. It runs `legion handoff write` in
|
|
508
508
|
`$LEGION_WORKSPACE` and returns its output.
|
|
509
509
|
|
|
510
|
+
A handoff built from the one already on disk (a test handoff that accumulates review rounds can
|
|
511
|
+
pass 128 KiB) can instead be piped from bash, so you never re-emit the whole payload:
|
|
512
|
+
`cd -- "$LEGION_WORKSPACE" && bun -e 'const h = await Bun.file(".legion/<phase>.json").json(); delete h.schemaVersion; delete h.phase; delete h.completed; <your edit to h>; console.log(JSON.stringify(h))' | legion handoff write --phase <phase>`.
|
|
513
|
+
The program is single-quoted, so strings in your edit take double quotes. It is `bun` because the
|
|
514
|
+
worker image a pod runs ships `bun` and not `jq`, and a devbox pane has the `bun` Legion builds
|
|
515
|
+
with. With `--data` omitted, `legion handoff write` reads the JSON object from stdin. The CLI adds
|
|
516
|
+
`schemaVersion`, `phase` and `completed` itself and refuses them in the data, hence the `delete`s.
|
|
517
|
+
|
|
510
518
|
`handoff_write` validates the payload against the phase's schema before writing: an
|
|
511
519
|
implement handoff without a well-formed `proof`, or a test handoff that reports no failure and
|
|
512
520
|
carries no `proof` of its own, exits 1 naming the field and writes nothing.
|
|
@@ -558,9 +566,9 @@ approved head and restarts the review loop this rule exists to end.
|
|
|
558
566
|
## Completion: report to the architect, then stay
|
|
559
567
|
|
|
560
568
|
Report completion to the architect: call the `legion` tool with `op: "handoff_complete"` and
|
|
561
|
-
`summary`: two sentences for the architect. A worker never runs `legion handoff` from
|
|
562
|
-
tool call is what the extension records, and a turn that
|
|
563
|
-
reminder.
|
|
569
|
+
`summary`: two sentences for the architect. A worker never runs `legion handoff complete` from
|
|
570
|
+
bash, where the extension refuses it: the tool call is what the extension records, and a turn that
|
|
571
|
+
ends with the phase still open gets one reminder.
|
|
564
572
|
|
|
565
573
|
This publishes your phase's completion to the architect's role and clears the daemon's
|
|
566
574
|
record of this issue's active phase. Do not add pipeline labels, run a controller loop, or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion-envoy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"omp": {
|
|
6
6
|
"extensions": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
15
|
"daemonApiVersion": 8,
|
|
16
|
-
"goDaemonApiVersion":
|
|
16
|
+
"goDaemonApiVersion": 4
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|