@sjawhar/pi-legion-envoy 7.11.1 → 7.12.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/dist/legion.js +49 -11
- package/dist/skills/legion-controller/SKILL.md +25 -8
- package/package.json +2 -2
package/dist/legion.js
CHANGED
|
@@ -18042,7 +18042,7 @@ function endInjectedUserTurns(sessionID) {
|
|
|
18042
18042
|
// package.json
|
|
18043
18043
|
var package_default = {
|
|
18044
18044
|
name: "@sjawhar/pi-legion-envoy",
|
|
18045
|
-
version: "7.
|
|
18045
|
+
version: "7.12.0",
|
|
18046
18046
|
license: "Apache-2.0",
|
|
18047
18047
|
type: "module",
|
|
18048
18048
|
omp: {
|
|
@@ -18055,7 +18055,7 @@ var package_default = {
|
|
|
18055
18055
|
]
|
|
18056
18056
|
},
|
|
18057
18057
|
legion: {
|
|
18058
|
-
daemonApiVersion:
|
|
18058
|
+
daemonApiVersion: 14
|
|
18059
18059
|
},
|
|
18060
18060
|
repository: {
|
|
18061
18061
|
type: "git",
|
|
@@ -18421,7 +18421,8 @@ var LegionGrantRequest = exports_external.strictObject({
|
|
|
18421
18421
|
sessionId: nonEmptyString,
|
|
18422
18422
|
secret: nonEmptyString,
|
|
18423
18423
|
tree: nonEmptyString,
|
|
18424
|
-
issue: nonEmptyString
|
|
18424
|
+
issue: nonEmptyString,
|
|
18425
|
+
push: exports_external.boolean().optional()
|
|
18425
18426
|
});
|
|
18426
18427
|
var LegionControllerGrantRequest = exports_external.strictObject({
|
|
18427
18428
|
sessionId: nonEmptyString,
|
|
@@ -18777,8 +18778,19 @@ function createControllerSession(deps) {
|
|
|
18777
18778
|
let mintControllerGrant;
|
|
18778
18779
|
let controllerTranscript;
|
|
18779
18780
|
const claim = async (context) => {
|
|
18781
|
+
const supervised = classifySession(process.env).kind === "controller" && (process.env.LEGION_BOOT_TOKEN_FILE !== undefined || process.env.LEGION_BOOT_TOKEN !== undefined);
|
|
18782
|
+
if (!supervised)
|
|
18783
|
+
return claimWith(context, false);
|
|
18784
|
+
try {
|
|
18785
|
+
await claimWith(context, true);
|
|
18786
|
+
} catch (error48) {
|
|
18787
|
+
console.error(`[legion] the daemon-launched controller's claim failed; exiting so the daemon launches it again: ${messageFor(error48)}`);
|
|
18788
|
+
deps.exitProcess(1);
|
|
18789
|
+
}
|
|
18790
|
+
};
|
|
18791
|
+
const claimWith = async (context, supervised) => {
|
|
18780
18792
|
const sessionID = context.sessionManager.getSessionId();
|
|
18781
|
-
const capability = controllerCapability ?? requiredControllerCapability(process.env);
|
|
18793
|
+
const capability = controllerCapability ?? (supervised ? requiredSecret(process.env, "LEGION_BOOT_TOKEN") : requiredControllerCapability(process.env));
|
|
18782
18794
|
controllerCapability = capability;
|
|
18783
18795
|
const { sessionFile, agentId } = await persistedTranscript(context);
|
|
18784
18796
|
const launched = classifySession(process.env).kind === "controller";
|
|
@@ -18809,6 +18821,14 @@ function createControllerSession(deps) {
|
|
|
18809
18821
|
const envoyContext = "setInterval" in context ? context : undefined;
|
|
18810
18822
|
await claimEnvoyRole(sessionID, registration.claimToken, envoyContext);
|
|
18811
18823
|
await subscribeLegionNotice(sessionID, legionControllerNoticeSubject(project), envoyContext, registration.claimToken);
|
|
18824
|
+
if (supervised) {
|
|
18825
|
+
await client.ready({
|
|
18826
|
+
claimToken: registration.claimToken,
|
|
18827
|
+
sessionId: sessionID,
|
|
18828
|
+
secret: registration.secret,
|
|
18829
|
+
generation: registration.generation
|
|
18830
|
+
});
|
|
18831
|
+
}
|
|
18812
18832
|
mintControllerGrant = () => client.controllerGrant({ sessionId: sessionID, secret: registration.secret });
|
|
18813
18833
|
controllerSessionID = sessionID;
|
|
18814
18834
|
controllerTranscript = launched ? sessionFile : undefined;
|
|
@@ -19413,8 +19433,24 @@ function splitShellCommands(command) {
|
|
|
19413
19433
|
endCommand();
|
|
19414
19434
|
return commands;
|
|
19415
19435
|
}
|
|
19436
|
+
function commandMatch(words, name, sequence) {
|
|
19437
|
+
for (let index = 0;index < words.length; index += 1) {
|
|
19438
|
+
const word = words[index];
|
|
19439
|
+
if (word !== name && !word?.endsWith(`/${name}`))
|
|
19440
|
+
continue;
|
|
19441
|
+
if (sequence.every((expected, offset) => words[index + 1 + offset] === expected))
|
|
19442
|
+
return index;
|
|
19443
|
+
}
|
|
19444
|
+
return -1;
|
|
19445
|
+
}
|
|
19446
|
+
function isPushInvocation(words) {
|
|
19447
|
+
return commandMatch(words, "legion", ["push"]) !== -1;
|
|
19448
|
+
}
|
|
19449
|
+
function commandsRunPush(commands) {
|
|
19450
|
+
return commands?.some(isPushInvocation) ?? false;
|
|
19451
|
+
}
|
|
19416
19452
|
function jjLogRewriteInvocation(words) {
|
|
19417
|
-
const jj = words
|
|
19453
|
+
const jj = commandMatch(words, "jj", []);
|
|
19418
19454
|
if (jj === -1)
|
|
19419
19455
|
return;
|
|
19420
19456
|
const args = words.slice(jj + 1);
|
|
@@ -19430,7 +19466,7 @@ var LEGION_HANDOFF_COMPLETE_MENTION = /\blegion[\s"'`,[\]]+handoff[\s"'`,[\]]+co
|
|
|
19430
19466
|
var LEGION_HANDOFF_COMPLETE = {
|
|
19431
19467
|
mention: (text) => LEGION_HANDOFF_COMPLETE_MENTION.test(text) ? "legion handoff complete" : undefined,
|
|
19432
19468
|
invocation: (words) => {
|
|
19433
|
-
const legion = words
|
|
19469
|
+
const legion = commandMatch(words, "legion", ["handoff", "complete"]);
|
|
19434
19470
|
return legion === -1 ? undefined : words.slice(legion).join(" ");
|
|
19435
19471
|
},
|
|
19436
19472
|
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."
|
|
@@ -19477,14 +19513,13 @@ function nonFileWriteScheme(toolCall) {
|
|
|
19477
19513
|
return;
|
|
19478
19514
|
return NON_FILE_WRITE_URL.exec(writeTarget(toolCall.input.path))?.[1]?.toLowerCase();
|
|
19479
19515
|
}
|
|
19480
|
-
function paneRuleRefusal(toolCall, rules) {
|
|
19516
|
+
function paneRuleRefusal(toolCall, rules, commands) {
|
|
19481
19517
|
if (rules.length === 0)
|
|
19482
19518
|
return;
|
|
19483
19519
|
const { toolName, input } = toolCall;
|
|
19484
19520
|
if (toolName === "bash") {
|
|
19485
19521
|
if (typeof input.command !== "string")
|
|
19486
19522
|
return;
|
|
19487
|
-
const commands = splitShellCommands(input.command);
|
|
19488
19523
|
for (const rule of rules) {
|
|
19489
19524
|
const attempt = refusedCommand(input.command, commands, rule);
|
|
19490
19525
|
if (attempt !== undefined)
|
|
@@ -19544,7 +19579,8 @@ function legionExtension(pi) {
|
|
|
19544
19579
|
});
|
|
19545
19580
|
const controllerSession = createControllerSession({
|
|
19546
19581
|
daemon: roleDaemon,
|
|
19547
|
-
persistedTranscript
|
|
19582
|
+
persistedTranscript,
|
|
19583
|
+
exitProcess: (code) => exitProcess(code)
|
|
19548
19584
|
});
|
|
19549
19585
|
const titleSession = async (context) => {
|
|
19550
19586
|
const title = legionSessionTitle(classifySession(process.env), process.env.LEGION_PROJECT);
|
|
@@ -19584,7 +19620,8 @@ function legionExtension(pi) {
|
|
|
19584
19620
|
toolName: toolCall.toolName
|
|
19585
19621
|
});
|
|
19586
19622
|
paneRules ??= PANE_RULES[classifySession(process.env).kind] ?? [];
|
|
19587
|
-
const
|
|
19623
|
+
const commands = toolCall.toolName === "bash" && typeof toolCall.input.command === "string" ? splitShellCommands(toolCall.input.command) : undefined;
|
|
19624
|
+
const refusal = paneRuleRefusal(toolCall, paneRules, commands);
|
|
19588
19625
|
if (refusal !== undefined)
|
|
19589
19626
|
return { block: true, reason: refusal };
|
|
19590
19627
|
if (await checkSubagentSession(context))
|
|
@@ -19614,7 +19651,8 @@ function legionExtension(pi) {
|
|
|
19614
19651
|
tree: active.tree,
|
|
19615
19652
|
issue: active.issue,
|
|
19616
19653
|
sessionId: sessionID,
|
|
19617
|
-
secret: active.secret
|
|
19654
|
+
secret: active.secret,
|
|
19655
|
+
push: commandsRunPush(commands)
|
|
19618
19656
|
}));
|
|
19619
19657
|
});
|
|
19620
19658
|
pi.on("message_start", async (event, context) => {
|
|
@@ -26,8 +26,8 @@ For an interactive takeover from a hand-started OMP session, start OMP with
|
|
|
26
26
|
`LEGION_DAEMON_URL`, `LEGION_PROJECT` (the daemon's project), `LEGION_STATE_DIR`, and `LEGION_GRANT_FILE`
|
|
27
27
|
(an absolute path to a file only you can read, under a 0700 directory; the extension writes
|
|
28
28
|
each command's grant there and every `bash` call is blocked without it) in its environment. Do
|
|
29
|
-
not set `LEGION_CONTROLLER=1` — that marker is
|
|
30
|
-
carrying it claims at startup. Then run:
|
|
29
|
+
not set `LEGION_CONTROLLER=1` — that marker is the launched controller's own (`legion controller
|
|
30
|
+
start`'s session, or the daemon's pod), and a session carrying it claims at startup. Then run:
|
|
31
31
|
|
|
32
32
|
```text
|
|
33
33
|
/legion-claim-controller
|
|
@@ -50,12 +50,27 @@ retry. And the role does not follow `/new` or `/fork` in a takeover session —
|
|
|
50
50
|
`LEGION_CONTROLLER=1` the new session is not a Legion session to the extension — so after either
|
|
51
51
|
command run `/legion-claim-controller` again.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
Daemon state and the Dispatch project remain authoritative, and the start procedure below reads
|
|
54
|
+
what happened while no controller ran. Who launched you is the deployment's `controller` setting:
|
|
55
|
+
the operator, or the daemon itself. The `How this controller runs` part of your system prompt says
|
|
56
|
+
which.
|
|
57
|
+
|
|
58
|
+
### Started by the daemon
|
|
59
|
+
|
|
60
|
+
Under `controller: daemon` the daemon launched you as a pod in the cluster and supervises you like
|
|
61
|
+
a root architect: you run headless (`omp --mode rpc`), nobody types into your session, and nobody
|
|
62
|
+
reads your replies as they appear. The extension registered with your launch's boot token, claimed
|
|
63
|
+
the role, subscribed to the controller topic and reported ready; the daemon then sent your start
|
|
64
|
+
message. When your pod dies the daemon relaunches it and resumes this same session, with a new
|
|
65
|
+
start message, so run the start procedure each time one arrives. A human reaches you through
|
|
66
|
+
Dispatch (a message to your session on the Agents page, a reply to an ask you opened, a mention) or
|
|
67
|
+
Envoy: answer them where they will read it, a Dispatch message or reply, since text left only in
|
|
68
|
+
your session reaches no one. `legion state` and `legion status <KEY> <status>` work as below. There
|
|
69
|
+
is no `legion controller start` against this daemon, and nobody can replace you with one.
|
|
55
70
|
|
|
56
71
|
### Started by the operator
|
|
57
72
|
|
|
58
|
-
|
|
73
|
+
Under `controller: operator`, the default, the daemon launches no controller: the operator ran
|
|
59
74
|
`legion controller start --config controller.yaml [--daemon-url <url>]` on
|
|
60
75
|
their own machine, and you are that foreground OMP session. The command fetched a fresh controller
|
|
61
76
|
secret from the daemon with the operator's token, wrote it to a 0600 file under `LEGION_STATE_DIR`
|
|
@@ -78,8 +93,10 @@ mints a new secret, so your grants stop working and the role moves to the new se
|
|
|
78
93
|
The Go daemon's controller topic is a wake for a session that is running when it is published.
|
|
79
94
|
Envoy hands an Oh My Pi session no retained copy of a notice published before it subscribed, so a
|
|
80
95
|
hold, a tree architect's failed claim, a new triage root, or a freed slot from while no controller
|
|
81
|
-
ran never arrives as a wake.
|
|
82
|
-
(`Legion controller start: …`)
|
|
96
|
+
ran never arrives as a wake. Every launch opens your first turn with a start message
|
|
97
|
+
(`Legion controller start: …`) — `legion controller start` passes it, and the daemon sends it to a
|
|
98
|
+
controller it launched once that controller is ready — so every start and restart runs this
|
|
99
|
+
procedure with nothing typed.
|
|
83
100
|
At every start, after the claim recheck ([Turn discipline](#turn-discipline)) and before anything
|
|
84
101
|
else:
|
|
85
102
|
|
|
@@ -350,7 +367,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
|
|
|
350
367
|
`unassigned`, `Unassigned — nobody's Inbox shows this tree's questions or its design approval
|
|
351
368
|
until someone takes it from the issue header (Assignee, beside Priority)`. The design approval
|
|
352
369
|
is promised only when the `Design gate policy:` line of your system prompt, which
|
|
353
|
-
|
|
370
|
+
whoever launched you writes from the daemon's own configuration, says
|
|
354
371
|
`gates.design: root-issues`. Under `gates.design: off` nobody approves a design, so drop "and
|
|
355
372
|
its design approval" (and "or its design approval") from the sentence. An unassigned root
|
|
356
373
|
still runs; the architect's asks wait in every Inbox's Unassigned band.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion-envoy",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.12.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"omp": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
]
|
|
14
14
|
},
|
|
15
15
|
"legion": {
|
|
16
|
-
"daemonApiVersion":
|
|
16
|
+
"daemonApiVersion": 14
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|