@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.
- package/dist/legion.js +1274 -1148
- 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
|
-
// ../
|
|
15559
|
-
|
|
15560
|
-
var
|
|
15561
|
-
var
|
|
15562
|
-
var
|
|
15563
|
-
|
|
15564
|
-
|
|
15565
|
-
|
|
15566
|
-
|
|
15567
|
-
|
|
15568
|
-
|
|
15569
|
-
|
|
15570
|
-
|
|
15571
|
-
|
|
15572
|
-
|
|
15573
|
-
|
|
15574
|
-
|
|
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
|
|
15753
|
-
"
|
|
15754
|
-
"
|
|
15755
|
-
"
|
|
15756
|
-
"
|
|
15757
|
-
"
|
|
15758
|
-
"
|
|
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
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
|
|
15765
|
-
|
|
15766
|
-
|
|
15767
|
-
|
|
15768
|
-
|
|
15769
|
-
|
|
15770
|
-
|
|
15771
|
-
|
|
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,
|
|
16617
|
-
super(`${method} ${
|
|
16618
|
-
this.method = method;
|
|
16619
|
-
this.path =
|
|
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
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
|
|
16634
|
-
|
|
16635
|
-
|
|
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
|
|
16640
|
-
|
|
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
|
-
|
|
16643
|
-
}
|
|
16644
|
-
|
|
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
|
|
16654
|
-
const
|
|
16655
|
-
const
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
|
|
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
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
|
|
16672
|
-
|
|
16673
|
-
|
|
16674
|
-
|
|
16675
|
-
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
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
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
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
|
-
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
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
|
-
|
|
16857
|
-
|
|
16858
|
-
|
|
16859
|
-
}
|
|
16860
|
-
|
|
16861
|
-
|
|
16862
|
-
|
|
16863
|
-
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
})
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
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
|
-
|
|
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:
|
|
17289
|
-
reason:
|
|
17462
|
+
to: phase.to,
|
|
17463
|
+
reason: phase.reason,
|
|
17290
17464
|
decision: z2.enum(["retry", "escalate"]).optional(),
|
|
17291
|
-
summary:
|
|
17292
|
-
verdict:
|
|
17293
|
-
ready:
|
|
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
|
|
17471
|
+
function requiredString2(parameters, operation2, name) {
|
|
17298
17472
|
const value = parameters[name];
|
|
17299
17473
|
if (typeof value !== "string" || value.trim() === "")
|
|
17300
|
-
throw new Error(`${
|
|
17474
|
+
throw new Error(`${operation2} requires ${name}`);
|
|
17301
17475
|
return value;
|
|
17302
17476
|
}
|
|
17303
|
-
function assertOperationInput(parameters,
|
|
17304
|
-
const fields = OPERATION_FIELDS[
|
|
17477
|
+
function assertOperationInput(parameters, operation2) {
|
|
17478
|
+
const fields = OPERATION_FIELDS[operation2];
|
|
17305
17479
|
if (fields === undefined)
|
|
17306
|
-
throw new Error(`Unsupported legion 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(`${
|
|
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:
|
|
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
|
|
17337
|
-
if (!OPERATIONS[active.kind].includes(
|
|
17338
|
-
throw new Error(`${
|
|
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,
|
|
17511
|
+
assertOperationInput(parameters, operation2);
|
|
17341
17512
|
const client = daemon();
|
|
17342
17513
|
if (active.kind === "controller") {
|
|
17343
|
-
if (
|
|
17514
|
+
if (operation2 === "read_state")
|
|
17344
17515
|
return jsonSuccess(await client.state());
|
|
17345
|
-
const issue2 =
|
|
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.
|
|
17521
|
+
const { grantId } = await controllerGrant(active.sessionID);
|
|
17351
17522
|
await client.issueStatus({ grantId, issue: issue2, status });
|
|
17352
17523
|
return jsonSuccess({});
|
|
17353
17524
|
}
|
|
17354
|
-
switch (
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
17558
|
+
const grantId = await mintClaimGrant(client, active);
|
|
17426
17559
|
await client.phaseRetry({
|
|
17427
17560
|
grantId,
|
|
17428
|
-
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
|
|
17567
|
+
const grantId = await mintClaimGrant(client, active);
|
|
17435
17568
|
await client.signOff({
|
|
17436
17569
|
grantId,
|
|
17437
|
-
issue:
|
|
17570
|
+
issue: requiredString2(parameters, operation2, "issue")
|
|
17438
17571
|
});
|
|
17439
17572
|
return jsonSuccess({});
|
|
17440
17573
|
}
|
|
17441
17574
|
case "close_root": {
|
|
17442
|
-
const grantId = await
|
|
17575
|
+
const grantId = await mintClaimGrant(client, active);
|
|
17443
17576
|
await client.rootClose({
|
|
17444
17577
|
grantId,
|
|
17445
|
-
issue:
|
|
17446
|
-
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
|
|
17453
|
-
const request = { grantId, issue:
|
|
17454
|
-
await (
|
|
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: ${
|
|
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",
|
|
17830
|
+
return { kind: "controller", sessionID };
|
|
17705
17831
|
}
|
|
17706
17832
|
throw new Error("legion is available only to this session's registered claim");
|
|
17707
17833
|
},
|