@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 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.0",
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`); " + "handoff_message leaves a message for another phase (`sender`, `recipient`, `body`); " + "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.";
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, "--data", JSON.stringify(data)];
31901
+ return { args: ["handoff", "write", "--phase", phase], stdin: JSON.stringify(data) };
31912
31902
  }
31913
31903
  case "handoff_read":
31914
- return parameters.phase === undefined ? ["handoff", "read"] : ["handoff", "read", "--phase", required2(parameters, operation, "phase")];
31915
- case "handoff_message":
31916
- return [
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(command) {
32578
- const commands = splitShellCommands(command);
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 jjLogRewriteMention(command) === undefined ? undefined : command.trim();
32591
+ return rule.mention(command) === undefined ? undefined : command.trim();
32581
32592
  }
32582
32593
  for (const words of commands) {
32583
- const mentioned = words.find((word) => jjLogRewriteMention(word) !== undefined);
32594
+ const mentioned = words.find((word) => rule.mention(word) !== undefined);
32584
32595
  if (mentioned !== undefined)
32585
32596
  return mentioned;
32586
- const jj = words.findIndex((word) => word === "jj" || word.endsWith("/jj"));
32587
- if (jj === -1)
32588
- continue;
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 jjLogRewriteAttempt(toolCall) {
32603
+ function paneRuleRefusal(toolCall, rules) {
32604
+ if (rules.length === 0)
32605
+ return;
32597
32606
  const { toolName, input } = toolCall;
32598
32607
  if (toolName === "bash") {
32599
- return typeof input.command === "string" ? jjLogRewriteInvocation(input.command) : undefined;
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 mention = jjLogRewriteMention(text);
32609
- return mention === undefined ? undefined : `${toolName}: jj ${mention}`;
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 phaseWorkerPane;
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
- phaseWorkerPane ??= classifySession(process.env).kind === "phase-worker";
32924
- if (phaseWorkerPane) {
32925
- const jjAttempt = jjLogRewriteAttempt(toolCall);
32926
- if (jjAttempt !== undefined) {
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 bash: the
562
- tool call is what the extension records, and a turn that ends with the phase still open gets one
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.68.0",
3
+ "version": "1.68.1",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [