@sjawhar/pi-legion 10.11.1 → 10.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/legion.js +1274 -1148
  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) {
@@ -15555,751 +15610,50 @@ function sessionDispatchSettings(file2, env, options) {
15555
15610
  };
15556
15611
  }
15557
15612
 
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"
15613
+ // ../contracts/src/legion-api.ts
15614
+ var nonEmptyString = exports_external.string().min(1);
15615
+ var issueKey = exports_external.string().regex(DISPATCH_KEY_PATTERN);
15616
+ var timestamp = exports_external.iso.datetime({ offset: true });
15617
+ var LEGION_WORKFLOW_PHASES = [
15618
+ "admitted",
15619
+ "planning",
15620
+ "implementing",
15621
+ "testing",
15622
+ "reviewing",
15623
+ "retro",
15624
+ "merging",
15625
+ "awaiting_merge",
15626
+ "integrating",
15627
+ "production_check",
15628
+ "done",
15629
+ "held"
15751
15630
  ];
15752
- var LIVE_CAPABILITIES = [
15753
- "subagents",
15754
- "web-search",
15755
- "mcp",
15756
- "repository-extensions",
15757
- "dispatch-envoy-tools",
15758
- "github"
15631
+ var LEGION_PHASE_BACKWARD_TARGETS = [
15632
+ "admitted",
15633
+ "planning",
15634
+ "implementing",
15635
+ "testing",
15636
+ "reviewing",
15637
+ "retro",
15638
+ "merging",
15639
+ "awaiting_merge",
15640
+ "production_check",
15641
+ "done",
15642
+ "held"
15759
15643
  ];
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"
15644
+ var LEGION_PHASES = [...LEGION_WORKFLOW_PHASES, "unrecorded"];
15645
+ var LEGION_CLAIM_STATES = [
15646
+ "queued",
15647
+ "launch_uncertain",
15648
+ "launching",
15649
+ "shim_connected",
15650
+ "registered",
15651
+ "ready",
15652
+ "working",
15653
+ "idle",
15654
+ "suspended",
15655
+ "failed",
15656
+ "retired"
16303
15657
  ];
16304
15658
  var legionLocator = exports_external.discriminatedUnion("runtime", [
16305
15659
  exports_external.strictObject({
@@ -16606,104 +15960,1015 @@ var LegionChildRequest = exports_external.strictObject({
16606
15960
  issue: nonEmptyString
16607
15961
  });
16608
15962
 
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";
15963
+ // ../pi-shared/src/daemon-client.ts
15964
+ class LegionDaemonApiError extends Error {
15965
+ method;
15966
+ path;
15967
+ status;
15968
+ code;
15969
+ detail;
15970
+ constructor(method, path2, status, code, detail) {
15971
+ super(`${method} ${path2} failed with ${status}${code === undefined ? "" : ` ${code}`}: ${detail}`);
15972
+ this.method = method;
15973
+ this.path = path2;
15974
+ this.status = status;
15975
+ this.code = code;
15976
+ this.detail = detail;
15977
+ this.name = "LegionDaemonApiError";
15978
+ }
15979
+ }
15980
+
15981
+ class LegionDaemonContractError extends Error {
15982
+ method;
15983
+ path;
15984
+ status;
15985
+ constructor(method, path2, status, problem) {
15986
+ super(`${method} ${path2} answered ${status} with a body the daemon API contract does not describe: ${problem}`);
15987
+ this.method = method;
15988
+ this.path = path2;
15989
+ this.status = status;
15990
+ this.name = "LegionDaemonContractError";
15991
+ }
15992
+ }
15993
+ var READY_RETRY_ATTEMPTS = 3;
15994
+ var READY_RETRY_DELAY_MS = 1000;
15995
+ function isNetworkOrTimeoutError(error48) {
15996
+ if (!(error48 instanceof Error))
15997
+ return false;
15998
+ if (error48.name === "NetworkError" || error48.name === "TimeoutError")
15999
+ return true;
16000
+ const code = "code" in error48 && typeof error48.code === "string" ? error48.code : undefined;
16001
+ 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");
16002
+ }
16003
+ async function callReadyWithRetry(label, call) {
16004
+ for (let attempt = 1;; attempt++) {
16005
+ try {
16006
+ await call();
16007
+ return;
16008
+ } catch (error48) {
16009
+ const retryable = error48 instanceof LegionDaemonApiError ? error48.status >= 500 && error48.status < 600 : isNetworkOrTimeoutError(error48);
16010
+ if (!retryable)
16011
+ throw error48;
16012
+ if (attempt === READY_RETRY_ATTEMPTS) {
16013
+ console.error(`[legion] ${label} failed after ${attempt} attempts: ${messageFor(error48)}`);
16014
+ throw error48;
16015
+ }
16016
+ console.error(`[legion] ${label} failed (attempt ${attempt}/${READY_RETRY_ATTEMPTS}), retrying: ${messageFor(error48)}`);
16017
+ const retryDelay = Promise.withResolvers();
16018
+ setTimeout(retryDelay.resolve, READY_RETRY_DELAY_MS);
16019
+ await retryDelay.promise;
16020
+ }
16021
+ }
16022
+ }
16023
+ function parseStrictly(schema, body) {
16024
+ let json2;
16025
+ try {
16026
+ json2 = JSON.parse(body);
16027
+ } catch (error48) {
16028
+ return { problem: `not JSON (${messageFor(error48)})` };
16029
+ }
16030
+ const parsed = schema.safeParse(json2);
16031
+ if (parsed.success)
16032
+ return { value: parsed.data };
16033
+ return {
16034
+ problem: parsed.error.issues.map((issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}`).join("; ")
16035
+ };
16036
+ }
16037
+ function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
16038
+ const endpoint = baseUrl.replace(/\/+$/, "");
16039
+ const call = async (method, path2, body) => {
16040
+ const response = await fetchFn(`${endpoint}${path2}`, body === undefined ? undefined : {
16041
+ method,
16042
+ headers: { "Content-Type": "application/json" },
16043
+ body: JSON.stringify(body)
16044
+ });
16045
+ const text = await response.text();
16046
+ if (response.ok)
16047
+ return { status: response.status, text };
16048
+ const refusal = parseStrictly(LegionErrorResponse, text);
16049
+ if ("value" in refusal) {
16050
+ const code = "code" in refusal.value && typeof refusal.value.code === "string" ? refusal.value.code : undefined;
16051
+ throw new LegionDaemonApiError(method, path2, response.status, code, refusal.value.error);
16052
+ }
16053
+ throw new LegionDaemonApiError(method, path2, response.status, undefined, `${JSON.stringify(text)} is not the daemon's refusal shape (${refusal.problem})`);
16054
+ };
16055
+ const read = async (method, path2, schema, body) => {
16056
+ const { status, text } = await call(method, path2, body);
16057
+ const parsed = parseStrictly(schema, text);
16058
+ if ("problem" in parsed) {
16059
+ throw new LegionDaemonContractError(method, path2, status, parsed.problem);
16060
+ }
16061
+ return parsed.value;
16062
+ };
16063
+ const post = (path2, request, response, body) => read("POST", path2, response, request.parse(body));
16064
+ const acknowledge = async (path2, body) => {
16065
+ const { status, text } = await call("POST", path2, body);
16066
+ if (status !== 204 || text !== "") {
16067
+ throw new LegionDaemonContractError("POST", path2, status, "the route answers 204 with no body");
16068
+ }
16069
+ };
16070
+ return {
16071
+ state: () => read("GET", "/legion/v1/state", LegionStateResponse),
16072
+ register: (input) => read("POST", "/legion/v1/claims/register", LegionRegisterResponse, input),
16073
+ registerController: (input) => read("POST", "/legion/v1/claims/register", LegionControllerRegisterResponse, input),
16074
+ ready: (input) => acknowledge("/legion/v1/claims/ready", input),
16075
+ exit: (input) => acknowledge("/legion/v1/claims/exit", input),
16076
+ grant: (input) => post("/legion/v1/grants", LegionGrantRequest, LegionGrantResponse, input),
16077
+ controllerGrant: (input) => post("/legion/v1/grants", LegionControllerGrantRequest, LegionGrantResponse, input),
16078
+ handoffComplete: (input) => post("/legion/v1/handoff/complete", LegionHandoffCompleteRequest, LegionHandoffCompleteResponse, input),
16079
+ issueStatus: (input) => post("/legion/v1/issues/status", LegionIssueStatusRequest, LegionEmptyResponse, input),
16080
+ gateRegister: (input) => post("/legion/v1/gates/register", LegionGateRegisterRequest, LegionEmptyResponse, input),
16081
+ waveRelease: (input) => post("/legion/v1/waves/release", LegionWaveReleaseRequest, LegionWaveReleaseResponse, input),
16082
+ phaseBackward: (input) => post("/legion/v1/phase/backward", LegionPhaseBackwardRequest, LegionEmptyResponse, input),
16083
+ phaseRetry: (input) => post("/legion/v1/phase/retry", LegionPhaseRetryRequest, LegionEmptyResponse, input),
16084
+ signOff: (input) => post("/legion/v1/signoff", LegionSignOffRequest, LegionEmptyResponse, input),
16085
+ rootClose: (input) => post("/legion/v1/roots/close", LegionRootCloseRequest, LegionEmptyResponse, input),
16086
+ childPark: (input) => post("/legion/v1/children/park", LegionChildRequest, LegionEmptyResponse, input),
16087
+ childRerun: (input) => post("/legion/v1/children/rerun", LegionChildRequest, LegionEmptyResponse, input)
16088
+ };
16089
+ }
16090
+
16091
+ // ../pi-shared/src/handoff-commit.ts
16092
+ import { execFile as execFile2 } from "child_process";
16093
+ import { promisify as promisify2 } from "util";
16094
+
16095
+ // ../pi-shared/src/git-lock.ts
16096
+ import { lstat } from "fs/promises";
16097
+ var GIT_LOCK_REPORTS = [
16098
+ /The lockfile at '([^']+)' might need manual deletion/g,
16099
+ /Unable to create '([^']+)': File exists/g
16100
+ ];
16101
+ function age(ms) {
16102
+ const total = Math.round(Math.abs(ms) / 1000);
16103
+ const sign = ms < 0 && total > 0 ? "-" : "";
16104
+ const hours = Math.floor(total / 3600);
16105
+ const minutes = Math.floor(total % 3600 / 60);
16106
+ const seconds = total % 60;
16107
+ if (hours > 0)
16108
+ return `${sign}${hours}h${minutes}m${seconds}s`;
16109
+ if (minutes > 0)
16110
+ return `${sign}${minutes}m${seconds}s`;
16111
+ return `${sign}${seconds}s`;
16112
+ }
16113
+ async function staleGitLockNote(output, now = Date.now()) {
16114
+ const locks = new Set;
16115
+ for (const report of GIT_LOCK_REPORTS) {
16116
+ for (const match of output.matchAll(report))
16117
+ locks.add(match[1]);
16118
+ }
16119
+ const notes = [];
16120
+ for (const lock of locks) {
16121
+ const written = await lstat(lock).then((info) => info.mtimeMs, () => {
16122
+ return;
16123
+ });
16124
+ if (written === undefined)
16125
+ continue;
16126
+ 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.`);
16127
+ }
16128
+ return notes.join(`
16129
+ `);
16130
+ }
16131
+
16132
+ // ../pi-shared/src/handoff-commit.ts
16133
+ var FILE_BACKED = {
16134
+ planning: { word: "plan", role: "planner" },
16135
+ implementing: { word: "implement", role: "implementer" },
16136
+ testing: { word: "test", role: "tester" },
16137
+ reviewing: { word: "review", role: "reviewer" }
16138
+ };
16139
+ var execFileAsync2 = promisify2(execFile2);
16140
+ var pathJj = async (args, cwd) => {
16141
+ try {
16142
+ const { stdout } = await execFileAsync2("jj", ["-R", cwd, ...args], {
16143
+ cwd,
16144
+ maxBuffer: 16 * 1024 * 1024
16145
+ });
16146
+ return stdout.trim();
16147
+ } catch (error48) {
16148
+ const detail = error48 !== null && typeof error48 === "object" && "stderr" in error48 ? String(error48.stderr).trim() : error48 instanceof Error ? error48.message : String(error48);
16149
+ const note = await staleGitLockNote(detail);
16150
+ throw new Error(`jj ${args.join(" ")} failed: ${detail}${note === "" ? "" : `
16151
+ ${note}`}`);
16152
+ }
16153
+ };
16154
+ function requiredVariable(env, name) {
16155
+ const value = env[name];
16156
+ if (value === undefined || value === "") {
16157
+ throw new Error(`${name} is not set, so handoff_complete cannot find the commit to report`);
16158
+ }
16159
+ return value;
16160
+ }
16161
+ async function findHandoffCommit(input) {
16162
+ const { phase, role, env } = input;
16163
+ const jj = input.jj ?? pathJj;
16164
+ const workspace = requiredVariable(env, "LEGION_WORKSPACE");
16165
+ const handoff = FILE_BACKED[phase];
16166
+ if (handoff === undefined || handoff.role !== role) {
16167
+ return jj(["log", "-r", "@-", "--no-graph", "-T", "commit_id"], workspace);
16168
+ }
16169
+ const issue2 = requiredVariable(env, "LEGION_ISSUE");
16170
+ if (!DISPATCH_KEY_PATTERN.test(issue2)) {
16171
+ throw new Error(`LEGION_ISSUE ${JSON.stringify(issue2)} is not a Dispatch issue key like LEGION-1`);
16172
+ }
16173
+ const file2 = `.legion/${issue2}/${handoff.word}.json`;
16174
+ const fileset = `root:${JSON.stringify(file2)}`;
16175
+ if (await jj(["diff", "-r", "@", "--name-only", fileset], workspace) !== "") {
16176
+ throw new Error(`${file2} has changes in the working copy that are not committed: commit this phase's handoff before completing`);
16177
+ }
16178
+ if (await jj(["file", "list", "-r", "@", fileset], workspace) === "") {
16179
+ throw new Error(`${file2} is missing from the workspace: write this phase's handoff, then commit and push it before completing`);
16180
+ }
16181
+ const carrying = await jj([
16182
+ "log",
16183
+ "-r",
16184
+ `latest((::@- ~ ::trunk()) & files(${fileset}))`,
16185
+ "--no-graph",
16186
+ "-T",
16187
+ "commit_id"
16188
+ ], workspace);
16189
+ if (carrying === "") {
16190
+ 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`);
16191
+ }
16192
+ const user = env["JJ_USER"];
16193
+ if (user !== undefined && user !== "") {
16194
+ const author = await jj(["log", "-r", carrying, "--no-graph", "-T", 'author.name() ++ "\\n" ++ author.email()'], workspace);
16195
+ const [name = "", email3 = ""] = author.split(`
16196
+ `, 2);
16197
+ if (name !== user || email3 !== (env["JJ_EMAIL"] ?? "")) {
16198
+ 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`);
16199
+ }
16200
+ }
16201
+ const bookmark = `legion/${issue2}`;
16202
+ const pushed = await jj([
16203
+ "log",
16204
+ "-r",
16205
+ `${carrying} & ::remote_bookmarks(exact:${JSON.stringify(bookmark)}, exact:"origin")`,
16206
+ "--no-graph",
16207
+ "-T",
16208
+ "commit_id"
16209
+ ], workspace);
16210
+ if (pushed === "") {
16211
+ throw new Error(`${file2} is carried by commit ${carrying}, which is not on ${bookmark}@origin: push the issue branch, then complete again`);
16212
+ }
16213
+ return carrying;
16214
+ }
16215
+
16216
+ // ../pi-shared/src/interface.ts
16217
+ import { logger } from "@oh-my-pi/pi-utils";
16218
+ var ENVOY_PLUGIN_INTERFACE_VERSION = 3;
16219
+ var ENVOY_PLUGIN_INTERFACE_KEY = Symbol.for("legion.pi-shared.envoy-plugin-interface");
16220
+ var LEGION_PLUGIN_LOADED_KEY = Symbol.for("legion.pi-legion.loaded");
16221
+ var LEGACY_LEGION_LOADED_KEY = Symbol.for("legion.pi-envoy.legion-loaded");
16222
+ var LOCAL_ENVOY_NOTICE = { localNotice: true };
16223
+ var store = globalThis;
16224
+ function envoyPluginInterface() {
16225
+ const existing = store[ENVOY_PLUGIN_INTERFACE_KEY];
16226
+ if (existing !== undefined)
16227
+ return existing;
16228
+ const created = {
16229
+ version: ENVOY_PLUGIN_INTERFACE_VERSION,
16230
+ publishers: [],
16231
+ roleClaim: {
16232
+ instances: [],
16233
+ managedSessions: new Set,
16234
+ regained: undefined,
16235
+ slackRoleOwned: false
16236
+ },
16237
+ injectedUserTurns: new Map,
16238
+ bootstrappedSession: { file: undefined }
16239
+ };
16240
+ store[ENVOY_PLUGIN_INTERFACE_KEY] = created;
16241
+ return created;
16242
+ }
16243
+ function readEnvoyPluginInterface() {
16244
+ const iface = store[ENVOY_PLUGIN_INTERFACE_KEY];
16245
+ const from = iface?.publishers[0];
16246
+ if (iface === undefined || from === undefined)
16247
+ return { kind: "absent" };
16248
+ if (iface.version !== ENVOY_PLUGIN_INTERFACE_VERSION) {
16249
+ return {
16250
+ kind: "mismatch",
16251
+ found: iface.version,
16252
+ expected: ENVOY_PLUGIN_INTERFACE_VERSION,
16253
+ from
16254
+ };
16255
+ }
16256
+ return { kind: "present", iface };
16257
+ }
16258
+
16259
+ // ../pi-shared/src/injected-user-turns.ts
16260
+ function userMessageText(content) {
16261
+ if (typeof content === "string")
16262
+ return content;
16263
+ if (!Array.isArray(content))
16264
+ return "";
16265
+ const pieces = [];
16266
+ for (const part of content) {
16267
+ if (typeof part !== "object" || part === null)
16268
+ continue;
16269
+ if ("type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
16270
+ pieces.push(part.text);
16271
+ }
16272
+ }
16273
+ return pieces.join(`
16274
+ `);
16275
+ }
16276
+ function matchInjectedUserTurn(sessionID, message) {
16277
+ if (typeof message !== "object" || message === null)
16278
+ return;
16279
+ if (!("role" in message) || message.role !== "user" && message.role !== "custom") {
16280
+ return;
16281
+ }
16282
+ if (!("timestamp" in message) || typeof message.timestamp !== "number")
16283
+ return;
16284
+ const session = envoyPluginInterface().injectedUserTurns.get(sessionID);
16285
+ if (session === undefined)
16286
+ return;
16287
+ const tag = messageTag(message);
16288
+ const memo = tag === undefined ? `${message.timestamp}` : `${message.timestamp}:${tag}`;
16289
+ const known = session.found.get(memo);
16290
+ if (known !== undefined)
16291
+ return known;
16292
+ let index = tag === undefined ? -1 : session.sent.findIndex((sent2) => sent2.messageId === tag);
16293
+ if (index === -1 && message.role === "user") {
16294
+ const text = userMessageText("content" in message ? message.content : undefined);
16295
+ index = session.sent.findIndex((sent2) => sent2.via === "text" && sent2.body === text);
16296
+ }
16297
+ const sent = session.sent[index];
16298
+ if (sent === undefined)
16299
+ return;
16300
+ session.sent.splice(index, 1);
16301
+ session.found.set(memo, sent.messageId);
16302
+ return sent.messageId;
16303
+ }
16304
+ function messageTag(message) {
16305
+ if (typeof message !== "object" || message === null || !("tag" in message))
16306
+ return;
16307
+ return typeof message.tag === "string" ? message.tag : undefined;
16308
+ }
16309
+
16310
+ // ../pi-shared/src/role-claim-bridge.ts
16311
+ function legionRoleClaimBridge() {
16312
+ return envoyPluginInterface().roleClaim;
16313
+ }
16314
+ async function claimEnvoyRole(sessionID, role, context) {
16315
+ const bridge = legionRoleClaimBridge();
16316
+ bridge.managedSessions.add(sessionID);
16317
+ const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
16318
+ if (instance === undefined)
16319
+ throw new Error("Envoy has no bound instance for a role claim");
16320
+ await instance.claim(sessionID, role, context);
16321
+ }
16322
+ async function subscribeLegionNotice(sessionID, topic, context, whileHolding) {
16323
+ const bridge = legionRoleClaimBridge();
16324
+ const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
16325
+ if (instance === undefined)
16326
+ throw new Error("Envoy has no bound instance for a notice subscription");
16327
+ await instance.subscribe(sessionID, topic, context, whileHolding);
16328
+ }
16329
+ function onEnvoyRoleRegained(listener) {
16330
+ legionRoleClaimBridge().regained = listener;
16331
+ }
16332
+
16333
+ // ../pi-shared/src/subagent-session.ts
16334
+ import fs from "fs";
16335
+ import path2 from "path";
16336
+ import * as host from "@oh-my-pi/pi-coding-agent";
16337
+ import { logger as logger2 } from "@oh-my-pi/pi-utils";
16338
+ function recordBootstrappedSession(sessionFile) {
16339
+ envoyPluginInterface().bootstrappedSession.file = sessionFile;
16340
+ }
16341
+ async function isSubagentSession(context) {
16342
+ const registered = registeredSubagent(context);
16343
+ if (registered !== undefined)
16344
+ return { subagent: registered, settled: true };
16345
+ try {
16346
+ await context.sessionManager.ensureOnDisk();
16347
+ } catch (error48) {
16348
+ const subagent = transcriptSaysSubagent(context);
16349
+ 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) });
16350
+ return { subagent, settled: false };
16351
+ }
16352
+ return { subagent: transcriptSaysSubagent(context), settled: true };
16353
+ }
16354
+ function transcriptSaysSubagent(context) {
16355
+ const sessionFile = context.sessionManager.getSessionFile();
16356
+ if (sessionFile !== undefined) {
16357
+ if (fs.existsSync(`${path2.dirname(sessionFile)}.jsonl`))
16358
+ return true;
16359
+ if (fs.existsSync(sessionFile))
16360
+ return false;
16361
+ }
16362
+ const bootstrapped = envoyPluginInterface().bootstrappedSession.file;
16363
+ return bootstrapped !== undefined && bootstrapped !== sessionFile;
16364
+ }
16365
+ function registeredSubagent(context) {
16366
+ const registry2 = host.AgentRegistry?.global();
16367
+ if (registry2 === undefined)
16368
+ return;
16369
+ const sessionID = context.sessionManager.getSessionId();
16370
+ const sessionFile = context.sessionManager.getSessionFile();
16371
+ const own = registry2.list().filter((ref) => ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID);
16372
+ if (own.length === 0)
16373
+ return;
16374
+ return own.some((ref) => ref.kind !== "main");
16375
+ }
16376
+ function subagentSessionCheck() {
16377
+ let settled;
16378
+ return async (context) => {
16379
+ if (settled !== undefined)
16380
+ return settled;
16381
+ const answer = await isSubagentSession(context);
16382
+ if (answer.settled)
16383
+ settled = answer.subagent;
16384
+ return answer.subagent;
16385
+ };
16386
+ }
16387
+
16388
+ // extensions/legion.ts
16389
+ import { logger as logger3 } from "@oh-my-pi/pi-utils";
16390
+
16391
+ // src/capability-report.ts
16392
+ import { readdir, stat } from "fs/promises";
16393
+ import path3 from "path";
16394
+ var CAPABILITY_CHECK_TIMEOUT_MS = 8000;
16395
+ var CAPABILITY_REPORT_TIMEOUT_MS = 1e4;
16396
+ var WEB_SEARCH_PROBE_QUERY = "jujutsu version control";
16397
+ var ENVOY_TOOL_NAMES = [
16398
+ "envoy_subscribe",
16399
+ "envoy_unsubscribe",
16400
+ "envoy_list",
16401
+ "envoy_inbox",
16402
+ "envoy_send",
16403
+ "envoy_publish",
16404
+ "envoy_role_set",
16405
+ "envoy_role_get",
16406
+ "envoy_whoami",
16407
+ "envoy_sessions",
16408
+ "slack_post"
16409
+ ];
16410
+ var LIVE_CAPABILITIES = [
16411
+ "subagents",
16412
+ "web-search",
16413
+ "mcp",
16414
+ "repository-extensions",
16415
+ "dispatch-envoy-tools",
16416
+ "github"
16417
+ ];
16418
+ var DEFAULT_BUDGETS = {
16419
+ checkMs: CAPABILITY_CHECK_TIMEOUT_MS,
16420
+ reportMs: CAPABILITY_REPORT_TIMEOUT_MS
16421
+ };
16422
+ var ok = (detail) => ({ ok: true, detail });
16423
+ var failed = (detail) => ({ ok: false, detail });
16424
+ function firstLine(text) {
16425
+ return text.trim().split(`
16426
+ `, 1)[0] ?? "";
16427
+ }
16428
+ function commandFailure(result) {
16429
+ return firstLine(result.stderr) || `exit code ${result.code ?? "none"}`;
16430
+ }
16431
+ function whichOnPath(name) {
16432
+ return Bun.which(name, { PATH: process.env.PATH ?? "" }) ?? name;
16433
+ }
16434
+ var checkSubagents = async (host2, input) => {
16435
+ if (!input.agentsExposed)
16436
+ return failed("pi.agents is not exposed");
16437
+ if (!input.activeTools.includes("task"))
16438
+ return failed("task is not a registered tool");
16439
+ const [discovered, disabled] = await Promise.all([
16440
+ host2.discoverAgents(input.cwd),
16441
+ host2.disabledAgents()
16442
+ ]);
16443
+ const names = new Set(discovered.agents.map((agent) => agent.name));
16444
+ const missing = input.promptAgents.filter((agent) => !names.has(agent));
16445
+ const off = input.promptAgents.filter((agent) => disabled.includes(agent));
16446
+ const problems = [];
16447
+ if (missing.length > 0) {
16448
+ problems.push(`agents the prompts dispatch that the task tool does not resolve: ${missing.join(", ")}`);
16449
+ }
16450
+ if (off.length > 0)
16451
+ problems.push(`disabled by task.disabledAgents: ${off.join(", ")}`);
16452
+ if (problems.length > 0)
16453
+ return failed(problems.join("; "));
16454
+ return ok(`task resolves ${names.size} agents, every one the prompts dispatch`);
16455
+ };
16456
+ var checkWebSearch = async (host2, input, signal) => {
16457
+ if (!input.activeTools.includes("web_search")) {
16458
+ return failed("web_search is not a registered tool");
16459
+ }
16460
+ const started = Date.now();
16461
+ const result = await host2.runSearchQuery({ query: WEB_SEARCH_PROBE_QUERY, limit: 1 }, { sessionId: input.sessionId, signal });
16462
+ const elapsed = Date.now() - started;
16463
+ const details = result.details ?? {};
16464
+ if (details.error !== undefined && details.error !== null) {
16465
+ return failed(`web_search failed: ${String(details.error)}`);
16466
+ }
16467
+ const provider = details.response?.provider ?? details.provider;
16468
+ if (provider === undefined || provider === null || provider === "none") {
16469
+ return failed("web_search failed: no provider answered");
16470
+ }
16471
+ const text = result.content?.[0]?.text ?? "";
16472
+ if (text.startsWith("Error"))
16473
+ return failed(`web_search failed: ${firstLine(text)}`);
16474
+ return ok(`web_search registered; provider ${String(provider)} answered in ${elapsed} ms`);
16475
+ };
16476
+ function mcpErrorFor(errors3, name) {
16477
+ for (const entry of errors3) {
16478
+ if (typeof entry !== "object" || entry === null) {
16479
+ if (!String(entry).includes(name))
16480
+ continue;
16481
+ return String(entry);
16482
+ }
16483
+ const record2 = entry;
16484
+ const names = [record2.server, record2.name, record2.path];
16485
+ if (!names.some((value) => value === name || value === `mcp:${name}`))
16486
+ continue;
16487
+ const message = record2.error ?? record2.message;
16488
+ return message === undefined ? String(entry) : String(message);
16489
+ }
16490
+ return;
16491
+ }
16492
+ var checkMCP = async (host2, input, signal) => {
16493
+ const { configs, sources } = await host2.loadMCPConfigs(input.cwd);
16494
+ const configured = Object.keys(configs);
16495
+ if (configured.length === 0)
16496
+ return ok("no MCP server configured");
16497
+ const { manager, errors: errors3 } = await host2.discoverMCPServers(input.cwd);
16498
+ let torn;
16499
+ const tearDown = () => {
16500
+ torn ??= manager.disconnectAll().catch(() => {
16501
+ return;
16502
+ });
16503
+ return torn;
16504
+ };
16505
+ signal.addEventListener("abort", tearDown, { once: true });
16506
+ if (signal.aborted)
16507
+ tearDown();
16508
+ try {
16509
+ await manager.waitForPendingConnections();
16510
+ const connected = [];
16511
+ const unconnected = [];
16512
+ for (const name of configured) {
16513
+ const status = manager.getConnectionStatus(name);
16514
+ if (status === "connected") {
16515
+ const source = sources[name]?.path;
16516
+ connected.push(source === undefined ? name : `${name} (${path3.relative(input.cwd, source)})`);
16517
+ } else {
16518
+ unconnected.push(`${name} (${mcpErrorFor(errors3, name) ?? `status ${status}`})`);
16519
+ }
16520
+ }
16521
+ if (unconnected.length === 0)
16522
+ return ok(`connected: ${connected.join(", ")}`);
16523
+ const suffix = connected.length === 0 ? "" : `; connected: ${connected.join(", ")}`;
16524
+ return failed(`not connected: ${unconnected.join(", ")}${suffix}`);
16525
+ } finally {
16526
+ signal.removeEventListener("abort", tearDown);
16527
+ await tearDown();
16528
+ }
16529
+ };
16530
+ var EXTENSION_MODULE = /\.(?:ts|js|mjs|cjs)$/;
16531
+ var EXTENSION_INDEXES = ["index.ts", "index.js", "index.mjs", "index.cjs"];
16532
+ var SKILL_DIRECTORIES = [path3.join(".omp", "skills"), path3.join(".claude", "skills")];
16533
+ async function namesIn(directory) {
16534
+ try {
16535
+ return await readdir(directory);
16536
+ } catch (error48) {
16537
+ if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
16538
+ return [];
16539
+ throw error48;
16540
+ }
16541
+ }
16542
+ async function kindOf(file2) {
16543
+ try {
16544
+ const stats = await stat(file2);
16545
+ return stats.isFile() ? "file" : stats.isDirectory() ? "directory" : undefined;
16546
+ } catch (error48) {
16547
+ if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
16548
+ return;
16549
+ throw error48;
16550
+ }
16551
+ }
16552
+ async function repositoryEntries(cwd) {
16553
+ const entries = [];
16554
+ const extensions = path3.join(cwd, ".omp", "extensions");
16555
+ for (const name of await namesIn(extensions)) {
16556
+ const candidate = path3.join(extensions, name);
16557
+ const kind = await kindOf(candidate);
16558
+ if (kind === "file" && EXTENSION_MODULE.test(name)) {
16559
+ entries.push({ path: candidate, directory: false });
16560
+ } else if (kind === "directory") {
16561
+ const indexes = await Promise.all(EXTENSION_INDEXES.map((index) => kindOf(path3.join(candidate, index))));
16562
+ if (indexes.includes("file"))
16563
+ entries.push({ path: candidate, directory: true });
16564
+ }
16565
+ }
16566
+ for (const skills of SKILL_DIRECTORIES) {
16567
+ for (const name of await namesIn(path3.join(cwd, skills))) {
16568
+ const skill = path3.join(cwd, skills, name, "SKILL.md");
16569
+ if (await kindOf(skill) === "file")
16570
+ entries.push({ path: skill, directory: false });
16571
+ }
16572
+ }
16573
+ return entries;
16574
+ }
16575
+ var checkRepositoryExtensions = async (host2, input) => {
16576
+ const entries = await repositoryEntries(input.cwd);
16577
+ if (entries.length === 0) {
16578
+ return ok("the repository carries no .omp/extensions, .omp/skills or .claude/skills");
16579
+ }
16580
+ const [extensionPaths, loaded] = await Promise.all([
16581
+ host2.discoverExtensionPaths(input.cwd),
16582
+ host2.loadSkills(input.cwd)
16583
+ ]);
16584
+ const discovered = new Set([...extensionPaths, ...loaded.skills.map((skill) => skill.filePath)].map((file2) => path3.resolve(file2)));
16585
+ const isDiscovered = (entry) => {
16586
+ const resolved = path3.resolve(entry.path);
16587
+ if (discovered.has(resolved))
16588
+ return true;
16589
+ if (!entry.directory)
16590
+ return false;
16591
+ const prefix = resolved + path3.sep;
16592
+ return [...discovered].some((file2) => file2.startsWith(prefix));
16593
+ };
16594
+ const relative = (selected) => selected.map((entry) => path3.relative(input.cwd, entry.path)).sort().join(", ");
16595
+ const found = [];
16596
+ const lost = [];
16597
+ for (const entry of entries)
16598
+ (isDiscovered(entry) ? found : lost).push(entry);
16599
+ if (lost.length === 0)
16600
+ return ok(`discovered: ${relative(found)}`);
16601
+ const suffix = found.length === 0 ? "" : `; discovered: ${relative(found)}`;
16602
+ return failed(`not discovered: ${relative(lost)}${suffix}`);
16603
+ };
16604
+ var checkDispatchEnvoyTools = async (host2, input, signal) => {
16605
+ const missing = ENVOY_TOOL_NAMES.filter((tool) => !input.activeTools.includes(tool));
16606
+ if (missing.length > 0)
16607
+ return failed(`Envoy tools missing: ${missing.join(", ")}`);
16608
+ const result = await host2.run("dispatch", ["read", "--issue", input.issue], {
16609
+ cwd: input.cwd,
16610
+ signal
16611
+ });
16612
+ if (result.code !== 0)
16613
+ return failed(`dispatch read failed: ${commandFailure(result)}`);
16614
+ return ok(`${ENVOY_TOOL_NAMES.length} Envoy tools registered; dispatch read ${input.issue} answered from ${whichOnPath("dispatch")}`);
16615
+ };
16616
+ var checkGitHub = async (host2, input, signal) => {
16617
+ const result = await host2.run("gh", ["api", "graphql", "-f", "query={viewer{login}}", "--jq", ".data.viewer.login"], { cwd: input.cwd, signal });
16618
+ if (result.code !== 0)
16619
+ return failed(`gh viewer login failed: ${commandFailure(result)}`);
16620
+ return ok(`gh viewer login: ${result.stdout.trim()}`);
16621
+ };
16622
+ var CHECKS = {
16623
+ subagents: checkSubagents,
16624
+ "web-search": checkWebSearch,
16625
+ mcp: checkMCP,
16626
+ "repository-extensions": checkRepositoryExtensions,
16627
+ "dispatch-envoy-tools": checkDispatchEnvoyTools,
16628
+ github: checkGitHub
16629
+ };
16630
+ var MAX_DETAIL_BYTES = 1024;
16631
+ var HELD = "\uFFFF";
16632
+ var C1_INTRODUCER = "\uFFFE";
16633
+ var OPENER = "\uFDD0";
16634
+ var HELD_READINGS = [
16635
+ [/(?:\uffff\[|\ufffe)[0-?]*[!-)+,/-]*/g, OPENER],
16636
+ [/\ufdd0[@-~]?/g, HELD],
16637
+ [/\uffff/g, " "]
16638
+ ];
16639
+ var HELD_SEQUENCE = String.raw`\ufffe|\ufdd0|\uffff(?:\[[0-?]*[@-~])?`;
16640
+ var JWT_FIRST_HELD_SEQUENCE = String.raw`\ufffe|\uffff(?:\[[0-?]*[@-~])?`;
16641
+ function isSpace(code) {
16642
+ 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;
16643
+ }
16644
+ function truncateUtf8(text, max) {
16645
+ let bytes = 0;
16646
+ let end = 0;
16647
+ for (const char of text) {
16648
+ const code = char.codePointAt(0) ?? 0;
16649
+ bytes += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
16650
+ if (bytes > max)
16651
+ break;
16652
+ end += char.length;
16653
+ }
16654
+ return text.slice(0, end);
16655
+ }
16656
+ var CREDENTIAL_RULES = [
16657
+ [/(?<![a-z0-9+.-])([0-9+.-]*[a-z][a-z0-9+.-]*:\/\/)[^ /?#]+@/gi, "$1***@"],
16658
+ [
16659
+ /([?&](?: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,
16660
+ "$1***"
16661
+ ],
16662
+ [
16663
+ /\b(password[ \ufffe\uffff]*=[ \ufffe\uffff]*)(?:(')(?:[^'\\]|\\.)*('?)|[^ &#"'<>]+)/gi,
16664
+ "$1$2***$3"
16665
+ ],
16666
+ [
16667
+ /(authorization\\?["']?[ \ufffe\uffff]*[:=][ \ufffe\uffff]*)(?:(\\["'](?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)[^"'\\]+|(["'](?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)(?:[^"'\\]|\\.)+|(\[?(?:(?:bearer|basic|token|digest)[ \ufffe\uffff]+)?)[^)\]}>]+)/gi,
16668
+ "$1$2$3$4***"
16669
+ ],
16670
+ [
16671
+ 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"),
16672
+ "$1***"
16673
+ ],
16674
+ [
16675
+ /(\\?["'](?:api_key|access_token)\\?["'][ \ufffe\uffff]*:[ \ufffe\uffff]*\\?["'])[^"'\\]+/gi,
16676
+ "$1***"
16677
+ ],
16678
+ [
16679
+ new RegExp(String.raw`\b(bearer[ \ufffe\uffff]+)(?:[A-Za-z0-9._~+/=*-]|${HELD_SEQUENCE})+`, "gi"),
16680
+ "$1***"
16681
+ ],
16682
+ [
16683
+ new RegExp(String.raw`\b(gh[pousr]_|github_pat_)(?:[A-Za-z0-9._-]|${HELD_SEQUENCE}){20,}`, "g"),
16684
+ "$1***"
16685
+ ],
16686
+ [
16687
+ 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"),
16688
+ "***"
16689
+ ],
16690
+ [
16691
+ new RegExp(String.raw`\b(sk-|dsp_|ddsat_|ddpat_|ddapp_|ya29\.|xox[a-z]-|glpat-|npm_)(?:[A-Za-z0-9_-]|${HELD_SEQUENCE}){20,}`, "g"),
16692
+ "$1***"
16693
+ ],
16694
+ [new RegExp(String.raw`\b(AIza)(?:[A-Za-z0-9_-]|${HELD_SEQUENCE}){35,}`, "g"), "$1***"],
16695
+ [new RegExp(String.raw`\b(AKIA|ASIA)(?:(?:${HELD_SEQUENCE})*[A-Z0-9]){16}\b`, "g"), "$1***"]
16696
+ ];
16697
+ var MAX_REDACTION_PASSES = 8;
16698
+ function redactCredentials(text) {
16699
+ let line = text;
16700
+ for (let pass = 0;pass < MAX_REDACTION_PASSES; pass++) {
16701
+ const next = CREDENTIAL_RULES.reduce((redacted, [pattern, replacement]) => redacted.replace(pattern, replacement), line);
16702
+ if (next === line)
16703
+ break;
16704
+ line = next;
16624
16705
  }
16706
+ return line;
16625
16707
  }
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";
16708
+ function withoutLastCodePoint(text) {
16709
+ const last = text.charCodeAt(text.length - 1);
16710
+ return text.slice(0, last >= 56320 && last <= 57343 && text.length > 1 ? -2 : -1);
16711
+ }
16712
+ function cleanDetail(text) {
16713
+ let marked = "";
16714
+ for (const char of text) {
16715
+ const code = char.codePointAt(0) ?? 0;
16716
+ const control = code < 32 || code >= 127 && code < 160 || code === 65534 || code === 64976 || code === 65533;
16717
+ marked += isSpace(code) ? " " : code === 155 ? C1_INTRODUCER : control ? HELD : char;
16637
16718
  }
16719
+ let redacted = redactCredentials(marked);
16720
+ if (marked.includes(HELD) || marked.includes(C1_INTRODUCER)) {
16721
+ for (const [pattern, replacement] of HELD_READINGS) {
16722
+ const read = redacted.replace(pattern, replacement);
16723
+ if (read !== redacted)
16724
+ redacted = redactCredentials(read);
16725
+ }
16726
+ }
16727
+ const line = redacted.split(" ").filter((field) => field !== "").join(" ");
16728
+ let cut = truncateUtf8(line, MAX_DETAIL_BYTES).replace(/ +$/, "");
16729
+ if (cut.length < line.length) {
16730
+ while (redactCredentials(cut) !== cut) {
16731
+ const space = cut.lastIndexOf(" ");
16732
+ cut = space > 0 ? cut.slice(0, space) : withoutLastCodePoint(cut);
16733
+ }
16734
+ }
16735
+ return cut;
16638
16736
  }
16639
- function parseStrictly(schema, body) {
16640
- let json2;
16737
+ function budgetLabel(ms) {
16738
+ return ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
16739
+ }
16740
+ async function withinBudget(work, ms, onDeadline) {
16741
+ const deadline = Promise.withResolvers();
16742
+ const timer = setTimeout(() => deadline.resolve(onDeadline()), ms);
16641
16743
  try {
16642
- json2 = JSON.parse(body);
16643
- } catch (error48) {
16644
- return { problem: `not JSON (${messageFor(error48)})` };
16744
+ return await Promise.race([work, deadline.promise]);
16745
+ } finally {
16746
+ clearTimeout(timer);
16645
16747
  }
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
16748
  }
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)
16749
+ async function measureCapabilities(host2, input, budgets = DEFAULT_BUDGETS) {
16750
+ const started = Date.now();
16751
+ const measuredAt = new Date(started).toISOString();
16752
+ const outcomes = new Map;
16753
+ const controllers = [];
16754
+ const checks3 = LIVE_CAPABILITIES.map(async (name) => {
16755
+ const controller = new AbortController;
16756
+ controllers.push(controller);
16757
+ const outcome = await withinBudget(CHECKS[name](host2, input, controller.signal).catch((error48) => failed(firstLine(messageFor(error48)))), budgets.checkMs, () => {
16758
+ controller.abort();
16759
+ return failed(`did not finish within ${budgetLabel(budgets.checkMs)}`);
16660
16760
  });
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
- };
16761
+ outcomes.set(name, outcome);
16762
+ });
16763
+ await withinBudget(Promise.all(checks3).then(() => {
16764
+ return;
16765
+ }), budgets.reportMs, () => {
16766
+ return;
16767
+ });
16768
+ for (const controller of controllers)
16769
+ controller.abort();
16770
+ const rows = LIVE_CAPABILITIES.map((name) => {
16771
+ const { ok: ok2, detail } = outcomes.get(name) ?? failed(`did not finish within ${budgetLabel(budgets.reportMs)}`);
16772
+ return { name, ok: ok2, detail: cleanDetail(detail) };
16773
+ });
16774
+ return { measuredAt, elapsedMs: Date.now() - started, rows };
16775
+ }
16776
+ async function runCommand2(command, args, options) {
16777
+ const child = Bun.spawn([command, ...args], {
16778
+ cwd: options.cwd,
16779
+ env: process.env,
16780
+ stdin: "ignore",
16781
+ stdout: "pipe",
16782
+ stderr: "pipe",
16783
+ signal: options.signal
16784
+ });
16785
+ const [stdout, stderr] = await Promise.all([
16786
+ new Response(child.stdout).text(),
16787
+ new Response(child.stderr).text(),
16788
+ child.exited
16789
+ ]);
16790
+ return { code: child.exitCode, stdout, stderr };
16791
+ }
16792
+ function ompCapabilityHost() {
16793
+ const hostSettings = async () => (await import("@oh-my-pi/pi-coding-agent/config/settings")).settings;
16686
16794
  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)
16795
+ discoverAgents: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverAgents(cwd),
16796
+ disabledAgents: async () => {
16797
+ const [{ cfgTaskDisabledAgents }, settings] = await Promise.all([
16798
+ import("@oh-my-pi/pi-coding-agent/task/settings"),
16799
+ hostSettings()
16800
+ ]);
16801
+ return cfgTaskDisabledAgents.get(settings);
16802
+ },
16803
+ runSearchQuery: async (args, options) => (await import("@oh-my-pi/pi-coding-agent")).runSearchQuery(args, options),
16804
+ loadMCPConfigs: async (cwd) => (await import("@oh-my-pi/pi-coding-agent/mcp/config")).loadAllMCPConfigs(cwd),
16805
+ discoverMCPServers: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverMCPServers(cwd),
16806
+ discoverExtensionPaths: async (cwd) => {
16807
+ const [{ discoverExtensionPaths }, { cfgDisabledExtensions }, settings] = await Promise.all([
16808
+ import("@oh-my-pi/pi-coding-agent/extensibility/extensions/loader"),
16809
+ import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
16810
+ hostSettings()
16811
+ ]);
16812
+ return discoverExtensionPaths([], cwd, cfgDisabledExtensions.get(settings));
16813
+ },
16814
+ loadSkills: async (cwd) => {
16815
+ const [{ loadSkills }, { cfgDisabledExtensions, cfgSkills }, settings] = await Promise.all([
16816
+ import("@oh-my-pi/pi-coding-agent"),
16817
+ import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
16818
+ hostSettings()
16819
+ ]);
16820
+ return loadSkills({
16821
+ ...cfgSkills.get(settings),
16822
+ cwd,
16823
+ disabledExtensions: cfgDisabledExtensions.get(settings)
16824
+ });
16825
+ },
16826
+ run: runCommand2
16704
16827
  };
16705
16828
  }
16706
16829
 
16830
+ // ../pi-shared/src/legion-claim-core.ts
16831
+ async function bootLegionClaim(host2) {
16832
+ const { daemon } = host2;
16833
+ let claim;
16834
+ try {
16835
+ claim = await daemon.register(host2.registration);
16836
+ } catch (error48) {
16837
+ if (!(error48 instanceof LegionDaemonApiError))
16838
+ throw error48;
16839
+ host2.log(`claims/register registration failed (${error48.status}): ${error48.detail}`);
16840
+ if (error48.status < 400 || error48.status >= 500)
16841
+ throw error48;
16842
+ host2.stop();
16843
+ return;
16844
+ }
16845
+ const { measure } = host2;
16846
+ const measuring = measure === undefined ? undefined : Promise.resolve().then(() => measure(claim)).catch((error48) => {
16847
+ host2.log(`capability measurement failed; ready carries no report: ${messageFor(error48)}`);
16848
+ return;
16849
+ });
16850
+ try {
16851
+ await host2.afterRegister?.(claim);
16852
+ host2.setCapability({
16853
+ sessionID: host2.registration.sessionId,
16854
+ tree: claim.tree,
16855
+ issue: claim.issue,
16856
+ role: claim.role,
16857
+ secret: claim.secret
16858
+ });
16859
+ await host2.claimRole(claim.claimToken);
16860
+ const capabilities = await measuring;
16861
+ const ready = {
16862
+ claimToken: claim.claimToken,
16863
+ sessionId: host2.registration.sessionId,
16864
+ secret: claim.secret,
16865
+ generation: claim.generation,
16866
+ ...capabilities === undefined ? {} : { capabilities }
16867
+ };
16868
+ await callReadyWithRetry("claims/ready", () => daemon.ready(ready));
16869
+ host2.onRoleRegained(async (role, reason) => {
16870
+ if (role !== claim.claimToken)
16871
+ return;
16872
+ try {
16873
+ await callReadyWithRetry("claims/ready after role regain", () => daemon.ready(ready));
16874
+ host2.log(`re-ran claims/ready after role ${role} was ${reason}`);
16875
+ } catch (error48) {
16876
+ 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)}`);
16877
+ }
16878
+ });
16879
+ } catch (error48) {
16880
+ host2.log(`claim boot failed after claims/register registered ${claim.claimToken}; ${host2.stopping}: ${messageFor(error48)}`);
16881
+ host2.stop();
16882
+ }
16883
+ }
16884
+ // package.json
16885
+ var package_default = {
16886
+ name: "@sjawhar/pi-legion",
16887
+ version: "10.12.0",
16888
+ license: "Apache-2.0",
16889
+ type: "module",
16890
+ omp: {
16891
+ extensions: [
16892
+ "dist/legion.js"
16893
+ ],
16894
+ skills: [
16895
+ "dist/skills"
16896
+ ]
16897
+ },
16898
+ legion: {
16899
+ daemonApiVersion: 32
16900
+ },
16901
+ repository: {
16902
+ type: "git",
16903
+ url: "https://github.com/sjawhar/legion"
16904
+ },
16905
+ files: [
16906
+ "dist",
16907
+ "agents"
16908
+ ],
16909
+ scripts: {
16910
+ 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",
16911
+ prepack: "../../scripts/pi-plugin-prepack.sh",
16912
+ postpack: "rm -rf dist/skills",
16913
+ typecheck: "bunx tsc --noEmit",
16914
+ test: "bun test",
16915
+ lint: "bunx biome lint extensions/ src/"
16916
+ },
16917
+ devDependencies: {
16918
+ "@biomejs/biome": "^2.3.14",
16919
+ "@legion/contracts": "workspace:*",
16920
+ "@legion/envoy-client": "workspace:*",
16921
+ "@legion/pi-shared": "workspace:*",
16922
+ "@oh-my-pi/pi-utils": "^18.6.0",
16923
+ "@types/bun": "latest",
16924
+ typescript: "^5.3.0",
16925
+ zod: "4.3.6"
16926
+ }
16927
+ };
16928
+
16929
+ // src/classify.ts
16930
+ function classifySession(env) {
16931
+ if (env.LEGION_CONTROLLER !== undefined && env.LEGION_TREE !== undefined) {
16932
+ throw new Error("Legion session has both controller and tree launch markers");
16933
+ }
16934
+ if (env.LEGION_CONTROLLER === "1")
16935
+ return { kind: "controller" };
16936
+ if (env.LEGION_ROLE === undefined)
16937
+ return { kind: "not-legion" };
16938
+ if (!isLegionRole(env.LEGION_ROLE)) {
16939
+ throw new Error(`LEGION_ROLE "${env.LEGION_ROLE}" is not a Legion role`);
16940
+ }
16941
+ const tree = requiredEnvironment(env, "LEGION_TREE");
16942
+ const issue2 = requiredEnvironment(env, "LEGION_ISSUE");
16943
+ if (env.LEGION_ROLE === "architect" && issue2 === tree) {
16944
+ return { kind: "root-architect", tree };
16945
+ }
16946
+ return { kind: "phase-worker", role: env.LEGION_ROLE, tree, issue: issue2 };
16947
+ }
16948
+ function requiredEnvironment(env, key) {
16949
+ const value = env[key];
16950
+ if (!value)
16951
+ throw new Error(`${key} is required for Legion`);
16952
+ return value;
16953
+ }
16954
+ function requiredSecret(env, key) {
16955
+ const fileKey = `${key}_FILE`;
16956
+ const file2 = env[fileKey];
16957
+ if (file2 !== undefined)
16958
+ return readSecretFile(fileKey, file2);
16959
+ return requiredEnvironment(env, key);
16960
+ }
16961
+ function requiredControllerCapability(env) {
16962
+ if (env.LEGION_CONTROLLER_SECRET_FILE !== undefined) {
16963
+ return readSecretFile("LEGION_CONTROLLER_SECRET_FILE", env.LEGION_CONTROLLER_SECRET_FILE);
16964
+ }
16965
+ const secret = env.LEGION_CONTROLLER_SECRET;
16966
+ if (!secret) {
16967
+ 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.");
16968
+ }
16969
+ return secret;
16970
+ }
16971
+
16707
16972
  // src/jj-attribution.ts
16708
16973
  import { mkdir, rename, writeFile } from "fs/promises";
16709
16974
  import path4 from "path";
@@ -16783,95 +17048,35 @@ commit_trailers = '"Omp-Session: ${id}"'
16783
17048
  }
16784
17049
 
16785
17050
  // 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
17051
  function createClaimSession(deps) {
16825
17052
  let capability;
16826
17053
  let booting;
16827
17054
  const boot = async (context, sessionID, bootToken, daemon) => {
16828
17055
  const { sessionFile, agentId } = await deps.persistedTranscript(context);
16829
17056
  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,
17057
+ await bootLegionClaim({
17058
+ daemon,
17059
+ registration: {
17060
+ bootToken,
16855
17061
  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
- }
17062
+ ompSessionFile: sessionFile,
17063
+ agentId,
17064
+ pluginContract: package_default.legion.daemonApiVersion
17065
+ },
17066
+ measure: (claim) => deps.measureCapabilities(context, {
17067
+ issue: claim.issue,
17068
+ promptAgents: claim.promptAgents
17069
+ }),
17070
+ afterRegister: () => exportJjSessionAttribution(sessionFile, requiredEnvironment(process.env, "LEGION_STATE_DIR")),
17071
+ setCapability: (registered) => {
17072
+ capability = registered;
17073
+ },
17074
+ claimRole: (role) => claimEnvoyRole(sessionID, role, context),
17075
+ onRoleRegained: onEnvoyRoleRegained,
17076
+ log: (line) => console.error(`[legion] ${line}`),
17077
+ stopping: "exiting so the daemon launches it again",
17078
+ stop: () => deps.exitProcess(1)
17079
+ });
16875
17080
  };
16876
17081
  const bootstrap = async (context) => {
16877
17082
  const sessionID = context.sessionManager.getSessionId();
@@ -17017,131 +17222,6 @@ function createControllerSession(deps) {
17017
17222
  return { claim, handleSessionStart, reclaimAfterSessionChange, isClaimedSession, mintGrant };
17018
17223
  }
17019
17224
 
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
17225
  // src/phase-stall.ts
17146
17226
  var PHASE_STALL_ENTRY = "legion-phase-stall";
17147
17227
  var WAITING_REPLY = /^\W*WAITING\b/m;
@@ -17223,6 +17303,115 @@ async function applySessionTitle(pi, context, title) {
17223
17303
  await pi.setSessionName(title);
17224
17304
  }
17225
17305
 
17306
+ // ../pi-shared/src/legion-phase-ops.ts
17307
+ var PHASE_OPS = ["read_record", "handoff_complete", "request_backward_move"];
17308
+ function isPhaseOp(op) {
17309
+ return PHASE_OPS.includes(op);
17310
+ }
17311
+ var requiredString = (op, field) => {
17312
+ const message = `${op} requires ${field}`;
17313
+ return exports_external.string({ error: message }).refine((value) => value.trim() !== "", { error: message });
17314
+ };
17315
+ var operation = (op, shape) => exports_external.strictObject({ op: exports_external.literal(op), ...shape }, {
17316
+ error: (issue2) => issue2.code === "unrecognized_keys" ? issue2.keys.map((key) => `${op} does not accept field "${key}"`).join("; ") : undefined
17317
+ });
17318
+ var PhaseOpArguments = exports_external.discriminatedUnion("op", [
17319
+ operation("read_record", { issue: requiredString("read_record", "issue") }),
17320
+ operation("handoff_complete", {
17321
+ summary: requiredString("handoff_complete", "summary"),
17322
+ verdict: exports_external.enum(["pass", "fail"], { error: "handoff_complete's verdict is pass or fail" }).optional(),
17323
+ ready: exports_external.boolean({ error: "handoff_complete's ready is true or false" }).optional()
17324
+ }),
17325
+ operation("request_backward_move", {
17326
+ to: exports_external.enum(LEGION_PHASE_BACKWARD_TARGETS, {
17327
+ 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"
17328
+ }),
17329
+ reason: requiredString("request_backward_move", "reason")
17330
+ })
17331
+ ], {
17332
+ error: (issue2) => issue2.code === "invalid_union" ? `op is one of ${PHASE_OPS.join(", ")}` : undefined
17333
+ });
17334
+ function phaseOpParameters(zod) {
17335
+ const api2 = zodSchemaApi(zod);
17336
+ const fields = {};
17337
+ for (const option of PhaseOpArguments.options) {
17338
+ for (const [name, declared] of Object.entries(option.shape)) {
17339
+ if (name === "op")
17340
+ continue;
17341
+ const field = declared instanceof exports_external.ZodOptional ? declared.unwrap() : declared;
17342
+ if (field instanceof exports_external.ZodString)
17343
+ fields[name] = api2.string().optional();
17344
+ else if (field instanceof exports_external.ZodBoolean)
17345
+ fields[name] = api2.boolean().optional();
17346
+ else if (field instanceof exports_external.ZodEnum) {
17347
+ const [first, ...rest] = field.options.map(String);
17348
+ if (first === undefined)
17349
+ throw new Error(`phase op field ${name} has no values`);
17350
+ fields[name] = api2.enum([first, ...rest]).optional();
17351
+ } else
17352
+ throw new Error(`phase op field ${name} has a kind phaseOpParameters does not map`);
17353
+ }
17354
+ }
17355
+ const byField = fields;
17356
+ return byField;
17357
+ }
17358
+ function legionToolDescription(host2) {
17359
+ 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}.`;
17360
+ }
17361
+ async function mintClaimGrant(daemon, claim) {
17362
+ const { grantId } = await daemon.grant({
17363
+ sessionId: claim.sessionID,
17364
+ secret: claim.secret,
17365
+ tree: claim.tree,
17366
+ issue: claim.issue
17367
+ });
17368
+ return grantId;
17369
+ }
17370
+ function recordFrom(state, issue2) {
17371
+ const record2 = state.issues[issue2];
17372
+ if (record2 === undefined)
17373
+ throw new Error(`The daemon has no record for ${issue2}`);
17374
+ return record2;
17375
+ }
17376
+ var ROOT_COMPLETION_REFUSAL = "handoff_complete is not available to a root architect session, which runs no phase";
17377
+ async function executePhaseOp(runtime, input) {
17378
+ const { daemon, claim } = runtime;
17379
+ const parsed = PhaseOpArguments.safeParse(input);
17380
+ const issues = parsed.success ? [] : parsed.error.issues;
17381
+ const unknownFields = issues.filter((issue2) => issue2.code === "unrecognized_keys");
17382
+ const fieldProblems = issues.filter((issue2) => issue2.code !== "unrecognized_keys");
17383
+ const rootCompletion = typeof input === "object" && input !== null && "op" in input && input.op === "handoff_complete" && claim.role === "architect" && claim.issue === claim.tree;
17384
+ if (rootCompletion) {
17385
+ throw new Error([...unknownFields.map((issue2) => issue2.message), ROOT_COMPLETION_REFUSAL].join("; "));
17386
+ }
17387
+ if (!parsed.success) {
17388
+ throw new Error([...unknownFields, ...fieldProblems].map((issue2) => issue2.message).join("; "));
17389
+ }
17390
+ const args = parsed.data;
17391
+ switch (args.op) {
17392
+ case "read_record":
17393
+ return { record: recordFrom(await daemon.state(), args.issue) };
17394
+ case "handoff_complete": {
17395
+ const { phase } = recordFrom(await daemon.state(), claim.issue);
17396
+ const commit = await runtime.handoffCommit(phase, claim.role);
17397
+ return daemon.handoffComplete({
17398
+ grantId: await mintClaimGrant(daemon, claim),
17399
+ summary: args.summary,
17400
+ verdict: args.verdict ?? "",
17401
+ ready: args.ready ?? false,
17402
+ commit
17403
+ });
17404
+ }
17405
+ case "request_backward_move":
17406
+ await daemon.phaseBackward({
17407
+ grantId: await mintClaimGrant(daemon, claim),
17408
+ to: args.to,
17409
+ reason: args.reason
17410
+ });
17411
+ return {};
17412
+ }
17413
+ }
17414
+
17226
17415
  // ../pi-shared/src/tool-result.ts
17227
17416
  function toolSuccess(text, details = {}, images = []) {
17228
17417
  return { content: [{ type: "text", text }, ...imageBlocks(images)], details };
@@ -17251,14 +17440,11 @@ var OPERATIONS = {
17251
17440
  var OPERATION_FIELDS = {
17252
17441
  register_gate: ["issue", "artifactId", "version"],
17253
17442
  release_children: ["issues"],
17254
- request_backward_move: ["to", "reason"],
17255
17443
  retry_or_escalate: ["issue", "decision"],
17256
17444
  sign_off: ["issue"],
17257
17445
  close_root: ["issue", "reason"],
17258
17446
  park_child: ["issue"],
17259
17447
  rerun_child: ["issue"],
17260
- read_record: ["issue"],
17261
- handoff_complete: ["summary", "verdict", "ready"],
17262
17448
  read_state: [],
17263
17449
  set_status: ["issue", "status"]
17264
17450
  };
@@ -17266,137 +17452,93 @@ var ISSUE_STATUSES2 = ["todo", "backlog", "icebox"];
17266
17452
  var jsonSuccess = (details) => toolSuccess(JSON.stringify(details), details);
17267
17453
  function toolSchema(pi) {
17268
17454
  const z2 = pi.zod;
17455
+ const phase = phaseOpParameters(z2);
17269
17456
  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(),
17457
+ op: z2.enum([...OPERATIONS.architect, ...OPERATIONS.controller]),
17458
+ issue: phase.issue,
17285
17459
  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
17460
  version: z2.number().optional(),
17287
17461
  issues: z2.array(z2.string()).optional(),
17288
- to: z2.enum(LEGION_PHASE_BACKWARD_TARGETS).optional(),
17289
- reason: z2.string().optional(),
17462
+ to: phase.to,
17463
+ reason: phase.reason,
17290
17464
  decision: z2.enum(["retry", "escalate"]).optional(),
17291
- summary: z2.string().optional(),
17292
- verdict: z2.enum(["pass", "fail"]).optional(),
17293
- ready: z2.boolean().optional(),
17465
+ summary: phase.summary,
17466
+ verdict: phase.verdict,
17467
+ ready: phase.ready,
17294
17468
  status: z2.enum(ISSUE_STATUSES2).optional()
17295
17469
  });
17296
17470
  }
17297
- function requiredString(parameters, operation, name) {
17471
+ function requiredString2(parameters, operation2, name) {
17298
17472
  const value = parameters[name];
17299
17473
  if (typeof value !== "string" || value.trim() === "")
17300
- throw new Error(`${operation} requires ${name}`);
17474
+ throw new Error(`${operation2} requires ${name}`);
17301
17475
  return value;
17302
17476
  }
17303
- function assertOperationInput(parameters, operation) {
17304
- const fields = OPERATION_FIELDS[operation];
17477
+ function assertOperationInput(parameters, operation2) {
17478
+ const fields = OPERATION_FIELDS[operation2];
17305
17479
  if (fields === undefined)
17306
- throw new Error(`Unsupported legion operation: ${operation}`);
17480
+ throw new Error(`Unsupported legion operation: ${operation2}`);
17307
17481
  for (const name of Object.keys(parameters)) {
17308
17482
  if (name !== "op" && !fields.includes(name)) {
17309
- throw new Error(`${operation} does not accept field "${name}"`);
17483
+ throw new Error(`${operation2} does not accept field "${name}"`);
17310
17484
  }
17311
17485
  }
17312
17486
  }
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
17487
  function createLegionTool(deps) {
17320
17488
  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
17489
  return {
17328
17490
  name: "legion",
17329
17491
  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.",
17492
+ description: legionToolDescription({
17493
+ envoyPublish: "envoy_publish",
17494
+ after: "The controller's read_state returns the daemon's whole state and set_status moves an issue to todo, backlog or icebox. "
17495
+ }),
17331
17496
  defaultInactive: true,
17332
17497
  parameters: toolSchema(pi),
17333
17498
  execute: async (_id, parameters, _signal, _onUpdate, context) => {
17334
17499
  try {
17335
17500
  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`);
17501
+ const operation2 = requiredString2(parameters, "legion", "op");
17502
+ if (!OPERATIONS[active.kind].includes(operation2)) {
17503
+ throw new Error(`${operation2} is not available to a ${active.kind} session`);
17504
+ }
17505
+ if (active.kind !== "controller" && isPhaseOp(operation2)) {
17506
+ const answer = await executePhaseOp({ daemon: daemon(), claim: active, handoffCommit }, parameters);
17507
+ if (operation2 === "handoff_complete")
17508
+ onPhaseCompleted(context);
17509
+ return jsonSuccess(answer);
17339
17510
  }
17340
- assertOperationInput(parameters, operation);
17511
+ assertOperationInput(parameters, operation2);
17341
17512
  const client = daemon();
17342
17513
  if (active.kind === "controller") {
17343
- if (operation === "read_state")
17514
+ if (operation2 === "read_state")
17344
17515
  return jsonSuccess(await client.state());
17345
- const issue2 = requiredString(parameters, operation, "issue");
17516
+ const issue2 = requiredString2(parameters, operation2, "issue");
17346
17517
  const status = ISSUE_STATUSES2.find((candidate) => candidate === parameters.status);
17347
17518
  if (status === undefined) {
17348
17519
  throw new Error(`set_status requires status ${ISSUE_STATUSES2.join(", ")}`);
17349
17520
  }
17350
- const { grantId } = await controllerGrant(active.sessionId);
17521
+ const { grantId } = await controllerGrant(active.sessionID);
17351
17522
  await client.issueStatus({ grantId, issue: issue2, status });
17352
17523
  return jsonSuccess({});
17353
17524
  }
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
- }
17525
+ switch (operation2) {
17384
17526
  case "register_gate": {
17385
17527
  const version2 = parameters.version;
17386
17528
  if (typeof version2 !== "number" || !Number.isSafeInteger(version2) || version2 < 1) {
17387
17529
  throw new Error("register_gate requires a positive integer version");
17388
17530
  }
17389
- const issue2 = requiredString(parameters, operation, "issue");
17531
+ const issue2 = requiredString2(parameters, operation2, "issue");
17390
17532
  if (active.issue !== active.tree) {
17391
17533
  throw new Error(`the design gate belongs to the tree root ${active.tree}; its root architect registers it`);
17392
17534
  }
17393
17535
  if (issue2 !== active.issue) {
17394
17536
  throw new Error(`the design gate belongs to the tree root ${active.tree}; register it there`);
17395
17537
  }
17396
- const reference = requiredString(parameters, operation, "artifactId");
17538
+ const reference = requiredString2(parameters, operation2, "artifactId");
17397
17539
  const isId = LegionGateRegisterRequest.shape.artifactId.safeParse(reference).success;
17398
17540
  const artifactId = isId ? reference : await resolveDocument(issue2, reference);
17399
- const grantId = await grantFor(client, active);
17541
+ const grantId = await mintClaimGrant(client, active);
17400
17542
  await client.gateRegister({ grantId, issue: issue2, artifactId, version: version2 });
17401
17543
  return jsonSuccess({});
17402
17544
  }
@@ -17405,57 +17547,48 @@ function createLegionTool(deps) {
17405
17547
  if (!Array.isArray(issues) || !issues.every((issue2) => typeof issue2 === "string")) {
17406
17548
  throw new Error("release_children requires issues");
17407
17549
  }
17408
- const grantId = await grantFor(client, active);
17550
+ const grantId = await mintClaimGrant(client, active);
17409
17551
  return jsonSuccess(await client.waveRelease({ grantId, issues }));
17410
17552
  }
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
17553
  case "retry_or_escalate": {
17421
17554
  const decision = parameters.decision;
17422
17555
  if (decision !== "retry" && decision !== "escalate") {
17423
17556
  throw new Error("retry_or_escalate requires decision retry or escalate");
17424
17557
  }
17425
- const grantId = await grantFor(client, active);
17558
+ const grantId = await mintClaimGrant(client, active);
17426
17559
  await client.phaseRetry({
17427
17560
  grantId,
17428
- issue: requiredString(parameters, operation, "issue"),
17561
+ issue: requiredString2(parameters, operation2, "issue"),
17429
17562
  decision
17430
17563
  });
17431
17564
  return jsonSuccess({});
17432
17565
  }
17433
17566
  case "sign_off": {
17434
- const grantId = await grantFor(client, active);
17567
+ const grantId = await mintClaimGrant(client, active);
17435
17568
  await client.signOff({
17436
17569
  grantId,
17437
- issue: requiredString(parameters, operation, "issue")
17570
+ issue: requiredString2(parameters, operation2, "issue")
17438
17571
  });
17439
17572
  return jsonSuccess({});
17440
17573
  }
17441
17574
  case "close_root": {
17442
- const grantId = await grantFor(client, active);
17575
+ const grantId = await mintClaimGrant(client, active);
17443
17576
  await client.rootClose({
17444
17577
  grantId,
17445
- issue: requiredString(parameters, operation, "issue"),
17446
- reason: requiredString(parameters, operation, "reason")
17578
+ issue: requiredString2(parameters, operation2, "issue"),
17579
+ reason: requiredString2(parameters, operation2, "reason")
17447
17580
  });
17448
17581
  return jsonSuccess({});
17449
17582
  }
17450
17583
  case "park_child":
17451
17584
  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));
17585
+ const grantId = await mintClaimGrant(client, active);
17586
+ const request = { grantId, issue: requiredString2(parameters, operation2, "issue") };
17587
+ await (operation2 === "park_child" ? client.childPark(request) : client.childRerun(request));
17455
17588
  return jsonSuccess({});
17456
17589
  }
17457
17590
  default:
17458
- throw new Error(`Unsupported legion operation: ${operation}`);
17591
+ throw new Error(`Unsupported legion operation: ${operation2}`);
17459
17592
  }
17460
17593
  } catch (error48) {
17461
17594
  return toolFailure(error48);
@@ -17691,17 +17824,10 @@ function legionExtension(pi) {
17691
17824
  const sessionID = context.sessionManager.getSessionId();
17692
17825
  const active = claimSession.capability(sessionID);
17693
17826
  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
- };
17827
+ return { kind: active.role === "architect" ? "architect" : "phase-worker", ...active };
17702
17828
  }
17703
17829
  if (controllerSession.isClaimedSession(sessionID)) {
17704
- return { kind: "controller", sessionId: sessionID };
17830
+ return { kind: "controller", sessionID };
17705
17831
  }
17706
17832
  throw new Error("legion is available only to this session's registered claim");
17707
17833
  },