@sjawhar/pi-legion 8.10.0 → 8.11.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 +2 -2
- package/dist/legion.js +180 -533
- package/dist/skills/legion-architect/SKILL.md +28 -23
- package/dist/skills/legion-controller/SKILL.md +48 -47
- package/dist/skills/legion-retro/SKILL.md +15 -9
- package/dist/skills/legion-worker/SKILL.md +151 -154
- package/dist/skills/legion-worker/references/conflicts-and-rewrites.md +33 -31
- package/dist/skills/legion-worker/references/merge-gate.md +60 -24
- package/dist/skills/legion-worker/references/pr-body.md +21 -18
- package/dist/skills/legion-worker/references/review-threads.md +68 -92
- package/package.json +2 -2
package/dist/legion.js
CHANGED
|
@@ -15,8 +15,8 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// extensions/legion.ts
|
|
18
|
-
import { randomUUID
|
|
19
|
-
import
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
19
|
+
import path4 from "path";
|
|
20
20
|
|
|
21
21
|
// ../envoy-client/src/dispatch-config.ts
|
|
22
22
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -14431,8 +14431,6 @@ var EnvelopeSchema = exports_external.object({
|
|
|
14431
14431
|
});
|
|
14432
14432
|
var MINTED_DEDUPE_KEY_PATTERN = "^(?:envoy\\.role\\.forward\\.)?(?:publish|agent\\.[^.]+)\\.(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$";
|
|
14433
14433
|
var mintedDedupeKey = new RegExp(MINTED_DEDUPE_KEY_PATTERN);
|
|
14434
|
-
// ../contracts/src/handoff-schema.ts
|
|
14435
|
-
var HANDOFF_PHASES = ["architect", "plan", "implement", "test", "review"];
|
|
14436
14434
|
// ../contracts/src/subject.ts
|
|
14437
14435
|
var LEGION_NOTICE_TOPIC_PREFIX = "notifications.legion.";
|
|
14438
14436
|
function legionControllerNoticeSubject(project) {
|
|
@@ -15163,70 +15161,6 @@ function matchInjectedUserTurn(sessionID, message) {
|
|
|
15163
15161
|
return sent.messageId;
|
|
15164
15162
|
}
|
|
15165
15163
|
|
|
15166
|
-
// ../pi-shared/src/shell-command.ts
|
|
15167
|
-
var HEREDOC_OPENING = /^(.*?)[ \t]*<<(-?)[ \t]*'([A-Za-z_][A-Za-z0-9_]*)'[ \t]*$/;
|
|
15168
|
-
var BLANK_LINE = /^[ \t]*$/;
|
|
15169
|
-
var REFUSED_UNQUOTED = "\n;&|$`<>()#\\";
|
|
15170
|
-
var REFUSED_DOUBLE_QUOTED = "$`\\";
|
|
15171
|
-
function dispatchCommandHead(command) {
|
|
15172
|
-
if (typeof command !== "string" || hasControlCharacter(command))
|
|
15173
|
-
return;
|
|
15174
|
-
const all = command.split(`
|
|
15175
|
-
`);
|
|
15176
|
-
const first = all.findIndex((line) => !BLANK_LINE.test(line));
|
|
15177
|
-
if (first === -1)
|
|
15178
|
-
return;
|
|
15179
|
-
const last = all.findLastIndex((line) => !BLANK_LINE.test(line));
|
|
15180
|
-
const lines = all.slice(first, last + 1);
|
|
15181
|
-
let head = lines[0] ?? "";
|
|
15182
|
-
if (lines.length > 1) {
|
|
15183
|
-
const opening = HEREDOC_OPENING.exec(head);
|
|
15184
|
-
if (opening === null)
|
|
15185
|
-
return;
|
|
15186
|
-
const stripTabs = opening[2] === "-";
|
|
15187
|
-
const delimiter = opening[3];
|
|
15188
|
-
const end = lines.findIndex((line, index) => index > 0 && (stripTabs ? line.replace(/^\t+/, "") : line) === delimiter);
|
|
15189
|
-
if (end !== lines.length - 1)
|
|
15190
|
-
return;
|
|
15191
|
-
head = opening[1] ?? "";
|
|
15192
|
-
}
|
|
15193
|
-
head = head.replace(/^[ \t]+|[ \t]+$/g, "");
|
|
15194
|
-
return firstWord(head) === "dispatch" && headScan(head) ? head : undefined;
|
|
15195
|
-
}
|
|
15196
|
-
function firstWord(head) {
|
|
15197
|
-
return head.split(/[ \t]/, 1)[0] ?? "";
|
|
15198
|
-
}
|
|
15199
|
-
function hasControlCharacter(text) {
|
|
15200
|
-
for (let index = 0;index < text.length; index += 1) {
|
|
15201
|
-
const code = text.charCodeAt(index);
|
|
15202
|
-
if (code < 32 && code !== 10 && code !== 9 || code === 127)
|
|
15203
|
-
return true;
|
|
15204
|
-
}
|
|
15205
|
-
return false;
|
|
15206
|
-
}
|
|
15207
|
-
function headScan(head) {
|
|
15208
|
-
let quote;
|
|
15209
|
-
for (const char of head) {
|
|
15210
|
-
if (quote === "'") {
|
|
15211
|
-
if (char === "'")
|
|
15212
|
-
quote = undefined;
|
|
15213
|
-
continue;
|
|
15214
|
-
}
|
|
15215
|
-
if (quote === '"') {
|
|
15216
|
-
if (char === '"')
|
|
15217
|
-
quote = undefined;
|
|
15218
|
-
else if (REFUSED_DOUBLE_QUOTED.includes(char))
|
|
15219
|
-
return false;
|
|
15220
|
-
continue;
|
|
15221
|
-
}
|
|
15222
|
-
if (char === "'" || char === '"')
|
|
15223
|
-
quote = char;
|
|
15224
|
-
else if (REFUSED_UNQUOTED.includes(char))
|
|
15225
|
-
return false;
|
|
15226
|
-
}
|
|
15227
|
-
return quote === undefined;
|
|
15228
|
-
}
|
|
15229
|
-
|
|
15230
15164
|
// ../pi-shared/src/subagent-session.ts
|
|
15231
15165
|
import fs from "fs";
|
|
15232
15166
|
import path2 from "path";
|
|
@@ -15310,7 +15244,7 @@ function onEnvoyRoleRegained(listener) {
|
|
|
15310
15244
|
// package.json
|
|
15311
15245
|
var package_default = {
|
|
15312
15246
|
name: "@sjawhar/pi-legion",
|
|
15313
|
-
version: "8.
|
|
15247
|
+
version: "8.11.0",
|
|
15314
15248
|
license: "Apache-2.0",
|
|
15315
15249
|
type: "module",
|
|
15316
15250
|
omp: {
|
|
@@ -15322,7 +15256,7 @@ var package_default = {
|
|
|
15322
15256
|
]
|
|
15323
15257
|
},
|
|
15324
15258
|
legion: {
|
|
15325
|
-
daemonApiVersion:
|
|
15259
|
+
daemonApiVersion: 19
|
|
15326
15260
|
},
|
|
15327
15261
|
repository: {
|
|
15328
15262
|
type: "git",
|
|
@@ -15603,39 +15537,10 @@ var LegionGrantResponse = exports_external.strictObject({
|
|
|
15603
15537
|
grantId: nonEmptyString,
|
|
15604
15538
|
expiresAt: timestamp
|
|
15605
15539
|
});
|
|
15606
|
-
var appLogin = exports_external.string().regex(/^[^[\]]+\[bot\]$/);
|
|
15607
|
-
var LegionGitHubTokenResponse = exports_external.strictObject({
|
|
15608
|
-
token: nonEmptyString,
|
|
15609
|
-
appLogin: exports_external.string().endsWith("[bot]"),
|
|
15610
|
-
legionAppLogins: exports_external.strictObject({ implement: appLogin, review: appLogin }).optional()
|
|
15611
|
-
});
|
|
15612
|
-
var LegionGitCredentialResponse = exports_external.strictObject({
|
|
15613
|
-
username: exports_external.literal("x-access-token"),
|
|
15614
|
-
password: nonEmptyString
|
|
15615
|
-
});
|
|
15616
|
-
var LegionThreadsResolveRequest = exports_external.strictObject({
|
|
15617
|
-
grantId: nonEmptyString,
|
|
15618
|
-
repo: nonEmptyString,
|
|
15619
|
-
number: exports_external.number().int().positive()
|
|
15620
|
-
});
|
|
15621
|
-
var LegionThreadOutcome = exports_external.union([
|
|
15622
|
-
exports_external.strictObject({
|
|
15623
|
-
url: nonEmptyString,
|
|
15624
|
-
resolved: nonEmptyString,
|
|
15625
|
-
newestBy: nonEmptyString.optional()
|
|
15626
|
-
}),
|
|
15627
|
-
exports_external.strictObject({
|
|
15628
|
-
url: nonEmptyString,
|
|
15629
|
-
leftOpen: nonEmptyString,
|
|
15630
|
-
newestBy: nonEmptyString.optional()
|
|
15631
|
-
})
|
|
15632
|
-
]);
|
|
15633
|
-
var LegionThreadsResolveResponse = exports_external.strictObject({
|
|
15634
|
-
threads: exports_external.array(LegionThreadOutcome),
|
|
15635
|
-
withheld: exports_external.number().int().nonnegative(),
|
|
15636
|
-
refused: exports_external.strictObject({ url: nonEmptyString, error: nonEmptyString }).optional()
|
|
15637
|
-
});
|
|
15638
15540
|
var LegionEmptyResponse = exports_external.strictObject({});
|
|
15541
|
+
var LegionHandoffCompleteResponse = exports_external.strictObject({
|
|
15542
|
+
note: nonEmptyString.optional()
|
|
15543
|
+
});
|
|
15639
15544
|
var LegionWaveReleaseResponse = exports_external.strictObject({
|
|
15640
15545
|
released: exports_external.array(nonEmptyString)
|
|
15641
15546
|
});
|
|
@@ -15643,16 +15548,12 @@ var LegionGrantRequest = exports_external.strictObject({
|
|
|
15643
15548
|
sessionId: nonEmptyString,
|
|
15644
15549
|
secret: nonEmptyString,
|
|
15645
15550
|
tree: nonEmptyString,
|
|
15646
|
-
issue: nonEmptyString
|
|
15647
|
-
push: exports_external.boolean().optional()
|
|
15551
|
+
issue: nonEmptyString
|
|
15648
15552
|
});
|
|
15649
15553
|
var LegionControllerGrantRequest = exports_external.strictObject({
|
|
15650
15554
|
sessionId: nonEmptyString,
|
|
15651
15555
|
secret: nonEmptyString
|
|
15652
15556
|
});
|
|
15653
|
-
var LegionGrantCredentialRequest = exports_external.strictObject({
|
|
15654
|
-
grantId: nonEmptyString
|
|
15655
|
-
});
|
|
15656
15557
|
var LegionHandoffCompleteRequest = exports_external.strictObject({
|
|
15657
15558
|
grantId: nonEmptyString,
|
|
15658
15559
|
summary: nonEmptyString,
|
|
@@ -15789,10 +15690,7 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
15789
15690
|
exit: (input) => acknowledge("/legion/v1/claims/exit", input),
|
|
15790
15691
|
grant: (input) => post("/legion/v1/grants", LegionGrantRequest, LegionGrantResponse, input),
|
|
15791
15692
|
controllerGrant: (input) => post("/legion/v1/grants", LegionControllerGrantRequest, LegionGrantResponse, input),
|
|
15792
|
-
|
|
15793
|
-
gitCredential: (input) => post("/legion/v1/git-credential", LegionGrantCredentialRequest, LegionGitCredentialResponse, input),
|
|
15794
|
-
provisioningCredential: (input) => post("/legion/v1/provisioning-credential", LegionGrantCredentialRequest, LegionGitHubTokenResponse, input),
|
|
15795
|
-
handoffComplete: (input) => post("/legion/v1/handoff/complete", LegionHandoffCompleteRequest, LegionEmptyResponse, input),
|
|
15693
|
+
handoffComplete: (input) => post("/legion/v1/handoff/complete", LegionHandoffCompleteRequest, LegionHandoffCompleteResponse, input),
|
|
15796
15694
|
issueStatus: (input) => post("/legion/v1/issues/status", LegionIssueStatusRequest, LegionEmptyResponse, input),
|
|
15797
15695
|
gateRegister: (input) => post("/legion/v1/gates/register", LegionGateRegisterRequest, LegionEmptyResponse, input),
|
|
15798
15696
|
waveRelease: (input) => post("/legion/v1/waves/release", LegionWaveReleaseRequest, LegionWaveReleaseResponse, input),
|
|
@@ -16044,7 +15942,7 @@ function createControllerSession(deps) {
|
|
|
16044
15942
|
try {
|
|
16045
15943
|
await claim(context);
|
|
16046
15944
|
} catch (error48) {
|
|
16047
|
-
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
|
|
15945
|
+
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 the \`legion\` tool's set_status and its other operations that call the daemon have no controller grant to mint and fail.`, "warning");
|
|
16048
15946
|
}
|
|
16049
15947
|
};
|
|
16050
15948
|
const isClaimedSession = (sessionID) => controllerSessionID === sessionID && mintControllerGrant !== undefined;
|
|
@@ -16057,31 +15955,88 @@ function createControllerSession(deps) {
|
|
|
16057
15955
|
return { claim, handleSessionStart, reclaimAfterSessionChange, isClaimedSession, mintGrant };
|
|
16058
15956
|
}
|
|
16059
15957
|
|
|
16060
|
-
// src/
|
|
16061
|
-
import {
|
|
16062
|
-
import {
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
}
|
|
16068
|
-
|
|
15958
|
+
// src/handoff-commit.ts
|
|
15959
|
+
import { execFile as execFile3 } from "child_process";
|
|
15960
|
+
import { promisify as promisify3 } from "util";
|
|
15961
|
+
var FILE_BACKED = {
|
|
15962
|
+
planning: { word: "plan", role: "planner" },
|
|
15963
|
+
implementing: { word: "implement", role: "implementer" },
|
|
15964
|
+
testing: { word: "test", role: "tester" },
|
|
15965
|
+
reviewing: { word: "review", role: "reviewer" }
|
|
15966
|
+
};
|
|
15967
|
+
var execFileAsync2 = promisify3(execFile3);
|
|
15968
|
+
var pathJj = async (args, cwd) => {
|
|
16069
15969
|
try {
|
|
16070
|
-
await
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
15970
|
+
const { stdout } = await execFileAsync2("jj", ["-R", cwd, ...args], {
|
|
15971
|
+
cwd,
|
|
15972
|
+
maxBuffer: 16 * 1024 * 1024
|
|
15973
|
+
});
|
|
15974
|
+
return stdout.trim();
|
|
16074
15975
|
} catch (error48) {
|
|
16075
|
-
|
|
16076
|
-
throw new Error(`
|
|
15976
|
+
const detail = error48 !== null && typeof error48 === "object" && "stderr" in error48 ? String(error48.stderr).trim() : error48 instanceof Error ? error48.message : String(error48);
|
|
15977
|
+
throw new Error(`jj ${args.join(" ")} failed: ${detail}`);
|
|
16077
15978
|
}
|
|
16078
|
-
}
|
|
16079
|
-
|
|
16080
|
-
const
|
|
16081
|
-
if (
|
|
16082
|
-
throw new Error(
|
|
15979
|
+
};
|
|
15980
|
+
function requiredVariable(env, name) {
|
|
15981
|
+
const value = env[name];
|
|
15982
|
+
if (value === undefined || value === "") {
|
|
15983
|
+
throw new Error(`${name} is not set, so handoff_complete cannot find the commit to report`);
|
|
16083
15984
|
}
|
|
16084
|
-
|
|
15985
|
+
return value;
|
|
15986
|
+
}
|
|
15987
|
+
async function findHandoffCommit(input) {
|
|
15988
|
+
const { phase, role, env } = input;
|
|
15989
|
+
const jj = input.jj ?? pathJj;
|
|
15990
|
+
const workspace = requiredVariable(env, "LEGION_WORKSPACE");
|
|
15991
|
+
const handoff = FILE_BACKED[phase];
|
|
15992
|
+
if (handoff === undefined || handoff.role !== role) {
|
|
15993
|
+
return jj(["log", "-r", "@-", "--no-graph", "-T", "commit_id"], workspace);
|
|
15994
|
+
}
|
|
15995
|
+
const issue2 = requiredVariable(env, "LEGION_ISSUE");
|
|
15996
|
+
if (!DISPATCH_KEY_PATTERN.test(issue2)) {
|
|
15997
|
+
throw new Error(`LEGION_ISSUE ${JSON.stringify(issue2)} is not a Dispatch issue key like LEGION-1`);
|
|
15998
|
+
}
|
|
15999
|
+
const file2 = `.legion/${issue2}/${handoff.word}.json`;
|
|
16000
|
+
const fileset = `root:${JSON.stringify(file2)}`;
|
|
16001
|
+
if (await jj(["diff", "-r", "@", "--name-only", fileset], workspace) !== "") {
|
|
16002
|
+
throw new Error(`${file2} has changes in the working copy that are not committed: commit this phase's handoff before completing`);
|
|
16003
|
+
}
|
|
16004
|
+
if (await jj(["file", "list", "-r", "@", fileset], workspace) === "") {
|
|
16005
|
+
throw new Error(`${file2} is missing from the workspace: write this phase's handoff, then commit and push it before completing`);
|
|
16006
|
+
}
|
|
16007
|
+
const carrying = await jj([
|
|
16008
|
+
"log",
|
|
16009
|
+
"-r",
|
|
16010
|
+
`latest((::@- ~ ::trunk()) & files(${fileset}))`,
|
|
16011
|
+
"--no-graph",
|
|
16012
|
+
"-T",
|
|
16013
|
+
"commit_id"
|
|
16014
|
+
], workspace);
|
|
16015
|
+
if (carrying === "") {
|
|
16016
|
+
throw new Error(`${file2} is not committed on this issue's branch (only the base branch carries it): write and commit this phase's handoff`);
|
|
16017
|
+
}
|
|
16018
|
+
const user = env.JJ_USER;
|
|
16019
|
+
if (user !== undefined && user !== "") {
|
|
16020
|
+
const author = await jj(["log", "-r", carrying, "--no-graph", "-T", 'author.name() ++ "\\n" ++ author.email()'], workspace);
|
|
16021
|
+
const [name = "", email3 = ""] = author.split(`
|
|
16022
|
+
`, 2);
|
|
16023
|
+
if (name !== user || email3 !== (env.JJ_EMAIL ?? "")) {
|
|
16024
|
+
throw new Error(`${file2} is carried by commit ${carrying}, authored by ${name} <${email3}>, not this pane's ${user}: run jj new, then write and commit this phase's handoff again`);
|
|
16025
|
+
}
|
|
16026
|
+
}
|
|
16027
|
+
const bookmark = `legion/${issue2}`;
|
|
16028
|
+
const pushed = await jj([
|
|
16029
|
+
"log",
|
|
16030
|
+
"-r",
|
|
16031
|
+
`${carrying} & ::remote_bookmarks(exact:${JSON.stringify(bookmark)}, exact:"origin")`,
|
|
16032
|
+
"--no-graph",
|
|
16033
|
+
"-T",
|
|
16034
|
+
"commit_id"
|
|
16035
|
+
], workspace);
|
|
16036
|
+
if (pushed === "") {
|
|
16037
|
+
throw new Error(`${file2} is carried by commit ${carrying}, which is not on ${bookmark}@origin: push the issue branch, then complete again`);
|
|
16038
|
+
}
|
|
16039
|
+
return carrying;
|
|
16085
16040
|
}
|
|
16086
16041
|
|
|
16087
16042
|
// src/phase-stall.ts
|
|
@@ -16171,132 +16126,6 @@ function toolFailure(error48) {
|
|
|
16171
16126
|
return { content: [{ type: "text", text: messageFor(error48) }], details: {}, isError: true };
|
|
16172
16127
|
}
|
|
16173
16128
|
|
|
16174
|
-
// src/handoff-actions.ts
|
|
16175
|
-
import { execFile as execFile3 } from "child_process";
|
|
16176
|
-
var HANDOFF_OPERATION_FIELDS = {
|
|
16177
|
-
handoff_write: ["phase", "data"],
|
|
16178
|
-
handoff_read: ["phase"],
|
|
16179
|
-
handoff_complete: ["summary", "verdict", "ready", "phase"]
|
|
16180
|
-
};
|
|
16181
|
-
var HANDOFF_OPERATIONS = Object.keys(HANDOFF_OPERATION_FIELDS);
|
|
16182
|
-
function isHandoffOperation(operation) {
|
|
16183
|
-
return Object.hasOwn(HANDOFF_OPERATION_FIELDS, operation);
|
|
16184
|
-
}
|
|
16185
|
-
function handoffSchemaFields(z2) {
|
|
16186
|
-
return {
|
|
16187
|
-
phase: z2.enum(HANDOFF_PHASES).optional(),
|
|
16188
|
-
data: z2.unknown().optional(),
|
|
16189
|
-
summary: z2.string().optional(),
|
|
16190
|
-
verdict: z2.enum(["pass", "fail"]).optional(),
|
|
16191
|
-
ready: z2.boolean().optional()
|
|
16192
|
-
};
|
|
16193
|
-
}
|
|
16194
|
-
var HANDOFF_DESCRIPTION = "Handoff actions (phase workers; the daemon accepts a completion only from the role working " + "the issue's current phase, never from an architect): handoff_write writes this phase's handoff " + "(`phase`, and `data`: the phase-specific fields as a JSON object, never the schemaVersion, " + "phase, completed and issue the command writes itself) to `.legion/<issue>/<phase>.json` in " + "the issue workspace; handoff_read returns the handoffs (every phase, or `phase`), a handoff " + "retro's removal of `.legion/<issue>/` took off the head read from the commit before it; " + "handoff_complete reports this phase complete to the daemon (`summary`: two sentences for the " + "architect, or the merger's READY packet; `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.";
|
|
16195
|
-
function rootArchitectHandoffRefusal(operation) {
|
|
16196
|
-
return `${operation} is not available to a root architect session; a root architect reads handoffs with \`legion handoff read [--phase <phase>]\` from bash`;
|
|
16197
|
-
}
|
|
16198
|
-
var ROLE_HANDOFF_PHASE = {
|
|
16199
|
-
architect: "architect",
|
|
16200
|
-
planner: "plan",
|
|
16201
|
-
implementer: "implement",
|
|
16202
|
-
tester: "test",
|
|
16203
|
-
reviewer: "review"
|
|
16204
|
-
};
|
|
16205
|
-
var COMMAND_TIMEOUT_MS = 300000;
|
|
16206
|
-
function required2(parameters, operation, name) {
|
|
16207
|
-
const value = parameters[name];
|
|
16208
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
16209
|
-
throw new Error(`${operation} requires ${name}`);
|
|
16210
|
-
}
|
|
16211
|
-
return value;
|
|
16212
|
-
}
|
|
16213
|
-
function commandArguments(operation, parameters) {
|
|
16214
|
-
const accepted = HANDOFF_OPERATION_FIELDS[operation];
|
|
16215
|
-
for (const name of Object.keys(parameters)) {
|
|
16216
|
-
if (name !== "op" && !accepted.includes(name)) {
|
|
16217
|
-
throw new Error(`${operation} does not accept field "${name}"`);
|
|
16218
|
-
}
|
|
16219
|
-
}
|
|
16220
|
-
switch (operation) {
|
|
16221
|
-
case "handoff_write": {
|
|
16222
|
-
const data = parameters.data;
|
|
16223
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
16224
|
-
throw new Error("handoff_write requires data: the phase's handoff fields as a JSON object");
|
|
16225
|
-
}
|
|
16226
|
-
const phase = required2(parameters, operation, "phase");
|
|
16227
|
-
return { args: ["handoff", "write", "--phase", phase], stdin: JSON.stringify(data) };
|
|
16228
|
-
}
|
|
16229
|
-
case "handoff_read":
|
|
16230
|
-
return {
|
|
16231
|
-
args: parameters.phase === undefined ? ["handoff", "read"] : ["handoff", "read", "--phase", required2(parameters, operation, "phase")]
|
|
16232
|
-
};
|
|
16233
|
-
case "handoff_complete": {
|
|
16234
|
-
const command = [
|
|
16235
|
-
"handoff",
|
|
16236
|
-
"complete",
|
|
16237
|
-
"--summary",
|
|
16238
|
-
required2(parameters, operation, "summary")
|
|
16239
|
-
];
|
|
16240
|
-
const { verdict, ready, phase } = parameters;
|
|
16241
|
-
if (phase !== undefined) {
|
|
16242
|
-
const role = requiredEnvironment(process.env, "LEGION_ROLE");
|
|
16243
|
-
const own = ROLE_HANDOFF_PHASE[role];
|
|
16244
|
-
if (own === undefined) {
|
|
16245
|
-
throw new Error(`handoff_complete takes no phase for a ${role}, which writes no handoff`);
|
|
16246
|
-
}
|
|
16247
|
-
if (phase !== own) {
|
|
16248
|
-
throw new Error(`handoff_complete's phase is "${own}" for a ${role}: omit phase, or pass "${own}"`);
|
|
16249
|
-
}
|
|
16250
|
-
}
|
|
16251
|
-
if (verdict !== undefined) {
|
|
16252
|
-
if (verdict !== "pass" && verdict !== "fail") {
|
|
16253
|
-
throw new Error("handoff_complete's verdict is pass or fail");
|
|
16254
|
-
}
|
|
16255
|
-
command.push("--verdict", verdict);
|
|
16256
|
-
}
|
|
16257
|
-
if (ready !== undefined && typeof ready !== "boolean") {
|
|
16258
|
-
throw new Error("handoff_complete's ready is true or false");
|
|
16259
|
-
}
|
|
16260
|
-
if (ready === true)
|
|
16261
|
-
command.push("--ready");
|
|
16262
|
-
return { args: command };
|
|
16263
|
-
}
|
|
16264
|
-
}
|
|
16265
|
-
}
|
|
16266
|
-
function runLegion(args, stdin, cwd, signal) {
|
|
16267
|
-
const { promise: promise2, resolve, reject } = Promise.withResolvers();
|
|
16268
|
-
const child = execFile3("legion", args, { cwd, env: process.env, signal, timeout: COMMAND_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, (error48, stdout, stderr) => {
|
|
16269
|
-
const output = [stdout, stderr].map((text) => text.trimEnd()).filter((text) => text !== "").join(`
|
|
16270
|
-
`);
|
|
16271
|
-
if (error48 === null) {
|
|
16272
|
-
resolve({ exitCode: 0, output });
|
|
16273
|
-
} else if (typeof error48.code === "number") {
|
|
16274
|
-
resolve({ exitCode: error48.code, output });
|
|
16275
|
-
} else {
|
|
16276
|
-
reject(new Error(`legion ${args.slice(0, 2).join(" ")} did not run: ${messageFor(error48)}`));
|
|
16277
|
-
}
|
|
16278
|
-
});
|
|
16279
|
-
child.stdin?.on("error", (error48) => {
|
|
16280
|
-
if (error48.code !== "EPIPE")
|
|
16281
|
-
reject(error48);
|
|
16282
|
-
});
|
|
16283
|
-
child.stdin?.end(stdin ?? "");
|
|
16284
|
-
return promise2;
|
|
16285
|
-
}
|
|
16286
|
-
async function runHandoffAction(input) {
|
|
16287
|
-
const { args, stdin } = commandArguments(input.operation, input.parameters);
|
|
16288
|
-
if (input.operation === "handoff_complete")
|
|
16289
|
-
await writeMintedGrant(input.mintGrant);
|
|
16290
|
-
const { exitCode, output } = await runLegion(args, stdin, requiredEnvironment(process.env, "LEGION_WORKSPACE"), input.signal);
|
|
16291
|
-
const text = output === "" ? `legion ${args.slice(0, 2).join(" ")} exited ${exitCode}` : output;
|
|
16292
|
-
const details = { operation: input.operation, exitCode };
|
|
16293
|
-
if (exitCode !== 0)
|
|
16294
|
-
return { content: [{ type: "text", text }], details, isError: true };
|
|
16295
|
-
if (input.operation === "handoff_complete")
|
|
16296
|
-
input.onPhaseCompleted();
|
|
16297
|
-
return { content: [{ type: "text", text }], details };
|
|
16298
|
-
}
|
|
16299
|
-
|
|
16300
16129
|
// src/tools.ts
|
|
16301
16130
|
var OPERATIONS = {
|
|
16302
16131
|
architect: [
|
|
@@ -16308,9 +16137,11 @@ var OPERATIONS = {
|
|
|
16308
16137
|
"close_root",
|
|
16309
16138
|
"park_child",
|
|
16310
16139
|
"rerun_child",
|
|
16311
|
-
"read_record"
|
|
16140
|
+
"read_record",
|
|
16141
|
+
"handoff_complete"
|
|
16312
16142
|
],
|
|
16313
|
-
"phase-worker": ["request_backward_move", "read_record"]
|
|
16143
|
+
"phase-worker": ["request_backward_move", "read_record", "handoff_complete"],
|
|
16144
|
+
controller: ["read_state", "set_status"]
|
|
16314
16145
|
};
|
|
16315
16146
|
var OPERATION_FIELDS = {
|
|
16316
16147
|
register_gate: ["issue", "artifactId", "version"],
|
|
@@ -16321,8 +16152,12 @@ var OPERATION_FIELDS = {
|
|
|
16321
16152
|
close_root: ["issue", "reason"],
|
|
16322
16153
|
park_child: ["issue"],
|
|
16323
16154
|
rerun_child: ["issue"],
|
|
16324
|
-
read_record: ["issue"]
|
|
16155
|
+
read_record: ["issue"],
|
|
16156
|
+
handoff_complete: ["summary", "verdict", "ready"],
|
|
16157
|
+
read_state: [],
|
|
16158
|
+
set_status: ["issue", "status"]
|
|
16325
16159
|
};
|
|
16160
|
+
var ISSUE_STATUSES2 = ["todo", "backlog", "icebox"];
|
|
16326
16161
|
var jsonSuccess = (details) => toolSuccess(JSON.stringify(details), details);
|
|
16327
16162
|
function toolSchema(pi) {
|
|
16328
16163
|
const z2 = pi.zod;
|
|
@@ -16337,7 +16172,9 @@ function toolSchema(pi) {
|
|
|
16337
16172
|
"park_child",
|
|
16338
16173
|
"rerun_child",
|
|
16339
16174
|
"read_record",
|
|
16340
|
-
|
|
16175
|
+
"handoff_complete",
|
|
16176
|
+
"read_state",
|
|
16177
|
+
"set_status"
|
|
16341
16178
|
]),
|
|
16342
16179
|
issue: z2.string().optional(),
|
|
16343
16180
|
artifactId: z2.string().describe("register_gate's root spec document: its artifact id, slug, or filename, as a dispatch command's --artifact takes it").optional(),
|
|
@@ -16346,7 +16183,10 @@ function toolSchema(pi) {
|
|
|
16346
16183
|
to: z2.enum(LEGION_PHASE_BACKWARD_TARGETS).optional(),
|
|
16347
16184
|
reason: z2.string().optional(),
|
|
16348
16185
|
decision: z2.enum(["retry", "escalate"]).optional(),
|
|
16349
|
-
|
|
16186
|
+
summary: z2.string().optional(),
|
|
16187
|
+
verdict: z2.enum(["pass", "fail"]).optional(),
|
|
16188
|
+
ready: z2.boolean().optional(),
|
|
16189
|
+
status: z2.enum(ISSUE_STATUSES2).optional()
|
|
16350
16190
|
});
|
|
16351
16191
|
}
|
|
16352
16192
|
function requiredString(parameters, operation, name) {
|
|
@@ -16365,14 +16205,6 @@ function assertOperationInput(parameters, operation) {
|
|
|
16365
16205
|
}
|
|
16366
16206
|
}
|
|
16367
16207
|
}
|
|
16368
|
-
async function grantFor(daemon, session) {
|
|
16369
|
-
return (await daemon.grant({
|
|
16370
|
-
sessionId: session.sessionId,
|
|
16371
|
-
secret: session.secret,
|
|
16372
|
-
tree: session.tree,
|
|
16373
|
-
issue: session.issue
|
|
16374
|
-
})).grantId;
|
|
16375
|
-
}
|
|
16376
16208
|
function recordFrom(state, issue2) {
|
|
16377
16209
|
const record2 = state.issues[issue2];
|
|
16378
16210
|
if (record2 === undefined)
|
|
@@ -16380,39 +16212,70 @@ function recordFrom(state, issue2) {
|
|
|
16380
16212
|
return record2;
|
|
16381
16213
|
}
|
|
16382
16214
|
function createLegionTool(deps) {
|
|
16383
|
-
const { pi, daemon, session, onPhaseCompleted, resolveDocument } = deps;
|
|
16215
|
+
const { pi, daemon, session, controllerGrant, onPhaseCompleted, handoffCommit, resolveDocument } = deps;
|
|
16216
|
+
const grantFor = async (client, active) => (await client.grant({
|
|
16217
|
+
sessionId: active.sessionId,
|
|
16218
|
+
secret: active.secret,
|
|
16219
|
+
tree: active.tree,
|
|
16220
|
+
issue: active.issue
|
|
16221
|
+
})).grantId;
|
|
16384
16222
|
return {
|
|
16385
16223
|
name: "legion",
|
|
16386
16224
|
label: "legion",
|
|
16387
|
-
description: "Perform the workflow operation the Legion daemon assigned this role. The daemon advances phases; this tool cannot spawn workers. " +
|
|
16225
|
+
description: "Perform the workflow operation the Legion daemon assigned this role. The daemon advances phases; this tool cannot spawn workers. " + "handoff_complete (phase workers and sub-architects; the daemon accepts a completion only from the role working the issue's current phase) " + "reports this phase complete: `summary` (two sentences for the architect, or the merger's READY packet), `verdict` pass|fail when your role's " + "instructions require one, `ready: true` for the merger's READY. A handoff is a committed file, `.legion/<issue>/<phase>.json`: the pushed commit " + "carrying it is found here, with the pane's jj, and reported, so write it, commit it and push the branch (`jj git push`) before calling; this call " + "refuses, posting nothing, a handoff still in the working copy, missing, not committed on this branch, carried by another pane's commit, or not yet " + "on `legion/<issue>@origin`, each naming the remedy. The daemon refuses HANDOFF_NOT_NEW (the commit is the one you reported for your previous " + "phase: write and commit this phase's handoff), and for READY READY_HEAD_CARRIES_HANDOFFS, READY_HEAD_CONFLICTS or READY_CHECKS_NOT_GREEN; a refused completion changed " + "nothing, so fix what it names and complete again. " + "The controller's read_state returns the daemon's whole state and set_status moves an issue to todo, backlog or icebox. " + "What a later phase needs goes in your handoff; a question for another live role goes to its role topic with envoy_publish.",
|
|
16388
16226
|
defaultInactive: true,
|
|
16389
16227
|
parameters: toolSchema(pi),
|
|
16390
|
-
execute: async (_id, parameters,
|
|
16228
|
+
execute: async (_id, parameters, _signal, _onUpdate, context) => {
|
|
16391
16229
|
try {
|
|
16392
16230
|
const active = session(context);
|
|
16393
16231
|
const operation = requiredString(parameters, "legion", "op");
|
|
16394
|
-
if (isHandoffOperation(operation)) {
|
|
16395
|
-
if (active.kind === "architect" && active.issue === active.tree) {
|
|
16396
|
-
throw new Error(rootArchitectHandoffRefusal(operation));
|
|
16397
|
-
}
|
|
16398
|
-
return await runHandoffAction({
|
|
16399
|
-
operation,
|
|
16400
|
-
parameters,
|
|
16401
|
-
signal,
|
|
16402
|
-
mintGrant: () => grantFor(daemon(), active),
|
|
16403
|
-
onPhaseCompleted: () => onPhaseCompleted(context)
|
|
16404
|
-
});
|
|
16405
|
-
}
|
|
16406
16232
|
if (!OPERATIONS[active.kind].includes(operation)) {
|
|
16407
16233
|
throw new Error(`${operation} is not available to a ${active.kind} session`);
|
|
16408
16234
|
}
|
|
16409
16235
|
assertOperationInput(parameters, operation);
|
|
16410
16236
|
const client = daemon();
|
|
16237
|
+
if (active.kind === "controller") {
|
|
16238
|
+
if (operation === "read_state")
|
|
16239
|
+
return jsonSuccess(await client.state());
|
|
16240
|
+
const issue2 = requiredString(parameters, operation, "issue");
|
|
16241
|
+
const status = ISSUE_STATUSES2.find((candidate) => candidate === parameters.status);
|
|
16242
|
+
if (status === undefined) {
|
|
16243
|
+
throw new Error(`set_status requires status ${ISSUE_STATUSES2.join(", ")}`);
|
|
16244
|
+
}
|
|
16245
|
+
const { grantId } = await controllerGrant(active.sessionId);
|
|
16246
|
+
await client.issueStatus({ grantId, issue: issue2, status });
|
|
16247
|
+
return jsonSuccess({});
|
|
16248
|
+
}
|
|
16411
16249
|
switch (operation) {
|
|
16412
16250
|
case "read_record": {
|
|
16413
16251
|
const issue2 = requiredString(parameters, operation, "issue");
|
|
16414
16252
|
return jsonSuccess({ record: recordFrom(await client.state(), issue2) });
|
|
16415
16253
|
}
|
|
16254
|
+
case "handoff_complete": {
|
|
16255
|
+
if (active.kind === "architect" && active.issue === active.tree) {
|
|
16256
|
+
throw new Error("handoff_complete is not available to a root architect session, which runs no phase");
|
|
16257
|
+
}
|
|
16258
|
+
const summary = requiredString(parameters, operation, "summary");
|
|
16259
|
+
const { verdict, ready } = parameters;
|
|
16260
|
+
if (verdict !== undefined && verdict !== "pass" && verdict !== "fail") {
|
|
16261
|
+
throw new Error("handoff_complete's verdict is pass or fail");
|
|
16262
|
+
}
|
|
16263
|
+
if (ready !== undefined && typeof ready !== "boolean") {
|
|
16264
|
+
throw new Error("handoff_complete's ready is true or false");
|
|
16265
|
+
}
|
|
16266
|
+
const { phase } = recordFrom(await client.state(), active.issue);
|
|
16267
|
+
const commit = await handoffCommit(phase, active.role);
|
|
16268
|
+
const grantId = await grantFor(client, active);
|
|
16269
|
+
const answer = await client.handoffComplete({
|
|
16270
|
+
grantId,
|
|
16271
|
+
summary,
|
|
16272
|
+
verdict: verdict ?? "",
|
|
16273
|
+
ready: ready ?? false,
|
|
16274
|
+
commit
|
|
16275
|
+
});
|
|
16276
|
+
onPhaseCompleted(context);
|
|
16277
|
+
return jsonSuccess(answer);
|
|
16278
|
+
}
|
|
16416
16279
|
case "register_gate": {
|
|
16417
16280
|
const version2 = parameters.version;
|
|
16418
16281
|
if (typeof version2 !== "number" || !Number.isSafeInteger(version2) || version2 < 1) {
|
|
@@ -16537,204 +16400,17 @@ var exitProcess = (code) => process.exit(code);
|
|
|
16537
16400
|
function setLegionBootstrapExitForTests(hook) {
|
|
16538
16401
|
exitProcess = hook;
|
|
16539
16402
|
}
|
|
16540
|
-
var GH_RESOLVED_URL = /(?:^|[\s;,"])(?:pr|issue):\/\//i;
|
|
16541
|
-
function needsGrant({ toolName, input }) {
|
|
16542
|
-
if (toolName === "bash") {
|
|
16543
|
-
return typeof input.command === "string" && dispatchCommandHead(input.command) === undefined;
|
|
16544
|
-
}
|
|
16545
|
-
if (toolName === "github")
|
|
16546
|
-
return true;
|
|
16547
|
-
const paths = Array.isArray(input.paths) ? input.paths : [input.path];
|
|
16548
|
-
return paths.some((entry) => typeof entry === "string" && GH_RESOLVED_URL.test(entry));
|
|
16549
|
-
}
|
|
16550
|
-
async function wrapWithGrant(mint) {
|
|
16551
|
-
try {
|
|
16552
|
-
await writeMintedGrant(async () => (await mint()).grantId);
|
|
16553
|
-
return;
|
|
16554
|
-
} catch (error48) {
|
|
16555
|
-
return { block: true, reason: messageFor(error48) };
|
|
16556
|
-
}
|
|
16557
|
-
}
|
|
16558
16403
|
async function persistedTranscript(context) {
|
|
16559
16404
|
await context.sessionManager.ensureOnDisk();
|
|
16560
16405
|
const sessionFile = context.sessionManager.getSessionFile();
|
|
16561
16406
|
if (!sessionFile?.endsWith(".jsonl")) {
|
|
16562
16407
|
throw new Error("Legion session must have a persisted transcript");
|
|
16563
16408
|
}
|
|
16564
|
-
const agentId =
|
|
16409
|
+
const agentId = path4.basename(sessionFile, ".jsonl");
|
|
16565
16410
|
if (!agentId)
|
|
16566
16411
|
throw new Error("Legion session transcript has no agent id");
|
|
16567
16412
|
return { sessionFile, agentId };
|
|
16568
16413
|
}
|
|
16569
|
-
var JJ_LOG_REWRITE_WORDS = ["undo", "abandon"];
|
|
16570
|
-
var JJ_OP_WORDS = ["op", "operation"];
|
|
16571
|
-
var JJ_OP_LOG_REWRITE_WORDS = ["restore", "revert"];
|
|
16572
|
-
var JJ_MENTION = /\bjj\b/;
|
|
16573
|
-
var JJ_LOG_REWRITE_MENTION = new RegExp(`\\b(?:${JJ_LOG_REWRITE_WORDS.join("|")})\\b|\\b(?:${JJ_OP_WORDS.join("|")})\\b\\W+(?:${JJ_OP_LOG_REWRITE_WORDS.join("|")})\\b`);
|
|
16574
|
-
function jjLogRewriteMention(text) {
|
|
16575
|
-
if (!JJ_MENTION.test(text))
|
|
16576
|
-
return;
|
|
16577
|
-
const match = JJ_LOG_REWRITE_MENTION.exec(text);
|
|
16578
|
-
return match === null ? undefined : `jj ${match[0].replace(/\W+/g, " ")}`;
|
|
16579
|
-
}
|
|
16580
|
-
function splitShellCommands(command) {
|
|
16581
|
-
const commands = [];
|
|
16582
|
-
let words = [];
|
|
16583
|
-
let text = "";
|
|
16584
|
-
let inWord = false;
|
|
16585
|
-
let quote;
|
|
16586
|
-
const endWord = () => {
|
|
16587
|
-
if (inWord)
|
|
16588
|
-
words.push(text);
|
|
16589
|
-
text = "";
|
|
16590
|
-
inWord = false;
|
|
16591
|
-
};
|
|
16592
|
-
const endCommand = () => {
|
|
16593
|
-
endWord();
|
|
16594
|
-
if (words.length > 0)
|
|
16595
|
-
commands.push(words);
|
|
16596
|
-
words = [];
|
|
16597
|
-
};
|
|
16598
|
-
for (let i = 0;i < command.length; i += 1) {
|
|
16599
|
-
const char = command.charAt(i);
|
|
16600
|
-
if (quote !== undefined) {
|
|
16601
|
-
if (char === quote)
|
|
16602
|
-
quote = undefined;
|
|
16603
|
-
else if (quote === '"' && char === "\\" && command.charAt(i + 1) === `
|
|
16604
|
-
`)
|
|
16605
|
-
i += 1;
|
|
16606
|
-
else if (quote === '"' && char === "\\" && i + 1 < command.length) {
|
|
16607
|
-
i += 1;
|
|
16608
|
-
text += command.charAt(i);
|
|
16609
|
-
} else
|
|
16610
|
-
text += char;
|
|
16611
|
-
continue;
|
|
16612
|
-
}
|
|
16613
|
-
if (char === '"' || char === "'") {
|
|
16614
|
-
quote = char;
|
|
16615
|
-
inWord = true;
|
|
16616
|
-
} else if (char === "\\") {
|
|
16617
|
-
if (command.charAt(i + 1) === `
|
|
16618
|
-
`)
|
|
16619
|
-
i += 1;
|
|
16620
|
-
else if (i + 1 < command.length) {
|
|
16621
|
-
i += 1;
|
|
16622
|
-
text += command.charAt(i);
|
|
16623
|
-
inWord = true;
|
|
16624
|
-
}
|
|
16625
|
-
} else if (char === " " || char === "\t" || char === "<" || char === ">") {
|
|
16626
|
-
endWord();
|
|
16627
|
-
} else if (char === "&" && (command.charAt(i - 1) === ">" || command.charAt(i - 1) === "<" || command.charAt(i + 1) === ">")) {
|
|
16628
|
-
endWord();
|
|
16629
|
-
} else if (";&|\n()`".includes(char)) {
|
|
16630
|
-
endCommand();
|
|
16631
|
-
} else {
|
|
16632
|
-
text += char;
|
|
16633
|
-
inWord = true;
|
|
16634
|
-
}
|
|
16635
|
-
}
|
|
16636
|
-
if (quote !== undefined)
|
|
16637
|
-
return;
|
|
16638
|
-
endCommand();
|
|
16639
|
-
return commands;
|
|
16640
|
-
}
|
|
16641
|
-
function commandMatch(words, name, sequence) {
|
|
16642
|
-
for (let index = 0;index < words.length; index += 1) {
|
|
16643
|
-
const word = words[index];
|
|
16644
|
-
if (word !== name && !word?.endsWith(`/${name}`))
|
|
16645
|
-
continue;
|
|
16646
|
-
if (sequence.every((expected, offset) => words[index + 1 + offset] === expected))
|
|
16647
|
-
return index;
|
|
16648
|
-
}
|
|
16649
|
-
return -1;
|
|
16650
|
-
}
|
|
16651
|
-
function isPushInvocation(words) {
|
|
16652
|
-
return commandMatch(words, "legion", ["push"]) !== -1;
|
|
16653
|
-
}
|
|
16654
|
-
function commandsRunPush(commands) {
|
|
16655
|
-
return commands?.some(isPushInvocation) ?? false;
|
|
16656
|
-
}
|
|
16657
|
-
function jjLogRewriteInvocation(words) {
|
|
16658
|
-
const jj = commandMatch(words, "jj", []);
|
|
16659
|
-
if (jj === -1)
|
|
16660
|
-
return;
|
|
16661
|
-
const args = words.slice(jj + 1);
|
|
16662
|
-
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));
|
|
16663
|
-
return rewritesLog ? words.slice(jj).join(" ") : undefined;
|
|
16664
|
-
}
|
|
16665
|
-
var JJ_LOG_REWRITE = {
|
|
16666
|
-
mention: jjLogRewriteMention,
|
|
16667
|
-
invocation: jjLogRewriteInvocation,
|
|
16668
|
-
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.'
|
|
16669
|
-
};
|
|
16670
|
-
var TREE_PANE_RULES = [JJ_LOG_REWRITE];
|
|
16671
|
-
var PANE_RULES = {
|
|
16672
|
-
"phase-worker": TREE_PANE_RULES,
|
|
16673
|
-
"root-architect": TREE_PANE_RULES
|
|
16674
|
-
};
|
|
16675
|
-
function refusedCommand(command, commands, rule) {
|
|
16676
|
-
if (commands === undefined) {
|
|
16677
|
-
return rule.mention(command) === undefined ? undefined : command.trim();
|
|
16678
|
-
}
|
|
16679
|
-
for (const words of commands) {
|
|
16680
|
-
const mentioned = words.find((word) => rule.mention(word) !== undefined);
|
|
16681
|
-
if (mentioned !== undefined)
|
|
16682
|
-
return mentioned;
|
|
16683
|
-
const invocation = rule.invocation(words);
|
|
16684
|
-
if (invocation !== undefined)
|
|
16685
|
-
return invocation;
|
|
16686
|
-
}
|
|
16687
|
-
return;
|
|
16688
|
-
}
|
|
16689
|
-
var NON_FILE_WRITE_URL = /^(xd|agent|proc):\/\//iu;
|
|
16690
|
-
var READ_HEADER_TAG = /#[0-9A-Fa-f]{4}$/u;
|
|
16691
|
-
var PREFIXED_CONFLICT_URL = /^(.+):(conflict:\/\/.+)$/u;
|
|
16692
|
-
function unwrapReadHeader(path6) {
|
|
16693
|
-
const trimmed = path6.trimEnd();
|
|
16694
|
-
if (trimmed.length < 2 || !trimmed.startsWith("[") || !trimmed.endsWith("]"))
|
|
16695
|
-
return path6;
|
|
16696
|
-
const inner = trimmed.slice(1, -1);
|
|
16697
|
-
const tag = READ_HEADER_TAG.exec(inner);
|
|
16698
|
-
const target = tag === null ? inner : inner.slice(0, tag.index);
|
|
16699
|
-
if (target.length === 0 || tag === null && target.includes("#"))
|
|
16700
|
-
return path6;
|
|
16701
|
-
return target;
|
|
16702
|
-
}
|
|
16703
|
-
function nonFileWriteScheme(toolCall) {
|
|
16704
|
-
if (toolCall.toolName !== "write" || typeof toolCall.input.path !== "string")
|
|
16705
|
-
return;
|
|
16706
|
-
const unwrapped = unwrapReadHeader(toolCall.input.path);
|
|
16707
|
-
const target = PREFIXED_CONFLICT_URL.exec(unwrapped)?.[2] ?? unwrapped;
|
|
16708
|
-
return NON_FILE_WRITE_URL.exec(target)?.[1]?.toLowerCase();
|
|
16709
|
-
}
|
|
16710
|
-
function paneRuleRefusal(toolCall, rules, commands) {
|
|
16711
|
-
if (rules.length === 0)
|
|
16712
|
-
return;
|
|
16713
|
-
const { toolName, input } = toolCall;
|
|
16714
|
-
if (toolName === "bash") {
|
|
16715
|
-
if (typeof input.command !== "string")
|
|
16716
|
-
return;
|
|
16717
|
-
for (const rule of rules) {
|
|
16718
|
-
const attempt = refusedCommand(input.command, commands, rule);
|
|
16719
|
-
if (attempt !== undefined)
|
|
16720
|
-
return rule.refusal(attempt);
|
|
16721
|
-
}
|
|
16722
|
-
return;
|
|
16723
|
-
}
|
|
16724
|
-
let text;
|
|
16725
|
-
if (toolName === "eval" && typeof input.code === "string")
|
|
16726
|
-
text = input.code;
|
|
16727
|
-
else if (nonFileWriteScheme(toolCall) === "proc" && typeof input.content === "string") {
|
|
16728
|
-
text = input.content;
|
|
16729
|
-
} else
|
|
16730
|
-
return;
|
|
16731
|
-
for (const rule of rules) {
|
|
16732
|
-
const mention = rule.mention(text);
|
|
16733
|
-
if (mention !== undefined)
|
|
16734
|
-
return rule.refusal(`${toolName}: ${mention}`);
|
|
16735
|
-
}
|
|
16736
|
-
return;
|
|
16737
|
-
}
|
|
16738
16414
|
function envoyPluginRefusal() {
|
|
16739
16415
|
const legacy = globalThis[LEGACY_LEGION_LOADED_KEY];
|
|
16740
16416
|
if (legacy !== undefined) {
|
|
@@ -16752,14 +16428,13 @@ function envoyPluginRefusal() {
|
|
|
16752
16428
|
}
|
|
16753
16429
|
}
|
|
16754
16430
|
function legionExtension(pi) {
|
|
16755
|
-
const instance =
|
|
16431
|
+
const instance = randomUUID().slice(0, 8);
|
|
16756
16432
|
logger3.debug("extension instance loaded", { extension: import.meta.url, instance });
|
|
16757
16433
|
globalThis[LEGION_PLUGIN_LOADED_KEY] = {
|
|
16758
16434
|
from: import.meta.url,
|
|
16759
16435
|
envoyInterface: ENVOY_PLUGIN_INTERFACE_VERSION
|
|
16760
16436
|
};
|
|
16761
16437
|
const checkSubagentSession = subagentSessionCheck();
|
|
16762
|
-
let paneRules;
|
|
16763
16438
|
let phaseStall = "closed";
|
|
16764
16439
|
const phaseWorkerSession = (context) => {
|
|
16765
16440
|
const role = claimSession.capability(context.sessionManager.getSessionId())?.role;
|
|
@@ -16815,16 +16490,16 @@ function legionExtension(pi) {
|
|
|
16815
16490
|
}
|
|
16816
16491
|
if (session.kind === "controller") {
|
|
16817
16492
|
await controllerSession.handleSessionStart(context);
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
}
|
|
16493
|
+
} else {
|
|
16494
|
+
recreatedNotice = workspaceRecreatedNotice(process.env, session.kind === "root-architect" ? session.tree : session.issue);
|
|
16495
|
+
if (recreatedNotice !== undefined) {
|
|
16496
|
+
pi.sendMessage(workspaceRecreatedMessage(recreatedNotice), {
|
|
16497
|
+
deliverAs: "steer",
|
|
16498
|
+
triggerTurn: false
|
|
16499
|
+
});
|
|
16500
|
+
}
|
|
16501
|
+
await claimSession.bootstrap(context);
|
|
16826
16502
|
}
|
|
16827
|
-
await claimSession.bootstrap(context);
|
|
16828
16503
|
registerLegionTool();
|
|
16829
16504
|
await activateLegionTool();
|
|
16830
16505
|
});
|
|
@@ -16857,43 +16532,6 @@ function legionExtension(pi) {
|
|
|
16857
16532
|
pi.on("session_switch", (_event, context) => afterSessionChange(context));
|
|
16858
16533
|
pi.on("session_branch", (_event, context) => afterSessionChange(context));
|
|
16859
16534
|
pi.on("session_tree", (_event, context) => afterSessionChange(context));
|
|
16860
|
-
pi.on("tool_call", async (toolCall, context) => {
|
|
16861
|
-
logger3.debug("legion tool_call hook", {
|
|
16862
|
-
instance,
|
|
16863
|
-
toolCallId: toolCall.toolCallId,
|
|
16864
|
-
toolName: toolCall.toolName
|
|
16865
|
-
});
|
|
16866
|
-
paneRules ??= PANE_RULES[classifySession(process.env).kind] ?? [];
|
|
16867
|
-
const commands = toolCall.toolName === "bash" && typeof toolCall.input.command === "string" ? splitShellCommands(dispatchCommandHead(toolCall.input.command) ?? toolCall.input.command) : undefined;
|
|
16868
|
-
const refusal = paneRuleRefusal(toolCall, paneRules, commands);
|
|
16869
|
-
if (refusal !== undefined)
|
|
16870
|
-
return { block: true, reason: refusal };
|
|
16871
|
-
if (await checkSubagentSession(context))
|
|
16872
|
-
return;
|
|
16873
|
-
const sessionID = context.sessionManager.getSessionId();
|
|
16874
|
-
const active = claimSession.capability(sessionID);
|
|
16875
|
-
if (!needsGrant(toolCall))
|
|
16876
|
-
return;
|
|
16877
|
-
if (active === undefined) {
|
|
16878
|
-
if (controllerSession.isClaimedSession(sessionID)) {
|
|
16879
|
-
return wrapWithGrant(() => controllerSession.mintGrant(sessionID));
|
|
16880
|
-
}
|
|
16881
|
-
if (process.env.LEGION_ROLE !== undefined && process.env.LEGION_CONTROLLER !== "1") {
|
|
16882
|
-
return {
|
|
16883
|
-
block: true,
|
|
16884
|
-
reason: "Legion worker session is not registered; cannot mint its grant"
|
|
16885
|
-
};
|
|
16886
|
-
}
|
|
16887
|
-
return;
|
|
16888
|
-
}
|
|
16889
|
-
return wrapWithGrant(() => roleDaemon().grant({
|
|
16890
|
-
tree: active.tree,
|
|
16891
|
-
issue: active.issue,
|
|
16892
|
-
sessionId: sessionID,
|
|
16893
|
-
secret: active.secret,
|
|
16894
|
-
push: commandsRunPush(commands)
|
|
16895
|
-
}));
|
|
16896
|
-
});
|
|
16897
16535
|
pi.on("message_start", async (event, context) => {
|
|
16898
16536
|
if (!phaseWorkerSession(context))
|
|
16899
16537
|
return;
|
|
@@ -16930,19 +16568,26 @@ function legionExtension(pi) {
|
|
|
16930
16568
|
pi,
|
|
16931
16569
|
daemon: roleDaemon,
|
|
16932
16570
|
session: (context) => {
|
|
16933
|
-
const
|
|
16934
|
-
|
|
16935
|
-
|
|
16571
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
16572
|
+
const active = claimSession.capability(sessionID);
|
|
16573
|
+
if (active !== undefined) {
|
|
16574
|
+
return {
|
|
16575
|
+
kind: active.role === "architect" ? "architect" : "phase-worker",
|
|
16576
|
+
role: active.role,
|
|
16577
|
+
sessionId: active.sessionID,
|
|
16578
|
+
tree: active.tree,
|
|
16579
|
+
issue: active.issue,
|
|
16580
|
+
secret: active.secret
|
|
16581
|
+
};
|
|
16936
16582
|
}
|
|
16937
|
-
|
|
16938
|
-
kind:
|
|
16939
|
-
|
|
16940
|
-
|
|
16941
|
-
issue: active.issue,
|
|
16942
|
-
secret: active.secret
|
|
16943
|
-
};
|
|
16583
|
+
if (controllerSession.isClaimedSession(sessionID)) {
|
|
16584
|
+
return { kind: "controller", sessionId: sessionID };
|
|
16585
|
+
}
|
|
16586
|
+
throw new Error("legion is available only to this session's registered claim");
|
|
16944
16587
|
},
|
|
16588
|
+
controllerGrant: (sessionId) => controllerSession.mintGrant(sessionId),
|
|
16945
16589
|
onPhaseCompleted,
|
|
16590
|
+
handoffCommit: (phase, role) => findHandoffCommit({ phase, role, env: process.env }),
|
|
16946
16591
|
resolveDocument: async (issue2, reference) => {
|
|
16947
16592
|
const config2 = activeDispatchConfig(process.env, { cwd: process.cwd() });
|
|
16948
16593
|
if (config2 === null) {
|
|
@@ -16963,6 +16608,8 @@ function legionExtension(pi) {
|
|
|
16963
16608
|
if (refusal !== undefined)
|
|
16964
16609
|
throw new Error(refusal);
|
|
16965
16610
|
await controllerSession.claim(context);
|
|
16611
|
+
registerLegionTool();
|
|
16612
|
+
await activateLegionTool();
|
|
16966
16613
|
}
|
|
16967
16614
|
});
|
|
16968
16615
|
}
|