@sjawhar/pi-legion 8.3.0 → 8.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/legion.js +16 -56
- package/dist/skills/legion-controller/SKILL.md +12 -2
- package/dist/skills/legion-worker/SKILL.md +6 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,8 +17,9 @@ pi-envoy").
|
|
|
17
17
|
|
|
18
18
|
The Legion daemon's boot gate (`packages/daemon/internal/daemon/bootgate.go`) refuses to start
|
|
19
19
|
unless the Oh My Pi a pane will run loads this plugin at the daemon's contract
|
|
20
|
-
(`legion.daemonApiVersion` in `package.json`,
|
|
21
|
-
|
|
20
|
+
(`legion.daemonApiVersion` in this package's `package.json`, equal to `DaemonAPIVersion` in
|
|
21
|
+
`packages/daemon/internal/api/version.go`) and loads `@sjawhar/pi-envoy` with it, so both go into
|
|
22
|
+
the Oh My Pi profile the daemon's panes use:
|
|
22
23
|
|
|
23
24
|
```sh
|
|
24
25
|
omp plugin install @sjawhar/pi-envoy && omp plugin install @sjawhar/pi-legion
|
package/dist/legion.js
CHANGED
|
@@ -15193,7 +15193,7 @@ function onEnvoyRoleRegained(listener) {
|
|
|
15193
15193
|
// package.json
|
|
15194
15194
|
var package_default = {
|
|
15195
15195
|
name: "@sjawhar/pi-legion",
|
|
15196
|
-
version: "8.
|
|
15196
|
+
version: "8.4.0",
|
|
15197
15197
|
license: "Apache-2.0",
|
|
15198
15198
|
type: "module",
|
|
15199
15199
|
omp: {
|
|
@@ -15205,7 +15205,7 @@ var package_default = {
|
|
|
15205
15205
|
]
|
|
15206
15206
|
},
|
|
15207
15207
|
legion: {
|
|
15208
|
-
daemonApiVersion:
|
|
15208
|
+
daemonApiVersion: 16
|
|
15209
15209
|
},
|
|
15210
15210
|
repository: {
|
|
15211
15211
|
type: "git",
|
|
@@ -15412,13 +15412,21 @@ var legionAgentSecretsLoginView = exports_external.strictObject({
|
|
|
15412
15412
|
state: exports_external.string(),
|
|
15413
15413
|
code: exports_external.string()
|
|
15414
15414
|
});
|
|
15415
|
+
var legionCapabilityState = exports_external.strictObject({
|
|
15416
|
+
name: nonEmptyString,
|
|
15417
|
+
status: exports_external.enum(["present", "installed", "unchecked", "live", "withheld", "decided", "open"]),
|
|
15418
|
+
detail: nonEmptyString,
|
|
15419
|
+
decision: nonEmptyString.optional(),
|
|
15420
|
+
configLine: nonEmptyString.optional()
|
|
15421
|
+
});
|
|
15415
15422
|
var LegionStateResponse = exports_external.strictObject({
|
|
15416
15423
|
daemon: daemonInfo,
|
|
15417
15424
|
admission: legionAdmission,
|
|
15418
15425
|
issues: exports_external.record(exports_external.string(), legionIssue),
|
|
15419
15426
|
pendingStatusWrites: exports_external.array(legionPendingStatusWrite),
|
|
15420
15427
|
controllerLocator: legionControllerLocator.optional(),
|
|
15421
|
-
agentSecretsLogin: legionAgentSecretsLoginView.optional()
|
|
15428
|
+
agentSecretsLogin: legionAgentSecretsLoginView.optional(),
|
|
15429
|
+
capabilities: exports_external.array(legionCapabilityState)
|
|
15422
15430
|
});
|
|
15423
15431
|
var LegionRegisterResponse = exports_external.strictObject({
|
|
15424
15432
|
claimToken: nonEmptyString,
|
|
@@ -16404,31 +16412,6 @@ async function persistedTranscript(context) {
|
|
|
16404
16412
|
throw new Error("Legion session transcript has no agent id");
|
|
16405
16413
|
return { sessionFile, agentId };
|
|
16406
16414
|
}
|
|
16407
|
-
function isSingleLegionCommand(command) {
|
|
16408
|
-
if (typeof command !== "string")
|
|
16409
|
-
return false;
|
|
16410
|
-
const trimmed = command.trim();
|
|
16411
|
-
if (trimmed.length === 0)
|
|
16412
|
-
return false;
|
|
16413
|
-
let quote;
|
|
16414
|
-
for (const char of trimmed) {
|
|
16415
|
-
if (quote !== undefined) {
|
|
16416
|
-
if (char === quote)
|
|
16417
|
-
quote = undefined;
|
|
16418
|
-
continue;
|
|
16419
|
-
}
|
|
16420
|
-
if (char === '"' || char === "'") {
|
|
16421
|
-
quote = char;
|
|
16422
|
-
continue;
|
|
16423
|
-
}
|
|
16424
|
-
if (char === `
|
|
16425
|
-
` || char === ";" || char === "&" || char === "|")
|
|
16426
|
-
return false;
|
|
16427
|
-
}
|
|
16428
|
-
if (quote !== undefined)
|
|
16429
|
-
return false;
|
|
16430
|
-
return trimmed.split(/\s+/, 1)[0] === "legion";
|
|
16431
|
-
}
|
|
16432
16415
|
var JJ_LOG_REWRITE_WORDS = ["undo", "abandon"];
|
|
16433
16416
|
var JJ_OP_WORDS = ["op", "operation"];
|
|
16434
16417
|
var JJ_OP_LOG_REWRITE_WORDS = ["restore", "revert"];
|
|
@@ -16530,16 +16513,7 @@ var JJ_LOG_REWRITE = {
|
|
|
16530
16513
|
invocation: jjLogRewriteInvocation,
|
|
16531
16514
|
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.'
|
|
16532
16515
|
};
|
|
16533
|
-
var
|
|
16534
|
-
var LEGION_HANDOFF_COMPLETE = {
|
|
16535
|
-
mention: (text) => LEGION_HANDOFF_COMPLETE_MENTION.test(text) ? "legion handoff complete" : undefined,
|
|
16536
|
-
invocation: (words) => {
|
|
16537
|
-
const legion = commandMatch(words, "legion", ["handoff", "complete"]);
|
|
16538
|
-
return legion === -1 ? undefined : words.slice(legion).join(" ");
|
|
16539
|
-
},
|
|
16540
|
-
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."
|
|
16541
|
-
};
|
|
16542
|
-
var TREE_PANE_RULES = [JJ_LOG_REWRITE, LEGION_HANDOFF_COMPLETE];
|
|
16516
|
+
var TREE_PANE_RULES = [JJ_LOG_REWRITE];
|
|
16543
16517
|
var PANE_RULES = {
|
|
16544
16518
|
"phase-worker": TREE_PANE_RULES,
|
|
16545
16519
|
"root-architect": TREE_PANE_RULES
|
|
@@ -16572,14 +16546,12 @@ function unwrapReadHeader(path6) {
|
|
|
16572
16546
|
return path6;
|
|
16573
16547
|
return target;
|
|
16574
16548
|
}
|
|
16575
|
-
function writeTarget(path6) {
|
|
16576
|
-
const unwrapped = unwrapReadHeader(path6);
|
|
16577
|
-
return PREFIXED_CONFLICT_URL.exec(unwrapped)?.[2] ?? unwrapped;
|
|
16578
|
-
}
|
|
16579
16549
|
function nonFileWriteScheme(toolCall) {
|
|
16580
16550
|
if (toolCall.toolName !== "write" || typeof toolCall.input.path !== "string")
|
|
16581
16551
|
return;
|
|
16582
|
-
|
|
16552
|
+
const unwrapped = unwrapReadHeader(toolCall.input.path);
|
|
16553
|
+
const target = PREFIXED_CONFLICT_URL.exec(unwrapped)?.[2] ?? unwrapped;
|
|
16554
|
+
return NON_FILE_WRITE_URL.exec(target)?.[1]?.toLowerCase();
|
|
16583
16555
|
}
|
|
16584
16556
|
function paneRuleRefusal(toolCall, rules, commands) {
|
|
16585
16557
|
if (rules.length === 0)
|
|
@@ -16625,12 +16597,6 @@ function envoyPluginRefusal() {
|
|
|
16625
16597
|
return `Legion plugin at ${import.meta.url} needs @sjawhar/pi-envoy at interface version ${reading.expected}, found version ${reading.found} from ${reading.from}; install the @sjawhar/pi-envoy released with this @sjawhar/pi-legion`;
|
|
16626
16598
|
}
|
|
16627
16599
|
}
|
|
16628
|
-
var CODE_MUTATION_TOOLS = ["edit", "write", "apply_patch"];
|
|
16629
|
-
var CODE_TOOL_REFUSAL = {
|
|
16630
|
-
architect: "the architect delegates all code work to phase workers",
|
|
16631
|
-
reviewer: "the reviewer edits no code; its only commits are its review handoffs, made via bash",
|
|
16632
|
-
merger: "the merger only verifies and reports"
|
|
16633
|
-
};
|
|
16634
16600
|
function legionExtension(pi) {
|
|
16635
16601
|
const instance = randomUUID2().slice(0, 8);
|
|
16636
16602
|
logger3.debug("extension instance loaded", { extension: import.meta.url, instance });
|
|
@@ -16720,11 +16686,6 @@ function legionExtension(pi) {
|
|
|
16720
16686
|
return;
|
|
16721
16687
|
const sessionID = context.sessionManager.getSessionId();
|
|
16722
16688
|
const active = claimSession.capability(sessionID);
|
|
16723
|
-
const codeToolRefusal = active === undefined ? undefined : CODE_TOOL_REFUSAL[active.role];
|
|
16724
|
-
const mutatesCode = CODE_MUTATION_TOOLS.includes(toolCall.toolName) || active?.role === "architect" && toolCall.toolName === "bash" && !isSingleLegionCommand(toolCall.input.command);
|
|
16725
|
-
if (codeToolRefusal !== undefined && mutatesCode && nonFileWriteScheme(toolCall) === undefined) {
|
|
16726
|
-
return { block: true, reason: codeToolRefusal };
|
|
16727
|
-
}
|
|
16728
16689
|
if (!needsGrant(toolCall))
|
|
16729
16690
|
return;
|
|
16730
16691
|
if (active === undefined) {
|
|
@@ -16821,6 +16782,5 @@ function legionExtension(pi) {
|
|
|
16821
16782
|
}
|
|
16822
16783
|
export {
|
|
16823
16784
|
setLegionBootstrapExitForTests,
|
|
16824
|
-
legionExtension as default
|
|
16825
|
-
CODE_TOOL_REFUSAL
|
|
16785
|
+
legionExtension as default
|
|
16826
16786
|
};
|
|
@@ -171,7 +171,13 @@ unsent:
|
|
|
171
171
|
- `tick on <PROJECT>`: the daemon's periodic wake, a minute after it starts and then every
|
|
172
172
|
`controller_wake_interval_seconds` (an hour by default), whatever the slots. An earlier walk
|
|
173
173
|
that found nothing, a day with no event, and [a tree waiting on a root
|
|
174
|
-
claim](#trees-waiting-on-a-root-claim-go-daemon) all get a turn from it.
|
|
174
|
+
claim](#trees-waiting-on-a-root-claim-go-daemon) all get a turn from it. Its payload's
|
|
175
|
+
`openCapabilities`, when present, names the deployment capabilities (the secrets broker, model
|
|
176
|
+
fallback, every role's CPU and memory limits) the deployment's own configuration leaves open with
|
|
177
|
+
no decision recorded. `legion state --json` lists every row under `capabilities`, each open one
|
|
178
|
+
with its `detail` and the `configLine` to write into `legion.yaml`
|
|
179
|
+
(`capabilities.decided.<name>: "<reason>"`); name them in the day's report. A gap is the
|
|
180
|
+
operator's to close or to decide, never yours, and it never stops the walk.
|
|
175
181
|
|
|
176
182
|
**Scope first.** The scope the deployment instructions state decides which issues are candidates
|
|
177
183
|
at all, before anything below. When they say you hand Legion no issue yourself, or that Legion
|
|
@@ -291,6 +297,10 @@ every pull request by its URL.
|
|
|
291
297
|
- **The slots and the walk.** The free slots, as **How many** counts them, then this turn's walk
|
|
292
298
|
when it made one: what it took, and how many candidates each row of the table skipped, so a
|
|
293
299
|
reader can see why a free slot stays empty.
|
|
300
|
+
- **Open capabilities.** Each row of `capabilities` in `legion state --json` whose `status` is
|
|
301
|
+
`open` (the `tick` payload's `openCapabilities`): its name, its `detail`, and its `configLine`,
|
|
302
|
+
the `legion.yaml` line that records a decision. The operator closes or decides it; you only
|
|
303
|
+
report it, and a report with none says nothing of them.
|
|
294
304
|
|
|
295
305
|
A day with nothing finished says so in one sentence. When the lists do not fit, keep the counts
|
|
296
306
|
and the highest-priority issues.
|
|
@@ -328,7 +338,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
|
|
|
328
338
|
| New issue created in the Dispatch project, status `triage`: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above); the start procedure above catches one sent while no controller ran | issue key + triage context (incl. pre-existing children) | Triage: `legion status <KEY> todo` to admit, or set `backlog`/`icebox` to park |
|
|
329
339
|
| `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `legion state --json`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
330
340
|
| `todo on <KEY>` from the Go daemon (payload `{kind: "todo"}`) | an issue not handed to Legion that changed while in `todo` and a slot stood free, sent half a minute later | Verify a free slot, then walk the whole `todo` list ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
331
|
-
| `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick"}`) | the project key; the daemon's periodic wake, whatever the slots | Recheck the trees waiting on a claim, then walk if a slot is free; post the day's report if this is the day's first turn |
|
|
341
|
+
| `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `legion state --json` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides it) |
|
|
332
342
|
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `dispatch_ask` only for a human to-do |
|
|
333
343
|
| Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
|
|
334
344
|
| `held on <KEY>` from the Go daemon (payload `{kind: "held", phase, role?, reason?}`) | the held issue, the phase it left, and the role whose claim failed, or `reason: "escalated"` | Verify the hold in `legion state` (the issue's phase is `held`). Without `reason`, a phase worker's launches or prompts ran out and the tree's architect decides retry or escalate: no action. With `reason: "escalated"` (on the record, `issues.<KEY>.holdReason` is `escalated`), the architect sent it to you: handle it as an architect escalation below. Parking the tree is `legion status <root> backlog`; setting the root back to `todo` later re-admits it as a new generation, which starts again from its architect |
|
|
@@ -424,9 +424,12 @@ report with `handoff_complete` alone (below).
|
|
|
424
424
|
## Completion: report to the architect, then stay
|
|
425
425
|
|
|
426
426
|
Report completion to the architect: call the `legion` tool with `op: "handoff_complete"` and
|
|
427
|
-
`summary`: two sentences for the architect.
|
|
428
|
-
|
|
429
|
-
ends with the phase still open gets one reminder.
|
|
427
|
+
`summary`: two sentences for the architect. Report it through that tool call, never the shell
|
|
428
|
+
command `legion handoff complete`: the tool call is what the extension's phase stall records, and
|
|
429
|
+
a turn that ends with the phase still open gets one reminder. A completion run from the shell is
|
|
430
|
+
recorded by the daemon all the same, but the stall does not see it and sends its one reminder; do
|
|
431
|
+
not complete again on that reminder — the first completion stands, and a second is refused
|
|
432
|
+
because the issue has already left your phase.
|
|
430
433
|
|
|
431
434
|
This publishes your phase's completion to the architect's role and clears the daemon's
|
|
432
435
|
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",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"omp": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
|
-
"daemonApiVersion":
|
|
15
|
+
"daemonApiVersion": 16
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|