@sjawhar/pi-legion 10.11.1 → 10.13.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.
Files changed (2) hide show
  1. package/dist/legion.js +1278 -1151
  2. package/package.json +1 -1
package/dist/legion.js CHANGED
@@ -14919,6 +14919,61 @@ var SLACK_CHANNEL_ID_PATTERN = /^[CG][A-Z0-9]+$/;
14919
14919
  function slackMentionTopic(channel) {
14920
14920
  return `notifications.slack.*.${channel}.mention`;
14921
14921
  }
14922
+ // ../contracts/src/tool-schema.ts
14923
+ function overCapMessage(length, max) {
14924
+ return `is ${length - max} characters over the ${max}-character limit (${length}/${max})`;
14925
+ }
14926
+ function zodSchemaApi(zod) {
14927
+ const api2 = zod;
14928
+ return {
14929
+ string: (opts = {}) => {
14930
+ let schema = api2.string();
14931
+ if (opts.min !== undefined)
14932
+ schema = schema.min(opts.min);
14933
+ if (opts.max !== undefined) {
14934
+ const max = opts.max;
14935
+ const hint = opts.maxHint === undefined ? "" : `; ${opts.maxHint}`;
14936
+ schema = schema.max(max, {
14937
+ error: (issue2) => overCapMessage(typeof issue2.input === "string" ? issue2.input.length : max + 1, max) + hint
14938
+ });
14939
+ }
14940
+ return schema;
14941
+ },
14942
+ number: (opts = {}) => {
14943
+ let schema = api2.number();
14944
+ if (opts.int)
14945
+ schema = schema.int();
14946
+ if (opts.min !== undefined)
14947
+ schema = schema.min(opts.min);
14948
+ if (opts.max !== undefined)
14949
+ schema = schema.max(opts.max);
14950
+ return schema;
14951
+ },
14952
+ boolean: () => api2.boolean(),
14953
+ enum: (values) => api2.enum(values),
14954
+ unknown: () => api2.unknown(),
14955
+ array: (item, opts = {}) => {
14956
+ let schema = api2.array(item);
14957
+ if (opts.min !== undefined)
14958
+ schema = schema.min(opts.min);
14959
+ if (opts.max !== undefined)
14960
+ schema = schema.max(opts.max);
14961
+ return schema;
14962
+ },
14963
+ object: (shape, opts = {}) => {
14964
+ let schema = api2.object(shape);
14965
+ if (opts.strict)
14966
+ schema = schema.strict();
14967
+ return schema;
14968
+ },
14969
+ refineObject: (shape, check2, message, opts = {}) => {
14970
+ let schema = api2.object(shape);
14971
+ if (opts.strict)
14972
+ schema = schema.strict();
14973
+ return schema.refine(check2, message);
14974
+ }
14975
+ };
14976
+ }
14922
14977
  // ../envoy-client/src/ask-answer.ts
14923
14978
  var HEAD_LENGTH = 120;
