@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 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`, currently 14) and loads `@sjawhar/pi-envoy` with it,
21
- so both go into the Oh My Pi profile the daemon's panes use:
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.3.0",
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: 15
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 LEGION_HANDOFF_COMPLETE_MENTION = /\blegion[\s"'`,[\]]+handoff[\s"'`,[\]]+complete\b/;
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
- return NON_FILE_WRITE_URL.exec(writeTarget(toolCall.input.path))?.[1]?.toLowerCase();
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. A worker never runs `legion handoff complete` from
428
- bash, where the extension refuses it: the tool call is what the extension records, and a turn that
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.0",
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
15
+ "daemonApiVersion": 16
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",