@sjawhar/pi-legion 8.15.0 → 8.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/legion.js +456 -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,385 @@ 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
|
+
function mcpErrorFor(errors3, name) {
|
|
15311
|
+
for (const entry of errors3) {
|
|
15312
|
+
if (typeof entry !== "object" || entry === null) {
|
|
15313
|
+
if (!String(entry).includes(name))
|
|
15314
|
+
continue;
|
|
15315
|
+
return String(entry);
|
|
15316
|
+
}
|
|
15317
|
+
const record2 = entry;
|
|
15318
|
+
const names = [record2.server, record2.name, record2.path];
|
|
15319
|
+
if (!names.some((value) => value === name || value === `mcp:${name}`))
|
|
15320
|
+
continue;
|
|
15321
|
+
const message = record2.error ?? record2.message;
|
|
15322
|
+
return message === undefined ? String(entry) : String(message);
|
|
15323
|
+
}
|
|
15324
|
+
return;
|
|
15325
|
+
}
|
|
15326
|
+
var checkMCP = async (host2, input, signal) => {
|
|
15327
|
+
const { configs, sources } = await host2.loadMCPConfigs(input.cwd);
|
|
15328
|
+
const configured = Object.keys(configs);
|
|
15329
|
+
if (configured.length === 0)
|
|
15330
|
+
return ok("no MCP server configured");
|
|
15331
|
+
const { manager, errors: errors3 } = await host2.discoverMCPServers(input.cwd);
|
|
15332
|
+
let torn;
|
|
15333
|
+
const tearDown = () => {
|
|
15334
|
+
torn ??= manager.disconnectAll().catch(() => {
|
|
15335
|
+
return;
|
|
15336
|
+
});
|
|
15337
|
+
return torn;
|
|
15338
|
+
};
|
|
15339
|
+
signal.addEventListener("abort", tearDown, { once: true });
|
|
15340
|
+
if (signal.aborted)
|
|
15341
|
+
tearDown();
|
|
15342
|
+
try {
|
|
15343
|
+
await manager.waitForPendingConnections();
|
|
15344
|
+
const connected = [];
|
|
15345
|
+
const unconnected = [];
|
|
15346
|
+
for (const name of configured) {
|
|
15347
|
+
const status = manager.getConnectionStatus(name);
|
|
15348
|
+
if (status === "connected") {
|
|
15349
|
+
const source = sources[name]?.path;
|
|
15350
|
+
connected.push(source === undefined ? name : `${name} (${path3.relative(input.cwd, source)})`);
|
|
15351
|
+
} else {
|
|
15352
|
+
unconnected.push(`${name} (${mcpErrorFor(errors3, name) ?? `status ${status}`})`);
|
|
15353
|
+
}
|
|
15354
|
+
}
|
|
15355
|
+
if (unconnected.length === 0)
|
|
15356
|
+
return ok(`connected: ${connected.join(", ")}`);
|
|
15357
|
+
const suffix = connected.length === 0 ? "" : `; connected: ${connected.join(", ")}`;
|
|
15358
|
+
return failed(`not connected: ${unconnected.join(", ")}${suffix}`);
|
|
15359
|
+
} finally {
|
|
15360
|
+
signal.removeEventListener("abort", tearDown);
|
|
15361
|
+
await tearDown();
|
|
15362
|
+
}
|
|
15363
|
+
};
|
|
15364
|
+
var EXTENSION_MODULE = /\.(?:ts|js|mjs|cjs)$/;
|
|
15365
|
+
var EXTENSION_INDEXES = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
15366
|
+
var SKILL_DIRECTORIES = [path3.join(".omp", "skills"), path3.join(".claude", "skills")];
|
|
15367
|
+
async function namesIn(directory) {
|
|
15368
|
+
try {
|
|
15369
|
+
return await readdir(directory);
|
|
15370
|
+
} catch (error48) {
|
|
15371
|
+
if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
|
|
15372
|
+
return [];
|
|
15373
|
+
throw error48;
|
|
15374
|
+
}
|
|
15375
|
+
}
|
|
15376
|
+
async function kindOf(file2) {
|
|
15377
|
+
try {
|
|
15378
|
+
const stats = await stat(file2);
|
|
15379
|
+
return stats.isFile() ? "file" : stats.isDirectory() ? "directory" : undefined;
|
|
15380
|
+
} catch (error48) {
|
|
15381
|
+
if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR"))
|
|
15382
|
+
return;
|
|
15383
|
+
throw error48;
|
|
15384
|
+
}
|
|
15385
|
+
}
|
|
15386
|
+
async function repositoryEntries(cwd) {
|
|
15387
|
+
const entries = [];
|
|
15388
|
+
const extensions = path3.join(cwd, ".omp", "extensions");
|
|
15389
|
+
for (const name of await namesIn(extensions)) {
|
|
15390
|
+
const candidate = path3.join(extensions, name);
|
|
15391
|
+
const kind = await kindOf(candidate);
|
|
15392
|
+
if (kind === "file" && EXTENSION_MODULE.test(name)) {
|
|
15393
|
+
entries.push({ path: candidate, directory: false });
|
|
15394
|
+
} else if (kind === "directory") {
|
|
15395
|
+
const indexes = await Promise.all(EXTENSION_INDEXES.map((index) => kindOf(path3.join(candidate, index))));
|
|
15396
|
+
if (indexes.includes("file"))
|
|
15397
|
+
entries.push({ path: candidate, directory: true });
|
|
15398
|
+
}
|
|
15399
|
+
}
|
|
15400
|
+
for (const skills of SKILL_DIRECTORIES) {
|
|
15401
|
+
for (const name of await namesIn(path3.join(cwd, skills))) {
|
|
15402
|
+
const skill = path3.join(cwd, skills, name, "SKILL.md");
|
|
15403
|
+
if (await kindOf(skill) === "file")
|
|
15404
|
+
entries.push({ path: skill, directory: false });
|
|
15405
|
+
}
|
|
15406
|
+
}
|
|
15407
|
+
return entries;
|
|
15408
|
+
}
|
|
15409
|
+
var checkRepositoryExtensions = async (host2, input) => {
|
|
15410
|
+
const entries = await repositoryEntries(input.cwd);
|
|
15411
|
+
if (entries.length === 0) {
|
|
15412
|
+
return ok("the repository carries no .omp/extensions, .omp/skills or .claude/skills");
|
|
15413
|
+
}
|
|
15414
|
+
const [extensionPaths, loaded] = await Promise.all([
|
|
15415
|
+
host2.discoverExtensionPaths(input.cwd),
|
|
15416
|
+
host2.loadSkills(input.cwd)
|
|
15417
|
+
]);
|
|
15418
|
+
const discovered = new Set([...extensionPaths, ...loaded.skills.map((skill) => skill.filePath)].map((file2) => path3.resolve(file2)));
|
|
15419
|
+
const isDiscovered = (entry) => {
|
|
15420
|
+
const resolved = path3.resolve(entry.path);
|
|
15421
|
+
if (discovered.has(resolved))
|
|
15422
|
+
return true;
|
|
15423
|
+
if (!entry.directory)
|
|
15424
|
+
return false;
|
|
15425
|
+
const prefix = resolved + path3.sep;
|
|
15426
|
+
return [...discovered].some((file2) => file2.startsWith(prefix));
|
|
15427
|
+
};
|
|
15428
|
+
const relative = (selected) => selected.map((entry) => path3.relative(input.cwd, entry.path)).sort().join(", ");
|
|
15429
|
+
const found = [];
|
|
15430
|
+
const lost = [];
|
|
15431
|
+
for (const entry of entries)
|
|
15432
|
+
(isDiscovered(entry) ? found : lost).push(entry);
|
|
15433
|
+
if (lost.length === 0)
|
|
15434
|
+
return ok(`discovered: ${relative(found)}`);
|
|
15435
|
+
const suffix = found.length === 0 ? "" : `; discovered: ${relative(found)}`;
|
|
15436
|
+
return failed(`not discovered: ${relative(lost)}${suffix}`);
|
|
15437
|
+
};
|
|
15438
|
+
var checkDispatchEnvoyTools = async (host2, input, signal) => {
|
|
15439
|
+
const missing = ENVOY_TOOL_NAMES.filter((tool) => !input.activeTools.includes(tool));
|
|
15440
|
+
if (missing.length > 0)
|
|
15441
|
+
return failed(`Envoy tools missing: ${missing.join(", ")}`);
|
|
15442
|
+
const result = await host2.run("dispatch", ["read", "--issue", input.issue], {
|
|
15443
|
+
cwd: input.cwd,
|
|
15444
|
+
signal
|
|
15445
|
+
});
|
|
15446
|
+
if (result.code !== 0)
|
|
15447
|
+
return failed(`dispatch read failed: ${commandFailure(result)}`);
|
|
15448
|
+
return ok(`ten Envoy tools registered; dispatch read ${input.issue} answered from ${whichOnPath("dispatch")}`);
|
|
15449
|
+
};
|
|
15450
|
+
var checkGitHub = async (host2, input, signal) => {
|
|
15451
|
+
const result = await host2.run("gh", ["api", "graphql", "-f", "query={viewer{login}}", "--jq", ".data.viewer.login"], { cwd: input.cwd, signal });
|
|
15452
|
+
if (result.code !== 0)
|
|
15453
|
+
return failed(`gh viewer login failed: ${commandFailure(result)}`);
|
|
15454
|
+
return ok(`gh viewer login: ${result.stdout.trim()}`);
|
|
15455
|
+
};
|
|
15456
|
+
var CHECKS = {
|
|
15457
|
+
subagents: checkSubagents,
|
|
15458
|
+
"web-search": checkWebSearch,
|
|
15459
|
+
mcp: checkMCP,
|
|
15460
|
+
"repository-extensions": checkRepositoryExtensions,
|
|
15461
|
+
"dispatch-envoy-tools": checkDispatchEnvoyTools,
|
|
15462
|
+
github: checkGitHub
|
|
15463
|
+
};
|
|
15464
|
+
var MAX_DETAIL_BYTES = 1024;
|
|
15465
|
+
var HELD = "\uFFFF";
|
|
15466
|
+
function isSpace(code) {
|
|
15467
|
+
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;
|
|
15468
|
+
}
|
|
15469
|
+
function truncateUtf8(text, max) {
|
|
15470
|
+
let bytes = 0;
|
|
15471
|
+
let end = 0;
|
|
15472
|
+
for (const char of text) {
|
|
15473
|
+
const code = char.codePointAt(0) ?? 0;
|
|
15474
|
+
bytes += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
|
|
15475
|
+
if (bytes > max)
|
|
15476
|
+
break;
|
|
15477
|
+
end += char.length;
|
|
15478
|
+
}
|
|
15479
|
+
return text.slice(0, end);
|
|
15480
|
+
}
|
|
15481
|
+
var CREDENTIAL_RULES = [
|
|
15482
|
+
[/(?<![a-z0-9+.-])([0-9+.-]*[a-z][a-z0-9+.-]*:\/\/)[^ /?#]+@/gi, "$1***@"],
|
|
15483
|
+
[
|
|
15484
|
+
/([?&](?:token|key|secret|password|access_token|refresh_token|id_token|api_key|apikey|client_secret|sig|x-amz-security-token|x-amz-signature)=)[^ &#"'<>]+/gi,
|
|
15485
|
+
"$1***"
|
|
15486
|
+
],
|
|
15487
|
+
[
|
|
15488
|
+
/(authorization["']?[ \uffff]*[:=][ \uffff]*)(?:(["'])((?:bearer|basic|token|digest)[ \uffff]+)?[^"']+|(\[?)((?:bearer|basic|token|digest)[ \uffff]+)?[^)\]}>]+)/gi,
|
|
15489
|
+
"$1$2$3$4$5***"
|
|
15490
|
+
],
|
|
15491
|
+
[/(x-api-key["']?[ \uffff]*[:=][ \uffff]*["']?)[A-Za-z0-9._~+/=*\uffff-]+/gi, "$1***"],
|
|
15492
|
+
[/\b(bearer[ \uffff]+)[A-Za-z0-9._~+/=*\uffff-]+/gi, "$1***"],
|
|
15493
|
+
[/\b(gh[pousr]_|github_pat_)[A-Za-z0-9._\uffff-]{20,}/g, "$1***"],
|
|
15494
|
+
[
|
|
15495
|
+
/(?<![A-Za-z0-9_\uffff-])eyJ[A-Za-z0-9_\uffff-]{8,}\.[A-Za-z0-9_\uffff-]{8,}\.[A-Za-z0-9_\uffff-]*/g,
|
|
15496
|
+
"***"
|
|
15497
|
+
],
|
|
15498
|
+
[/\b(sk-|dsp_)[A-Za-z0-9_\uffff-]{20,}/g, "$1***"],
|
|
15499
|
+
[/\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, "$1***"]
|
|
15500
|
+
];
|
|
15501
|
+
function cleanDetail(text) {
|
|
15502
|
+
let marked = "";
|
|
15503
|
+
for (const char of text) {
|
|
15504
|
+
const code = char.codePointAt(0) ?? 0;
|
|
15505
|
+
const control = code < 32 || code >= 127 && code < 160;
|
|
15506
|
+
marked += isSpace(code) ? " " : control ? HELD : char;
|
|
15507
|
+
}
|
|
15508
|
+
const redacted = CREDENTIAL_RULES.reduce((line2, [pattern, replacement]) => line2.replace(pattern, replacement), marked);
|
|
15509
|
+
const line = redacted.replaceAll(HELD, " ").split(" ").filter((field) => field !== "").join(" ");
|
|
15510
|
+
return truncateUtf8(line, MAX_DETAIL_BYTES);
|
|
15511
|
+
}
|
|
15512
|
+
function budgetLabel(ms) {
|
|
15513
|
+
return ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
|
|
15514
|
+
}
|
|
15515
|
+
async function withinBudget(work, ms, onDeadline) {
|
|
15516
|
+
const deadline = Promise.withResolvers();
|
|
15517
|
+
const timer = setTimeout(() => deadline.resolve(onDeadline()), ms);
|
|
15518
|
+
try {
|
|
15519
|
+
return await Promise.race([work, deadline.promise]);
|
|
15520
|
+
} finally {
|
|
15521
|
+
clearTimeout(timer);
|
|
15522
|
+
}
|
|
15523
|
+
}
|
|
15524
|
+
async function measureCapabilities(host2, input, budgets = DEFAULT_BUDGETS) {
|
|
15525
|
+
const started = Date.now();
|
|
15526
|
+
const measuredAt = new Date(started).toISOString();
|
|
15527
|
+
const outcomes = new Map;
|
|
15528
|
+
const controllers = [];
|
|
15529
|
+
const checks3 = LIVE_CAPABILITIES.map(async (name) => {
|
|
15530
|
+
const controller = new AbortController;
|
|
15531
|
+
controllers.push(controller);
|
|
15532
|
+
const outcome = await withinBudget(CHECKS[name](host2, input, controller.signal).catch((error48) => failed(firstLine(messageFor(error48)))), budgets.checkMs, () => {
|
|
15533
|
+
controller.abort();
|
|
15534
|
+
return failed(`did not finish within ${budgetLabel(budgets.checkMs)}`);
|
|
15535
|
+
});
|
|
15536
|
+
outcomes.set(name, outcome);
|
|
15537
|
+
});
|
|
15538
|
+
await withinBudget(Promise.all(checks3).then(() => {
|
|
15539
|
+
return;
|
|
15540
|
+
}), budgets.reportMs, () => {
|
|
15541
|
+
return;
|
|
15542
|
+
});
|
|
15543
|
+
for (const controller of controllers)
|
|
15544
|
+
controller.abort();
|
|
15545
|
+
const rows = LIVE_CAPABILITIES.map((name) => {
|
|
15546
|
+
const { ok: ok2, detail } = outcomes.get(name) ?? failed(`did not finish within ${budgetLabel(budgets.reportMs)}`);
|
|
15547
|
+
return { name, ok: ok2, detail: cleanDetail(detail) };
|
|
15548
|
+
});
|
|
15549
|
+
return { measuredAt, elapsedMs: Date.now() - started, rows };
|
|
15550
|
+
}
|
|
15551
|
+
async function runCommand(command, args, options) {
|
|
15552
|
+
const child = Bun.spawn([command, ...args], {
|
|
15553
|
+
cwd: options.cwd,
|
|
15554
|
+
env: process.env,
|
|
15555
|
+
stdin: "ignore",
|
|
15556
|
+
stdout: "pipe",
|
|
15557
|
+
stderr: "pipe",
|
|
15558
|
+
signal: options.signal
|
|
15559
|
+
});
|
|
15560
|
+
const [stdout, stderr] = await Promise.all([
|
|
15561
|
+
new Response(child.stdout).text(),
|
|
15562
|
+
new Response(child.stderr).text(),
|
|
15563
|
+
child.exited
|
|
15564
|
+
]);
|
|
15565
|
+
return { code: child.exitCode, stdout, stderr };
|
|
15566
|
+
}
|
|
15567
|
+
function ompCapabilityHost() {
|
|
15568
|
+
const hostSettings = async () => (await import("@oh-my-pi/pi-coding-agent/config/settings")).settings;
|
|
15569
|
+
return {
|
|
15570
|
+
discoverAgents: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverAgents(cwd),
|
|
15571
|
+
disabledAgents: async () => {
|
|
15572
|
+
const [{ cfgTaskDisabledAgents }, settings] = await Promise.all([
|
|
15573
|
+
import("@oh-my-pi/pi-coding-agent/task/settings"),
|
|
15574
|
+
hostSettings()
|
|
15575
|
+
]);
|
|
15576
|
+
return cfgTaskDisabledAgents.get(settings);
|
|
15577
|
+
},
|
|
15578
|
+
runSearchQuery: async (args, options) => (await import("@oh-my-pi/pi-coding-agent")).runSearchQuery(args, options),
|
|
15579
|
+
loadMCPConfigs: async (cwd) => (await import("@oh-my-pi/pi-coding-agent/mcp/config")).loadAllMCPConfigs(cwd),
|
|
15580
|
+
discoverMCPServers: async (cwd) => (await import("@oh-my-pi/pi-coding-agent")).discoverMCPServers(cwd),
|
|
15581
|
+
discoverExtensionPaths: async (cwd) => {
|
|
15582
|
+
const [{ discoverExtensionPaths }, { cfgDisabledExtensions }, settings] = await Promise.all([
|
|
15583
|
+
import("@oh-my-pi/pi-coding-agent/extensibility/extensions/loader"),
|
|
15584
|
+
import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
|
|
15585
|
+
hostSettings()
|
|
15586
|
+
]);
|
|
15587
|
+
return discoverExtensionPaths([], cwd, cfgDisabledExtensions.get(settings));
|
|
15588
|
+
},
|
|
15589
|
+
loadSkills: async (cwd) => {
|
|
15590
|
+
const [{ loadSkills }, { cfgDisabledExtensions, cfgSkills }, settings] = await Promise.all([
|
|
15591
|
+
import("@oh-my-pi/pi-coding-agent"),
|
|
15592
|
+
import("@oh-my-pi/pi-coding-agent/extensibility/settings"),
|
|
15593
|
+
hostSettings()
|
|
15594
|
+
]);
|
|
15595
|
+
return loadSkills({
|
|
15596
|
+
...cfgSkills.get(settings),
|
|
15597
|
+
cwd,
|
|
15598
|
+
disabledExtensions: cfgDisabledExtensions.get(settings)
|
|
15599
|
+
});
|
|
15600
|
+
},
|
|
15601
|
+
run: runCommand
|
|
15602
|
+
};
|
|
15603
|
+
}
|
|
15604
|
+
|
|
15222
15605
|
// ../pi-shared/src/role-claim-bridge.ts
|
|
15223
15606
|
function legionRoleClaimBridge() {
|
|
15224
15607
|
return envoyPluginInterface().roleClaim;
|
|
@@ -15244,7 +15627,7 @@ function onEnvoyRoleRegained(listener) {
|
|
|
15244
15627
|
// package.json
|
|
15245
15628
|
var package_default = {
|
|
15246
15629
|
name: "@sjawhar/pi-legion",
|
|
15247
|
-
version: "8.
|
|
15630
|
+
version: "8.16.1",
|
|
15248
15631
|
license: "Apache-2.0",
|
|
15249
15632
|
type: "module",
|
|
15250
15633
|
omp: {
|
|
@@ -15256,7 +15639,7 @@ var package_default = {
|
|
|
15256
15639
|
]
|
|
15257
15640
|
},
|
|
15258
15641
|
legion: {
|
|
15259
|
-
daemonApiVersion:
|
|
15642
|
+
daemonApiVersion: 21
|
|
15260
15643
|
},
|
|
15261
15644
|
repository: {
|
|
15262
15645
|
type: "git",
|
|
@@ -15394,10 +15777,17 @@ var legionLocator = exports_external.discriminatedUnion("runtime", [
|
|
|
15394
15777
|
})
|
|
15395
15778
|
})
|
|
15396
15779
|
]);
|
|
15780
|
+
var legionCapabilityReportView = exports_external.strictObject({
|
|
15781
|
+
measuredAt: timestamp,
|
|
15782
|
+
incarnation: exports_external.string(),
|
|
15783
|
+
ok: exports_external.array(nonEmptyString),
|
|
15784
|
+
open: exports_external.array(exports_external.strictObject({ name: nonEmptyString, detail: nonEmptyString }))
|
|
15785
|
+
});
|
|
15397
15786
|
var legionClaimView = exports_external.strictObject({
|
|
15398
15787
|
session: exports_external.string(),
|
|
15399
15788
|
state: exports_external.enum(LEGION_CLAIM_STATES),
|
|
15400
|
-
locator: legionLocator.optional()
|
|
15789
|
+
locator: legionLocator.optional(),
|
|
15790
|
+
capabilities: legionCapabilityReportView.optional()
|
|
15401
15791
|
});
|
|
15402
15792
|
var legionPhaseView = exports_external.strictObject({
|
|
15403
15793
|
claim: legionClaimView,
|
|
@@ -15465,7 +15855,7 @@ var legionAgentSecretsLoginView = exports_external.strictObject({
|
|
|
15465
15855
|
});
|
|
15466
15856
|
var legionCapabilityState = exports_external.strictObject({
|
|
15467
15857
|
name: nonEmptyString,
|
|
15468
|
-
status: exports_external.enum(["present", "installed", "unchecked", "
|
|
15858
|
+
status: exports_external.enum(["present", "installed", "unchecked", "withheld", "decided", "open"]),
|
|
15469
15859
|
detail: nonEmptyString,
|
|
15470
15860
|
decision: nonEmptyString.optional(),
|
|
15471
15861
|
configLine: nonEmptyString.optional()
|
|
@@ -15485,7 +15875,20 @@ var LegionRegisterResponse = exports_external.strictObject({
|
|
|
15485
15875
|
issue: nonEmptyString,
|
|
15486
15876
|
role: exports_external.enum(LEGION_ROLES),
|
|
15487
15877
|
generation: exports_external.number().int().positive(),
|
|
15488
|
-
secret: nonEmptyString
|
|
15878
|
+
secret: nonEmptyString,
|
|
15879
|
+
promptAgents: exports_external.array(nonEmptyString)
|
|
15880
|
+
});
|
|
15881
|
+
var LegionCapabilityReport = exports_external.strictObject({
|
|
15882
|
+
measuredAt: timestamp,
|
|
15883
|
+
elapsedMs: exports_external.number().int().nonnegative(),
|
|
15884
|
+
rows: exports_external.array(exports_external.strictObject({ name: nonEmptyString, ok: exports_external.boolean(), detail: exports_external.string() }))
|
|
15885
|
+
});
|
|
15886
|
+
var LegionReadyRequest = exports_external.strictObject({
|
|
15887
|
+
claimToken: nonEmptyString,
|
|
15888
|
+
sessionId: nonEmptyString,
|
|
15889
|
+
secret: nonEmptyString,
|
|
15890
|
+
generation: exports_external.number().int().positive(),
|
|
15891
|
+
capabilities: LegionCapabilityReport.optional()
|
|
15489
15892
|
});
|
|
15490
15893
|
var LegionControllerRegisterResponse = exports_external.strictObject({
|
|
15491
15894
|
claimToken: nonEmptyString,
|
|
@@ -15618,10 +16021,10 @@ class LegionDaemonApiError extends Error {
|
|
|
15618
16021
|
status;
|
|
15619
16022
|
code;
|
|
15620
16023
|
detail;
|
|
15621
|
-
constructor(method,
|
|
15622
|
-
super(`${method} ${
|
|
16024
|
+
constructor(method, path4, status, code, detail) {
|
|
16025
|
+
super(`${method} ${path4} failed with ${status}${code === undefined ? "" : ` ${code}`}: ${detail}`);
|
|
15623
16026
|
this.method = method;
|
|
15624
|
-
this.path =
|
|
16027
|
+
this.path = path4;
|
|
15625
16028
|
this.status = status;
|
|
15626
16029
|
this.code = code;
|
|
15627
16030
|
this.detail = detail;
|
|
@@ -15633,10 +16036,10 @@ class LegionDaemonContractError extends Error {
|
|
|
15633
16036
|
method;
|
|
15634
16037
|
path;
|
|
15635
16038
|
status;
|
|
15636
|
-
constructor(method,
|
|
15637
|
-
super(`${method} ${
|
|
16039
|
+
constructor(method, path4, status, problem) {
|
|
16040
|
+
super(`${method} ${path4} answered ${status} with a body the daemon API contract does not describe: ${problem}`);
|
|
15638
16041
|
this.method = method;
|
|
15639
|
-
this.path =
|
|
16042
|
+
this.path = path4;
|
|
15640
16043
|
this.status = status;
|
|
15641
16044
|
this.name = "LegionDaemonContractError";
|
|
15642
16045
|
}
|
|
@@ -15657,8 +16060,8 @@ function parseStrictly(schema, body) {
|
|
|
15657
16060
|
}
|
|
15658
16061
|
function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
|
|
15659
16062
|
const endpoint = baseUrl.replace(/\/+$/, "");
|
|
15660
|
-
const call = async (method,
|
|
15661
|
-
const response = await fetchFn(`${endpoint}${
|
|
16063
|
+
const call = async (method, path4, body) => {
|
|
16064
|
+
const response = await fetchFn(`${endpoint}${path4}`, body === undefined ? undefined : {
|
|
15662
16065
|
method,
|
|
15663
16066
|
headers: { "Content-Type": "application/json" },
|
|
15664
16067
|
body: JSON.stringify(body)
|
|
@@ -15669,23 +16072,23 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
15669
16072
|
const refusal = parseStrictly(LegionErrorResponse, text);
|
|
15670
16073
|
if ("value" in refusal) {
|
|
15671
16074
|
const code = "code" in refusal.value && typeof refusal.value.code === "string" ? refusal.value.code : undefined;
|
|
15672
|
-
throw new LegionDaemonApiError(method,
|
|
16075
|
+
throw new LegionDaemonApiError(method, path4, response.status, code, refusal.value.error);
|
|
15673
16076
|
}
|
|
15674
|
-
throw new LegionDaemonApiError(method,
|
|
16077
|
+
throw new LegionDaemonApiError(method, path4, response.status, undefined, `${JSON.stringify(text)} is not the daemon's refusal shape (${refusal.problem})`);
|
|
15675
16078
|
};
|
|
15676
|
-
const read = async (method,
|
|
15677
|
-
const { status, text } = await call(method,
|
|
16079
|
+
const read = async (method, path4, schema, body) => {
|
|
16080
|
+
const { status, text } = await call(method, path4, body);
|
|
15678
16081
|
const parsed = parseStrictly(schema, text);
|
|
15679
16082
|
if ("problem" in parsed) {
|
|
15680
|
-
throw new LegionDaemonContractError(method,
|
|
16083
|
+
throw new LegionDaemonContractError(method, path4, status, parsed.problem);
|
|
15681
16084
|
}
|
|
15682
16085
|
return parsed.value;
|
|
15683
16086
|
};
|
|
15684
|
-
const post = (
|
|
15685
|
-
const acknowledge = async (
|
|
15686
|
-
const { status, text } = await call("POST",
|
|
16087
|
+
const post = (path4, request, response, body) => read("POST", path4, response, request.parse(body));
|
|
16088
|
+
const acknowledge = async (path4, body) => {
|
|
16089
|
+
const { status, text } = await call("POST", path4, body);
|
|
15687
16090
|
if (status !== 204 || text !== "") {
|
|
15688
|
-
throw new LegionDaemonContractError("POST",
|
|
16091
|
+
throw new LegionDaemonContractError("POST", path4, status, "the route answers 204 with no body");
|
|
15689
16092
|
}
|
|
15690
16093
|
};
|
|
15691
16094
|
return {
|
|
@@ -15712,16 +16115,16 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
15712
16115
|
// src/jj-attribution.ts
|
|
15713
16116
|
import { execFile as execFile2 } from "child_process";
|
|
15714
16117
|
import { mkdir, rename, writeFile } from "fs/promises";
|
|
15715
|
-
import
|
|
16118
|
+
import path4 from "path";
|
|
15716
16119
|
import { promisify as promisify2 } from "util";
|
|
15717
16120
|
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
16121
|
var TOP_LEVEL_TRANSCRIPT = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T[\\d-]+Z_(${UUID})\\.jsonl$`);
|
|
15719
16122
|
var ATTRIBUTION_OVERLAY = /^omp-attribution-.*\.toml$/;
|
|
15720
16123
|
function sessionIdFromTranscript(sessionFile) {
|
|
15721
|
-
return TOP_LEVEL_TRANSCRIPT.exec(
|
|
16124
|
+
return TOP_LEVEL_TRANSCRIPT.exec(path4.basename(sessionFile))?.[1];
|
|
15722
16125
|
}
|
|
15723
16126
|
function withoutOverlays(chain) {
|
|
15724
|
-
return chain.split(":").filter((entry) => entry && !ATTRIBUTION_OVERLAY.test(
|
|
16127
|
+
return chain.split(":").filter((entry) => entry && !ATTRIBUTION_OVERLAY.test(path4.basename(entry))).join(":");
|
|
15725
16128
|
}
|
|
15726
16129
|
async function userConfigChain() {
|
|
15727
16130
|
try {
|
|
@@ -15737,7 +16140,7 @@ async function exportJjSessionAttribution(sessionFile, stateDir, env = process.e
|
|
|
15737
16140
|
const id = sessionIdFromTranscript(sessionFile);
|
|
15738
16141
|
if (!id)
|
|
15739
16142
|
return;
|
|
15740
|
-
const overlay =
|
|
16143
|
+
const overlay = path4.join(stateDir, `omp-attribution-${id}.toml`);
|
|
15741
16144
|
if (env.JJ_CONFIG?.split(":").includes(overlay))
|
|
15742
16145
|
return;
|
|
15743
16146
|
const base = (env.JJ_CONFIG ? withoutOverlays(env.JJ_CONFIG) : "") || await resolveBase();
|
|
@@ -15808,12 +16211,10 @@ function createClaimSession(deps) {
|
|
|
15808
16211
|
agentId,
|
|
15809
16212
|
pluginContract: package_default.legion.daemonApiVersion
|
|
15810
16213
|
}).catch((error48) => exitOnRegistrationRefusal(error48, deps.exitProcess));
|
|
15811
|
-
const
|
|
15812
|
-
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
generation: claim.generation
|
|
15816
|
-
};
|
|
16214
|
+
const measuring = Promise.resolve().then(() => deps.measureCapabilities(context, { issue: claim.issue, promptAgents: claim.promptAgents })).catch((error48) => {
|
|
16215
|
+
console.error(`[legion] capability measurement failed; ready carries no report: ${messageFor(error48)}`);
|
|
16216
|
+
return;
|
|
16217
|
+
});
|
|
15817
16218
|
try {
|
|
15818
16219
|
const stateDir = requiredEnvironment(process.env, "LEGION_STATE_DIR");
|
|
15819
16220
|
await exportJjSessionAttribution(sessionFile, stateDir);
|
|
@@ -15825,6 +16226,14 @@ function createClaimSession(deps) {
|
|
|
15825
16226
|
secret: claim.secret
|
|
15826
16227
|
};
|
|
15827
16228
|
await claimEnvoyRole(sessionID, claim.claimToken, context);
|
|
16229
|
+
const capabilities = await measuring;
|
|
16230
|
+
const ready = {
|
|
16231
|
+
claimToken: claim.claimToken,
|
|
16232
|
+
sessionId: sessionID,
|
|
16233
|
+
secret: claim.secret,
|
|
16234
|
+
generation: claim.generation,
|
|
16235
|
+
...capabilities === undefined ? {} : { capabilities }
|
|
16236
|
+
};
|
|
15828
16237
|
await callReadyWithRetry("claims/ready", () => daemon.ready(ready));
|
|
15829
16238
|
onEnvoyRoleRegained(async (role, reason) => {
|
|
15830
16239
|
if (role !== claim.claimToken)
|
|
@@ -16406,13 +16815,17 @@ var exitProcess = (code) => process.exit(code);
|
|
|
16406
16815
|
function setLegionBootstrapExitForTests(hook) {
|
|
16407
16816
|
exitProcess = hook;
|
|
16408
16817
|
}
|
|
16818
|
+
var capabilityHost = ompCapabilityHost;
|
|
16819
|
+
function setLegionCapabilityHostForTests(host2) {
|
|
16820
|
+
capabilityHost = host2;
|
|
16821
|
+
}
|
|
16409
16822
|
async function persistedTranscript(context) {
|
|
16410
16823
|
await context.sessionManager.ensureOnDisk();
|
|
16411
16824
|
const sessionFile = context.sessionManager.getSessionFile();
|
|
16412
16825
|
if (!sessionFile?.endsWith(".jsonl")) {
|
|
16413
16826
|
throw new Error("Legion session must have a persisted transcript");
|
|
16414
16827
|
}
|
|
16415
|
-
const agentId =
|
|
16828
|
+
const agentId = path5.basename(sessionFile, ".jsonl");
|
|
16416
16829
|
if (!agentId)
|
|
16417
16830
|
throw new Error("Legion session transcript has no agent id");
|
|
16418
16831
|
return { sessionFile, agentId };
|
|
@@ -16462,6 +16875,14 @@ function legionExtension(pi) {
|
|
|
16462
16875
|
const claimSession = createClaimSession({
|
|
16463
16876
|
daemon: roleDaemon,
|
|
16464
16877
|
persistedTranscript,
|
|
16878
|
+
measureCapabilities: (context, input) => measureCapabilities(capabilityHost(), {
|
|
16879
|
+
cwd: context.cwd,
|
|
16880
|
+
sessionId: context.sessionManager.getSessionId(),
|
|
16881
|
+
issue: input.issue,
|
|
16882
|
+
promptAgents: input.promptAgents,
|
|
16883
|
+
activeTools: pi.getActiveTools(),
|
|
16884
|
+
agentsExposed: pi.agents !== undefined
|
|
16885
|
+
}),
|
|
16465
16886
|
exitProcess: (code) => exitProcess(code)
|
|
16466
16887
|
});
|
|
16467
16888
|
const controllerSession = createControllerSession({
|
|
@@ -16620,6 +17041,7 @@ function legionExtension(pi) {
|
|
|
16620
17041
|
});
|
|
16621
17042
|
}
|
|
16622
17043
|
export {
|
|
17044
|
+
setLegionCapabilityHostForTests,
|
|
16623
17045
|
setLegionBootstrapExitForTests,
|
|
16624
17046
|
legionExtension as default
|
|
16625
17047
|
};
|
|
@@ -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.1",
|
|
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",
|