14924
14979
  function textHead(text) {
@@ -15513,9 +15568,10 @@ var PRIVATE_DIR = 448;
15513
15568
  var PRIVATE_FILE = 384;
15514
15569
  var IDLE_SESSION_MS = 14 * 86400000;
15515
15570
  function sessionStateRoot(env) {
15516
- if (env.DISPATCH_STATE_DIR)
15517
- return env.DISPATCH_STATE_DIR;
15518
- return join2(env.XDG_STATE_HOME || join2(homedir2(), ".local", "state"), "dispatch");
15571
+ const stateDir = env["DISPATCH_STATE_DIR"];
15572
+ if (stateDir)
15573
+ return stateDir;
15574
+ return join2(env["XDG_STATE_HOME"] || join2(homedir2(), ".local", "state"), "dispatch");
15519
15575
  }
15520
15576
  function sessionDirectory(env, sessionId) {
15521
15577
  if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
@@ -15555,751 +15611,50 @@ function sessionDispatchSettings(file2, env, options) {
15555
15611
  };
15556
15612
  }
15557
15613
 
15558
- // ../pi-shared/src/interface.ts
15559
- import { logger } from "@oh-my-pi/pi-utils";
15560
- var ENVOY_PLUGIN_INTERFACE_VERSION = 3;
15561
- var ENVOY_PLUGIN_INTERFACE_KEY = Symbol.for("legion.pi-shared.envoy-plugin-interface");
15562
- var LEGION_PLUGIN_LOADED_KEY = Symbol.for("legion.pi-legion.loaded");
15563
- var LEGACY_LEGION_LOADED_KEY = Symbol.for("legion.pi-envoy.legion-loaded");
15564
- var LOCAL_ENVOY_NOTICE = { localNotice: true };
15565
- var store = globalThis;
15566
- function envoyPluginInterface() {
15567
- const existing = store[ENVOY_PLUGIN_INTERFACE_KEY];
15568
- if (existing !== undefined)
15569
- return existing;
15570
- const created = {
15571
- version: ENVOY_PLUGIN_INTERFACE_VERSION,
15572
- publishers: [],
15573
- roleClaim: {
15574
- instances: [],
15575
- managedSessions: new Set,
15576
- regained: undefined,
15577
- slackRoleOwned: false
15578
- },
15579
- injectedUserTurns: new Map,
15580
- bootstrappedSession: { file: undefined }
15581
- };
15582
- store[ENVOY_PLUGIN_INTERFACE_KEY] = created;
15583
- return created;
15584
- }
15585
- function readEnvoyPluginInterface() {
15586
- const iface = store[ENVOY_PLUGIN_INTERFACE_KEY];
15587
- const from = iface?.publishers[0];
15588
- if (iface === undefined || from === undefined)
15589
- return { kind: "absent" };
15590
- if (iface.version !== ENVOY_PLUGIN_INTERFACE_VERSION) {
15591
- return {
15592
- kind: "mismatch",
15593
- found: iface.version,
15594
- expected: ENVOY_PLUGIN_INTERFACE_VERSION,
15595
- from
15596
- };
15597
- }
15598
- return { kind: "present", iface };
15599
- }
15600
-
15601
- // ../pi-shared/src/injected-user-turns.ts
15602
- function userMessageText(content) {
15603
- if (typeof content === "string")
15604
- return content;
15605
- if (!Array.isArray(content))
15606
- return "";
15607
- const pieces = [];
15608
- for (const part of content) {
15609
- if (typeof part !== "object" || part === null)
15610
- continue;
15611
- if ("type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
15612
- pieces.push(part.text);
15613
- }
15614
- }
15615
- return pieces.join(`
15616
- `);
15617
- }
15618
- function matchInjectedUserTurn(sessionID, message) {
15619
- if (typeof message !== "object" || message === null)
15620
- return;
15621
- if (!("role" in message) || message.role !== "user" && message.role !== "custom") {
15622
- return;
15623
- }
15624
- if (!("timestamp" in message) || typeof message.timestamp !== "number")
15625
- return;
15626
- const session = envoyPluginInterface().injectedUserTurns.get(sessionID);
15627
- if (session === undefined)
15628
- return;
15629
- const tag = messageTag(message);
15630
- const memo = tag === undefined ? `${message.timestamp}` : `${message.timestamp}:${tag}`;
15631
- const known = session.found.get(memo);
15632
- if (known !== undefined)
15633
- return known;
15634
- let index = tag === undefined ? -1 : session.sent.findIndex((sent2) => sent2.messageId === tag);
15635
- if (index === -1 && message.role === "user") {
15636
- const text = userMessageText("content" in message ? message.content : undefined);
15637
- index = session.sent.findIndex((sent2) => sent2.via === "text" && sent2.body === text);
15638
- }
15639
- const sent = session.sent[index];
15640
- if (sent === undefined)
15641
- return;
15642
- session.sent.splice(index, 1);
15643
- session.found.set(memo, sent.messageId);
15644
- return sent.messageId;
15645
- }
15646
- function messageTag(message) {
15647
- if (typeof message !== "object" || message === null || !("tag" in message))
15648
- return;
15649
- return typeof message.tag === "string" ? message.tag : undefined;
15650
- }
15651
-
15652
- // ../pi-shared/src/role-claim-bridge.ts
15653
- function legionRoleClaimBridge() {
15654
- return envoyPluginInterface().roleClaim;
15655
- }
15656
- async function claimEnvoyRole(sessionID, role, context) {
15657
- const bridge = legionRoleClaimBridge();
15658
- bridge.managedSessions.add(sessionID);
15659
- const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
15660
- if (instance === undefined)
15661
- throw new Error("Envoy has no bound instance for a role claim");
15662
- await instance.claim(sessionID, role, context);
15663
- }
15664
- async function subscribeLegionNotice(sessionID, topic, context, whileHolding) {
15665
- const bridge = legionRoleClaimBridge();
15666
- const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
15667
- if (instance === undefined)
15668
- throw new Error("Envoy has no bound instance for a notice subscription");
15669
- await instance.subscribe(sessionID, topic, context, whileHolding);
15670
- }
15671
- function onEnvoyRoleRegained(listener) {
15672
- legionRoleClaimBridge().regained = listener;
15673
- }
15674
-
15675
- // ../pi-shared/src/subagent-session.ts
15676
- import fs from "fs";
15677
- import path2 from "path";
15678
- import * as host from "@oh-my-pi/pi-coding-agent";
15679
- import { logger as logger2 } from "@oh-my-pi/pi-utils";
15680
- function recordBootstrappedSession(sessionFile) {
15681
- envoyPluginInterface().bootstrappedSession.file = sessionFile;
15682
- }
15683
- async function isSubagentSession(context) {
15684
- const registered = registeredSubagent(context);
15685
- if (registered !== undefined)
15686
- return { subagent: registered, settled: true };
15687
- try {
15688
- await context.sessionManager.ensureOnDisk();
15689
- } catch (error48) {
15690
- const subagent = transcriptSaysSubagent(context);
15691
- logger2.warn("subagent check: publishing the transcript failed; answered from the transcript on disk, and the next call asks again", { sessionFile: context.sessionManager.getSessionFile(), subagent, error: messageFor(error48) });
15692
- return { subagent, settled: false };
15693
- }
15694
- return { subagent: transcriptSaysSubagent(context), settled: true };
15695
- }
15696
- function transcriptSaysSubagent(context) {
15697
- const sessionFile = context.sessionManager.getSessionFile();
15698
- if (sessionFile !== undefined) {
15699
- if (fs.existsSync(`${path2.dirname(sessionFile)}.jsonl`))
15700
- return true;
15701
- if (fs.existsSync(sessionFile))
15702
- return false;
15703
- }
15704
- const bootstrapped = envoyPluginInterface().bootstrappedSession.file;
15705
- return bootstrapped !== undefined && bootstrapped !== sessionFile;
15706
- }
15707
- function registeredSubagent(context) {
15708
- const registry2 = host.AgentRegistry?.global();
15709
- if (registry2 === undefined)
15710
- return;
15711
- const sessionID = context.sessionManager.getSessionId();
15712
- const sessionFile = context.sessionManager.getSessionFile();
15713
- const own = registry2.list().filter((ref) => ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID);
15714
- if (own.length === 0)
15715
- return;
15716
- return own.some((ref) => ref.kind !== "main");
15717
- }
15718
- function subagentSessionCheck() {
15719
- let settled;
15720
- return async (context) => {
15721
- if (settled !== undefined)
15722
- return settled;
15723
- const answer = await isSubagentSession(context);
15724
- if (answer.settled)
15725
- settled = answer.subagent;
15726
- return answer.subagent;
15727
- };
15728
- }
15729
-
15730
- // extensions/legion.ts
15731
- import { logger as logger3 } from "@oh-my-pi/pi-utils";
15732
-
15733
- // src/capability-report.ts
15734
- import { readdir, stat } from "fs/promises";
15735
- import path3 from "path";
15736
- var CAPABILITY_CHECK_TIMEOUT_MS = 8000;
15737
- var CAPABILITY_REPORT_TIMEOUT_MS = 1e4;
15738
- var WEB_SEARCH_PROBE_QUERY = "jujutsu version control";
15739
- var ENVOY_TOOL_NAMES = [
15740
- "envoy_subscribe",
15741
- "envoy_unsubscribe",
15742
- "envoy_list",
15743
- "envoy_inbox",
15744
- "envoy_send",
15745
- "envoy_publish",
15746
- "envoy_role_set",
15747
- "envoy_role_get",
15748
- "envoy_whoami",
15749
- "envoy_sessions",
15750
- "slack_post"
15614
+ // ../contracts/src/legion-api.ts
15615
+ var nonEmptyString = exports_external.string().min(1);
15616
+ var issueKey = exports_external.string().regex(DISPATCH_KEY_PATTERN);
15617
+ var timestamp = exports_external.iso.datetime({ offset: true });
15618
+ var LEGION_WORKFLOW_PHASES = [
15619
+ "admitted",
15620
+ "planning",
15621
+ "implementing",
15622
+ "testing",
15623
+ "reviewing",
15624
+ "retro",
15625
+ "merging",
15626
+ "awaiting_merge",
15627
+ "integrating",
15628
+ "production_check",
15629
+ "done",
15630
+ "held"
15751
15631
  ];
15752
- var LIVE_CAPABILITIES = [
15753
- "subagents",
15754
- "web-search",
15755
- "mcp",
15756
- "repository-extensions",
15757
- "dispatch-envoy-tools",
15758
- "github"
15632
+ var LEGION_PHASE_BACKWARD_TARGETS = [
15633
+ "admitted",
15634
+ "planning",
15635
+ "implementing",
15636
+ "testing",
15637
+ "reviewing",
15638
+ "retro",
15639
+ "merging",
15640
+ "awaiting_merge",
15641
+ "production_check",
15642
+ "done",
15643
+ "held"
15759
15644
  ];
15760
- var DEFAULT_BUDGETS = {
15761
- checkMs: CAPABILITY_CHECK_TIMEOUT_MS,
15762
- reportMs: CAPABILITY_REPORT_TIMEOUT_MS
15763
- };
15764
- var ok = (detail) => ({ ok: true, detail });
15765
- var failed = (detail) => ({ ok: false, detail });
15766
- function firstLine(text) {
15767
- return text.trim().split(`
15768
- `, 1)[0] ?? "";
15769
- }
15770
- function commandFailure(result) {
15771
- return firstLine(result.stderr) || `exit code ${result.code ?? "none"}`;
15772
- }
15773
- function whichOnPath(name) {
15774
- return Bun.which(name, { PATH: process.env.PATH ?? "" }) ?? name;
15775
- }
15776
- var checkSubagents = async (host2, input) => {
15777
- if (!input.agentsExposed)
15778
- return failed("pi.agents is not exposed");
15779
- if (!input.activeTools.includes("task"))
15780
- return failed("task is not a registered tool");
15781
- const [discovered, disabled] = await Promise.all([
15782
- host2.discoverAgents(input.cwd),
15783
- host2.disabledAgents()
15784
- ]);
15785
- const names = new Set(discovered.agents.map((agent) => agent.name));
15786
- const missing = input.promptAgents.filter((agent) => !names.has(agent));
15787
- const off = input.promptAgents.filter((agent) => disabled.includes(agent));
15788
- const problems = [];
15789
- if (missing.length > 0) {
15790
- problems.push(`agents the prompts dispatch that the task tool does not resolve: ${missing.join(", ")}`);
15791
- }
15792
- if (off.length > 0)
15793
- problems.push(`disabled by task.disabledAgents: ${off.join(", ")}`);
15794
- if (problems.length > 0)
15795
- return failed(problems.join("; "));
15796
- return ok(`task resolves ${names.size} agents, every one the prompts dispatch`);
15797
- };
15798
- var checkWebSearch = async (host2, input, signal) => {
15799
- if (!input.activeTools.includes("web_search")) {
15800
- return failed("web_search is not a registered tool");
15801
- }
15802
- const started = Date.now();
15803
- const result = await host2.runSearchQuery({ query: WEB_SEARCH_PROBE_QUERY, limit: 1 }, { sessionId: input.sessionId, signal });
15804
- const elapsed = Date.now() - started;
15805
- const details = result.details ?? {};
15806
- if (details.error !== undefined && details.error !== null) {
15807
- return failed(`web_search failed: ${String(details.error)}`);
15808
- }
15809
- const provider = details.response?.provider ?? details.provider;
15810
- if (provider === undefined || provider === null || provider === "none") {
15811
- return failed("web_search failed: no provider answered");
15812
- }
15813
- const text = result.content?.[0]?.text ?? "";
15814
- if (text.startsWith("Error"))
15815
- return failed(`web_search failed: ${firstLine(text)}`);
15816
- return ok(`web_search registered; provider ${String(provider)} answered in ${elapsed} ms`);
15817
- };
15818
- function mcpErrorFor(errors3, name) {
15819
- for (const entry of errors3) {
15820
- if (typeof entry !== "object" || entry === null) {
15821
- if (!String(entry).includes(name))
15822
- continue;
15823
- return String(entry);
15824
- }
15825
- const record2 = entry;
15826
- const names = [record2.server, record2.name, record2.path];
15827
- if (!names.some((value) => value === name || value === `mcp:${name}`))
15828
- continue;
15829
- const message = record2.error ?? record2.message;
15830
- return message === undefined ? String(entry) : String(message);
15831
- }
15832
- return;
15833
- }
15834
- var checkMCP = async (host2, input, signal) => {
15835
- const { configs, sources } = await host2.loadMCPConfigs(input.cwd);
15836
- const configured = Object.keys(configs);
15837
- if (configured.length === 0)
15838
- return ok("no MCP server configured");
15839
- const { manager, errors: errors3 } = await host2.discoverMCPServers(input.cwd);
15840
- let torn;
15841
- const tearDown = () => {
15842
- torn ??= manager.disconnectAll().catch(() => {
15843
- return;
15844
- });
15845
- return torn;
15846
- };
15847
- signal.addEventListener("abort", tearDown, { once: true });
15848
- if (signal.aborted)
15849
- tearDown();
15850
- try {
15851
- await manager.waitForPendingConnections();
15852
- const connected = [];
15853
- const unconnected = [];
15854
- for (const name of configured) {
15855
- const status = manager.getConnectionStatus(name);
15856
- if (status === "connected") {
15857
- const source = sources[name]?.path;
15858
- connected.push(source === undefined ? name : `${name} (${path3.relative(input.cwd, source)})`);
15859
- } else {
15860
- unconnected.push(`${name} (${mcpErrorFor(errors3, name) ?? `status ${status}`})`);
15861
- }
15862
- }
15863
- if (unconnected.length === 0)
15864
- return ok(`connected: ${connected.join(", ")}`);
15865
- const suffix = connected.length === 0 ? "" : `; connected: ${connected.join(", ")}`;
15866
- return failed(`not connected: ${unconnected.join(", ")}${suffix}`);
15867
- } finally {
15868
- signal.removeEventListener("abort", tearDown);
15869
- await tearDown();
15870
- }
15871
- };
15872
- var EXTENSION_MODULE = /\.(?:ts|js|mjs|cjs)$/;
15873
- var EXTENSION_INDEXES = ["index.ts", "index.js", "index.mjs", "index.cjs"];
15874
- var SKILL_DIRECTORIES = [path3.join(".omp", "skills"), path3.join(".claude", "skills")];
15875
- async function namesIn(directory) {
15876
- try {
15877
- return await readdir(directory);
15878
- } catch (error48) {
15879
- if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
15880
- return [];
15881
- throw error48;
15882
- }
15883
- }
15884
- async function kindOf(file2) {
15885
- try {
15886
- const stats = await stat(file2);
15887
- return stats.isFile() ? "file" : stats.isDirectory() ? "directory" : undefined;
15888
- } catch (error48) {
15889
- if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
15890
- return;
15891
- throw error48;
15892
- }
15893
- }
15894
- async function repositoryEntries(cwd) {
15895
- const entries = [];
15896
- const extensions = path3.join(cwd, ".omp", "extensions");
15897
- for (const name of await namesIn(extensions)) {
15898
- const candidate = path3.join(extensions, name);
15899
- const kind = await kindOf(candidate);
15900
- if (kind === "file" && EXTENSION_MODULE.test(name)) {
15901
- entries.push({ path: candidate, directory: false });
15902
- } else if (kind === "directory") {
15903
- const indexes = await Promise.all(EXTENSION_INDEXES.map((index) => kindOf(path3.join(candidate, index))));
15904
- if (indexes.includes("file"))
15905
- entries.push({ path: candidate, directory: true });
15906
- }
15907
- }
15908
- for (const skills of SKILL_DIRECTORIES) {
15909
- for (const name of await namesIn(path3.join(cwd, skills))) {
15910
- const skill = path3.join(cwd, skills, name, "SKILL.md");
15911
- if (await kindOf(skill) === "file")
15912
- entries.push({ path: skill, directory: false });
15913
- }
15914
- }
15915
- return entries;
15916
- }
15917
- var checkRepositoryExtensions = async (host2, input) => {
15918
- const entries = await repositoryEntries(input.cwd);
15919
- if (entries.length === 0) {
15920
- return ok("the repository carries no .omp/extensions, .omp/skills or .claude/skills");
15921
- }
15922
- const [extensionPaths, loaded] = await Promise.all([
15923
- host2.discoverExtensionPaths(input.cwd),
15924
- host2.loadSkills(input.cwd)
15925
- ]);
15926
- const discovered = new Set([...extensionPaths, ...loaded.skills.map((skill) => skill.filePath)].map((file2) => path3.resolve(file2)));
15927
- const isDiscovered = (entry) => {
15928
- const resolved = path3.resolve(entry.path);
15929
- if (discovered.has(resolved))
15930
- return true;
15931
- if (!entry.directory)
15932
- return false;
15933
- const prefix = resolved + path3.sep;
15934
- return [...discovered].some((file2) => file2.startsWith(prefix));
15935
- };
15936
- const relative = (selected) => selected.map((entry) => path3.relative(input.cwd, entry.path)).sort().join(", ");
15937
- const found = [];
15938
- const lost = [];
15939
- for (const entry of entries)
15940
- (isDiscovered(entry) ? found : lost).push(entry);
15941
- if (lost.length === 0)
15942
- return ok(`discovered: ${relative(found)}`);
15943
- const suffix = found.length === 0 ? "" : `; discovered: ${relative(found)}`;
15944
- return failed(`not discovered: ${relative(lost)}${suffix}`);
15945
- };
15946
- var checkDispatchEnvoyTools = async (host2, input, signal) => {
15947
- const missing = ENVOY_TOOL_NAMES.filter((tool) => !input.activeTools.includes(tool));
15948
- if (missing.length > 0)
15949
- return failed(`Envoy tools missing: ${missing.join(", ")}`);
15950
- const result = await host2.run("dispatch", ["read", "--issue", input.issue], {
15951
- cwd: input.cwd,
15952
- signal
15953
- });
15954
- if (result.code !== 0)
15955
- return failed(`dispatch read failed: ${commandFailure(result)}`);
15956
- return ok(`${ENVOY_TOOL_NAMES.length} Envoy tools registered; dispatch read ${input.issue} answered from ${whichOnPath("dispatch")}`);
15957
- };
15958
- var checkGitHub = async (host2, input, signal) => {
15959
- const result = await host2.run("gh", ["api", "graphql", "-f", "query={viewer{login}}", "--jq", ".data.viewer.login"], { cwd: input.cwd, signal });
15960
- if (result.code !== 0)
15961
- return failed(`gh viewer login failed: ${commandFailure(result)}`);
15962
- return ok(`gh viewer login: ${result.stdout.trim()}`);
15963
- };
15964
- var CHECKS = {
15965
- subagents: checkSubagents,
15966
- "web-search": checkWebSearch,
15967
- mcp: checkMCP,
15968
- "repository-extensions": checkRepositoryExtensions,
15969
- "dispatch-envoy-tools": checkDispatchEnvoyTools,
15970
- github: checkGitHub
15971
- };
15972
- var MAX_DETAIL_BYTES = 1024;
15973
- var HELD = "\uFFFF";
15974
- var C1_INTRODUCER = "\uFFFE";
15975
- var OPENER = "\uFDD0";
15976
- var HELD_READINGS = [
15977
- [/(?:\uffff\[|\ufffe)[0-?]*[!-)+,/-]*/g, OPENER],
15978
- [/\ufdd0[@-~]?/g, HELD],
15979
- [/\uffff/g, " "]
15980
- ];
15981
- var HELD_SEQUENCE = String.raw`\ufffe|\ufdd0|\uffff(?:\[[0-?]*[@-~])?`;
15982
- var JWT_FIRST_HELD_SEQUENCE = String.raw`\ufffe|\uffff(?:\[[0-?]*[@-~])?`;
15983
- function isSpace(code) {
15984
- return code >= 9 && code <= 13 || code === 32 || code === 133 || code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288;
15985
- }
15986
- function truncateUtf8(text, max) {
15987
- let bytes = 0;
15988
- let end = 0;
15989
- for (const char of text) {
15990
- const code = char.codePointAt(0) ?? 0;
15991
- bytes += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
15992
- if (bytes > max)
15993
- break;
15994
- end += char.length;
15995
- }
15996
- return text.slice(0, end);
15997
- }
15998
- var CREDENTIAL_RULES = [
15999
- [/(?<![a-z0-9+.-])([0-9+.-]*[a-z][a-z0-9+.-]*:\/\/)[^ /?#]+@/gi, "$1***@"],
16000
- [
16001
- /([?&](?:token|key|secret|password|access_token|accesstoken|refresh_token|id_token|auth_token|private_token|api_key|apikey|client_secret|sig|signature|x-amz-security-token|x-amz-signature|x-goog-signature)=)[^ &#"'<>]+/gi,
16002
- "$1***"
16003
- ],
16004
- [
16005
- /\b(password[ \ufffe\uffff]*=[ \ufffe\uffff]*)(?:(')(?:[^'\\]|\\.)*('?)|[^ &#"'<>]+)/gi,
16006
- "$1$2***$3"
16007
- ],
16008
- [
16009
- /(authorization\\?["']?[ \ufffe\uffff]*[:=][ \ufffe\uffff]*)(?:(\\["'](?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)[^"'\\]+|(["'](?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)(?:[^"'\\]|\\.)+|(\[?(?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)[^)\]}>]+)/gi,
16010
- "$1$2$3$4***"
16011
- ],
16012
- [
16013
- new RegExp(String.raw`((?:api-key|application-key|ocp-apim-subscription-key|x-subscription-token)\\?["']?[ \ufffe\uffff]*[:=][ \ufffe\uffff]*\\?["']?)(?:[A-Za-z0-9._~+/=*-]|${HELD_SEQUENCE})+`, "gi"),
16014
- "$1***"
16015
- ],
16016
- [
16017
- /(\\?["'](?:api_key|access_token)\\?["'][ \ufffe\uffff]*:[ \ufffe\uffff]*\\?["'])[^"'\\]+/gi,
16018
- "$1***"
16019
- ],
16020
- [
16021
- new RegExp(String.raw`\b(bearer[ \ufffe\uffff]+)(?:[A-Za-z0-9._~+/=*-]|${HELD_SEQUENCE})+`, "gi"),
16022
- "$1***"
16023
- ],
16024
- [
16025
- new RegExp(String.raw`\b(gh[pousr]_|github_pat_)(?:[A-Za-z0-9._-]|${HELD_SEQUENCE}){20,}`, "g"),
16026
- "$1***"
16027
- ],
16028
- [
16029
- new RegExp(String.raw`(?:(?=eyJ)(?<!\uffff\[[0-?]*(?:[@-~]?|\\u[0-9A-Fa-f]{4}))(?:(?<![A-Za-z0-9_\ufffe\uffff-])|(?<=%[0-9A-Fa-f]{2}|\\u[0-9A-Fa-f]{4}))eyJ(?:[A-Za-z0-9_-]|${JWT_FIRST_HELD_SEQUENCE}){8,}|(?<=\uffff)eyJ(?:[A-Za-z0-9_-]|\ufffe){8,}|(?=eyJ)(?<=\uffff\[[0-?]*(?:[@-~]|\\u[0-9A-Fa-f]{4})?)eyJ[A-Za-z0-9_-]*\ufffe(?:[A-Za-z0-9_-]|\ufffe)*)\.(?:[A-Za-z0-9_-]|${HELD_SEQUENCE}){8,}\.(?:[A-Za-z0-9_-]|${HELD_SEQUENCE})*`, "g"),
16030
- "***"
16031
- ],
16032
- [
16033
- new RegExp(String.raw`\b(sk-|dsp_|ddsat_|ddpat_|ddapp_|ya29\.|xox[a-z]-|glpat-|npm_)(?:[A-Za-z0-9_-]|${HELD_SEQUENCE}){20,}`, "g"),
16034
- "$1***"
16035
- ],
16036
- [new RegExp(String.raw`\b(AIza)(?:[A-Za-z0-9_-]|${HELD_SEQUENCE}){35,}`, "g"), "$1***"],
16037
- [new RegExp(String.raw`\b(AKIA|ASIA)(?:(?:${HELD_SEQUENCE})*[A-Z0-9]){16}\b`, "g"), "$1***"]
16038
- ];
16039
- var MAX_REDACTION_PASSES = 8;
16040
- function redactCredentials(text) {
16041
- let line = text;
16042
- for (let pass = 0;pass < MAX_REDACTION_PASSES; pass++) {
16043
- const next = CREDENTIAL_RULES.reduce((redacted, [pattern, replacement]) => redacted.replace(pattern, replacement), line);
16044
- if (next === line)
16045
- break;
16046
- line = next;
16047
- }
16048
- return line;
16049
- }
16050
- function withoutLastCodePoint(text) {
16051
- const last = text.charCodeAt(text.length - 1);
16052
- return text.slice(0, last >= 56320 && last <= 57343 && text.length > 1 ? -2 : -1);
16053
- }
16054
- function cleanDetail(text) {
16055
- let marked = "";
16056
- for (const char of text) {
16057
- const code = char.codePointAt(0) ?? 0;
16058
- const control = code < 32 || code >= 127 && code < 160 || code === 65534 || code === 64976 || code === 65533;
16059
- marked += isSpace(code) ? " " : code === 155 ? C1_INTRODUCER : control ? HELD : char;
16060
- }
16061
- let redacted = redactCredentials(marked);
16062
- if (marked.includes(HELD) || marked.includes(C1_INTRODUCER)) {
16063
- for (const [pattern, replacement] of HELD_READINGS) {
16064
- const read = redacted.replace(pattern, replacement);
16065
- if (read !== redacted)
16066
- redacted = redactCredentials(read);
16067
- }
16068
- }
16069
- const line = redacted.split(" ").filter((field) => field !== "").join(" ");
16070
- let cut = truncateUtf8(line, MAX_DETAIL_BYTES).replace(/ +$/, "");
16071
- if (cut.length < line.length) {
16072
- while (redactCredentials(cut) !== cut) {
16073
- const space = cut.lastIndexOf(" ");
16074
- cut = space > 0 ? cut.slice(0, space) : withoutLastCodePoint(cut);
16075
- }
16076
- }
16077
- return cut;
16078
- }
16079
- function budgetLabel(ms) {
16080
- return ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
16081
- }
16082
- async function withinBudget(work, ms, onDeadline) {
16083
- const deadline = Promise.withResolvers();
16084
- const timer = setTimeout(() => deadline.resolve(onDeadline()), ms);
16085
- try {
16086
- return await Promise.race([work, deadline.promise]);
16087
- } finally {
16088
- clearTimeout(timer);
16089
- }
16090
- }
16091
- async function measureCapabilities(host2, input, budgets = DEFAULT_BUDGETS) {
16092
- const started = Date.now();
16093
- const measuredAt = new Date(started).toISOString();
16094
- const outcomes = new Map;
16095
- const controllers = [];
16096
- const checks3 = LIVE_CAPABILITIES.map(async (name) => {
16097
- const controller = new AbortController;
16098
- controllers.push(controller);
16099
- const outcome = await withinBudget(CHECKS[name](host2, input, controller.signal).catch((error48) => failed(firstLine(messageFor(error48)))), budgets.checkMs, () => {
16100
- controller.abort();
16101
- return failed(`did not finish within ${budgetLabel(budgets.checkMs)}`);
16102
- });
16103
- outcomes.set(name, outcome);
16104
- });
16105
- await withinBudget(Promise.all(checks3).then(() => {
16106
- return;
16107
- }), budgets.reportMs, () => {
16108
- return;
16109
- });
16110
- for (const controller of controllers)
16111
- controller.abort();
16112
- const rows = LIVE_CAPABILITIES.map((name) => {
16113
- const { ok: ok2, detail } = outcomes.get(name) ?? failed(`did not finish within ${budgetLabel(budgets.reportMs)}`);
16114
- return { name, ok: ok2, detail: cleanDetail(detail) };
16115
- });
16116
- return { measuredAt, elapsedMs: Date.now() - started, rows };
16117
- }
16118
- async function runCommand2(command, args, options) {
16119
- const child = Bun.spawn([command, ...args], {
16120
- cwd: options.cwd,
16121
- env: process.env,
16122
- stdin: "ignore",
16123
- stdout: "pipe",
16124
- stderr: "pipe",
16125
- signal: options.signal
16126
- });
16127
- const [stdout, stderr] = await Promise.all([
16128
- new Response(child.stdout).text(),
16129
- new Response(child.stderr).text(),
16130
- child.exited
16131
- ]);
16132
- return { code: child.exitCode, stdout, stderr };
16133
- }
16134
- function ompCapabilityHost() {
16135
- const hostSettings = async () => (await import("@oh-my-pi/pi-coding-agent/config/settings")).settings;
16136
- return {
16137
- discoverAgents: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverAgents(cwd),
16138
- disabledAgents: async () => {
16139
- const [{ cfgTaskDisabledAgents }, settings] = await Promise.all([
16140
- import("@oh-my-pi/pi-coding-agent/task/settings"),
16141
- hostSettings()
16142
- ]);
16143
- return cfgTaskDisabledAgents.get(settings);
16144
- },
16145
- runSearchQuery: async (args, options) => (await import("@oh-my-pi/pi-coding-agent")).runSearchQuery(args, options),
16146
- loadMCPConfigs: async (cwd) => (await import("@oh-my-pi/pi-coding-agent/mcp/config")).loadAllMCPConfigs(cwd),
16147
- discoverMCPServers: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverMCPServers(cwd),
16148
- discoverExtensionPaths: async (cwd) => {
16149
- const [{ discoverExtensionPaths }, { cfgDisabledExtensions }, settings] = await Promise.all([
16150
- import("@oh-my-pi/pi-coding-agent/extensibility/extensions/loader"),
16151
- import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
16152
- hostSettings()
16153
- ]);
16154
- return discoverExtensionPaths([], cwd, cfgDisabledExtensions.get(settings));
16155
- },
16156
- loadSkills: async (cwd) => {
16157
- const [{ loadSkills }, { cfgDisabledExtensions, cfgSkills }, settings] = await Promise.all([
16158
- import("@oh-my-pi/pi-coding-agent"),
16159
- import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
16160
- hostSettings()
16161
- ]);
16162
- return loadSkills({
16163
- ...cfgSkills.get(settings),
16164
- cwd,
16165
- disabledExtensions: cfgDisabledExtensions.get(settings)
16166
- });
16167
- },
16168
- run: runCommand2
16169
- };
16170
- }
16171
- // package.json
16172
- var package_default = {
16173
- name: "@sjawhar/pi-legion",
16174
- version: "10.11.1",
16175
- license: "Apache-2.0",
16176
- type: "module",
16177
- omp: {
16178
- extensions: [
16179
- "dist/legion.js"
16180
- ],
16181
- skills: [
16182
- "dist/skills"
16183
- ]
16184
- },
16185
- legion: {
16186
- daemonApiVersion: 32
16187
- },
16188
- repository: {
16189
- type: "git",
16190
- url: "https://github.com/sjawhar/legion"
16191
- },
16192
- files: [
16193
- "dist",
16194
- "agents"
16195
- ],
16196
- scripts: {
16197
- build: "bun build extensions/legion.ts --outdir dist --target bun --format esm --external @oh-my-pi/pi-coding-agent --external @oh-my-pi/pi-tui --external @oh-my-pi/pi-utils",
16198
- prepack: "../../scripts/pi-plugin-prepack.sh",
16199
- postpack: "rm -rf dist/skills",
16200
- typecheck: "bunx tsc --noEmit",
16201
- test: "bun test",
16202
- lint: "bunx biome lint extensions/ src/"
16203
- },
16204
- devDependencies: {
16205
- "@biomejs/biome": "^2.3.14",
16206
- "@legion/contracts": "workspace:*",
16207
- "@legion/envoy-client": "workspace:*",
16208
- "@legion/pi-shared": "workspace:*",
16209
- "@oh-my-pi/pi-utils": "^18.6.0",
16210
- "@types/bun": "latest",
16211
- typescript: "^5.3.0",
16212
- zod: "4.3.6"
16213
- }
16214
- };
16215
-
16216
- // src/classify.ts
16217
- function classifySession(env) {
16218
- if (env.LEGION_CONTROLLER !== undefined && env.LEGION_TREE !== undefined) {
16219
- throw new Error("Legion session has both controller and tree launch markers");
16220
- }
16221
- if (env.LEGION_CONTROLLER === "1")
16222
- return { kind: "controller" };
16223
- if (env.LEGION_ROLE === undefined)
16224
- return { kind: "not-legion" };
16225
- if (!isLegionRole(env.LEGION_ROLE)) {
16226
- throw new Error(`LEGION_ROLE "${env.LEGION_ROLE}" is not a Legion role`);
16227
- }
16228
- const tree = requiredEnvironment(env, "LEGION_TREE");
16229
- const issue2 = requiredEnvironment(env, "LEGION_ISSUE");
16230
- if (env.LEGION_ROLE === "architect" && issue2 === tree) {
16231
- return { kind: "root-architect", tree };
16232
- }
16233
- return { kind: "phase-worker", role: env.LEGION_ROLE, tree, issue: issue2 };
16234
- }
16235
- function requiredEnvironment(env, key) {
16236
- const value = env[key];
16237
- if (!value)
16238
- throw new Error(`${key} is required for Legion`);
16239
- return value;
16240
- }
16241
- function requiredSecret(env, key) {
16242
- const fileKey = `${key}_FILE`;
16243
- const file2 = env[fileKey];
16244
- if (file2 !== undefined)
16245
- return readSecretFile(fileKey, file2);
16246
- return requiredEnvironment(env, key);
16247
- }
16248
- function requiredControllerCapability(env) {
16249
- if (env.LEGION_CONTROLLER_SECRET_FILE !== undefined) {
16250
- return readSecretFile("LEGION_CONTROLLER_SECRET_FILE", env.LEGION_CONTROLLER_SECRET_FILE);
16251
- }
16252
- const secret = env.LEGION_CONTROLLER_SECRET;
16253
- if (!secret) {
16254
- throw new Error("LEGION_CONTROLLER_SECRET or LEGION_CONTROLLER_SECRET_FILE is required to claim the controller. " + "Launch OMP with one of them in its environment before running /legion-claim-controller.");
16255
- }
16256
- return secret;
16257
- }
16258
-
16259
- // ../contracts/src/legion-api.ts
16260
- var nonEmptyString = exports_external.string().min(1);
16261
- var issueKey = exports_external.string().regex(DISPATCH_KEY_PATTERN);
16262
- var timestamp = exports_external.iso.datetime({ offset: true });
16263
- var LEGION_WORKFLOW_PHASES = [
16264
- "admitted",
16265
- "planning",
16266
- "implementing",
16267
- "testing",
16268
- "reviewing",
16269
- "retro",
16270
- "merging",
16271
- "awaiting_merge",
16272
- "integrating",
16273
- "production_check",
16274
- "done",
16275
- "held"
16276
- ];
16277
- var LEGION_PHASE_BACKWARD_TARGETS = [
16278
- "admitted",
16279
- "planning",
16280
- "implementing",
16281
- "testing",
16282
- "reviewing",
16283
- "retro",
16284
- "merging",
16285
- "awaiting_merge",
16286
- "production_check",
16287
- "done",
16288
- "held"
16289
- ];
16290
- var LEGION_PHASES = [...LEGION_WORKFLOW_PHASES, "unrecorded"];
16291
- var LEGION_CLAIM_STATES = [
16292
- "queued",
16293
- "launch_uncertain",
16294
- "launching",
16295
- "shim_connected",
16296
- "registered",
16297
- "ready",
16298
- "working",
16299
- "idle",
16300
- "suspended",
16301
- "failed",
16302
- "retired"
15645
+ var LEGION_PHASES = [...LEGION_WORKFLOW_PHASES, "unrecorded"];
15646
+ var LEGION_CLAIM_STATES = [
15647
+ "queued",
15648
+ "launch_uncertain",
15649
+ "launching",
15650
+ "shim_connected",
15651
+ "registered",
15652
+ "ready",
15653
+ "working",
15654
+ "idle",
15655
+ "suspended",
15656
+ "failed",
15657
+ "retired"
16303
15658
  ];
16304
15659
  var legionLocator = exports_external.discriminatedUnion("runtime", [
16305
15660
  exports_external.strictObject({
@@ -16606,104 +15961,1015 @@ var LegionChildRequest = exports_external.strictObject({
16606
15961
  issue: nonEmptyString
16607
15962
  });
16608
15963
 
16609
- // src/daemon-client.ts
16610
- class LegionDaemonApiError extends Error {
16611
- method;
16612
- path;
16613
- status;
16614
- code;
16615
- detail;
16616
- constructor(method, path4, status, code, detail) {
16617
- super(`${method} ${path4} failed with ${status}${code === undefined ? "" : ` ${code}`}: ${detail}`);
16618
- this.method = method;
16619
- this.path = path4;
16620
- this.status = status;
16621
- this.code = code;
16622
- this.detail = detail;
16623
- this.name = "LegionDaemonApiError";
15964
+ // ../pi-shared/src/daemon-client.ts
15965
+ class LegionDaemonApiError extends Error {
15966
+ method;
15967
+ path;
15968
+ status;
15969
+ code;
15970
+ detail;
15971
+ constructor(method, path2, status, code, detail) {
15972
+ super(`${method} ${path2} failed with ${status}${code === undefined ? "" : ` ${code}`}: ${detail}`);
15973
+ this.method = method;
15974
+ this.path = path2;
15975
+ this.status = status;
15976
+ this.code = code;
15977
+ this.detail = detail;
15978
+ this.name = "LegionDaemonApiError";
15979
+ }
15980
+ }
15981
+
15982
+ class LegionDaemonContractError extends Error {
15983
+ method;
15984
+ path;
15985
+ status;
15986
+ constructor(method, path2, status, problem) {
15987
+ super(`${method} ${path2} answered ${status} with a body the daemon API contract does not describe: ${problem}`);
15988
+ this.method = method;
15989
+ this.path = path2;
15990
+ this.status = status;
15991
+ this.name = "LegionDaemonContractError";
15992
+ }
15993
+ }
15994
+ var READY_RETRY_ATTEMPTS = 3;
15995
+ var READY_RETRY_DELAY_MS = 1000;
15996
+ function isNetworkOrTimeoutError(error48) {
15997
+ if (!(error48 instanceof Error))
15998
+ return false;
15999
+ if (error48.name === "NetworkError" || error48.name === "TimeoutError")
16000
+ return true;
16001
+ const code = "code" in error48 && typeof error48.code === "string" ? error48.code : undefined;
16002
+ return code === "ConnectionRefused" || code === "ConnectionTimeout" || code === "EAI_AGAIN" || code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EHOSTUNREACH" || code === "ENETUNREACH" || code === "ENOTFOUND" || code === "ETIMEDOUT" || error48 instanceof TypeError && (error48.message === "Failed to fetch" || error48.message === "fetch failed");
16003
+ }
16004
+ async function callReadyWithRetry(label, call) {
16005
+ for (let attempt = 1;; attempt++) {
16006
+ try {
16007
+ await call();
16008
+ return;
16009
+ } catch (error48) {
16010
+ const retryable = error48 instanceof LegionDaemonApiError ? error48.status >= 500 && error48.status < 600 : isNetworkOrTimeoutError(error48);
16011
+ if (!retryable)
16012
+ throw error48;
16013
+ if (attempt === READY_RETRY_ATTEMPTS) {
16014
+ console.error(`[legion] ${label} failed after ${attempt} attempts: ${messageFor(error48)}`);
16015
+ throw error48;
16016
+ }
16017
+ console.error(`[legion] ${label} failed (attempt ${attempt}/${READY_RETRY_ATTEMPTS}), retrying: ${messageFor(error48)}`);
16018
+ const retryDelay = Promise.withResolvers();
16019
+ setTimeout(retryDelay.resolve, READY_RETRY_DELAY_MS);
16020
+ await retryDelay.promise;
16021
+ }
16022
+ }
16023
+ }
16024
+ function parseStrictly(schema, body) {
16025
+ let json2;
16026
+ try {
16027
+ json2 = JSON.parse(body);
16028
+ } catch (error48) {
16029
+ return { problem: `not JSON (${messageFor(error48)})` };
16030
+ }
16031
+ const parsed = schema.safeParse(json2);
16032
+ if (parsed.success)
16033
+ return { value: parsed.data };
16034
+ return {
16035
+ problem: parsed.error.issues.map((issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}`).join("; ")
16036
+ };
16037
+ }
16038
+ function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
16039
+ const endpoint = baseUrl.replace(/\/+$/, "");
16040
+ const call = async (method, path2, body) => {
16041
+ const response = await fetchFn(`${endpoint}${path2}`, body === undefined ? undefined : {
16042
+ method,
16043
+ headers: { "Content-Type": "application/json" },
16044
+ body: JSON.stringify(body)
16045
+ });
16046
+ const text = await response.text();
16047
+ if (response.ok)
16048
+ return { status: response.status, text };
16049
+ const refusal = parseStrictly(LegionErrorResponse, text);
16050
+ if ("value" in refusal) {
16051
+ const code = "code" in refusal.value && typeof refusal.value.code === "string" ? refusal.value.code : undefined;
16052
+ throw new LegionDaemonApiError(method, path2, response.status, code, refusal.value.error);
16053
+ }
16054
+ throw new LegionDaemonApiError(method, path2, response.status, undefined, `${JSON.stringify(text)} is not the daemon's refusal shape (${refusal.problem})`);
16055
+ };
16056
+ const read = async (method, path2, schema, body) => {
16057
+ const { status, text } = await call(method, path2, body);
16058
+ const parsed = parseStrictly(schema, text);
16059
+ if ("problem" in parsed) {
16060
+ throw new LegionDaemonContractError(method, path2, status, parsed.problem);
16061
+ }
16062
+ return parsed.value;
16063
+ };
16064
+ const post = (path2, request, response, body) => read("POST", path2, response, request.parse(body));
16065
+ const acknowledge = async (path2, body) => {
16066
+ const { status, text } = await call("POST", path2, body);
16067
+ if (status !== 204 || text !== "") {
16068
+ throw new LegionDaemonContractError("POST", path2, status, "the route answers 204 with no body");
16069
+ }
16070
+ };
16071
+ return {
16072
+ state: () => read("GET", "/legion/v1/state", LegionStateResponse),
16073
+ register: (input) => read("POST", "/legion/v1/claims/register", LegionRegisterResponse, input),
16074
+ registerController: (input) => read("POST", "/legion/v1/claims/register", LegionControllerRegisterResponse, input),
16075
+ ready: (input) => acknowledge("/legion/v1/claims/ready", input),
16076
+ exit: (input) => acknowledge("/legion/v1/claims/exit", input),
16077
+ grant: (input) => post("/legion/v1/grants", LegionGrantRequest, LegionGrantResponse, input),
16078
+ controllerGrant: (input) => post("/legion/v1/grants", LegionControllerGrantRequest, LegionGrantResponse, input),
16079
+ handoffComplete: (input) => post("/legion/v1/handoff/complete", LegionHandoffCompleteRequest, LegionHandoffCompleteResponse, input),
16080
+ issueStatus: (input) => post("/legion/v1/issues/status", LegionIssueStatusRequest, LegionEmptyResponse, input),
16081
+ gateRegister: (input) => post("/legion/v1/gates/register", LegionGateRegisterRequest, LegionEmptyResponse, input),
16082
+ waveRelease: (input) => post("/legion/v1/waves/release", LegionWaveReleaseRequest, LegionWaveReleaseResponse, input),
16083
+ phaseBackward: (input) => post("/legion/v1/phase/backward", LegionPhaseBackwardRequest, LegionEmptyResponse, input),
16084
+ phaseRetry: (input) => post("/legion/v1/phase/retry", LegionPhaseRetryRequest, LegionEmptyResponse, input),
16085
+ signOff: (input) => post("/legion/v1/signoff", LegionSignOffRequest, LegionEmptyResponse, input),
16086
+ rootClose: (input) => post("/legion/v1/roots/close", LegionRootCloseRequest, LegionEmptyResponse, input),
16087
+ childPark: (input) => post("/legion/v1/children/park", LegionChildRequest, LegionEmptyResponse, input),
16088
+ childRerun: (input) => post("/legion/v1/children/rerun", LegionChildRequest, LegionEmptyResponse, input)
16089
+ };
16090
+ }
16091
+
16092
+ // ../pi-shared/src/handoff-commit.ts
16093
+ import { execFile as execFile2 } from "child_process";
16094
+ import { promisify as promisify2 } from "util";
16095
+
16096
+ // ../pi-shared/src/git-lock.ts
16097
+ import { lstat } from "fs/promises";
16098
+ var GIT_LOCK_REPORTS = [
16099
+ /The lockfile at '([^']+)' might need manual deletion/g,
16100
+ /Unable to create '([^']+)': File exists/g
16101
+ ];
16102
+ function age(ms) {
16103
+ const total = Math.round(Math.abs(ms) / 1000);
16104
+ const sign = ms < 0 && total > 0 ? "-" : "";
16105
+ const hours = Math.floor(total / 3600);
16106
+ const minutes = Math.floor(total % 3600 / 60);
16107
+ const seconds = total % 60;
16108
+ if (hours > 0)
16109
+ return `${sign}${hours}h${minutes}m${seconds}s`;
16110
+ if (minutes > 0)
16111
+ return `${sign}${minutes}m${seconds}s`;
16112
+ return `${sign}${seconds}s`;
16113
+ }
16114
+ async function staleGitLockNote(output, now = Date.now()) {
16115
+ const locks = new Set;
16116
+ for (const report of GIT_LOCK_REPORTS) {
16117
+ for (const match of output.matchAll(report))
16118
+ locks.add(match[1]);
16119
+ }
16120
+ const notes = [];
16121
+ for (const lock of locks) {
16122
+ const written = await lstat(lock).then((info) => info.mtimeMs, () => {
16123
+ return;
16124
+ });
16125
+ if (written === undefined)
16126
+ continue;
16127
+ notes.push(`git lock ${lock} was last written ${age(now - written)} ago and blocks this command. Do not remove it while the issue's pod runs: another role's git, which this container cannot see, may hold it. Replacing the issue's pod removes it. Under the tmux runtime nothing removes it: remove it by hand only once no git or jj process on the host is using the repository.`);
16128
+ }
16129
+ return notes.join(`
16130
+ `);
16131
+ }
16132
+
16133
+ // ../pi-shared/src/handoff-commit.ts
16134
+ var FILE_BACKED = {
16135
+ planning: { word: "plan", role: "planner" },
16136
+ implementing: { word: "implement", role: "implementer" },
16137
+ testing: { word: "test", role: "tester" },
16138
+ reviewing: { word: "review", role: "reviewer" }
16139
+ };
16140
+ var execFileAsync2 = promisify2(execFile2);
16141
+ var pathJj = async (args, cwd) => {
16142
+ try {
16143
+ const { stdout } = await execFileAsync2("jj", ["-R", cwd, ...args], {
16144
+ cwd,
16145
+ maxBuffer: 16 * 1024 * 1024
16146
+ });
16147
+ return stdout.trim();
16148
+ } catch (error48) {
16149
+ const detail = error48 !== null && typeof error48 === "object" && "stderr" in error48 ? String(error48.stderr).trim() : error48 instanceof Error ? error48.message : String(error48);
16150
+ const note = await staleGitLockNote(detail);
16151
+ throw new Error(`jj ${args.join(" ")} failed: ${detail}${note === "" ? "" : `
16152
+ ${note}`}`);
16153
+ }
16154
+ };
16155
+ function requiredVariable(env, name) {
16156
+ const value = env[name];
16157
+ if (value === undefined || value === "") {
16158
+ throw new Error(`${name} is not set, so handoff_complete cannot find the commit to report`);
16159
+ }
16160
+ return value;
16161
+ }
16162
+ async function findHandoffCommit(input) {
16163
+ const { phase, role, env } = input;
16164
+ const jj = input.jj ?? pathJj;
16165
+ const workspace = requiredVariable(env, "LEGION_WORKSPACE");
16166
+ const handoff = FILE_BACKED[phase];
16167
+ if (handoff === undefined || handoff.role !== role) {
16168
+ return jj(["log", "-r", "@-", "--no-graph", "-T", "commit_id"], workspace);
16169
+ }
16170
+ const issue2 = requiredVariable(env, "LEGION_ISSUE");
16171
+ if (!DISPATCH_KEY_PATTERN.test(issue2)) {
16172
+ throw new Error(`LEGION_ISSUE ${JSON.stringify(issue2)} is not a Dispatch issue key like LEGION-1`);
16173
+ }
16174
+ const file2 = `.legion/${issue2}/${handoff.word}.json`;
16175
+ const fileset = `root:${JSON.stringify(file2)}`;
16176
+ if (await jj(["diff", "-r", "@", "--name-only", fileset], workspace) !== "") {
16177
+ throw new Error(`${file2} has changes in the working copy that are not committed: commit this phase's handoff before completing`);
16178
+ }
16179
+ if (await jj(["file", "list", "-r", "@", fileset], workspace) === "") {
16180
+ throw new Error(`${file2} is missing from the workspace: write this phase's handoff, then commit and push it before completing`);
16181
+ }
16182
+ const carrying = await jj([
16183
+ "log",
16184
+ "-r",
16185
+ `latest((::@- ~ ::trunk()) & files(${fileset}))`,
16186
+ "--no-graph",
16187
+ "-T",
16188
+ "commit_id"
16189
+ ], workspace);
16190
+ if (carrying === "") {
16191
+ throw new Error(`${file2} is not committed on this issue's branch (only the base branch carries it): write and commit this phase's handoff`);
16192
+ }
16193
+ const user = env["JJ_USER"];
16194
+ if (user !== undefined && user !== "") {
16195
+ const author = await jj(["log", "-r", carrying, "--no-graph", "-T", 'author.name() ++ "\\n" ++ author.email()'], workspace);
16196
+ const [name = "", email3 = ""] = author.split(`
16197
+ `, 2);
16198
+ if (name !== user || email3 !== (env["JJ_EMAIL"] ?? "")) {
16199
+ throw new Error(`${file2} is carried by commit ${carrying}, authored by ${name} <${email3}>, not this pane's ${user}: run jj new, then write and commit this phase's handoff again`);
16200
+ }
16201
+ }
16202
+ const bookmark = `legion/${issue2}`;
16203
+ const pushed = await jj([
16204
+ "log",
16205
+ "-r",
16206
+ `${carrying} & ::remote_bookmarks(exact:${JSON.stringify(bookmark)}, exact:"origin")`,
16207
+ "--no-graph",
16208
+ "-T",
16209
+ "commit_id"
16210
+ ], workspace);
16211
+ if (pushed === "") {
16212
+ throw new Error(`${file2} is carried by commit ${carrying}, which is not on ${bookmark}@origin: push the issue branch, then complete again`);
16213
+ }
16214
+ return carrying;
16215
+ }
16216
+
16217
+ // ../pi-shared/src/interface.ts
16218
+ import { logger } from "@oh-my-pi/pi-utils";
16219
+ var ENVOY_PLUGIN_INTERFACE_VERSION = 3;
16220
+ var ENVOY_PLUGIN_INTERFACE_KEY = Symbol.for("legion.pi-shared.envoy-plugin-interface");
16221
+ var LEGION_PLUGIN_LOADED_KEY = Symbol.for("legion.pi-legion.loaded");
16222
+ var LEGACY_LEGION_LOADED_KEY = Symbol.for("legion.pi-envoy.legion-loaded");
16223
+ var LOCAL_ENVOY_NOTICE = { localNotice: true };
16224
+ var store = globalThis;
16225
+ function envoyPluginInterface() {
16226
+ const existing = store[ENVOY_PLUGIN_INTERFACE_KEY];
16227
+ if (existing !== undefined)
16228
+ return existing;
16229
+ const created = {
16230
+ version: ENVOY_PLUGIN_INTERFACE_VERSION,
16231
+ publishers: [],
16232
+ roleClaim: {
16233
+ instances: [],
16234
+ managedSessions: new Set,
16235
+ regained: undefined,
16236
+ slackRoleOwned: false
16237
+ },
16238
+ injectedUserTurns: new Map,
16239
+ bootstrappedSession: { file: undefined }
16240
+ };
16241
+ store[ENVOY_PLUGIN_INTERFACE_KEY] = created;
16242
+ return created;
16243
+ }
16244
+ function readEnvoyPluginInterface() {
16245
+ const iface = store[ENVOY_PLUGIN_INTERFACE_KEY];
16246
+ const from = iface?.publishers[0];
16247
+ if (iface === undefined || from === undefined)
16248
+ return { kind: "absent" };
16249
+ if (iface.version !== ENVOY_PLUGIN_INTERFACE_VERSION) {
16250
+ return {
16251
+ kind: "mismatch",
16252
+ found: iface.version,
16253
+ expected: ENVOY_PLUGIN_INTERFACE_VERSION,
16254
+ from
16255
+ };
16256
+ }
16257
+ return { kind: "present", iface };
16258
+ }
16259
+
16260
+ // ../pi-shared/src/injected-user-turns.ts
16261
+ function userMessageText(content) {
16262
+ if (typeof content === "string")
16263
+ return content;
16264
+ if (!Array.isArray(content))
16265
+ return "";
16266
+ const pieces = [];
16267
+ for (const part of content) {
16268
+ if (typeof part !== "object" || part === null)
16269
+ continue;
16270
+ if ("type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
16271
+ pieces.push(part.text);
16272
+ }
16273
+ }
16274
+ return pieces.join(`
16275
+ `);
16276
+ }
16277
+ function matchInjectedUserTurn(sessionID, message) {
16278
+ if (typeof message !== "object" || message === null)
16279
+ return;
16280
+ if (!("role" in message) || message.role !== "user" && message.role !== "custom") {
16281
+ return;
16282
+ }
16283
+ if (!("timestamp" in message) || typeof message.timestamp !== "number")
16284
+ return;
16285
+ const session = envoyPluginInterface().injectedUserTurns.get(sessionID);
16286
+ if (session === undefined)
16287
+ return;
16288
+ const tag = messageTag(message);
16289
+ const memo = tag === undefined ? `${message.timestamp}` : `${message.timestamp}:${tag}`;
16290
+ const known = session.found.get(memo);
16291
+ if (known !== undefined)
16292
+ return known;
16293
+ let index = tag === undefined ? -1 : session.sent.findIndex((sent2) => sent2.messageId === tag);
16294
+ if (index === -1 && message.role === "user") {
16295
+ const text = userMessageText("content" in message ? message.content : undefined);
16296
+ index = session.sent.findIndex((sent2) => sent2.via === "text" && sent2.body === text);
16297
+ }
16298
+ const sent = session.sent[index];
16299
+ if (sent === undefined)
16300
+ return;
16301
+ session.sent.splice(index, 1);
16302
+ session.found.set(memo, sent.messageId);
16303
+ return sent.messageId;
16304
+ }
16305
+ function messageTag(message) {
16306
+ if (typeof message !== "object" || message === null || !("tag" in message))
16307
+ return;
16308
+ return typeof message.tag === "string" ? message.tag : undefined;
16309
+ }
16310
+
16311
+ // ../pi-shared/src/role-claim-bridge.ts
16312
+ function legionRoleClaimBridge() {
16313
+ return envoyPluginInterface().roleClaim;
16314
+ }
16315
+ async function claimEnvoyRole(sessionID, role, context) {
16316
+ const bridge = legionRoleClaimBridge();
16317
+ bridge.managedSessions.add(sessionID);
16318
+ const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
16319
+ if (instance === undefined)
16320
+ throw new Error("Envoy has no bound instance for a role claim");
16321
+ await instance.claim(sessionID, role, context);
16322
+ }
16323
+ async function subscribeLegionNotice(sessionID, topic, context, whileHolding) {
16324
+ const bridge = legionRoleClaimBridge();
16325
+ const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
16326
+ if (instance === undefined)
16327
+ throw new Error("Envoy has no bound instance for a notice subscription");
16328
+ await instance.subscribe(sessionID, topic, context, whileHolding);
16329
+ }
16330
+ function onEnvoyRoleRegained(listener) {
16331
+ legionRoleClaimBridge().regained = listener;
16332
+ }
16333
+
16334
+ // ../pi-shared/src/subagent-session.ts
16335
+ import fs from "fs";
16336
+ import path2 from "path";
16337
+ import * as host from "@oh-my-pi/pi-coding-agent";
16338
+ import { logger as logger2 } from "@oh-my-pi/pi-utils";
16339
+ function recordBootstrappedSession(sessionFile) {
16340
+ envoyPluginInterface().bootstrappedSession.file = sessionFile;
16341
+ }
16342
+ async function isSubagentSession(context) {
16343
+ const registered = registeredSubagent(context);
16344
+ if (registered !== undefined)
16345
+ return { subagent: registered, settled: true };
16346
+ try {
16347
+ await context.sessionManager.ensureOnDisk();
16348
+ } catch (error48) {
16349
+ const subagent = transcriptSaysSubagent(context);
16350
+ logger2.warn("subagent check: publishing the transcript failed; answered from the transcript on disk, and the next call asks again", { sessionFile: context.sessionManager.getSessionFile(), subagent, error: messageFor(error48) });
16351
+ return { subagent, settled: false };
16352
+ }
16353
+ return { subagent: transcriptSaysSubagent(context), settled: true };
16354
+ }
16355
+ function transcriptSaysSubagent(context) {
16356
+ const sessionFile = context.sessionManager.getSessionFile();
16357
+ if (sessionFile !== undefined) {
16358
+ if (fs.existsSync(`${path2.dirname(sessionFile)}.jsonl`))
16359
+ return true;
16360
+ if (fs.existsSync(sessionFile))
16361
+ return false;
16362
+ }
16363
+ const bootstrapped = envoyPluginInterface().bootstrappedSession.file;
16364
+ return bootstrapped !== undefined && bootstrapped !== sessionFile;
16365
+ }
16366
+ function registeredSubagent(context) {
16367
+ const registry2 = host.AgentRegistry?.global();
16368
+ if (registry2 === undefined)
16369
+ return;
16370
+ const sessionID = context.sessionManager.getSessionId();
16371
+ const sessionFile = context.sessionManager.getSessionFile();
16372
+ const own = registry2.list().filter((ref) => ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID);
16373
+ if (own.length === 0)
16374
+ return;
16375
+ return own.some((ref) => ref.kind !== "main");
16376
+ }
16377
+ function subagentSessionCheck() {
16378
+ let settled;
16379
+ return async (context) => {
16380
+ if (settled !== undefined)
16381
+ return settled;
16382
+ const answer = await isSubagentSession(context);
16383
+ if (answer.settled)
16384
+ settled = answer.subagent;
16385
+ return answer.subagent;
16386
+ };
16387
+ }
16388
+
16389
+ // extensions/legion.ts
16390
+ import { logger as logger3 } from "@oh-my-pi/pi-utils";
16391
+
16392
+ // src/capability-report.ts
16393
+ import { readdir, stat } from "fs/promises";
16394
+ import path3 from "path";
16395
+ var CAPABILITY_CHECK_TIMEOUT_MS = 8000;
16396
+ var CAPABILITY_REPORT_TIMEOUT_MS = 1e4;
16397
+ var WEB_SEARCH_PROBE_QUERY = "jujutsu version control";
16398
+ var ENVOY_TOOL_NAMES = [
16399
+ "envoy_subscribe",
16400
+ "envoy_unsubscribe",
16401
+ "envoy_list",
16402
+ "envoy_inbox",
16403
+ "envoy_send",
16404
+ "envoy_publish",
16405
+ "envoy_role_set",
16406
+ "envoy_role_get",
16407
+ "envoy_whoami",
16408
+ "envoy_sessions",
16409
+ "slack_post"
16410
+ ];
16411
+ var LIVE_CAPABILITIES = [
16412
+ "subagents",
16413
+ "web-search",
16414
+ "mcp",
16415
+ "repository-extensions",
16416
+ "dispatch-envoy-tools",
16417
+ "github"
16418
+ ];
16419
+ var DEFAULT_BUDGETS = {
16420
+ checkMs: CAPABILITY_CHECK_TIMEOUT_MS,
16421
+ reportMs: CAPABILITY_REPORT_TIMEOUT_MS
16422
+ };
16423
+ var ok = (detail) => ({ ok: true, detail });
16424
+ var failed = (detail) => ({ ok: false, detail });
16425
+ function firstLine(text) {
16426
+ return text.trim().split(`
16427
+ `, 1)[0] ?? "";
16428
+ }
16429
+ function commandFailure(result) {
16430
+ return firstLine(result.stderr) || `exit code ${result.code ?? "none"}`;
16431
+ }
16432
+ function whichOnPath(name) {
16433
+ return Bun.which(name, { PATH: process.env.PATH ?? "" }) ?? name;
16434
+ }
16435
+ var checkSubagents = async (host2, input) => {
16436
+ if (!input.agentsExposed)
16437
+ return failed("pi.agents is not exposed");
16438
+ if (!input.activeTools.includes("task"))
16439
+ return failed("task is not a registered tool");
16440
+ const [discovered, disabled] = await Promise.all([
16441
+ host2.discoverAgents(input.cwd),
16442
+ host2.disabledAgents()
16443
+ ]);
16444
+ const names = new Set(discovered.agents.map((agent) => agent.name));
16445
+ const missing = input.promptAgents.filter((agent) => !names.has(agent));
16446
+ const off = input.promptAgents.filter((agent) => disabled.includes(agent));
16447
+ const problems = [];
16448
+ if (missing.length > 0) {
16449
+ problems.push(`agents the prompts dispatch that the task tool does not resolve: ${missing.join(", ")}`);
16450
+ }
16451
+ if (off.length > 0)
16452
+ problems.push(`disabled by task.disabledAgents: ${off.join(", ")}`);
16453
+ if (problems.length > 0)
16454
+ return failed(problems.join("; "));
16455
+ return ok(`task resolves ${names.size} agents, every one the prompts dispatch`);
16456
+ };
16457
+ var checkWebSearch = async (host2, input, signal) => {
16458
+ if (!input.activeTools.includes("web_search")) {
16459
+ return failed("web_search is not a registered tool");
16460
+ }
16461
+ const started = Date.now();
16462
+ const result = await host2.runSearchQuery({ query: WEB_SEARCH_PROBE_QUERY, limit: 1 }, { sessionId: input.sessionId, signal });
16463
+ const elapsed = Date.now() - started;
16464
+ const details = result.details ?? {};
16465
+ if (details.error !== undefined && details.error !== null) {
16466
+ return failed(`web_search failed: ${String(details.error)}`);
16467
+ }
16468
+ const provider = details.response?.provider ?? details.provider;
16469
+ if (provider === undefined || provider === null || provider === "none") {
16470
+ return failed("web_search failed: no provider answered");
16471
+ }
16472
+ const text = result.content?.[0]?.text ?? "";
16473
+ if (text.startsWith("Error"))
16474
+ return failed(`web_search failed: ${firstLine(text)}`);
16475
+ return ok(`web_search registered; provider ${String(provider)} answered in ${elapsed} ms`);
16476
+ };
16477
+ function mcpErrorFor(errors3, name) {
16478
+ for (const entry of errors3) {
16479
+ if (typeof entry !== "object" || entry === null) {
16480
+ if (!String(entry).includes(name))
16481
+ continue;
16482
+ return String(entry);
16483
+ }
16484
+ const record2 = entry;
16485
+ const names = [record2.server, record2.name, record2.path];
16486
+ if (!names.some((value) => value === name || value === `mcp:${name}`))
16487
+ continue;
16488
+ const message = record2.error ?? record2.message;
16489
+ return message === undefined ? String(entry) : String(message);
16490
+ }
16491
+ return;
16492
+ }
16493
+ var checkMCP = async (host2, input, signal) => {
16494
+ const { configs, sources } = await host2.loadMCPConfigs(input.cwd);
16495
+ const configured = Object.keys(configs);
16496
+ if (configured.length === 0)
16497
+ return ok("no MCP server configured");
16498
+ const { manager, errors: errors3 } = await host2.discoverMCPServers(input.cwd);
16499
+ let torn;
16500
+ const tearDown = () => {
16501
+ torn ??= manager.disconnectAll().catch(() => {
16502
+ return;
16503
+ });
16504
+ return torn;
16505
+ };
16506
+ signal.addEventListener("abort", tearDown, { once: true });
16507
+ if (signal.aborted)
16508
+ tearDown();
16509
+ try {
16510
+ await manager.waitForPendingConnections();
16511
+ const connected = [];
16512
+ const unconnected = [];
16513
+ for (const name of configured) {
16514
+ const status = manager.getConnectionStatus(name);
16515
+ if (status === "connected") {
16516
+ const source = sources[name]?.path;
16517
+ connected.push(source === undefined ? name : `${name} (${path3.relative(input.cwd, source)})`);
16518
+ } else {
16519
+ unconnected.push(`${name} (${mcpErrorFor(errors3, name) ?? `status ${status}`})`);
16520
+ }
16521
+ }
16522
+ if (unconnected.length === 0)
16523
+ return ok(`connected: ${connected.join(", ")}`);
16524
+ const suffix = connected.length === 0 ? "" : `; connected: ${connected.join(", ")}`;
16525
+ return failed(`not connected: ${unconnected.join(", ")}${suffix}`);
16526
+ } finally {
16527
+ signal.removeEventListener("abort", tearDown);
16528
+ await tearDown();
16529
+ }
16530
+ };
16531
+ var EXTENSION_MODULE = /\.(?:ts|js|mjs|cjs)$/;
16532
+ var EXTENSION_INDEXES = ["index.ts", "index.js", "index.mjs", "index.cjs"];
16533
+ var SKILL_DIRECTORIES = [path3.join(".omp", "skills"), path3.join(".claude", "skills")];
16534
+ async function namesIn(directory) {
16535
+ try {
16536
+ return await readdir(directory);
16537
+ } catch (error48) {
16538
+ if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
16539
+ return [];
16540
+ throw error48;
16541
+ }
16542
+ }
16543
+ async function kindOf(file2) {
16544
+ try {
16545
+ const stats = await stat(file2);
16546
+ return stats.isFile() ? "file" : stats.isDirectory() ? "directory" : undefined;
16547
+ } catch (error48) {
16548
+ if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
16549
+ return;
16550
+ throw error48;
16551
+ }
16552
+ }
16553
+ async function repositoryEntries(cwd) {
16554
+ const entries = [];
16555
+ const extensions = path3.join(cwd, ".omp", "extensions");
16556
+ for (const name of await namesIn(extensions)) {
16557
+ const candidate = path3.join(extensions, name);
16558
+ const kind = await kindOf(candidate);
16559
+ if (kind === "file" && EXTENSION_MODULE.test(name)) {
16560
+ entries.push({ path: candidate, directory: false });
16561
+ } else if (kind === "directory") {
16562
+ const indexes = await Promise.all(EXTENSION_INDEXES.map((index) => kindOf(path3.join(candidate, index))));
16563
+ if (indexes.includes("file"))
16564
+ entries.push({ path: candidate, directory: true });
16565
+ }
16566
+ }
16567
+ for (const skills of SKILL_DIRECTORIES) {
16568
+ for (const name of await namesIn(path3.join(cwd, skills))) {
16569
+ const skill = path3.join(cwd, skills, name, "SKILL.md");
16570
+ if (await kindOf(skill) === "file")
16571
+ entries.push({ path: skill, directory: false });
16572
+ }
16573
+ }
16574
+ return entries;
16575
+ }
16576
+ var checkRepositoryExtensions = async (host2, input) => {
16577
+ const entries = await repositoryEntries(input.cwd);
16578
+ if (entries.length === 0) {
16579
+ return ok("the repository carries no .omp/extensions, .omp/skills or .claude/skills");
16580
+ }
16581
+ const [extensionPaths, loaded] = await Promise.all([
16582
+ host2.discoverExtensionPaths(input.cwd),
16583
+ host2.loadSkills(input.cwd)
16584
+ ]);
16585
+ const discovered = new Set([...extensionPaths, ...loaded.skills.map((skill) => skill.filePath)].map((file2) => path3.resolve(file2)));
16586
+ const isDiscovered = (entry) => {
16587
+ const resolved = path3.resolve(entry.path);
16588
+ if (discovered.has(resolved))
16589
+ return true;
16590
+ if (!entry.directory)
16591
+ return false;
16592
+ const prefix = resolved + path3.sep;
16593
+ return [...discovered].some((file2) => file2.startsWith(prefix));
16594
+ };
16595
+ const relative = (selected) => selected.map((entry) => path3.relative(input.cwd, entry.path)).sort().join(", ");
16596
+ const found = [];
16597
+ const lost = [];
16598
+ for (const entry of entries)
16599
+ (isDiscovered(entry) ? found : lost).push(entry);
16600
+ if (lost.length === 0)
16601
+ return ok(`discovered: ${relative(found)}`);
16602
+ const suffix = found.length === 0 ? "" : `; discovered: ${relative(found)}`;
16603
+ return failed(`not discovered: ${relative(lost)}${suffix}`);
16604
+ };
16605
+ var checkDispatchEnvoyTools = async (host2, input, signal) => {
16606
+ const missing = ENVOY_TOOL_NAMES.filter((tool) => !input.activeTools.includes(tool));
16607
+ if (missing.length > 0)
16608
+ return failed(`Envoy tools missing: ${missing.join(", ")}`);
16609
+ const result = await host2.run("dispatch", ["read", "--issue", input.issue], {
16610
+ cwd: input.cwd,
16611
+ signal
16612
+ });
16613
+ if (result.code !== 0)
16614
+ return failed(`dispatch read failed: ${commandFailure(result)}`);
16615
+ return ok(`${ENVOY_TOOL_NAMES.length} Envoy tools registered; dispatch read ${input.issue} answered from ${whichOnPath("dispatch")}`);
16616
+ };
16617
+ var checkGitHub = async (host2, input, signal) => {
16618
+ const result = await host2.run("gh", ["api", "graphql", "-f", "query={viewer{login}}", "--jq", ".data.viewer.login"], { cwd: input.cwd, signal });
16619
+ if (result.code !== 0)
16620
+ return failed(`gh viewer login failed: ${commandFailure(result)}`);
16621
+ return ok(`gh viewer login: ${result.stdout.trim()}`);
16622
+ };
16623
+ var CHECKS = {
16624
+ subagents: checkSubagents,
16625
+ "web-search": checkWebSearch,
16626
+ mcp: checkMCP,
16627
+ "repository-extensions": checkRepositoryExtensions,
16628
+ "dispatch-envoy-tools": checkDispatchEnvoyTools,
16629
+ github: checkGitHub
16630
+ };
16631
+ var MAX_DETAIL_BYTES = 1024;
16632
+ var HELD = "\uFFFF";
16633
+ var C1_INTRODUCER = "\uFFFE";
16634
+ var OPENER = "\uFDD0";
16635
+ var HELD_READINGS = [
16636
+ [/(?:\uffff\[|\ufffe)[0-?]*[!-)+,/-]*/g, OPENER],
16637
+ [/\ufdd0[@-~]?/g, HELD],
16638
+ [/\uffff/g, " "]
16639
+ ];
16640
+ var HELD_SEQUENCE = String.raw`\ufffe|\ufdd0|\uffff(?:\[[0-?]*[@-~])?`;
16641
+ var JWT_FIRST_HELD_SEQUENCE = String.raw`\ufffe|\uffff(?:\[[0-?]*[@-~])?`;
16642
+ function isSpace(code) {
16643
+ return code >= 9 && code <= 13 || code === 32 || code === 133 || code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288;
16644
+ }
16645
+ function truncateUtf8(text, max) {
16646
+ let bytes = 0;
16647
+ let end = 0;
16648
+ for (const char of text) {
16649
+ const code = char.codePointAt(0) ?? 0;
16650
+ bytes += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
16651
+ if (bytes > max)
16652
+ break;
16653
+ end += char.length;
16654
+ }
16655
+ return text.slice(0, end);
16656
+ }
16657
+ var CREDENTIAL_RULES = [
16658
+ [/(?<![a-z0-9+.-])([0-9+.-]*[a-z][a-z0-9+.-]*:\/\/)[^ /?#]+@/gi, "$1***@"],
16659
+ [
16660
+ /([?&](?:token|key|secret|password|access_token|accesstoken|refresh_token|id_token|auth_token|private_token|api_key|apikey|client_secret|sig|signature|x-amz-security-token|x-amz-signature|x-goog-signature)=)[^ &#"'<>]+/gi,
16661
+ "$1***"
16662
+ ],
16663
+ [
16664
+ /\b(password[ \ufffe\uffff]*=[ \ufffe\uffff]*)(?:(')(?:[^'\\]|\\.)*('?)|[^ &#"'<>]+)/gi,
16665
+ "$1$2***$3"
16666
+ ],
16667
+ [
16668
+ /(authorization\\?["']?[ \ufffe\uffff]*[:=][ \ufffe\uffff]*)(?:(\\["'](?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)[^"'\\]+|(["'](?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)(?:[^"'\\]|\\.)+|(\[?(?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)[^)\]}>]+)/gi,
16669
+ "$1$2$3$4***"
16670
+ ],
16671
+ [
16672
+ new RegExp(String.raw`((?:api-key|application-key|ocp-apim-subscription-key|x-subscription-token)\\?["']?[ \ufffe\uffff]*[:=][ \ufffe\uffff]*\\?["']?)(?:[A-Za-z0-9._~+/=*-]|${HELD_SEQUENCE})+`, "gi"),
16673
+ "$1***"
16674
+ ],
16675
+ [
16676
+ /(\\?["'](?:api_key|access_token)\\?["'][ \ufffe\uffff]*:[ \ufffe\uffff]*\\?["'])[^"'\\]+/gi,
16677
+ "$1***"
16678
+ ],
16679
+ [
16680
+ new RegExp(String.raw`\b(bearer[ \ufffe\uffff]+)(?:[A-Za-z0-9._~+/=*-]|${HELD_SEQUENCE})+`, "gi"),
16681
+ "$1***"
16682
+ ],
16683
+ [
16684
+ new RegExp(String.raw`\b(gh[pousr]_|github_pat_)(?:[A-Za-z0-9._-]|${HELD_SEQUENCE}){20,}`, "g"),
16685
+ "$1***"
16686
+ ],
16687
+ [
16688
+ new RegExp(String.raw`(?:(?=eyJ)(?<!\uffff\[[0-?]*(?:[@-~]?|\\u[0-9A-Fa-f]{4}))(?:(?<![A-Za-z0-9_\ufffe\uffff-])|(?<=%[0-9A-Fa-f]{2}|\\u[0-9A-Fa-f]{4}))eyJ(?:[A-Za-z0-9_-]|${JWT_FIRST_HELD_SEQUENCE}){8,}|(?<=\uffff)eyJ(?:[A-Za-z0-9_-]|\ufffe){8,}|(?=eyJ)(?<=\uffff\[[0-?]*(?:[@-~]|\\u[0-9A-Fa-f]{4})?)eyJ[A-Za-z0-9_-]*\ufffe(?:[A-Za-z0-9_-]|\ufffe)*)\.(?:[A-Za-z0-9_-]|${HELD_SEQUENCE}){8,}\.(?:[A-Za-z0-9_-]|${HELD_SEQUENCE})*`, "g"),
16689
+ "***"
16690
+ ],
16691
+ [
16692
+ new RegExp(String.raw`\b(sk-|dsp_|ddsat_|ddpat_|ddapp_|ya29\.|xox[a-z]-|glpat-|npm_)(?:[A-Za-z0-9_-]|${HELD_SEQUENCE}){20,}`, "g"),
16693
+ "$1***"
16694
+ ],
16695
+ [new RegExp(String.raw`\b(AIza)(?:[A-Za-z0-9_-]|${HELD_SEQUENCE}){35,}`, "g"), "$1***"],
16696
+ [new RegExp(String.raw`\b(AKIA|ASIA)(?:(?:${HELD_SEQUENCE})*[A-Z0-9]){16}\b`, "g"), "$1***"]
16697
+ ];
16698
+ var MAX_REDACTION_PASSES = 8;
16699
+ function redactCredentials(text) {
16700
+ let line = text;
16701
+ for (let pass = 0;pass < MAX_REDACTION_PASSES; pass++) {
16702
+ const next = CREDENTIAL_RULES.reduce((redacted, [pattern, replacement]) => redacted.replace(pattern, replacement), line);
16703
+ if (next === line)
16704
+ break;
16705
+ line = next;
16624
16706
  }
16707
+ return line;
16625
16708
  }
16626
-
16627
- class LegionDaemonContractError extends Error {
16628
- method;
16629
- path;
16630
- status;
16631
- constructor(method, path4, status, problem) {
16632
- super(`${method} ${path4} answered ${status} with a body the daemon API contract does not describe: ${problem}`);
16633
- this.method = method;
16634
- this.path = path4;
16635
- this.status = status;
16636
- this.name = "LegionDaemonContractError";
16709
+ function withoutLastCodePoint(text) {
16710
+ const last = text.charCodeAt(text.length - 1);
16711
+ return text.slice(0, last >= 56320 && last <= 57343 && text.length > 1 ? -2 : -1);
16712
+ }
16713
+ function cleanDetail(text) {
16714
+ let marked = "";
16715
+ for (const char of text) {
16716
+ const code = char.codePointAt(0) ?? 0;
16717
+ const control = code < 32 || code >= 127 && code < 160 || code === 65534 || code === 64976 || code === 65533;
16718
+ marked += isSpace(code) ? " " : code === 155 ? C1_INTRODUCER : control ? HELD : char;
16637
16719
  }
16720
+ let redacted = redactCredentials(marked);
16721
+ if (marked.includes(HELD) || marked.includes(C1_INTRODUCER)) {
16722
+ for (const [pattern, replacement] of HELD_READINGS) {
16723
+ const read = redacted.replace(pattern, replacement);
16724
+ if (read !== redacted)
16725
+ redacted = redactCredentials(read);
16726
+ }
16727
+ }
16728
+ const line = redacted.split(" ").filter((field) => field !== "").join(" ");
16729
+ let cut = truncateUtf8(line, MAX_DETAIL_BYTES).replace(/ +$/, "");
16730
+ if (cut.length < line.length) {
16731
+ while (redactCredentials(cut) !== cut) {
16732
+ const space = cut.lastIndexOf(" ");
16733
+ cut = space > 0 ? cut.slice(0, space) : withoutLastCodePoint(cut);
16734
+ }
16735
+ }
16736
+ return cut;
16638
16737
  }
16639
- function parseStrictly(schema, body) {
16640
- let json2;
16738
+ function budgetLabel(ms) {
16739
+ return ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
16740
+ }
16741
+ async function withinBudget(work, ms, onDeadline) {
16742
+ const deadline = Promise.withResolvers();
16743
+ const timer = setTimeout(() => deadline.resolve(onDeadline()), ms);
16641
16744
  try {
16642
- json2 = JSON.parse(body);
16643
- } catch (error48) {
16644
- return { problem: `not JSON (${messageFor(error48)})` };
16745
+ return await Promise.race([work, deadline.promise]);
16746
+ } finally {
16747
+ clearTimeout(timer);
16645
16748
  }
16646
- const parsed = schema.safeParse(json2);
16647
- if (parsed.success)
16648
- return { value: parsed.data };
16649
- return {
16650
- problem: parsed.error.issues.map((issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}`).join("; ")
16651
- };
16652
16749
  }
16653
- function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
16654
- const endpoint = baseUrl.replace(/\/+$/, "");
16655
- const call = async (method, path4, body) => {
16656
- const response = await fetchFn(`${endpoint}${path4}`, body === undefined ? undefined : {
16657
- method,
16658
- headers: { "Content-Type": "application/json" },
16659
- body: JSON.stringify(body)
16750
+ async function measureCapabilities(host2, input, budgets = DEFAULT_BUDGETS) {
16751
+ const started = Date.now();
16752
+ const measuredAt = new Date(started).toISOString();
16753
+ const outcomes = new Map;
16754
+ const controllers = [];
16755
+ const checks3 = LIVE_CAPABILITIES.map(async (name) => {
16756
+ const controller = new AbortController;
16757
+ controllers.push(controller);
16758
+ const outcome = await withinBudget(CHECKS[name](host2, input, controller.signal).catch((error48) => failed(firstLine(messageFor(error48)))), budgets.checkMs, () => {
16759
+ controller.abort();
16760
+ return failed(`did not finish within ${budgetLabel(budgets.checkMs)}`);
16660
16761
  });
16661
- const text = await response.text();
16662
- if (response.ok)
16663
- return { status: response.status, text };
16664
- const refusal = parseStrictly(LegionErrorResponse, text);
16665
- if ("value" in refusal) {
16666
- const code = "code" in refusal.value && typeof refusal.value.code === "string" ? refusal.value.code : undefined;
16667
- throw new LegionDaemonApiError(method, path4, response.status, code, refusal.value.error);
16668
- }
16669
- throw new LegionDaemonApiError(method, path4, response.status, undefined, `${JSON.stringify(text)} is not the daemon's refusal shape (${refusal.problem})`);
16670
- };
16671
- const read = async (method, path4, schema, body) => {
16672
- const { status, text } = await call(method, path4, body);
16673
- const parsed = parseStrictly(schema, text);
16674
- if ("problem" in parsed) {
16675
- throw new LegionDaemonContractError(method, path4, status, parsed.problem);
16676
- }
16677
- return parsed.value;
16678
- };
16679
- const post = (path4, request, response, body) => read("POST", path4, response, request.parse(body));
16680
- const acknowledge = async (path4, body) => {
16681
- const { status, text } = await call("POST", path4, body);
16682
- if (status !== 204 || text !== "") {
16683
- throw new LegionDaemonContractError("POST", path4, status, "the route answers 204 with no body");
16684
- }
16685
- };
16762
+ outcomes.set(name, outcome);
16763
+ });
16764
+ await withinBudget(Promise.all(checks3).then(() => {
16765
+ return;
16766
+ }), budgets.reportMs, () => {
16767
+ return;
16768
+ });
16769
+ for (const controller of controllers)
16770
+ controller.abort();
16771
+ const rows = LIVE_CAPABILITIES.map((name) => {
16772
+ const { ok: ok2, detail } = outcomes.get(name) ?? failed(`did not finish within ${budgetLabel(budgets.reportMs)}`);
16773
+ return { name, ok: ok2, detail: cleanDetail(detail) };
16774
+ });
16775
+ return { measuredAt, elapsedMs: Date.now() - started, rows };
16776
+ }
16777
+ async function runCommand2(command, args, options) {
16778
+ const child = Bun.spawn([command, ...args], {
16779
+ cwd: options.cwd,
16780
+ env: process.env,
16781
+ stdin: "ignore",
16782
+ stdout: "pipe",
16783
+ stderr: "pipe",
16784
+ signal: options.signal
16785
+ });
16786
+ const [stdout, stderr] = await Promise.all([
16787
+ new Response(child.stdout).text(),
16788
+ new Response(child.stderr).text(),
16789
+ child.exited
16790
+ ]);
16791
+ return { code: child.exitCode, stdout, stderr };
16792
+ }
16793
+ function ompCapabilityHost() {
16794
+ const hostSettings = async () => (await import("@oh-my-pi/pi-coding-agent/config/settings")).settings;
16686
16795
  return {
16687
- state: () => read("GET", "/legion/v1/state", LegionStateResponse),
16688
- register: (input) => read("POST", "/legion/v1/claims/register", LegionRegisterResponse, input),
16689
- registerController: (input) => read("POST", "/legion/v1/claims/register", LegionControllerRegisterResponse, input),
16690
- ready: (input) => acknowledge("/legion/v1/claims/ready", input),
16691
- exit: (input) => acknowledge("/legion/v1/claims/exit", input),
16692
- grant: (input) => post("/legion/v1/grants", LegionGrantRequest, LegionGrantResponse, input),
16693
- controllerGrant: (input) => post("/legion/v1/grants", LegionControllerGrantRequest, LegionGrantResponse, input),
16694
- handoffComplete: (input) => post("/legion/v1/handoff/complete", LegionHandoffCompleteRequest, LegionHandoffCompleteResponse, input),
16695
- issueStatus: (input) => post("/legion/v1/issues/status", LegionIssueStatusRequest, LegionEmptyResponse, input),
16696
- gateRegister: (input) => post("/legion/v1/gates/register", LegionGateRegisterRequest, LegionEmptyResponse, input),
16697
- waveRelease: (input) => post("/legion/v1/waves/release", LegionWaveReleaseRequest, LegionWaveReleaseResponse, input),
16698
- phaseBackward: (input) => post("/legion/v1/phase/backward", LegionPhaseBackwardRequest, LegionEmptyResponse, input),
16699
- phaseRetry: (input) => post("/legion/v1/phase/retry", LegionPhaseRetryRequest, LegionEmptyResponse, input),
16700
- signOff: (input) => post("/legion/v1/signoff", LegionSignOffRequest, LegionEmptyResponse, input),
16701
- rootClose: (input) => post("/legion/v1/roots/close", LegionRootCloseRequest, LegionEmptyResponse, input),
16702
- childPark: (input) => post("/legion/v1/children/park", LegionChildRequest, LegionEmptyResponse, input),
16703
- childRerun: (input) => post("/legion/v1/children/rerun", LegionChildRequest, LegionEmptyResponse, input)
16796
+ discoverAgents: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverAgents(cwd),
16797
+ disabledAgents: async () => {
16798
+ const [{ cfgTaskDisabledAgents }, settings] = await Promise.all([
16799
+ import("@oh-my-pi/pi-coding-agent/task/settings"),
16800
+ hostSettings()
16801
+ ]);
16802
+ return cfgTaskDisabledAgents.get(settings);
16803
+ },
16804
+ runSearchQuery: async (args, options) => (await import("@oh-my-pi/pi-coding-agent")).runSearchQuery(args, options),
16805
+ loadMCPConfigs: async (cwd) => (await import("@oh-my-pi/pi-coding-agent/mcp/config")).loadAllMCPConfigs(cwd),
16806
+ discoverMCPServers: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverMCPServers(cwd),
16807
+ discoverExtensionPaths: async (cwd) => {
16808
+ const [{ discoverExtensionPaths }, { cfgDisabledExtensions }, settings] = await Promise.all([
16809
+ import("@oh-my-pi/pi-coding-agent/extensibility/extensions/loader"),
16810
+ import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
16811
+ hostSettings()
16812
+ ]);
16813
+ return discoverExtensionPaths([], cwd, cfgDisabledExtensions.get(settings));
16814
+ },
16815
+ loadSkills: async (cwd) => {
16816
+ const [{ loadSkills }, { cfgDisabledExtensions, cfgSkills }, settings] = await Promise.all([
16817
+ import("@oh-my-pi/pi-coding-agent"),
16818
+ import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
16819
+ hostSettings()
16820
+ ]);
16821
+ return loadSkills({
16822
+ ...cfgSkills.get(settings),
16823
+ cwd,
16824
+ disabledExtensions: cfgDisabledExtensions.get(settings)
16825
+ });
16826
+ },
16827
+ run: runCommand2
16704
16828
  };
16705
16829
  }
16706
16830
 
16831
+ // ../pi-shared/src/legion-claim-core.ts
16832
+ async function bootLegionClaim(host2) {
16833
+ const { daemon } = host2;
16834
+ let claim;
16835
+ try {
16836
+ claim = await daemon.register(host2.registration);
16837
+ } catch (error48) {
16838
+ if (!(error48 instanceof LegionDaemonApiError))
16839
+ throw error48;
16840
+ host2.log(`claims/register registration failed (${error48.status}): ${error48.detail}`);
16841
+ if (error48.status < 400 || error48.status >= 500)
16842
+ throw error48;
16843
+ host2.stop();
16844
+ return;
16845
+ }
16846
+ const { measure } = host2;
16847
+ const measuring = measure === undefined ? undefined : Promise.resolve().then(() => measure(claim)).catch((error48) => {
16848
+ host2.log(`capability measurement failed; ready carries no report: ${messageFor(error48)}`);
16849
+ return;
16850
+ });
16851
+ try {
16852
+ await host2.afterRegister?.(claim);
16853
+ host2.setCapability({
16854
+ sessionID: host2.registration.sessionId,
16855
+ tree: claim.tree,
16856
+ issue: claim.issue,
16857
+ role: claim.role,
16858
+ secret: claim.secret
16859
+ });
16860
+ await host2.claimRole(claim.claimToken);
16861
+ const capabilities = await measuring;
16862
+ const ready = {
16863
+ claimToken: claim.claimToken,
16864
+ sessionId: host2.registration.sessionId,
16865
+ secret: claim.secret,
16866
+ generation: claim.generation,
16867
+ ...capabilities === undefined ? {} : { capabilities }
16868
+ };
16869
+ await callReadyWithRetry("claims/ready", () => daemon.ready(ready));
16870
+ host2.onRoleRegained(async (role, reason) => {
16871
+ if (role !== claim.claimToken)
16872
+ return;
16873
+ try {
16874
+ await callReadyWithRetry("claims/ready after role regain", () => daemon.ready(ready));
16875
+ host2.log(`re-ran claims/ready after role ${role} was ${reason}`);
16876
+ } catch (error48) {
16877
+ host2.log(`claims/ready after role ${role} was ${reason} failed; the daemon's queued task stays undelivered until the next regain or boot: ${messageFor(error48)}`);
16878
+ }
16879
+ });
16880
+ } catch (error48) {
16881
+ host2.log(`claim boot failed after claims/register registered ${claim.claimToken}; ${host2.stopping}: ${messageFor(error48)}`);
16882
+ host2.stop();
16883
+ }
16884
+ }
16885
+ // package.json
16886
+ var package_default = {
16887
+ name: "@sjawhar/pi-legion",
16888
+ version: "10.13.0",
16889
+ license: "Apache-2.0",
16890
+ type: "module",
16891
+ omp: {
16892
+ extensions: [
16893
+ "dist/legion.js"
16894
+ ],
16895
+ skills: [
16896
+ "dist/skills"
16897
+ ]
16898
+ },
16899
+ legion: {
16900
+ daemonApiVersion: 32
16901
+ },
16902
+ repository: {
16903
+ type: "git",
16904
+ url: "https://github.com/sjawhar/legion"
16905
+ },
16906
+ files: [
16907
+ "dist",
16908
+ "agents"
16909
+ ],
16910
+ scripts: {
16911
+ build: "bun build extensions/legion.ts --outdir dist --target bun --format esm --external @oh-my-pi/pi-coding-agent --external @oh-my-pi/pi-tui --external @oh-my-pi/pi-utils",
16912
+ prepack: "../../scripts/pi-plugin-prepack.sh",
16913
+ postpack: "rm -rf dist/skills",
16914
+ typecheck: "bunx tsc --noEmit",
16915
+ test: "bun test",
16916
+ lint: "bunx biome lint extensions/ src/"
16917
+ },
16918
+ devDependencies: {
16919
+ "@biomejs/biome": "^2.3.14",
16920
+ "@legion/contracts": "workspace:*",
16921
+ "@legion/envoy-client": "workspace:*",
16922
+ "@legion/pi-shared": "workspace:*",
16923
+ "@oh-my-pi/pi-utils": "^18.6.0",
16924
+ "@types/bun": "latest",
16925
+ typescript: "^5.3.0",
16926
+ zod: "4.3.6"
16927
+ }
16928
+ };
16929
+
16930
+ // src/classify.ts
16931
+ function classifySession(env) {
16932
+ if (env.LEGION_CONTROLLER !== undefined && env.LEGION_TREE !== undefined) {
16933
+ throw new Error("Legion session has both controller and tree launch markers");
16934
+ }
16935
+ if (env.LEGION_CONTROLLER === "1")
16936
+ return { kind: "controller" };
16937
+ if (env.LEGION_ROLE === undefined)
16938
+ return { kind: "not-legion" };
16939
+ if (!isLegionRole(env.LEGION_ROLE)) {
16940
+ throw new Error(`LEGION_ROLE "${env.LEGION_ROLE}" is not a Legion role`);
16941
+ }
16942
+ const tree = requiredEnvironment(env, "LEGION_TREE");
16943
+ const issue2 = requiredEnvironment(env, "LEGION_ISSUE");
16944
+ if (env.LEGION_ROLE === "architect" && issue2 === tree) {
16945
+ return { kind: "root-architect", tree };
16946
+ }
16947
+ return { kind: "phase-worker", role: env.LEGION_ROLE, tree, issue: issue2 };
16948
+ }
16949
+ function requiredEnvironment(env, key) {
16950
+ const value = env[key];
16951
+ if (!value)
16952
+ throw new Error(`${key} is required for Legion`);
16953
+ return value;
16954
+ }
16955
+ function requiredSecret(env, key) {
16956
+ const fileKey = `${key}_FILE`;
16957
+ const file2 = env[fileKey];
16958
+ if (file2 !== undefined)
16959
+ return readSecretFile(fileKey, file2);
16960
+ return requiredEnvironment(env, key);
16961
+ }
16962
+ function requiredControllerCapability(env) {
16963
+ if (env.LEGION_CONTROLLER_SECRET_FILE !== undefined) {
16964
+ return readSecretFile("LEGION_CONTROLLER_SECRET_FILE", env.LEGION_CONTROLLER_SECRET_FILE);
16965
+ }
16966
+ const secret = env.LEGION_CONTROLLER_SECRET;
16967
+ if (!secret) {
16968
+ throw new Error("LEGION_CONTROLLER_SECRET or LEGION_CONTROLLER_SECRET_FILE is required to claim the controller. " + "Launch OMP with one of them in its environment before running /legion-claim-controller.");
16969
+ }
16970
+ return secret;
16971
+ }
16972
+
16707
16973
  // src/jj-attribution.ts
16708
16974
  import { mkdir, rename, writeFile } from "fs/promises";
16709
16975
  import path4 from "path";
@@ -16783,95 +17049,35 @@ commit_trailers = '"Omp-Session: ${id}"'
16783
17049
  }
16784
17050
 
16785
17051
  // src/claim-session.ts
16786
- var READY_RETRY_ATTEMPTS = 3;
16787
- var READY_RETRY_DELAY_MS = 1000;
16788
- function isNetworkOrTimeoutError(error48) {
16789
- if (!(error48 instanceof Error))
16790
- return false;
16791
- if (error48.name === "NetworkError" || error48.name === "TimeoutError")
16792
- return true;
16793
- const code = "code" in error48 && typeof error48.code === "string" ? error48.code : undefined;
16794
- return code === "ConnectionRefused" || code === "ConnectionTimeout" || code === "EAI_AGAIN" || code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EHOSTUNREACH" || code === "ENETUNREACH" || code === "ENOTFOUND" || code === "ETIMEDOUT" || error48 instanceof TypeError && (error48.message === "Failed to fetch" || error48.message === "fetch failed");
16795
- }
16796
- function exitOnRegistrationRefusal(error48, exitProcess) {
16797
- if (error48 instanceof LegionDaemonApiError) {
16798
- console.error(`[legion] claims/register registration failed (${error48.status}): ${error48.detail}`);
16799
- if (error48.status >= 400 && error48.status < 500)
16800
- exitProcess(1);
16801
- }
16802
- throw error48;
16803
- }
16804
- async function callReadyWithRetry(label, call) {
16805
- for (let attempt = 1;; attempt++) {
16806
- try {
16807
- await call();
16808
- return;
16809
- } catch (error48) {
16810
- const retryable = error48 instanceof LegionDaemonApiError ? error48.status >= 500 && error48.status < 600 : isNetworkOrTimeoutError(error48);
16811
- if (!retryable)
16812
- throw error48;
16813
- if (attempt === READY_RETRY_ATTEMPTS) {
16814
- console.error(`[legion] ${label} failed after ${attempt} attempts: ${messageFor(error48)}`);
16815
- throw error48;
16816
- }
16817
- console.error(`[legion] ${label} failed (attempt ${attempt}/${READY_RETRY_ATTEMPTS}), retrying: ${messageFor(error48)}`);
16818
- const retryDelay = Promise.withResolvers();
16819
- setTimeout(retryDelay.resolve, READY_RETRY_DELAY_MS);
16820
- await retryDelay.promise;
16821
- }
16822
- }
16823
- }
16824
17052
  function createClaimSession(deps) {
16825
17053
  let capability;
16826
17054
  let booting;
16827
17055
  const boot = async (context, sessionID, bootToken, daemon) => {
16828
17056
  const { sessionFile, agentId } = await deps.persistedTranscript(context);
16829
17057
  recordBootstrappedSession(sessionFile);
16830
- const claim = await daemon.register({
16831
- bootToken,
16832
- sessionId: sessionID,
16833
- ompSessionFile: sessionFile,
16834
- agentId,
16835
- pluginContract: package_default.legion.daemonApiVersion
16836
- }).catch((error48) => exitOnRegistrationRefusal(error48, deps.exitProcess));
16837
- const measuring = Promise.resolve().then(() => deps.measureCapabilities(context, { issue: claim.issue, promptAgents: claim.promptAgents })).catch((error48) => {
16838
- console.error(`[legion] capability measurement failed; ready carries no report: ${messageFor(error48)}`);
16839
- return;
16840
- });
16841
- try {
16842
- const stateDir = requiredEnvironment(process.env, "LEGION_STATE_DIR");
16843
- await exportJjSessionAttribution(sessionFile, stateDir);
16844
- capability = {
16845
- sessionID,
16846
- tree: claim.tree,
16847
- issue: claim.issue,
16848
- role: claim.role,
16849
- secret: claim.secret
16850
- };
16851
- await claimEnvoyRole(sessionID, claim.claimToken, context);
16852
- const capabilities = await measuring;
16853
- const ready = {
16854
- claimToken: claim.claimToken,
17058
+ await bootLegionClaim({
17059
+ daemon,
17060
+ registration: {
17061
+ bootToken,
16855
17062
  sessionId: sessionID,
16856
- secret: claim.secret,
16857
- generation: claim.generation,
16858
- ...capabilities === undefined ? {} : { capabilities }
16859
- };
16860
- await callReadyWithRetry("claims/ready", () => daemon.ready(ready));
16861
- onEnvoyRoleRegained(async (role, reason) => {
16862
- if (role !== claim.claimToken)
16863
- return;
16864
- try {
16865
- await callReadyWithRetry("claims/ready after role regain", () => daemon.ready(ready));
16866
- console.error(`[legion] re-ran claims/ready after role ${role} was ${reason}`);
16867
- } catch (error48) {
16868
- console.error(`[legion] claims/ready after role ${role} was ${reason} failed; the daemon's queued task stays undelivered until the next regain or boot: ${messageFor(error48)}`);
16869
- }
16870
- });
16871
- } catch (error48) {
16872
- console.error(`[legion] claim boot failed after claims/register registered ${claim.claimToken}; exiting so the daemon launches it again: ${messageFor(error48)}`);
16873
- deps.exitProcess(1);
16874
- }
17063
+ ompSessionFile: sessionFile,
17064
+ agentId,
17065
+ pluginContract: package_default.legion.daemonApiVersion
17066
+ },
17067
+ measure: (claim) => deps.measureCapabilities(context, {
17068
+ issue: claim.issue,
17069
+ promptAgents: claim.promptAgents
17070
+ }),
17071
+ afterRegister: () => exportJjSessionAttribution(sessionFile, requiredEnvironment(process.env, "LEGION_STATE_DIR")),
17072
+ setCapability: (registered) => {
17073
+ capability = registered;
17074
+ },
17075
+ claimRole: (role) => claimEnvoyRole(sessionID, role, context),
17076
+ onRoleRegained: onEnvoyRoleRegained,
17077
+ log: (line) => console.error(`[legion] ${line}`),
17078
+ stopping: "exiting so the daemon launches it again",
17079
+ stop: () => deps.exitProcess(1)
17080
+ });
16875
17081
  };
16876
17082
  const bootstrap = async (context) => {
16877
17083
  const sessionID = context.sessionManager.getSessionId();
@@ -17017,131 +17223,6 @@ function createControllerSession(deps) {
17017
17223
  return { claim, handleSessionStart, reclaimAfterSessionChange, isClaimedSession, mintGrant };
17018
17224
  }
17019
17225
 
17020
- // src/handoff-commit.ts
17021
- import { execFile as execFile2 } from "child_process";
17022
- import { promisify as promisify2 } from "util";
17023
-
17024
- // src/git-lock.ts
17025
- import { lstat } from "fs/promises";
17026
- var GIT_LOCK_REPORTS = [
17027
- /The lockfile at '([^']+)' might need manual deletion/g,
17028
- /Unable to create '([^']+)': File exists/g
17029
- ];
17030
- function age(ms) {
17031
- const total = Math.round(Math.abs(ms) / 1000);
17032
- const sign = ms < 0 && total > 0 ? "-" : "";
17033
- const hours = Math.floor(total / 3600);
17034
- const minutes = Math.floor(total % 3600 / 60);
17035
- const seconds = total % 60;
17036
- if (hours > 0)
17037
- return `${sign}${hours}h${minutes}m${seconds}s`;
17038
- if (minutes > 0)
17039
- return `${sign}${minutes}m${seconds}s`;
17040
- return `${sign}${seconds}s`;
17041
- }
17042
- async function staleGitLockNote(output, now = Date.now()) {
17043
- const locks = new Set;
17044
- for (const report of GIT_LOCK_REPORTS) {
17045
- for (const match of output.matchAll(report))
17046
- locks.add(match[1]);
17047
- }
17048
- const notes = [];
17049
- for (const lock of locks) {
17050
- const written = await lstat(lock).then((info) => info.mtimeMs, () => {
17051
- return;
17052
- });
17053
- if (written === undefined)
17054
- continue;
17055
- notes.push(`git lock ${lock} was last written ${age(now - written)} ago and blocks this command. Do not remove it while the issue's pod runs: another role's git, which this container cannot see, may hold it. Replacing the issue's pod removes it. Under the tmux runtime nothing removes it: remove it by hand only once no git or jj process on the host is using the repository.`);
17056
- }
17057
- return notes.join(`
17058
- `);
17059
- }
17060
-
17061
- // src/handoff-commit.ts
17062
- var FILE_BACKED = {
17063
- planning: { word: "plan", role: "planner" },
17064
- implementing: { word: "implement", role: "implementer" },
17065
- testing: { word: "test", role: "tester" },
17066
- reviewing: { word: "review", role: "reviewer" }
17067
- };
17068
- var execFileAsync2 = promisify2(execFile2);
17069
- var pathJj = async (args, cwd) => {
17070
- try {
17071
- const { stdout } = await execFileAsync2("jj", ["-R", cwd, ...args], {
17072
- cwd,
17073
- maxBuffer: 16 * 1024 * 1024
17074
- });
17075
- return stdout.trim();
17076
- } catch (error48) {
17077
- const detail = error48 !== null && typeof error48 === "object" && "stderr" in error48 ? String(error48.stderr).trim() : error48 instanceof Error ? error48.message : String(error48);
17078
- const note = await staleGitLockNote(detail);
17079
- throw new Error(`jj ${args.join(" ")} failed: ${detail}${note === "" ? "" : `
17080
- ${note}`}`);
17081
- }
17082
- };
17083
- function requiredVariable(env, name) {
17084
- const value = env[name];
17085
- if (value === undefined || value === "") {
17086
- throw new Error(`${name} is not set, so handoff_complete cannot find the commit to report`);
17087
- }
17088
- return value;
17089
- }
17090
- async function findHandoffCommit(input) {
17091
- const { phase, role, env } = input;
17092
- const jj = input.jj ?? pathJj;
17093
- const workspace = requiredVariable(env, "LEGION_WORKSPACE");
17094
- const handoff = FILE_BACKED[phase];
17095
- if (handoff === undefined || handoff.role !== role) {
17096
- return jj(["log", "-r", "@-", "--no-graph", "-T", "commit_id"], workspace);
17097
- }
17098
- const issue2 = requiredVariable(env, "LEGION_ISSUE");
17099
- if (!DISPATCH_KEY_PATTERN.test(issue2)) {
17100
- throw new Error(`LEGION_ISSUE ${JSON.stringify(issue2)} is not a Dispatch issue key like LEGION-1`);
17101
- }
17102
- const file2 = `.legion/${issue2}/${handoff.word}.json`;
17103
- const fileset = `root:${JSON.stringify(file2)}`;
17104
- if (await jj(["diff", "-r", "@", "--name-only", fileset], workspace) !== "") {
17105
- throw new Error(`${file2} has changes in the working copy that are not committed: commit this phase's handoff before completing`);
17106
- }
17107
- if (await jj(["file", "list", "-r", "@", fileset], workspace) === "") {
17108
- throw new Error(`${file2} is missing from the workspace: write this phase's handoff, then commit and push it before completing`);
17109
- }
17110
- const carrying = await jj([
17111
- "log",
17112
- "-r",
17113
- `latest((::@- ~ ::trunk()) & files(${fileset}))`,
17114
- "--no-graph",
17115
- "-T",
17116
- "commit_id"
17117
- ], workspace);
17118
- if (carrying === "") {
17119
- throw new Error(`${file2} is not committed on this issue's branch (only the base branch carries it): write and commit this phase's handoff`);
17120
- }
17121
- const user = env.JJ_USER;
17122
- if (user !== undefined && user !== "") {
17123
- const author = await jj(["log", "-r", carrying, "--no-graph", "-T", 'author.name() ++ "\\n" ++ author.email()'], workspace);
17124
- const [name = "", email3 = ""] = author.split(`
17125
- `, 2);
17126
- if (name !== user || email3 !== (env.JJ_EMAIL ?? "")) {
17127
- throw new Error(`${file2} is carried by commit ${carrying}, authored by ${name} <${email3}>, not this pane's ${user}: run jj new, then write and commit this phase's handoff again`);
17128
- }
17129
- }
17130
- const bookmark = `legion/${issue2}`;
17131
- const pushed = await jj([
17132
- "log",
17133
- "-r",
17134
- `${carrying} & ::remote_bookmarks(exact:${JSON.stringify(bookmark)}, exact:"origin")`,
17135
- "--no-graph",
17136
- "-T",
17137
- "commit_id"
17138
- ], workspace);
17139
- if (pushed === "") {
17140
- throw new Error(`${file2} is carried by commit ${carrying}, which is not on ${bookmark}@origin: push the issue branch, then complete again`);
17141
- }
17142
- return carrying;
17143
- }
17144
-
17145
17226
  // src/phase-stall.ts
17146
17227
  var PHASE_STALL_ENTRY = "legion-phase-stall";
17147
17228
  var WAITING_REPLY = /^\W*WAITING\b/m;
@@ -17223,6 +17304,115 @@ async function applySessionTitle(pi, context, title) {
17223
17304
  await pi.setSessionName(title);
17224
17305
  }
17225
17306
 
17307
+ // ../pi-shared/src/legion-phase-ops.ts
17308
+ var PHASE_OPS = ["read_record", "handoff_complete", "request_backward_move"];
17309
+ function isPhaseOp(op) {
17310
+ return PHASE_OPS.includes(op);
17311
+ }
17312
+ var requiredString = (op, field) => {
17313
+ const message = `${op} requires ${field}`;
17314
+ return exports_external.string({ error: message }).refine((value) => value.trim() !== "", { error: message });
17315
+ };
17316
+ var operation = (op, shape) => exports_external.strictObject({ op: exports_external.literal(op), ...shape }, {
17317
+ error: (issue2) => issue2.code === "unrecognized_keys" ? issue2.keys.map((key) => `${op} does not accept field "${key}"`).join("; ") : undefined
17318
+ });
17319
+ var PhaseOpArguments = exports_external.discriminatedUnion("op", [
17320
+ operation("read_record", { issue: requiredString("read_record", "issue") }),
17321
+ operation("handoff_complete", {
17322
+ summary: requiredString("handoff_complete", "summary"),
17323
+ verdict: exports_external.enum(["pass", "fail"], { error: "handoff_complete's verdict is pass or fail" }).optional(),
17324
+ ready: exports_external.boolean({ error: "handoff_complete's ready is true or false" }).optional()
17325
+ }),
17326
+ operation("request_backward_move", {
17327
+ to: exports_external.enum(LEGION_PHASE_BACKWARD_TARGETS, {
17328
+ error: (issue2) => typeof issue2.input === "string" && issue2.input.trim() !== "" ? `request_backward_move's to is one of ${LEGION_PHASE_BACKWARD_TARGETS.join(", ")}` : "request_backward_move requires to"
17329
+ }),
17330
+ reason: requiredString("request_backward_move", "reason")
17331
+ })
17332
+ ], {
17333
+ error: (issue2) => issue2.code === "invalid_union" ? `op is one of ${PHASE_OPS.join(", ")}` : undefined
17334
+ });
17335
+ function phaseOpParameters(zod) {
17336
+ const api2 = zodSchemaApi(zod);
17337
+ const fields = {};
17338
+ for (const option of PhaseOpArguments.options) {
17339
+ for (const [name, declared] of Object.entries(option.shape)) {
17340
+ if (name === "op")
17341
+ continue;
17342
+ const field = declared instanceof exports_external.ZodOptional ? declared.unwrap() : declared;
17343
+ if (field instanceof exports_external.ZodString)
17344
+ fields[name] = api2.string().optional();
17345
+ else if (field instanceof exports_external.ZodBoolean)
17346
+ fields[name] = api2.boolean().optional();
17347
+ else if (field instanceof exports_external.ZodEnum) {
17348
+ const [first, ...rest] = field.options.map(String);
17349
+ if (first === undefined)
17350
+ throw new Error(`phase op field ${name} has no values`);
17351
+ fields[name] = api2.enum([first, ...rest]).optional();
17352
+ } else
17353
+ throw new Error(`phase op field ${name} has a kind phaseOpParameters does not map`);
17354
+ }
17355
+ }
17356
+ const byField = fields;
17357
+ return byField;
17358
+ }
17359
+ function legionToolDescription(host2) {
17360
+ return "Perform the workflow operation the Legion daemon assigned this role. The daemon advances phases; this tool cannot spawn workers. " + (host2.before ?? "") + "handoff_complete (phase workers and sub-architects; the daemon accepts a completion only from the role working the issue's current phase) " + "reports this phase complete: `summary` (two sentences for the architect, or the merger's READY packet), `verdict` pass|fail from the tester alone " + "(the daemon refuses a verdict from any other role), `ready: true` for the merger's READY. A handoff is a committed file, `.legion/<issue>/<phase>.json`: the pushed commit " + "carrying it is found here, with the pane's jj, and reported, so write it, commit it and push the branch (`jj git push`) before calling; this call " + "refuses, posting nothing, a handoff still in the working copy, missing, not committed on this branch, carried by another pane's commit, or not yet " + "on `legion/<issue>@origin`, each naming the remedy. The daemon refuses HANDOFF_NOT_NEW (the commit is the one you reported for your previous " + "phase: write and commit this phase's handoff) and, for READY, each refusal the merger's role names; a refused completion changed " + "nothing, so fix what it names and complete again. An accepted READY whose `note` begins MANUAL_MERGE_REQUIRED: is one a person merges: arm nothing. " + (host2.after ?? "") + `What a later phase needs goes in your handoff; a question for another live role goes to its role topic with ${host2.envoyPublish}.`;
17361
+ }
17362
+ async function mintClaimGrant(daemon, claim) {
17363
+ const { grantId } = await daemon.grant({
17364
+ sessionId: claim.sessionID,
17365
+ secret: claim.secret,
17366
+ tree: claim.tree,
17367
+ issue: claim.issue
17368
+ });
17369
+ return grantId;
17370
+ }
17371
+ function recordFrom(state, issue2) {
17372
+ const record2 = state.issues[issue2];
17373
+ if (record2 === undefined)
17374
+ throw new Error(`The daemon has no record for ${issue2}`);
17375
+ return record2;
17376
+ }
17377
+ var ROOT_COMPLETION_REFUSAL = "handoff_complete is not available to a root architect session, which runs no phase";
17378
+ async function executePhaseOp(runtime, input) {
17379
+ const { daemon, claim } = runtime;
17380
+ const parsed = PhaseOpArguments.safeParse(input);
17381
+ const issues = parsed.success ? [] : parsed.error.issues;
17382
+ const unknownFields = issues.filter((issue2) => issue2.code === "unrecognized_keys");
17383
+ const fieldProblems = issues.filter((issue2) => issue2.code !== "unrecognized_keys");
17384
+ const rootCompletion = typeof input === "object" && input !== null && "op" in input && input.op === "handoff_complete" && claim.role === "architect" && claim.issue === claim.tree;
17385
+ if (rootCompletion) {
17386
+ throw new Error([...unknownFields.map((issue2) => issue2.message), ROOT_COMPLETION_REFUSAL].join("; "));
17387
+ }
17388
+ if (!parsed.success) {
17389
+ throw new Error([...unknownFields, ...fieldProblems].map((issue2) => issue2.message).join("; "));
17390
+ }
17391
+ const args = parsed.data;
17392
+ switch (args.op) {
17393
+ case "read_record":
17394
+ return { record: recordFrom(await daemon.state(), args.issue) };
17395
+ case "handoff_complete": {
17396
+ const { phase } = recordFrom(await daemon.state(), claim.issue);
17397
+ const commit = await runtime.handoffCommit(phase, claim.role);
17398
+ return daemon.handoffComplete({
17399
+ grantId: await mintClaimGrant(daemon, claim),
17400
+ summary: args.summary,
17401
+ verdict: args.verdict ?? "",
17402
+ ready: args.ready ?? false,
17403
+ commit
17404
+ });
17405
+ }
17406
+ case "request_backward_move":
17407
+ await daemon.phaseBackward({
17408
+ grantId: await mintClaimGrant(daemon, claim),
17409
+ to: args.to,
17410
+ reason: args.reason
17411
+ });
17412
+ return {};
17413
+ }
17414
+ }
17415
+
17226
17416
  // ../pi-shared/src/tool-result.ts
17227
17417
  function toolSuccess(text, details = {}, images = []) {
17228
17418
  return { content: [{ type: "text", text }, ...imageBlocks(images)], details };
@@ -17251,14 +17441,11 @@ var OPERATIONS = {
17251
17441
  var OPERATION_FIELDS = {
17252
17442
  register_gate: ["issue", "artifactId", "version"],
17253
17443
  release_children: ["issues"],
17254
- request_backward_move: ["to", "reason"],
17255
17444
  retry_or_escalate: ["issue", "decision"],
17256
17445
  sign_off: ["issue"],
17257
17446
  close_root: ["issue", "reason"],
17258
17447
  park_child: ["issue"],
17259
17448
  rerun_child: ["issue"],
17260
- read_record: ["issue"],
17261
- handoff_complete: ["summary", "verdict", "ready"],
17262
17449
  read_state: [],
17263
17450
  set_status: ["issue", "status"]
17264
17451
  };
@@ -17266,137 +17453,93 @@ var ISSUE_STATUSES2 = ["todo", "backlog", "icebox"];
17266
17453
  var jsonSuccess = (details) => toolSuccess(JSON.stringify(details), details);
17267
17454
  function toolSchema(pi) {
17268
17455
  const z2 = pi.zod;
17456
+ const phase = phaseOpParameters(z2);
17269
17457
  return z2.object({
17270
- op: z2.enum([
17271
- "register_gate",
17272
- "release_children",
17273
- "request_backward_move",
17274
- "retry_or_escalate",
17275
- "sign_off",
17276
- "close_root",
17277
- "park_child",
17278
- "rerun_child",
17279
- "read_record",
17280
- "handoff_complete",
17281
- "read_state",
17282
- "set_status"
17283
- ]),
17284
- issue: z2.string().optional(),
17458
+ op: z2.enum([...OPERATIONS.architect, ...OPERATIONS.controller]),
17459
+ issue: phase.issue,
17285
17460
  artifactId: z2.string().describe("register_gate's root spec document: its artifact id, slug, or filename, as a dispatch command's --artifact takes it").optional(),
17286
17461
  version: z2.number().optional(),
17287
17462
  issues: z2.array(z2.string()).optional(),
17288
- to: z2.enum(LEGION_PHASE_BACKWARD_TARGETS).optional(),
17289
- reason: z2.string().optional(),
17463
+ to: phase.to,
17464
+ reason: phase.reason,
17290
17465
  decision: z2.enum(["retry", "escalate"]).optional(),
17291
- summary: z2.string().optional(),
17292
- verdict: z2.enum(["pass", "fail"]).optional(),
17293
- ready: z2.boolean().optional(),
17466
+ summary: phase.summary,
17467
+ verdict: phase.verdict,
17468
+ ready: phase.ready,
17294
17469
  status: z2.enum(ISSUE_STATUSES2).optional()
17295
17470
  });
17296
17471
  }
17297
- function requiredString(parameters, operation, name) {
17472
+ function requiredString2(parameters, operation2, name) {
17298
17473
  const value = parameters[name];
17299
17474
  if (typeof value !== "string" || value.trim() === "")
17300
- throw new Error(`${operation} requires ${name}`);
17475
+ throw new Error(`${operation2} requires ${name}`);
17301
17476
  return value;
17302
17477
  }
17303
- function assertOperationInput(parameters, operation) {
17304
- const fields = OPERATION_FIELDS[operation];
17478
+ function assertOperationInput(parameters, operation2) {
17479
+ const fields = OPERATION_FIELDS[operation2];
17305
17480
  if (fields === undefined)
17306
- throw new Error(`Unsupported legion operation: ${operation}`);
17481
+ throw new Error(`Unsupported legion operation: ${operation2}`);
17307
17482
  for (const name of Object.keys(parameters)) {
17308
17483
  if (name !== "op" && !fields.includes(name)) {
17309
- throw new Error(`${operation} does not accept field "${name}"`);
17484
+ throw new Error(`${operation2} does not accept field "${name}"`);
17310
17485
  }
17311
17486
  }
17312
17487
  }
17313
- function recordFrom(state, issue2) {
17314
- const record2 = state.issues[issue2];
17315
- if (record2 === undefined)
17316
- throw new Error(`The daemon has no record for ${issue2}`);
17317
- return record2;
17318
- }
17319
17488
  function createLegionTool(deps) {
17320
17489
  const { pi, daemon, session, controllerGrant, onPhaseCompleted, handoffCommit, resolveDocument } = deps;
17321
- const grantFor = async (client, active) => (await client.grant({
17322
- sessionId: active.sessionId,
17323
- secret: active.secret,
17324
- tree: active.tree,
17325
- issue: active.issue
17326
- })).grantId;
17327
17490
  return {
17328
17491
  name: "legion",
17329
17492
  label: "legion",
17330
- description: "Perform the workflow operation the Legion daemon assigned this role. The daemon advances phases; this tool cannot spawn workers. " + "handoff_complete (phase workers and sub-architects; the daemon accepts a completion only from the role working the issue's current phase) " + "reports this phase complete: `summary` (two sentences for the architect, or the merger's READY packet), `verdict` pass|fail from the tester alone " + "(the daemon refuses a verdict from any other role), `ready: true` for the merger's READY. A handoff is a committed file, `.legion/<issue>/<phase>.json`: the pushed commit " + "carrying it is found here, with the pane's jj, and reported, so write it, commit it and push the branch (`jj git push`) before calling; this call " + "refuses, posting nothing, a handoff still in the working copy, missing, not committed on this branch, carried by another pane's commit, or not yet " + "on `legion/<issue>@origin`, each naming the remedy. The daemon refuses HANDOFF_NOT_NEW (the commit is the one you reported for your previous " + "phase: write and commit this phase's handoff) and, for READY, each refusal the merger's role names; a refused completion changed " + "nothing, so fix what it names and complete again. An accepted READY whose `note` begins MANUAL_MERGE_REQUIRED: is one a person merges: arm nothing. " + "The controller's read_state returns the daemon's whole state and set_status moves an issue to todo, backlog or icebox. " + "What a later phase needs goes in your handoff; a question for another live role goes to its role topic with envoy_publish.",
17493
+ description: legionToolDescription({
17494
+ envoyPublish: "envoy_publish",
17495
+ after: "The controller's read_state returns the daemon's whole state and set_status moves an issue to todo, backlog or icebox. "
17496
+ }),
17331
17497
  defaultInactive: true,
17332
17498
  parameters: toolSchema(pi),
17333
17499
  execute: async (_id, parameters, _signal, _onUpdate, context) => {
17334
17500
  try {
17335
17501
  const active = session(context);
17336
- const operation = requiredString(parameters, "legion", "op");
17337
- if (!OPERATIONS[active.kind].includes(operation)) {
17338
- throw new Error(`${operation} is not available to a ${active.kind} session`);
17502
+ const operation2 = requiredString2(parameters, "legion", "op");
17503
+ if (!OPERATIONS[active.kind].includes(operation2)) {
17504
+ throw new Error(`${operation2} is not available to a ${active.kind} session`);
17505
+ }
17506
+ if (active.kind !== "controller" && isPhaseOp(operation2)) {
17507
+ const answer = await executePhaseOp({ daemon: daemon(), claim: active, handoffCommit }, parameters);
17508
+ if (operation2 === "handoff_complete")
17509
+ onPhaseCompleted(context);
17510
+ return jsonSuccess(answer);
17339
17511
  }
17340
- assertOperationInput(parameters, operation);
17512
+ assertOperationInput(parameters, operation2);
17341
17513
  const client = daemon();
17342
17514
  if (active.kind === "controller") {
17343
- if (operation === "read_state")
17515
+ if (operation2 === "read_state")
17344
17516
  return jsonSuccess(await client.state());
17345
- const issue2 = requiredString(parameters, operation, "issue");
17517
+ const issue2 = requiredString2(parameters, operation2, "issue");
17346
17518
  const status = ISSUE_STATUSES2.find((candidate) => candidate === parameters.status);
17347
17519
  if (status === undefined) {
17348
17520
  throw new Error(`set_status requires status ${ISSUE_STATUSES2.join(", ")}`);
17349
17521
  }
17350
- const { grantId } = await controllerGrant(active.sessionId);
17522
+ const { grantId } = await controllerGrant(active.sessionID);
17351
17523
  await client.issueStatus({ grantId, issue: issue2, status });
17352
17524
  return jsonSuccess({});
17353
17525
  }
17354
- switch (operation) {
17355
- case "read_record": {
17356
- const issue2 = requiredString(parameters, operation, "issue");
17357
- return jsonSuccess({ record: recordFrom(await client.state(), issue2) });
17358
- }
17359
- case "handoff_complete": {
17360
- if (active.kind === "architect" && active.issue === active.tree) {
17361
- throw new Error("handoff_complete is not available to a root architect session, which runs no phase");
17362
- }
17363
- const summary = requiredString(parameters, operation, "summary");
17364
- const { verdict, ready } = parameters;
17365
- if (verdict !== undefined && verdict !== "pass" && verdict !== "fail") {
17366
- throw new Error("handoff_complete's verdict is pass or fail");
17367
- }
17368
- if (ready !== undefined && typeof ready !== "boolean") {
17369
- throw new Error("handoff_complete's ready is true or false");
17370
- }
17371
- const { phase } = recordFrom(await client.state(), active.issue);
17372
- const commit = await handoffCommit(phase, active.role);
17373
- const grantId = await grantFor(client, active);
17374
- const answer = await client.handoffComplete({
17375
- grantId,
17376
- summary,
17377
- verdict: verdict ?? "",
17378
- ready: ready ?? false,
17379
- commit
17380
- });
17381
- onPhaseCompleted(context);
17382
- return jsonSuccess(answer);
17383
- }
17526
+ switch (operation2) {
17384
17527
  case "register_gate": {
17385
17528
  const version2 = parameters.version;
17386
17529
  if (typeof version2 !== "number" || !Number.isSafeInteger(version2) || version2 < 1) {
17387
17530
  throw new Error("register_gate requires a positive integer version");
17388
17531
  }
17389
- const issue2 = requiredString(parameters, operation, "issue");
17532
+ const issue2 = requiredString2(parameters, operation2, "issue");
17390
17533
  if (active.issue !== active.tree) {
17391
17534
  throw new Error(`the design gate belongs to the tree root ${active.tree}; its root architect registers it`);
17392
17535
  }
17393
17536
  if (issue2 !== active.issue) {
17394
17537
  throw new Error(`the design gate belongs to the tree root ${active.tree}; register it there`);
17395
17538
  }
17396
- const reference = requiredString(parameters, operation, "artifactId");
17539
+ const reference = requiredString2(parameters, operation2, "artifactId");
17397
17540
  const isId = LegionGateRegisterRequest.shape.artifactId.safeParse(reference).success;
17398
17541
  const artifactId = isId ? reference : await resolveDocument(issue2, reference);
17399
- const grantId = await grantFor(client, active);
17542
+ const grantId = await mintClaimGrant(client, active);
17400
17543
  await client.gateRegister({ grantId, issue: issue2, artifactId, version: version2 });
17401
17544
  return jsonSuccess({});
17402
17545
  }
@@ -17405,57 +17548,48 @@ function createLegionTool(deps) {
17405
17548
  if (!Array.isArray(issues) || !issues.every((issue2) => typeof issue2 === "string")) {
17406
17549
  throw new Error("release_children requires issues");
17407
17550
  }
17408
- const grantId = await grantFor(client, active);
17551
+ const grantId = await mintClaimGrant(client, active);
17409
17552
  return jsonSuccess(await client.waveRelease({ grantId, issues }));
17410
17553
  }
17411
- case "request_backward_move": {
17412
- const grantId = await grantFor(client, active);
17413
- await client.phaseBackward({
17414
- grantId,
17415
- to: requiredString(parameters, operation, "to"),
17416
- reason: requiredString(parameters, operation, "reason")
17417
- });
17418
- return jsonSuccess({});
17419
- }
17420
17554
  case "retry_or_escalate": {
17421
17555
  const decision = parameters.decision;
17422
17556
  if (decision !== "retry" && decision !== "escalate") {
17423
17557
  throw new Error("retry_or_escalate requires decision retry or escalate");
17424
17558
  }
17425
- const grantId = await grantFor(client, active);
17559
+ const grantId = await mintClaimGrant(client, active);
17426
17560
  await client.phaseRetry({
17427
17561
  grantId,
17428
- issue: requiredString(parameters, operation, "issue"),
17562
+ issue: requiredString2(parameters, operation2, "issue"),
17429
17563
  decision
17430
17564
  });
17431
17565
  return jsonSuccess({});
17432
17566
  }
17433
17567
  case "sign_off": {
17434
- const grantId = await grantFor(client, active);
17568
+ const grantId = await mintClaimGrant(client, active);
17435
17569
  await client.signOff({
17436
17570
  grantId,
17437
- issue: requiredString(parameters, operation, "issue")
17571
+ issue: requiredString2(parameters, operation2, "issue")
17438
17572
  });
17439
17573
  return jsonSuccess({});
17440
17574
  }
17441
17575
  case "close_root": {
17442
- const grantId = await grantFor(client, active);
17576
+ const grantId = await mintClaimGrant(client, active);
17443
17577
  await client.rootClose({
17444
17578
  grantId,
17445
- issue: requiredString(parameters, operation, "issue"),
17446
- reason: requiredString(parameters, operation, "reason")
17579
+ issue: requiredString2(parameters, operation2, "issue"),
17580
+ reason: requiredString2(parameters, operation2, "reason")
17447
17581
  });
17448
17582
  return jsonSuccess({});
17449
17583
  }
17450
17584
  case "park_child":
17451
17585
  case "rerun_child": {
17452
- const grantId = await grantFor(client, active);
17453
- const request = { grantId, issue: requiredString(parameters, operation, "issue") };
17454
- await (operation === "park_child" ? client.childPark(request) : client.childRerun(request));
17586
+ const grantId = await mintClaimGrant(client, active);
17587
+ const request = { grantId, issue: requiredString2(parameters, operation2, "issue") };
17588
+ await (operation2 === "park_child" ? client.childPark(request) : client.childRerun(request));
17455
17589
  return jsonSuccess({});
17456
17590
  }
17457
17591
  default:
17458
- throw new Error(`Unsupported legion operation: ${operation}`);
17592
+ throw new Error(`Unsupported legion operation: ${operation2}`);
17459
17593
  }
17460
17594
  } catch (error48) {
17461
17595
  return toolFailure(error48);
@@ -17691,17 +17825,10 @@ function legionExtension(pi) {
17691
17825
  const sessionID = context.sessionManager.getSessionId();
17692
17826
  const active = claimSession.capability(sessionID);
17693
17827
  if (active !== undefined) {
17694
- return {
17695
- kind: active.role === "architect" ? "architect" : "phase-worker",
17696
- role: active.role,
17697
- sessionId: active.sessionID,
17698
- tree: active.tree,
17699
- issue: active.issue,
17700
- secret: active.secret
17701
- };
17828
+ return { kind: active.role === "architect" ? "architect" : "phase-worker", ...active };
17702
17829
  }
17703
17830
  if (controllerSession.isClaimedSession(sessionID)) {
17704
- return { kind: "controller", sessionId: sessionID };
17831
+ return { kind: "controller", sessionID };
17705
17832
  }
17706
17833
  throw new Error("legion is available only to this session's registered claim");
17707
17834
  },