@sjawhar/pi-legion 8.15.0 → 8.16.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 +412 -34
- package/dist/skills/legion-controller/SKILL.md +18 -10
- package/package.json +2 -2
package/dist/legion.js
CHANGED
|
@@ -13,10 +13,11 @@ var __export = (target, all) => {
|
|
|
13
13
|
set: __exportSetter.bind(all, name)
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
|
+
var __require = import.meta.require;
|
|
16
17
|
|
|
17
18
|
// extensions/legion.ts
|
|
18
19
|
import { randomUUID } from "crypto";
|
|
19
|
-
import
|
|
20
|
+
import path5 from "path";
|
|
20
21
|
|
|
21
22
|
// ../envoy-client/src/dispatch-config.ts
|
|
22
23
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -13559,6 +13560,9 @@ config(en_default());
|
|
|
13559
13560
|
function messageFor(error48) {
|
|
13560
13561
|
return error48 instanceof Error ? error48.message : String(error48);
|
|
13561
13562
|
}
|
|
13563
|
+
function hasErrnoCode(error48, code) {
|
|
13564
|
+
return error48 instanceof Error && "code" in error48 && error48.code === code;
|
|
13565
|
+
}
|
|
13562
13566
|
|
|
13563
13567
|
// ../envoy-client/src/secret-file.ts
|
|
13564
13568
|
import { readFileSync } from "fs";
|
|
@@ -15219,6 +15223,341 @@ function subagentSessionCheck() {
|
|
|
15219
15223
|
// extensions/legion.ts
|
|
15220
15224
|
import { logger as logger3 } from "@oh-my-pi/pi-utils";
|
|
15221
15225
|
|
|
15226
|
+
// src/capability-report.ts
|
|
15227
|
+
import { readdir, stat } from "fs/promises";
|
|
15228
|
+
import path3 from "path";
|
|
15229
|
+
var CAPABILITY_CHECK_TIMEOUT_MS = 8000;
|
|
15230
|
+
var CAPABILITY_REPORT_TIMEOUT_MS = 1e4;
|
|
15231
|
+
var WEB_SEARCH_PROBE_QUERY = "jujutsu version control";
|
|
15232
|
+
var ENVOY_TOOL_NAMES = [
|
|
15233
|
+
"envoy_subscribe",
|
|
15234
|
+
"envoy_unsubscribe",
|
|
15235
|
+
"envoy_list",
|
|
15236
|
+
"envoy_inbox",
|
|
15237
|
+
"envoy_send",
|
|
15238
|
+
"envoy_publish",
|
|
15239
|
+
"envoy_role_set",
|
|
15240
|
+
"envoy_role_get",
|
|
15241
|
+
"envoy_whoami",
|
|
15242
|
+
"envoy_sessions"
|
|
15243
|
+
];
|
|
15244
|
+
var LIVE_CAPABILITIES = [
|
|
15245
|
+
"subagents",
|
|
15246
|
+
"web-search",
|
|
15247
|
+
"mcp",
|
|
15248
|
+
"repository-extensions",
|
|
15249
|
+
"dispatch-envoy-tools",
|
|
15250
|
+
"github"
|
|
15251
|
+
];
|
|
15252
|
+
var DEFAULT_BUDGETS = {
|
|
15253
|
+
checkMs: CAPABILITY_CHECK_TIMEOUT_MS,
|
|
15254
|
+
reportMs: CAPABILITY_REPORT_TIMEOUT_MS
|
|
15255
|
+
};
|
|
15256
|
+
var ok = (detail) => ({ ok: true, detail });
|
|
15257
|
+
var failed = (detail) => ({ ok: false, detail });
|
|
15258
|
+
function firstLine(text) {
|
|
15259
|
+
return text.trim().split(`
|
|
15260
|
+
`, 1)[0] ?? "";
|
|
15261
|
+
}
|
|
15262
|
+
function commandFailure(result) {
|
|
15263
|
+
return firstLine(result.stderr) || `exit code ${result.code ?? "none"}`;
|
|
15264
|
+
}
|
|
15265
|
+
function whichOnPath(name) {
|
|
15266
|
+
return Bun.which(name, { PATH: process.env.PATH ?? "" }) ?? name;
|
|
15267
|
+
}
|
|
15268
|
+
var checkSubagents = async (host2, input) => {
|
|
15269
|
+
if (!input.agentsExposed)
|
|
15270
|
+
return failed("pi.agents is not exposed");
|
|
15271
|
+
if (!input.activeTools.includes("task"))
|
|
15272
|
+
return failed("task is not a registered tool");
|
|
15273
|
+
const [discovered, disabled] = await Promise.all([
|
|
15274
|
+
host2.discoverAgents(input.cwd),
|
|
15275
|
+
host2.disabledAgents()
|
|
15276
|
+
]);
|
|
15277
|
+
const names = new Set(discovered.agents.map((agent) => agent.name));
|
|
15278
|
+
const missing = input.promptAgents.filter((agent) => !names.has(agent));
|
|
15279
|
+
const off = input.promptAgents.filter((agent) => disabled.includes(agent));
|
|
15280
|
+
const problems = [];
|
|
15281
|
+
if (missing.length > 0) {
|
|
15282
|
+
problems.push(`agents the prompts dispatch that the task tool does not resolve: ${missing.join(", ")}`);
|
|
15283
|
+
}
|
|
15284
|
+
if (off.length > 0)
|
|
15285
|
+
problems.push(`disabled by task.disabledAgents: ${off.join(", ")}`);
|
|
15286
|
+
if (problems.length > 0)
|
|
15287
|
+
return failed(problems.join("; "));
|
|
15288
|
+
return ok(`task resolves ${names.size} agents, every one the prompts dispatch`);
|
|
15289
|
+
};
|
|
15290
|
+
var checkWebSearch = async (host2, input, signal) => {
|
|
15291
|
+
if (!input.activeTools.includes("web_search")) {
|
|
15292
|
+
return failed("web_search is not a registered tool");
|
|
15293
|
+
}
|
|
15294
|
+
const started = Date.now();
|
|
15295
|
+
const result = await host2.runSearchQuery({ query: WEB_SEARCH_PROBE_QUERY, limit: 1 }, { sessionId: input.sessionId, signal });
|
|
15296
|
+
const elapsed = Date.now() - started;
|
|
15297
|
+
const details = result.details ?? {};
|
|
15298
|
+
if (details.error !== undefined && details.error !== null) {
|
|
15299
|
+
return failed(`web_search failed: ${String(details.error)}`);
|
|
15300
|
+
}
|
|
15301
|
+
const provider = details.response?.provider ?? details.provider;
|
|
15302
|
+
if (provider === undefined || provider === null || provider === "none") {
|
|
15303
|
+
return failed("web_search failed: no provider answered");
|
|
15304
|
+
}
|
|
15305
|
+
const text = result.content?.[0]?.text ?? "";
|
|
15306
|
+
if (text.startsWith("Error"))
|
|
15307
|
+
return failed(`web_search failed: ${firstLine(text)}`);
|
|
15308
|
+
return ok(`web_search registered; provider ${String(provider)} answered in ${elapsed} ms`);
|
|
15309
|
+
};
|
|
15310
|
+
var URL_USERINFO = /([a-z][a-z0-9+.-]*:\/\/)[^/\s@]+@/gi;
|
|
15311
|
+
function mcpErrorFor(errors3, name) {
|
|
15312
|
+
let quoted;
|
|
15313
|
+
for (const entry of errors3) {
|
|
15314
|
+
if (typeof entry !== "object" || entry === null) {
|
|
15315
|
+
if (!String(entry).includes(name))
|
|
15316
|
+
continue;
|
|
15317
|
+
quoted = String(entry);
|
|
15318
|
+
break;
|
|
15319
|
+
}
|
|
15320
|
+
const record2 = entry;
|
|
15321
|
+
const names = [record2.server, record2.name, record2.path];
|
|
15322
|
+
if (!names.some((value) => value === name || value === `mcp:${name}`))
|
|
15323
|
+
continue;
|
|
15324
|
+
const message = record2.error ?? record2.message;
|
|
15325
|
+
quoted = message === undefined ? String(entry) : String(message);
|
|
15326
|
+
break;
|
|
15327
|
+
}
|
|
15328
|
+
return quoted?.replace(URL_USERINFO, "$1***@");
|
|
15329
|
+
}
|
|
15330
|
+
var checkMCP = async (host2, input, signal) => {
|
|
15331
|
+
const { configs, sources } = await host2.loadMCPConfigs(input.cwd);
|
|
15332
|
+
const configured = Object.keys(configs);
|
|
15333
|
+
if (configured.length === 0)
|
|
15334
|
+
return ok("no MCP server configured");
|
|
15335
|
+
const { manager, errors: errors3 } = await host2.discoverMCPServers(input.cwd);
|
|
15336
|
+
let torn;
|
|
15337
|
+
const tearDown = () => {
|
|
15338
|
+
torn ??= manager.disconnectAll().catch(() => {
|
|
15339
|
+
return;
|
|
15340
|
+
});
|
|
15341
|
+
return torn;
|
|
15342
|
+
};
|
|
15343
|
+
signal.addEventListener("abort", tearDown, { once: true });
|
|
15344
|
+
if (signal.aborted)
|
|
15345
|
+
tearDown();
|
|
15346
|
+
try {
|
|
15347
|
+
await manager.waitForPendingConnections();
|
|
15348
|
+
const connected = [];
|
|
15349
|
+
const unconnected = [];
|
|
15350
|
+
for (const name of configured) {
|
|
15351
|
+
const status = manager.getConnectionStatus(name);
|
|
15352
|
+
if (status === "connected") {
|
|
15353
|
+
const source = sources[name]?.path;
|
|
15354
|
+
connected.push(source === undefined ? name : `${name} (${path3.relative(input.cwd, source)})`);
|
|
15355
|
+
} else {
|
|
15356
|
+
unconnected.push(`${name} (${mcpErrorFor(errors3, name) ?? `status ${status}`})`);
|
|
15357
|
+
}
|
|
15358
|
+
}
|
|
15359
|
+
if (unconnected.length === 0)
|
|
15360
|
+
return ok(`connected: ${connected.join(", ")}`);
|
|
15361
|
+
const suffix = connected.length === 0 ? "" : `; connected: ${connected.join(", ")}`;
|
|
15362
|
+
return failed(`not connected: ${unconnected.join(", ")}${suffix}`);
|
|
15363
|
+
} finally {
|
|
15364
|
+
signal.removeEventListener("abort", tearDown);
|
|
15365
|
+
await tearDown();
|
|
15366
|
+
}
|
|
15367
|
+
};
|
|
15368
|
+
var EXTENSION_MODULE = /\.(?:ts|js|mjs|cjs)$/;
|
|
15369
|
+
var EXTENSION_INDEXES = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
15370
|
+
var SKILL_DIRECTORIES = [path3.join(".omp", "skills"), path3.join(".claude", "skills")];
|
|
15371
|
+
async function namesIn(directory) {
|
|
15372
|
+
try {
|
|
15373
|
+
return await readdir(directory);
|
|
15374
|
+
} catch (error48) {
|
|
15375
|
+
if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
|
|
15376
|
+
return [];
|
|
15377
|
+
throw error48;
|
|
15378
|
+
}
|
|
15379
|
+
}
|
|
15380
|
+
async function kindOf(file2) {
|
|
15381
|
+
try {
|
|
15382
|
+
const stats = await stat(file2);
|
|
15383
|
+
return stats.isFile() ? "file" : stats.isDirectory() ? "directory" : undefined;
|
|
15384
|
+
} catch (error48) {
|
|
15385
|
+
if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
|
|
15386
|
+
return;
|
|
15387
|
+
throw error48;
|
|
15388
|
+
}
|
|
15389
|
+
}
|
|
15390
|
+
async function repositoryEntries(cwd) {
|
|
15391
|
+
const entries = [];
|
|
15392
|
+
const extensions = path3.join(cwd, ".omp", "extensions");
|
|
15393
|
+
for (const name of await namesIn(extensions)) {
|
|
15394
|
+
const candidate = path3.join(extensions, name);
|
|
15395
|
+
const kind = await kindOf(candidate);
|
|
15396
|
+
if (kind === "file" && EXTENSION_MODULE.test(name)) {
|
|
15397
|
+
entries.push({ path: candidate, directory: false });
|
|
15398
|
+
} else if (kind === "directory") {
|
|
15399
|
+
const indexes = await Promise.all(EXTENSION_INDEXES.map((index) => kindOf(path3.join(candidate, index))));
|
|
15400
|
+
if (indexes.includes("file"))
|
|
15401
|
+
entries.push({ path: candidate, directory: true });
|
|
15402
|
+
}
|
|
15403
|
+
}
|
|
15404
|
+
for (const skills of SKILL_DIRECTORIES) {
|
|
15405
|
+
for (const name of await namesIn(path3.join(cwd, skills))) {
|
|
15406
|
+
const skill = path3.join(cwd, skills, name, "SKILL.md");
|
|
15407
|
+
if (await kindOf(skill) === "file")
|
|
15408
|
+
entries.push({ path: skill, directory: false });
|
|
15409
|
+
}
|
|
15410
|
+
}
|
|
15411
|
+
return entries;
|
|
15412
|
+
}
|
|
15413
|
+
var checkRepositoryExtensions = async (host2, input) => {
|
|
15414
|
+
const entries = await repositoryEntries(input.cwd);
|
|
15415
|
+
if (entries.length === 0) {
|
|
15416
|
+
return ok("the repository carries no .omp/extensions, .omp/skills or .claude/skills");
|
|
15417
|
+
}
|
|
15418
|
+
const [extensionPaths, loaded] = await Promise.all([
|
|
15419
|
+
host2.discoverExtensionPaths(input.cwd),
|
|
15420
|
+
host2.loadSkills(input.cwd)
|
|
15421
|
+
]);
|
|
15422
|
+
const discovered = new Set([...extensionPaths, ...loaded.skills.map((skill) => skill.filePath)].map((file2) => path3.resolve(file2)));
|
|
15423
|
+
const isDiscovered = (entry) => {
|
|
15424
|
+
const resolved = path3.resolve(entry.path);
|
|
15425
|
+
if (discovered.has(resolved))
|
|
15426
|
+
return true;
|
|
15427
|
+
if (!entry.directory)
|
|
15428
|
+
return false;
|
|
15429
|
+
const prefix = resolved + path3.sep;
|
|
15430
|
+
return [...discovered].some((file2) => file2.startsWith(prefix));
|
|
15431
|
+
};
|
|
15432
|
+
const relative = (selected) => selected.map((entry) => path3.relative(input.cwd, entry.path)).sort().join(", ");
|
|
15433
|
+
const found = [];
|
|
15434
|
+
const lost = [];
|
|
15435
|
+
for (const entry of entries)
|
|
15436
|
+
(isDiscovered(entry) ? found : lost).push(entry);
|
|
15437
|
+
if (lost.length === 0)
|
|
15438
|
+
return ok(`discovered: ${relative(found)}`);
|
|
15439
|
+
const suffix = found.length === 0 ? "" : `; discovered: ${relative(found)}`;
|
|
15440
|
+
return failed(`not discovered: ${relative(lost)}${suffix}`);
|
|
15441
|
+
};
|
|
15442
|
+
var checkDispatchEnvoyTools = async (host2, input, signal) => {
|
|
15443
|
+
const missing = ENVOY_TOOL_NAMES.filter((tool) => !input.activeTools.includes(tool));
|
|
15444
|
+
if (missing.length > 0)
|
|
15445
|
+
return failed(`Envoy tools missing: ${missing.join(", ")}`);
|
|
15446
|
+
const result = await host2.run("dispatch", ["read", "--issue", input.issue], {
|
|
15447
|
+
cwd: input.cwd,
|
|
15448
|
+
signal
|
|
15449
|
+
});
|
|
15450
|
+
if (result.code !== 0)
|
|
15451
|
+
return failed(`dispatch read failed: ${commandFailure(result)}`);
|
|
15452
|
+
return ok(`ten Envoy tools registered; dispatch read ${input.issue} answered from ${whichOnPath("dispatch")}`);
|
|
15453
|
+
};
|
|
15454
|
+
var checkGitHub = async (host2, input, signal) => {
|
|
15455
|
+
const result = await host2.run("gh", ["api", "graphql", "-f", "query={viewer{login}}", "--jq", ".data.viewer.login"], { cwd: input.cwd, signal });
|
|
15456
|
+
if (result.code !== 0)
|
|
15457
|
+
return failed(`gh viewer login failed: ${commandFailure(result)}`);
|
|
15458
|
+
return ok(`gh viewer login: ${result.stdout.trim()}`);
|
|
15459
|
+
};
|
|
15460
|
+
var CHECKS = {
|
|
15461
|
+
subagents: checkSubagents,
|
|
15462
|
+
"web-search": checkWebSearch,
|
|
15463
|
+
mcp: checkMCP,
|
|
15464
|
+
"repository-extensions": checkRepositoryExtensions,
|
|
15465
|
+
"dispatch-envoy-tools": checkDispatchEnvoyTools,
|
|
15466
|
+
github: checkGitHub
|
|
15467
|
+
};
|
|
15468
|
+
function budgetLabel(ms) {
|
|
15469
|
+
return ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
|
|
15470
|
+
}
|
|
15471
|
+
async function withinBudget(work, ms, onDeadline) {
|
|
15472
|
+
const deadline = Promise.withResolvers();
|
|
15473
|
+
const timer = setTimeout(() => deadline.resolve(onDeadline()), ms);
|
|
15474
|
+
try {
|
|
15475
|
+
return await Promise.race([work, deadline.promise]);
|
|
15476
|
+
} finally {
|
|
15477
|
+
clearTimeout(timer);
|
|
15478
|
+
}
|
|
15479
|
+
}
|
|
15480
|
+
async function measureCapabilities(host2, input, budgets = DEFAULT_BUDGETS) {
|
|
15481
|
+
const started = Date.now();
|
|
15482
|
+
const measuredAt = new Date(started).toISOString();
|
|
15483
|
+
const outcomes = new Map;
|
|
15484
|
+
const controllers = [];
|
|
15485
|
+
const checks3 = LIVE_CAPABILITIES.map(async (name) => {
|
|
15486
|
+
const controller = new AbortController;
|
|
15487
|
+
controllers.push(controller);
|
|
15488
|
+
const outcome = await withinBudget(CHECKS[name](host2, input, controller.signal).catch((error48) => failed(firstLine(messageFor(error48)))), budgets.checkMs, () => {
|
|
15489
|
+
controller.abort();
|
|
15490
|
+
return failed(`did not finish within ${budgetLabel(budgets.checkMs)}`);
|
|
15491
|
+
});
|
|
15492
|
+
outcomes.set(name, outcome);
|
|
15493
|
+
});
|
|
15494
|
+
await withinBudget(Promise.all(checks3).then(() => {
|
|
15495
|
+
return;
|
|
15496
|
+
}), budgets.reportMs, () => {
|
|
15497
|
+
return;
|
|
15498
|
+
});
|
|
15499
|
+
for (const controller of controllers)
|
|
15500
|
+
controller.abort();
|
|
15501
|
+
const rows = LIVE_CAPABILITIES.map((name) => ({
|
|
15502
|
+
name,
|
|
15503
|
+
...outcomes.get(name) ?? failed(`did not finish within ${budgetLabel(budgets.reportMs)}`)
|
|
15504
|
+
}));
|
|
15505
|
+
return { measuredAt, elapsedMs: Date.now() - started, rows };
|
|
15506
|
+
}
|
|
15507
|
+
async function runCommand(command, args, options) {
|
|
15508
|
+
const child = Bun.spawn([command, ...args], {
|
|
15509
|
+
cwd: options.cwd,
|
|
15510
|
+
env: process.env,
|
|
15511
|
+
stdin: "ignore",
|
|
15512
|
+
stdout: "pipe",
|
|
15513
|
+
stderr: "pipe",
|
|
15514
|
+
signal: options.signal
|
|
15515
|
+
});
|
|
15516
|
+
const [stdout, stderr] = await Promise.all([
|
|
15517
|
+
new Response(child.stdout).text(),
|
|
15518
|
+
new Response(child.stderr).text(),
|
|
15519
|
+
child.exited
|
|
15520
|
+
]);
|
|
15521
|
+
return { code: child.exitCode, stdout, stderr };
|
|
15522
|
+
}
|
|
15523
|
+
function ompCapabilityHost() {
|
|
15524
|
+
const hostSettings = async () => (await import("@oh-my-pi/pi-coding-agent/config/settings")).settings;
|
|
15525
|
+
return {
|
|
15526
|
+
discoverAgents: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverAgents(cwd),
|
|
15527
|
+
disabledAgents: async () => {
|
|
15528
|
+
const [{ cfgTaskDisabledAgents }, settings] = await Promise.all([
|
|
15529
|
+
import("@oh-my-pi/pi-coding-agent/task/settings"),
|
|
15530
|
+
hostSettings()
|
|
15531
|
+
]);
|
|
15532
|
+
return cfgTaskDisabledAgents.get(settings);
|
|
15533
|
+
},
|
|
15534
|
+
runSearchQuery: async (args, options) => (await import("@oh-my-pi/pi-coding-agent")).runSearchQuery(args, options),
|
|
15535
|
+
loadMCPConfigs: async (cwd) => (await import("@oh-my-pi/pi-coding-agent/mcp/config")).loadAllMCPConfigs(cwd),
|
|
15536
|
+
discoverMCPServers: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverMCPServers(cwd),
|
|
15537
|
+
discoverExtensionPaths: async (cwd) => {
|
|
15538
|
+
const [{ discoverExtensionPaths }, { cfgDisabledExtensions }, settings] = await Promise.all([
|
|
15539
|
+
import("@oh-my-pi/pi-coding-agent/extensibility/extensions/loader"),
|
|
15540
|
+
import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
|
|
15541
|
+
hostSettings()
|
|
15542
|
+
]);
|
|
15543
|
+
return discoverExtensionPaths([], cwd, cfgDisabledExtensions.get(settings));
|
|
15544
|
+
},
|
|
15545
|
+
loadSkills: async (cwd) => {
|
|
15546
|
+
const [{ loadSkills }, { cfgDisabledExtensions, cfgSkills }, settings] = await Promise.all([
|
|
15547
|
+
import("@oh-my-pi/pi-coding-agent"),
|
|
15548
|
+
import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
|
|
15549
|
+
hostSettings()
|
|
15550
|
+
]);
|
|
15551
|
+
return loadSkills({
|
|
15552
|
+
...cfgSkills.get(settings),
|
|
15553
|
+
cwd,
|
|
15554
|
+
disabledExtensions: cfgDisabledExtensions.get(settings)
|
|
15555
|
+
});
|
|
15556
|
+
},
|
|
15557
|
+
run: runCommand
|
|
15558
|
+
};
|
|
15559
|
+
}
|
|
15560
|
+
|
|
15222
15561
|
// ../pi-shared/src/role-claim-bridge.ts
|
|
15223
15562
|
function legionRoleClaimBridge() {
|
|
15224
15563
|
return envoyPluginInterface().roleClaim;
|
|
@@ -15244,7 +15583,7 @@ function onEnvoyRoleRegained(listener) {
|
|
|
15244
15583
|
// package.json
|
|
15245
15584
|
var package_default = {
|
|
15246
15585
|
name: "@sjawhar/pi-legion",
|
|
15247
|
-
version: "8.
|
|
15586
|
+
version: "8.16.0",
|
|
15248
15587
|
license: "Apache-2.0",
|
|
15249
15588
|
type: "module",
|
|
15250
15589
|
omp: {
|
|
@@ -15256,7 +15595,7 @@ var package_default = {
|
|
|
15256
15595
|
]
|
|
15257
15596
|
},
|
|
15258
15597
|
legion: {
|
|
15259
|
-
daemonApiVersion:
|
|
15598
|
+
daemonApiVersion: 21
|
|
15260
15599
|
},
|
|
15261
15600
|
repository: {
|
|
15262
15601
|
type: "git",
|
|
@@ -15394,10 +15733,17 @@ var legionLocator = exports_external.discriminatedUnion("runtime", [
|
|
|
15394
15733
|
})
|
|
15395
15734
|
})
|
|
15396
15735
|
]);
|
|
15736
|
+
var legionCapabilityReportView = exports_external.strictObject({
|
|
15737
|
+
measuredAt: timestamp,
|
|
15738
|
+
incarnation: exports_external.string(),
|
|
15739
|
+
ok: exports_external.array(nonEmptyString),
|
|
15740
|
+
open: exports_external.array(exports_external.strictObject({ name: nonEmptyString, detail: nonEmptyString }))
|
|
15741
|
+
});
|
|
15397
15742
|
var legionClaimView = exports_external.strictObject({
|
|
15398
15743
|
session: exports_external.string(),
|
|
15399
15744
|
state: exports_external.enum(LEGION_CLAIM_STATES),
|
|
15400
|
-
locator: legionLocator.optional()
|
|
15745
|
+
locator: legionLocator.optional(),
|
|
15746
|
+
capabilities: legionCapabilityReportView.optional()
|
|
15401
15747
|
});
|
|
15402
15748
|
var legionPhaseView = exports_external.strictObject({
|
|
15403
15749
|
claim: legionClaimView,
|
|
@@ -15465,7 +15811,7 @@ var legionAgentSecretsLoginView = exports_external.strictObject({
|
|
|
15465
15811
|
});
|
|
15466
15812
|
var legionCapabilityState = exports_external.strictObject({
|
|
15467
15813
|
name: nonEmptyString,
|
|
15468
|
-
status: exports_external.enum(["present", "installed", "unchecked", "
|
|
15814
|
+
status: exports_external.enum(["present", "installed", "unchecked", "withheld", "decided", "open"]),
|
|
15469
15815
|
detail: nonEmptyString,
|
|
15470
15816
|
decision: nonEmptyString.optional(),
|
|
15471
15817
|
configLine: nonEmptyString.optional()
|
|
@@ -15485,7 +15831,20 @@ var LegionRegisterResponse = exports_external.strictObject({
|
|
|
15485
15831
|
issue: nonEmptyString,
|
|
15486
15832
|
role: exports_external.enum(LEGION_ROLES),
|
|
15487
15833
|
generation: exports_external.number().int().positive(),
|
|
15488
|
-
secret: nonEmptyString
|
|
15834
|
+
secret: nonEmptyString,
|
|
15835
|
+
promptAgents: exports_external.array(nonEmptyString)
|
|
15836
|
+
});
|
|
15837
|
+
var LegionCapabilityReport = exports_external.strictObject({
|
|
15838
|
+
measuredAt: timestamp,
|
|
15839
|
+
elapsedMs: exports_external.number().int().nonnegative(),
|
|
15840
|
+
rows: exports_external.array(exports_external.strictObject({ name: nonEmptyString, ok: exports_external.boolean(), detail: exports_external.string() }))
|
|
15841
|
+
});
|
|
15842
|
+
var LegionReadyRequest = exports_external.strictObject({
|
|
15843
|
+
claimToken: nonEmptyString,
|
|
15844
|
+
sessionId: nonEmptyString,
|
|
15845
|
+
secret: nonEmptyString,
|
|
15846
|
+
generation: exports_external.number().int().positive(),
|
|
15847
|
+
capabilities: LegionCapabilityReport.optional()
|
|
15489
15848
|
});
|
|
15490
15849
|
var LegionControllerRegisterResponse = exports_external.strictObject({
|
|
15491
15850
|
claimToken: nonEmptyString,
|
|
@@ -15618,10 +15977,10 @@ class LegionDaemonApiError extends Error {
|
|
|
15618
15977
|
status;
|
|
15619
15978
|
code;
|
|
15620
15979
|
detail;
|
|
15621
|
-
constructor(method,
|
|
15622
|
-
super(`${method} ${
|
|
15980
|
+
constructor(method, path4, status, code, detail) {
|
|
15981
|
+
super(`${method} ${path4} failed with ${status}${code === undefined ? "" : ` ${code}`}: ${detail}`);
|
|
15623
15982
|
this.method = method;
|
|
15624
|
-
this.path =
|
|
15983
|
+
this.path = path4;
|
|
15625
15984
|
this.status = status;
|
|
15626
15985
|
this.code = code;
|
|
15627
15986
|
this.detail = detail;
|
|
@@ -15633,10 +15992,10 @@ class LegionDaemonContractError extends Error {
|
|
|
15633
15992
|
method;
|
|
15634
15993
|
path;
|
|
15635
15994
|
status;
|
|
15636
|
-
constructor(method,
|
|
15637
|
-
super(`${method} ${
|
|
15995
|
+
constructor(method, path4, status, problem) {
|
|
15996
|
+
super(`${method} ${path4} answered ${status} with a body the daemon API contract does not describe: ${problem}`);
|
|
15638
15997
|
this.method = method;
|
|
15639
|
-
this.path =
|
|
15998
|
+
this.path = path4;
|
|
15640
15999
|
this.status = status;
|
|
15641
16000
|
this.name = "LegionDaemonContractError";
|
|
15642
16001
|
}
|
|
@@ -15657,8 +16016,8 @@ function parseStrictly(schema, body) {
|
|
|
15657
16016
|
}
|
|
15658
16017
|
function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
|
|
15659
16018
|
const endpoint = baseUrl.replace(/\/+$/, "");
|
|
15660
|
-
const call = async (method,
|
|
15661
|
-
const response = await fetchFn(`${endpoint}${
|
|
16019
|
+
const call = async (method, path4, body) => {
|
|
16020
|
+
const response = await fetchFn(`${endpoint}${path4}`, body === undefined ? undefined : {
|
|
15662
16021
|
method,
|
|
15663
16022
|
headers: { "Content-Type": "application/json" },
|
|
15664
16023
|
body: JSON.stringify(body)
|
|
@@ -15669,23 +16028,23 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
15669
16028
|
const refusal = parseStrictly(LegionErrorResponse, text);
|
|
15670
16029
|
if ("value" in refusal) {
|
|
15671
16030
|
const code = "code" in refusal.value && typeof refusal.value.code === "string" ? refusal.value.code : undefined;
|
|
15672
|
-
throw new LegionDaemonApiError(method,
|
|
16031
|
+
throw new LegionDaemonApiError(method, path4, response.status, code, refusal.value.error);
|
|
15673
16032
|
}
|
|
15674
|
-
throw new LegionDaemonApiError(method,
|
|
16033
|
+
throw new LegionDaemonApiError(method, path4, response.status, undefined, `${JSON.stringify(text)} is not the daemon's refusal shape (${refusal.problem})`);
|
|
15675
16034
|
};
|
|
15676
|
-
const read = async (method,
|
|
15677
|
-
const { status, text } = await call(method,
|
|
16035
|
+
const read = async (method, path4, schema, body) => {
|
|
16036
|
+
const { status, text } = await call(method, path4, body);
|
|
15678
16037
|
const parsed = parseStrictly(schema, text);
|
|
15679
16038
|
if ("problem" in parsed) {
|
|
15680
|
-
throw new LegionDaemonContractError(method,
|
|
16039
|
+
throw new LegionDaemonContractError(method, path4, status, parsed.problem);
|
|
15681
16040
|
}
|
|
15682
16041
|
return parsed.value;
|
|
15683
16042
|
};
|
|
15684
|
-
const post = (
|
|
15685
|
-
const acknowledge = async (
|
|
15686
|
-
const { status, text } = await call("POST",
|
|
16043
|
+
const post = (path4, request, response, body) => read("POST", path4, response, request.parse(body));
|
|
16044
|
+
const acknowledge = async (path4, body) => {
|
|
16045
|
+
const { status, text } = await call("POST", path4, body);
|
|
15687
16046
|
if (status !== 204 || text !== "") {
|
|
15688
|
-
throw new LegionDaemonContractError("POST",
|
|
16047
|
+
throw new LegionDaemonContractError("POST", path4, status, "the route answers 204 with no body");
|
|
15689
16048
|
}
|
|
15690
16049
|
};
|
|
15691
16050
|
return {
|
|
@@ -15712,16 +16071,16 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
15712
16071
|
// src/jj-attribution.ts
|
|
15713
16072
|
import { execFile as execFile2 } from "child_process";
|
|
15714
16073
|
import { mkdir, rename, writeFile } from "fs/promises";
|
|
15715
|
-
import
|
|
16074
|
+
import path4 from "path";
|
|
15716
16075
|
import { promisify as promisify2 } from "util";
|
|
15717
16076
|
var UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
15718
16077
|
var TOP_LEVEL_TRANSCRIPT = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T[\\d-]+Z_(${UUID})\\.jsonl$`);
|
|
15719
16078
|
var ATTRIBUTION_OVERLAY = /^omp-attribution-.*\.toml$/;
|
|
15720
16079
|
function sessionIdFromTranscript(sessionFile) {
|
|
15721
|
-
return TOP_LEVEL_TRANSCRIPT.exec(
|
|
16080
|
+
return TOP_LEVEL_TRANSCRIPT.exec(path4.basename(sessionFile))?.[1];
|
|
15722
16081
|
}
|
|
15723
16082
|
function withoutOverlays(chain) {
|
|
15724
|
-
return chain.split(":").filter((entry) => entry && !ATTRIBUTION_OVERLAY.test(
|
|
16083
|
+
return chain.split(":").filter((entry) => entry && !ATTRIBUTION_OVERLAY.test(path4.basename(entry))).join(":");
|
|
15725
16084
|
}
|
|
15726
16085
|
async function userConfigChain() {
|
|
15727
16086
|
try {
|
|
@@ -15737,7 +16096,7 @@ async function exportJjSessionAttribution(sessionFile, stateDir, env = process.e
|
|
|
15737
16096
|
const id = sessionIdFromTranscript(sessionFile);
|
|
15738
16097
|
if (!id)
|
|
15739
16098
|
return;
|
|
15740
|
-
const overlay =
|
|
16099
|
+
const overlay = path4.join(stateDir, `omp-attribution-${id}.toml`);
|
|
15741
16100
|
if (env.JJ_CONFIG?.split(":").includes(overlay))
|
|
15742
16101
|
return;
|
|
15743
16102
|
const base = (env.JJ_CONFIG ? withoutOverlays(env.JJ_CONFIG) : "") || await resolveBase();
|
|
@@ -15808,12 +16167,10 @@ function createClaimSession(deps) {
|
|
|
15808
16167
|
agentId,
|
|
15809
16168
|
pluginContract: package_default.legion.daemonApiVersion
|
|
15810
16169
|
}).catch((error48) => exitOnRegistrationRefusal(error48, deps.exitProcess));
|
|
15811
|
-
const
|
|
15812
|
-
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
generation: claim.generation
|
|
15816
|
-
};
|
|
16170
|
+
const measuring = Promise.resolve().then(() => deps.measureCapabilities(context, { issue: claim.issue, promptAgents: claim.promptAgents })).catch((error48) => {
|
|
16171
|
+
console.error(`[legion] capability measurement failed; ready carries no report: ${messageFor(error48)}`);
|
|
16172
|
+
return;
|
|
16173
|
+
});
|
|
15817
16174
|
try {
|
|
15818
16175
|
const stateDir = requiredEnvironment(process.env, "LEGION_STATE_DIR");
|
|
15819
16176
|
await exportJjSessionAttribution(sessionFile, stateDir);
|
|
@@ -15825,6 +16182,14 @@ function createClaimSession(deps) {
|
|
|
15825
16182
|
secret: claim.secret
|
|
15826
16183
|
};
|
|
15827
16184
|
await claimEnvoyRole(sessionID, claim.claimToken, context);
|
|
16185
|
+
const capabilities = await measuring;
|
|
16186
|
+
const ready = {
|
|
16187
|
+
claimToken: claim.claimToken,
|
|
16188
|
+
sessionId: sessionID,
|
|
16189
|
+
secret: claim.secret,
|
|
16190
|
+
generation: claim.generation,
|
|
16191
|
+
...capabilities === undefined ? {} : { capabilities }
|
|
16192
|
+
};
|
|
15828
16193
|
await callReadyWithRetry("claims/ready", () => daemon.ready(ready));
|
|
15829
16194
|
onEnvoyRoleRegained(async (role, reason) => {
|
|
15830
16195
|
if (role !== claim.claimToken)
|
|
@@ -16406,13 +16771,17 @@ var exitProcess = (code) => process.exit(code);
|
|
|
16406
16771
|
function setLegionBootstrapExitForTests(hook) {
|
|
16407
16772
|
exitProcess = hook;
|
|
16408
16773
|
}
|
|
16774
|
+
var capabilityHost = ompCapabilityHost;
|
|
16775
|
+
function setLegionCapabilityHostForTests(host2) {
|
|
16776
|
+
capabilityHost = host2;
|
|
16777
|
+
}
|
|
16409
16778
|
async function persistedTranscript(context) {
|
|
16410
16779
|
await context.sessionManager.ensureOnDisk();
|
|
16411
16780
|
const sessionFile = context.sessionManager.getSessionFile();
|
|
16412
16781
|
if (!sessionFile?.endsWith(".jsonl")) {
|
|
16413
16782
|
throw new Error("Legion session must have a persisted transcript");
|
|
16414
16783
|
}
|
|
16415
|
-
const agentId =
|
|
16784
|
+
const agentId = path5.basename(sessionFile, ".jsonl");
|
|
16416
16785
|
if (!agentId)
|
|
16417
16786
|
throw new Error("Legion session transcript has no agent id");
|
|
16418
16787
|
return { sessionFile, agentId };
|
|
@@ -16462,6 +16831,14 @@ function legionExtension(pi) {
|
|
|
16462
16831
|
const claimSession = createClaimSession({
|
|
16463
16832
|
daemon: roleDaemon,
|
|
16464
16833
|
persistedTranscript,
|
|
16834
|
+
measureCapabilities: (context, input) => measureCapabilities(capabilityHost(), {
|
|
16835
|
+
cwd: context.cwd,
|
|
16836
|
+
sessionId: context.sessionManager.getSessionId(),
|
|
16837
|
+
issue: input.issue,
|
|
16838
|
+
promptAgents: input.promptAgents,
|
|
16839
|
+
activeTools: pi.getActiveTools(),
|
|
16840
|
+
agentsExposed: pi.agents !== undefined
|
|
16841
|
+
}),
|
|
16465
16842
|
exitProcess: (code) => exitProcess(code)
|
|
16466
16843
|
});
|
|
16467
16844
|
const controllerSession = createControllerSession({
|
|
@@ -16620,6 +16997,7 @@ function legionExtension(pi) {
|
|
|
16620
16997
|
});
|
|
16621
16998
|
}
|
|
16622
16999
|
export {
|
|
17000
|
+
setLegionCapabilityHostForTests,
|
|
16623
17001
|
setLegionBootstrapExitForTests,
|
|
16624
17002
|
legionExtension as default
|
|
16625
17003
|
};
|
|
@@ -193,12 +193,17 @@ unsent:
|
|
|
193
193
|
`controller_wake_interval_seconds` (an hour by default), whatever the slots. An earlier walk
|
|
194
194
|
that found nothing, a day with no event, and [a tree waiting on a root
|
|
195
195
|
claim](#trees-waiting-on-a-root-claim-go-daemon) all get a turn from it. Its payload's
|
|
196
|
-
`openCapabilities`, when present, names the
|
|
197
|
-
fallback, every role's CPU and memory limits) the deployment's own
|
|
198
|
-
no decision recorded
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
`openCapabilities`, when present, names the open capability rows: the deployment capabilities
|
|
197
|
+
(the secrets broker, model fallback, every role's CPU and memory limits) the deployment's own
|
|
198
|
+
configuration leaves open with no decision recorded, and the live capabilities (`subagents`,
|
|
199
|
+
`web-search`, `mcp`, `repository-extensions`, `dispatch-envoy-tools`, `github`) a running
|
|
200
|
+
session's own check at its start failed. `read_state` lists every row under `capabilities`, each
|
|
201
|
+
open one with its `detail`: on a deployment row, with the `configLine` to write into
|
|
202
|
+
`legion.yaml` (`capabilities.decided.<name>: "<reason>"`); on a live row, naming each failing
|
|
203
|
+
session (its pod or pane, its role, its issue) and the fact its check found, with no
|
|
204
|
+
`configLine`, since nothing in `legion.yaml` decides a live row. Name them in the day's report.
|
|
205
|
+
A gap is the operator's to close or to decide — a live one the operator's or the issue's
|
|
206
|
+
architect's to fix in the session's environment — never yours, and it never stops the walk.
|
|
202
207
|
|
|
203
208
|
**Scope first.** The scope the deployment instructions state decides which issues are candidates
|
|
204
209
|
at all, before anything below. When they say you hand Legion no issue yourself, or that Legion
|
|
@@ -350,9 +355,12 @@ every pull request by its URL.
|
|
|
350
355
|
when it made one: what it took, and how many candidates each row of the table skipped, so a
|
|
351
356
|
reader can see why a free slot stays empty.
|
|
352
357
|
- **Open capabilities.** Each row of `capabilities` in `read_state` whose `status` is
|
|
353
|
-
`open` (the `tick` payload's `openCapabilities`): its name, its `detail`, and
|
|
354
|
-
the `legion.yaml` line that records a decision.
|
|
355
|
-
|
|
358
|
+
`open` (the `tick` payload's `openCapabilities`): its name, its `detail`, and, on a deployment
|
|
359
|
+
row, its `configLine`, the `legion.yaml` line that records a decision. A live row a running
|
|
360
|
+
session failed has no `configLine`: its `detail` names the session (pod or pane), the role, the
|
|
361
|
+
issue and the fact, and nothing in `legion.yaml` decides it. The operator closes or decides a
|
|
362
|
+
deployment row, and the operator or the issue's architect fixes a live row's session
|
|
363
|
+
environment; you only report either, and a report with none says nothing of them.
|
|
356
364
|
|
|
357
365
|
A day with nothing finished says so in one sentence. When the lists do not fit, keep the counts
|
|
358
366
|
and the highest-priority issues.
|
|
@@ -394,7 +402,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
|
|
|
394
402
|
| New issue created in the Dispatch project, status `triage`: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above); the start procedure above catches one sent while no controller ran | issue key + triage context (incl. pre-existing children) | Triage: `set_status` to `todo` to admit, or to `backlog`/`icebox` to park |
|
|
395
403
|
| `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `read_state`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
396
404
|
| `todo on <KEY>` from the Go daemon (payload `{kind: "todo"}`) | an issue not handed to Legion that changed while in `todo` and a slot stood free, sent half a minute later | Verify a free slot, then walk the whole `todo` list ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
|
|
397
|
-
| `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `read_state` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides
|
|
405
|
+
| `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `read_state`, and the live capabilities a running session's check failed, each with the detail naming the session (pod or pane), role, issue and fact, and no `configLine` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides a deployment row; the operator or the issue's architect fixes a live row's session; you take no other action) |
|
|
398
406
|
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `dispatch ask` only for a human to-do |
|
|
399
407
|
| Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
|
|
400
408
|
| `held on <KEY>` from the Go daemon (payload `{kind: "held", phase, role?, reason?}`) | the held issue, the phase it left, and the role whose claim failed, or `reason: "escalated"` | Verify the hold in `read_state` (the issue's phase is `held`). Without `reason`, a phase worker's launches or prompts ran out and the tree's architect decides retry or escalate: no action. With `reason: "escalated"` (on the record, `issues.<KEY>.holdReason` is `escalated`), the architect sent it to you: handle it as an architect escalation below. Parking the tree is `set_status` of the root to `backlog`; setting the root back to `todo` later re-admits it as a new generation, which starts again from its architect |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.16.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"omp": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
|
-
"daemonApiVersion":
|
|
15
|
+
"daemonApiVersion": 21
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|