@sjawhar/pi-legion-envoy 1.68.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 +72 -57
- package/dist/skills/legion-worker/SKILL.md +11 -3
- package/package.json +1 -1
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.68.
|
|
16145
|
+
version: "1.68.1",
|
|
16146
16146
|
type: "module",
|
|
16147
16147
|
omp: {
|
|
16148
16148
|
extensions: [
|
|
@@ -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.";
|
|
@@ -31855,7 +31849,6 @@ import { execFile as execFile2 } from "child_process";
|
|
|
31855
31849
|
var HANDOFF_OPERATION_FIELDS = {
|
|
31856
31850
|
handoff_write: ["phase", "data"],
|
|
31857
31851
|
handoff_read: ["phase"],
|
|
31858
|
-
handoff_message: ["sender", "recipient", "body"],
|
|
31859
31852
|
handoff_complete: ["summary", "verdict", "ready", "phase"]
|
|
31860
31853
|
};
|
|
31861
31854
|
var HANDOFF_OPERATIONS = Object.keys(HANDOFF_OPERATION_FIELDS);
|
|
@@ -31866,15 +31859,12 @@ function handoffSchemaFields(z2) {
|
|
|
31866
31859
|
return {
|
|
31867
31860
|
phase: z2.enum(HANDOFF_PHASES).optional(),
|
|
31868
31861
|
data: z2.unknown().optional(),
|
|
31869
|
-
sender: z2.enum(HANDOFF_PHASES).optional(),
|
|
31870
|
-
recipient: z2.enum(HANDOFF_PHASES).optional(),
|
|
31871
|
-
body: z2.string().optional(),
|
|
31872
31862
|
summary: z2.string().optional(),
|
|
31873
31863
|
verdict: z2.enum(["pass", "fail"]).optional(),
|
|
31874
31864
|
ready: z2.boolean().optional()
|
|
31875
31865
|
};
|
|
31876
31866
|
}
|
|
31877
|
-
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.";
|
|
31878
31868
|
var ROLE_HANDOFF_PHASE = {
|
|
31879
31869
|
architect: "architect",
|
|
31880
31870
|
planner: "plan",
|
|
@@ -31908,21 +31898,12 @@ function commandArguments(operation, parameters) {
|
|
|
31908
31898
|
throw new Error("handoff_write requires data: the phase's handoff fields as a JSON object");
|
|
31909
31899
|
}
|
|
31910
31900
|
const phase = required2(parameters, operation, "phase");
|
|
31911
|
-
return ["handoff", "write", "--phase", phase,
|
|
31901
|
+
return { args: ["handoff", "write", "--phase", phase], stdin: JSON.stringify(data) };
|
|
31912
31902
|
}
|
|
31913
31903
|
case "handoff_read":
|
|
31914
|
-
return
|
|
31915
|
-
|
|
31916
|
-
|
|
31917
|
-
"handoff",
|
|
31918
|
-
"message",
|
|
31919
|
-
"--from",
|
|
31920
|
-
required2(parameters, operation, "sender"),
|
|
31921
|
-
"--to",
|
|
31922
|
-
required2(parameters, operation, "recipient"),
|
|
31923
|
-
"--body",
|
|
31924
|
-
required2(parameters, operation, "body")
|
|
31925
|
-
];
|
|
31904
|
+
return {
|
|
31905
|
+
args: parameters.phase === undefined ? ["handoff", "read"] : ["handoff", "read", "--phase", required2(parameters, operation, "phase")]
|
|
31906
|
+
};
|
|
31926
31907
|
case "handoff_complete": {
|
|
31927
31908
|
const command = [
|
|
31928
31909
|
"handoff",
|
|
@@ -31952,13 +31933,13 @@ function commandArguments(operation, parameters) {
|
|
|
31952
31933
|
}
|
|
31953
31934
|
if (ready === true)
|
|
31954
31935
|
command.push("--ready");
|
|
31955
|
-
return command;
|
|
31936
|
+
return { args: command };
|
|
31956
31937
|
}
|
|
31957
31938
|
}
|
|
31958
31939
|
}
|
|
31959
|
-
function runLegion(args, cwd, signal) {
|
|
31940
|
+
function runLegion(args, stdin, cwd, signal) {
|
|
31960
31941
|
const { promise: promise2, resolve, reject } = Promise.withResolvers();
|
|
31961
|
-
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) => {
|
|
31962
31943
|
const output = [stdout, stderr].map((text) => text.trimEnd()).filter((text) => text !== "").join(`
|
|
31963
31944
|
`);
|
|
31964
31945
|
if (error48 === null) {
|
|
@@ -31969,13 +31950,18 @@ function runLegion(args, cwd, signal) {
|
|
|
31969
31950
|
reject(new Error(`legion ${args.slice(0, 2).join(" ")} did not run: ${messageFor(error48)}`));
|
|
31970
31951
|
}
|
|
31971
31952
|
});
|
|
31953
|
+
child.stdin?.on("error", (error48) => {
|
|
31954
|
+
if (error48.code !== "EPIPE")
|
|
31955
|
+
reject(error48);
|
|
31956
|
+
});
|
|
31957
|
+
child.stdin?.end(stdin ?? "");
|
|
31972
31958
|
return promise2;
|
|
31973
31959
|
}
|
|
31974
31960
|
async function runHandoffAction(input) {
|
|
31975
|
-
const args = commandArguments(input.operation, input.parameters);
|
|
31961
|
+
const { args, stdin } = commandArguments(input.operation, input.parameters);
|
|
31976
31962
|
if (input.operation === "handoff_complete")
|
|
31977
31963
|
await writeMintedGrant(input.mintGrant);
|
|
31978
|
-
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);
|
|
31979
31965
|
const text = output === "" ? `legion ${args.slice(0, 2).join(" ")} exited ${exitCode}` : output;
|
|
31980
31966
|
const details = { operation: input.operation, exitCode };
|
|
31981
31967
|
if (exitCode !== 0)
|
|
@@ -32511,7 +32497,7 @@ function jjLogRewriteMention(text) {
|
|
|
32511
32497
|
if (!JJ_MENTION.test(text))
|
|
32512
32498
|
return;
|
|
32513
32499
|
const match = JJ_LOG_REWRITE_MENTION.exec(text);
|
|
32514
|
-
return match === null ? undefined : match[0].replace(/\W+/g, " ")
|
|
32500
|
+
return match === null ? undefined : `jj ${match[0].replace(/\W+/g, " ")}`;
|
|
32515
32501
|
}
|
|
32516
32502
|
function splitShellCommands(command) {
|
|
32517
32503
|
const commands = [];
|
|
@@ -32574,29 +32560,60 @@ function splitShellCommands(command) {
|
|
|
32574
32560
|
endCommand();
|
|
32575
32561
|
return commands;
|
|
32576
32562
|
}
|
|
32577
|
-
function jjLogRewriteInvocation(
|
|
32578
|
-
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) {
|
|
32579
32590
|
if (commands === undefined) {
|
|
32580
|
-
return
|
|
32591
|
+
return rule.mention(command) === undefined ? undefined : command.trim();
|
|
32581
32592
|
}
|
|
32582
32593
|
for (const words of commands) {
|
|
32583
|
-
const mentioned = words.find((word) =>
|
|
32594
|
+
const mentioned = words.find((word) => rule.mention(word) !== undefined);
|
|
32584
32595
|
if (mentioned !== undefined)
|
|
32585
32596
|
return mentioned;
|
|
32586
|
-
const
|
|
32587
|
-
if (
|
|
32588
|
-
|
|
32589
|
-
const args = words.slice(jj + 1);
|
|
32590
|
-
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));
|
|
32591
|
-
if (rewritesLog)
|
|
32592
|
-
return words.slice(jj).join(" ");
|
|
32597
|
+
const invocation = rule.invocation(words);
|
|
32598
|
+
if (invocation !== undefined)
|
|
32599
|
+
return invocation;
|
|
32593
32600
|
}
|
|
32594
32601
|
return;
|
|
32595
32602
|
}
|
|
32596
|
-
function
|
|
32603
|
+
function paneRuleRefusal(toolCall, rules) {
|
|
32604
|
+
if (rules.length === 0)
|
|
32605
|
+
return;
|
|
32597
32606
|
const { toolName, input } = toolCall;
|
|
32598
32607
|
if (toolName === "bash") {
|
|
32599
|
-
|
|
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;
|
|
32600
32617
|
}
|
|
32601
32618
|
let text;
|
|
32602
32619
|
if (toolName === "eval" && typeof input.code === "string")
|
|
@@ -32605,8 +32622,12 @@ function jjLogRewriteAttempt(toolCall) {
|
|
|
32605
32622
|
text = [input.application, ...Array.isArray(input.args) ? input.args : [], input.text].filter((part) => typeof part === "string").join(" ");
|
|
32606
32623
|
} else
|
|
32607
32624
|
return;
|
|
32608
|
-
const
|
|
32609
|
-
|
|
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;
|
|
32610
32631
|
}
|
|
32611
32632
|
var LEGION_LOADED_MARKER = Symbol.for("legion.pi-envoy.legion-loaded");
|
|
32612
32633
|
var CODE_MUTATION_TOOLS = ["edit", "write", "apply_patch"];
|
|
@@ -32625,7 +32646,7 @@ function legionExtension(pi) {
|
|
|
32625
32646
|
let capability;
|
|
32626
32647
|
let bootstrap;
|
|
32627
32648
|
const checkSubagentSession = subagentSessionCheck();
|
|
32628
|
-
let
|
|
32649
|
+
let paneRules;
|
|
32629
32650
|
let phaseStall = "closed";
|
|
32630
32651
|
const phaseWorkerSession = (context) => capability !== undefined && capability.sessionID === context.sessionManager.getSessionId() && capability.role !== "architect";
|
|
32631
32652
|
const advancePhaseStall = (input) => {
|
|
@@ -32920,16 +32941,10 @@ function legionExtension(pi) {
|
|
|
32920
32941
|
toolCallId: toolCall.toolCallId,
|
|
32921
32942
|
toolName: toolCall.toolName
|
|
32922
32943
|
});
|
|
32923
|
-
|
|
32924
|
-
|
|
32925
|
-
|
|
32926
|
-
|
|
32927
|
-
return {
|
|
32928
|
-
block: true,
|
|
32929
|
-
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.'
|
|
32930
|
-
};
|
|
32931
|
-
}
|
|
32932
|
-
}
|
|
32944
|
+
paneRules ??= PANE_RULES[classifySession(process.env).kind] ?? [];
|
|
32945
|
+
const refusal = paneRuleRefusal(toolCall, paneRules);
|
|
32946
|
+
if (refusal !== undefined)
|
|
32947
|
+
return { block: true, reason: refusal };
|
|
32933
32948
|
if (await checkSubagentSession(context))
|
|
32934
32949
|
return;
|
|
32935
32950
|
const sessionID = context.sessionManager.getSessionId();
|
|
@@ -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
|