@sjawhar/pi-legion-envoy 1.60.2 → 1.61.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/envoy.js +22 -0
- package/dist/legion.js +327 -27
- package/package.json +2 -2
package/dist/envoy.js
CHANGED
|
@@ -30472,6 +30472,10 @@ var handoffMessageSchema = object({
|
|
|
30472
30472
|
// ../contracts/src/subject.ts
|
|
30473
30473
|
var AGENT_TOPIC_PREFIX = "notifications.agent.";
|
|
30474
30474
|
var ROLE_TOPIC_PREFIX = "notifications.role.";
|
|
30475
|
+
var LEGION_NOTICE_TOPIC_PREFIX = "notifications.legion.";
|
|
30476
|
+
function legionNoticeSubject(project, issue) {
|
|
30477
|
+
return `${LEGION_NOTICE_TOPIC_PREFIX}${project}.${issue}`;
|
|
30478
|
+
}
|
|
30475
30479
|
var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
|
|
30476
30480
|
var DISPATCH_ISSUE_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}issue.`;
|
|
30477
30481
|
var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
|
|
@@ -34656,6 +34660,13 @@ async function claimEnvoyRole(sessionID, role, context) {
|
|
|
34656
34660
|
throw new Error("Envoy has no bound instance for a role claim");
|
|
34657
34661
|
await instance.claim(sessionID, role, context);
|
|
34658
34662
|
}
|
|
34663
|
+
async function subscribeLegionNotice(sessionID, topic, context) {
|
|
34664
|
+
const bridge = legionRoleClaimBridge();
|
|
34665
|
+
const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
|
|
34666
|
+
if (instance === undefined)
|
|
34667
|
+
throw new Error("Envoy has no bound instance for a notice subscription");
|
|
34668
|
+
await instance.subscribe(sessionID, topic, context);
|
|
34669
|
+
}
|
|
34659
34670
|
function onEnvoyRoleRegained(listener) {
|
|
34660
34671
|
legionRoleClaimBridge().regained = listener;
|
|
34661
34672
|
}
|
|
@@ -35190,8 +35201,19 @@ function envoyExtension(pi) {
|
|
|
35190
35201
|
await registerSession();
|
|
35191
35202
|
await setEnvoyRole(role);
|
|
35192
35203
|
};
|
|
35204
|
+
const subscribeNotice = async (targetSessionID, topic, callerContext) => {
|
|
35205
|
+
const context = callerContext ?? activeSessionContext;
|
|
35206
|
+
if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
|
|
35207
|
+
throw new Error(`Envoy has no active session for Legion notice subscription: ${targetSessionID}`);
|
|
35208
|
+
}
|
|
35209
|
+
if (sessionID !== targetSessionID)
|
|
35210
|
+
await establishSession(context);
|
|
35211
|
+
await subscribe(topic);
|
|
35212
|
+
await registerSession();
|
|
35213
|
+
};
|
|
35193
35214
|
const claimInstance = {
|
|
35194
35215
|
claim,
|
|
35216
|
+
subscribe: subscribeNotice,
|
|
35195
35217
|
sessionID: () => activeSessionContext?.sessionManager.getSessionId() ?? sessionID
|
|
35196
35218
|
};
|
|
35197
35219
|
bridge.instances.push(claimInstance);
|
package/dist/legion.js
CHANGED
|
@@ -16113,7 +16113,7 @@ var require_mod4 = __commonJS(function(exports) {
|
|
|
16113
16113
|
|
|
16114
16114
|
// extensions/legion.ts
|
|
16115
16115
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
16116
|
-
import
|
|
16116
|
+
import path5 from "path";
|
|
16117
16117
|
|
|
16118
16118
|
// ../envoy-client/src/defaults.ts
|
|
16119
16119
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
@@ -16144,7 +16144,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
16144
16144
|
// package.json
|
|
16145
16145
|
var package_default = {
|
|
16146
16146
|
name: "@sjawhar/pi-legion-envoy",
|
|
16147
|
-
version: "1.
|
|
16147
|
+
version: "1.61.0",
|
|
16148
16148
|
type: "module",
|
|
16149
16149
|
omp: {
|
|
16150
16150
|
extensions: [
|
|
@@ -16157,7 +16157,7 @@ var package_default = {
|
|
|
16157
16157
|
},
|
|
16158
16158
|
legion: {
|
|
16159
16159
|
daemonApiVersion: 8,
|
|
16160
|
-
goDaemonApiVersion:
|
|
16160
|
+
goDaemonApiVersion: 3
|
|
16161
16161
|
},
|
|
16162
16162
|
repository: {
|
|
16163
16163
|
type: "git",
|
|
@@ -30544,6 +30544,10 @@ var handoffMessageSchema = object({
|
|
|
30544
30544
|
// ../contracts/src/subject.ts
|
|
30545
30545
|
var AGENT_TOPIC_PREFIX = "notifications.agent.";
|
|
30546
30546
|
var ROLE_TOPIC_PREFIX = "notifications.role.";
|
|
30547
|
+
var LEGION_NOTICE_TOPIC_PREFIX = "notifications.legion.";
|
|
30548
|
+
function legionNoticeSubject(project, issue) {
|
|
30549
|
+
return `${LEGION_NOTICE_TOPIC_PREFIX}${project}.${issue}`;
|
|
30550
|
+
}
|
|
30547
30551
|
var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
|
|
30548
30552
|
var DISPATCH_ISSUE_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}issue.`;
|
|
30549
30553
|
var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
|
|
@@ -31183,6 +31187,13 @@ async function claimEnvoyRole(sessionID, role, context) {
|
|
|
31183
31187
|
throw new Error("Envoy has no bound instance for a role claim");
|
|
31184
31188
|
await instance.claim(sessionID, role, context);
|
|
31185
31189
|
}
|
|
31190
|
+
async function subscribeLegionNotice(sessionID, topic, context) {
|
|
31191
|
+
const bridge = legionRoleClaimBridge();
|
|
31192
|
+
const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
|
|
31193
|
+
if (instance === undefined)
|
|
31194
|
+
throw new Error("Envoy has no bound instance for a notice subscription");
|
|
31195
|
+
await instance.subscribe(sessionID, topic, context);
|
|
31196
|
+
}
|
|
31186
31197
|
function onEnvoyRoleRegained(listener) {
|
|
31187
31198
|
legionRoleClaimBridge().regained = listener;
|
|
31188
31199
|
}
|
|
@@ -31277,6 +31288,9 @@ async function writeGrantFile(file, grantId) {
|
|
|
31277
31288
|
}
|
|
31278
31289
|
}
|
|
31279
31290
|
|
|
31291
|
+
// src/legion/go-bootstrap.ts
|
|
31292
|
+
import path3 from "path";
|
|
31293
|
+
|
|
31280
31294
|
// ../contracts/src/legion-go-api.ts
|
|
31281
31295
|
var nonEmptyString2 = string2().min(1);
|
|
31282
31296
|
var timestamp = exports_iso.datetime({ offset: true });
|
|
@@ -31346,10 +31360,18 @@ var legionGoSlotView = strictObject({
|
|
|
31346
31360
|
index: number2().int().nonnegative(),
|
|
31347
31361
|
admittedAt: timestamp
|
|
31348
31362
|
});
|
|
31363
|
+
var legionGoPendingStatusWrite = strictObject({
|
|
31364
|
+
issue: nonEmptyString2,
|
|
31365
|
+
payload: record(string2(), unknown()),
|
|
31366
|
+
attempts: number2().int().nonnegative(),
|
|
31367
|
+
nextAt: timestamp,
|
|
31368
|
+
lastError: string2().optional()
|
|
31369
|
+
});
|
|
31349
31370
|
var legionGoIssue = strictObject({
|
|
31350
31371
|
key: nonEmptyString2,
|
|
31351
31372
|
generation: number2().int().nonnegative(),
|
|
31352
31373
|
phase: _enum2(LEGION_GO_PHASES),
|
|
31374
|
+
status: nonEmptyString2,
|
|
31353
31375
|
architect: legionGoClaimView.optional(),
|
|
31354
31376
|
workers: partialRecord(_enum2(LEGION_ROLES), legionGoPhaseView),
|
|
31355
31377
|
pullRequest: legionGoPullRequestView.optional(),
|
|
@@ -31371,7 +31393,8 @@ var legionGoAdmission = strictObject({
|
|
|
31371
31393
|
var LegionGoStateResponse = strictObject({
|
|
31372
31394
|
daemon: goDaemonInfo,
|
|
31373
31395
|
admission: legionGoAdmission,
|
|
31374
|
-
issues: record(string2(), legionGoIssue)
|
|
31396
|
+
issues: record(string2(), legionGoIssue),
|
|
31397
|
+
pendingStatusWrites: array(legionGoPendingStatusWrite)
|
|
31375
31398
|
});
|
|
31376
31399
|
var LegionGoRegisterResponse = strictObject({
|
|
31377
31400
|
claimToken: nonEmptyString2,
|
|
@@ -31381,9 +31404,10 @@ var LegionGoRegisterResponse = strictObject({
|
|
|
31381
31404
|
generation: number2().int().positive(),
|
|
31382
31405
|
secret: nonEmptyString2
|
|
31383
31406
|
});
|
|
31384
|
-
var LegionGoErrorResponse =
|
|
31385
|
-
error: nonEmptyString2
|
|
31386
|
-
})
|
|
31407
|
+
var LegionGoErrorResponse = union([
|
|
31408
|
+
strictObject({ error: nonEmptyString2 }),
|
|
31409
|
+
strictObject({ code: nonEmptyString2, error: nonEmptyString2 })
|
|
31410
|
+
]);
|
|
31387
31411
|
var legionGoDeliveryView = strictObject({
|
|
31388
31412
|
id: nonEmptyString2,
|
|
31389
31413
|
task: nonEmptyString2,
|
|
@@ -31412,18 +31436,81 @@ var LegionGoOperatorClaimResponse = strictObject({
|
|
|
31412
31436
|
var LegionGoOperatorClaimsResponse = strictObject({
|
|
31413
31437
|
claims: array(LegionGoOperatorClaimResponse)
|
|
31414
31438
|
});
|
|
31439
|
+
var LegionGoGrantResponse = strictObject({
|
|
31440
|
+
grantId: nonEmptyString2,
|
|
31441
|
+
expiresAt: timestamp
|
|
31442
|
+
});
|
|
31443
|
+
var LegionGoGitHubTokenResponse = strictObject({
|
|
31444
|
+
token: nonEmptyString2,
|
|
31445
|
+
appLogin: string2().endsWith("[bot]")
|
|
31446
|
+
});
|
|
31447
|
+
var LegionGoGitCredentialResponse = strictObject({
|
|
31448
|
+
username: literal("x-access-token"),
|
|
31449
|
+
password: nonEmptyString2
|
|
31450
|
+
});
|
|
31451
|
+
var LegionGoEmptyResponse = strictObject({});
|
|
31452
|
+
var LegionGoWaveReleaseResponse = strictObject({
|
|
31453
|
+
released: array(nonEmptyString2)
|
|
31454
|
+
});
|
|
31455
|
+
var LegionGoGrantRequest = strictObject({
|
|
31456
|
+
sessionId: nonEmptyString2,
|
|
31457
|
+
secret: nonEmptyString2,
|
|
31458
|
+
tree: nonEmptyString2,
|
|
31459
|
+
issue: nonEmptyString2
|
|
31460
|
+
});
|
|
31461
|
+
var LegionGoGrantCredentialRequest = strictObject({
|
|
31462
|
+
grantId: nonEmptyString2
|
|
31463
|
+
});
|
|
31464
|
+
var LegionGoHandoffCompleteRequest = strictObject({
|
|
31465
|
+
grantId: nonEmptyString2,
|
|
31466
|
+
summary: nonEmptyString2,
|
|
31467
|
+
verdict: string2(),
|
|
31468
|
+
ready: boolean2(),
|
|
31469
|
+
commit: nonEmptyString2
|
|
31470
|
+
});
|
|
31471
|
+
var LegionGoIssueStatusRequest = strictObject({
|
|
31472
|
+
grantId: nonEmptyString2,
|
|
31473
|
+
issue: nonEmptyString2,
|
|
31474
|
+
status: _enum2(["todo", "backlog", "icebox"])
|
|
31475
|
+
});
|
|
31476
|
+
var LegionGoGateRegisterRequest = strictObject({
|
|
31477
|
+
grantId: nonEmptyString2,
|
|
31478
|
+
issue: nonEmptyString2,
|
|
31479
|
+
artifactId: uuid2(),
|
|
31480
|
+
version: number2().int().positive()
|
|
31481
|
+
});
|
|
31482
|
+
var LegionGoWaveReleaseRequest = strictObject({
|
|
31483
|
+
grantId: nonEmptyString2,
|
|
31484
|
+
issues: array(nonEmptyString2)
|
|
31485
|
+
});
|
|
31486
|
+
var LegionGoPhaseBackwardRequest = strictObject({
|
|
31487
|
+
grantId: nonEmptyString2,
|
|
31488
|
+
to: _enum2(LEGION_GO_PHASES),
|
|
31489
|
+
reason: nonEmptyString2
|
|
31490
|
+
});
|
|
31491
|
+
var LegionGoPhaseRetryRequest = strictObject({
|
|
31492
|
+
grantId: nonEmptyString2,
|
|
31493
|
+
issue: nonEmptyString2,
|
|
31494
|
+
decision: _enum2(["retry", "escalate"])
|
|
31495
|
+
});
|
|
31496
|
+
var LegionGoSignOffRequest = strictObject({
|
|
31497
|
+
grantId: nonEmptyString2,
|
|
31498
|
+
issue: nonEmptyString2
|
|
31499
|
+
});
|
|
31415
31500
|
|
|
31416
31501
|
// src/legion/go-daemon-client.ts
|
|
31417
31502
|
class LegionGoDaemonApiError extends Error {
|
|
31418
31503
|
method;
|
|
31419
31504
|
path;
|
|
31420
31505
|
status;
|
|
31506
|
+
code;
|
|
31421
31507
|
detail;
|
|
31422
|
-
constructor(method, path, status, detail) {
|
|
31423
|
-
super(`${method} ${path} failed with ${status}: ${detail}`);
|
|
31508
|
+
constructor(method, path, status, code, detail) {
|
|
31509
|
+
super(`${method} ${path} failed with ${status}${code === undefined ? "" : ` ${code}`}: ${detail}`);
|
|
31424
31510
|
this.method = method;
|
|
31425
31511
|
this.path = path;
|
|
31426
31512
|
this.status = status;
|
|
31513
|
+
this.code = code;
|
|
31427
31514
|
this.detail = detail;
|
|
31428
31515
|
this.name = "LegionGoDaemonApiError";
|
|
31429
31516
|
}
|
|
@@ -31467,7 +31554,11 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
31467
31554
|
if (response.ok)
|
|
31468
31555
|
return { status: response.status, text };
|
|
31469
31556
|
const refusal = parseStrictly(LegionGoErrorResponse, text);
|
|
31470
|
-
|
|
31557
|
+
if ("value" in refusal) {
|
|
31558
|
+
const code = "code" in refusal.value && typeof refusal.value.code === "string" ? refusal.value.code : undefined;
|
|
31559
|
+
throw new LegionGoDaemonApiError(method, path, response.status, code, refusal.value.error);
|
|
31560
|
+
}
|
|
31561
|
+
throw new LegionGoDaemonApiError(method, path, response.status, undefined, `${JSON.stringify(text)} is not the Go daemon's refusal shape (${refusal.problem})`);
|
|
31471
31562
|
};
|
|
31472
31563
|
const read = async (method, path, schema, body) => {
|
|
31473
31564
|
const { status, text } = await call(method, path, body);
|
|
@@ -31477,6 +31568,7 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
31477
31568
|
}
|
|
31478
31569
|
return parsed.value;
|
|
31479
31570
|
};
|
|
31571
|
+
const post = (path, request, response, body) => read("POST", path, response, request.parse(body));
|
|
31480
31572
|
const acknowledge = async (path, body) => {
|
|
31481
31573
|
const { status, text } = await call("POST", path, body);
|
|
31482
31574
|
if (status !== 204 || text !== "") {
|
|
@@ -31487,7 +31579,18 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
|
|
|
31487
31579
|
state: () => read("GET", "/legion/v1/state", LegionGoStateResponse),
|
|
31488
31580
|
register: (input) => read("POST", "/legion/v1/claims/register", LegionGoRegisterResponse, input),
|
|
31489
31581
|
ready: (input) => acknowledge("/legion/v1/claims/ready", input),
|
|
31490
|
-
exit: (input) => acknowledge("/legion/v1/claims/exit", input)
|
|
31582
|
+
exit: (input) => acknowledge("/legion/v1/claims/exit", input),
|
|
31583
|
+
grant: (input) => post("/legion/v1/grants", LegionGoGrantRequest, LegionGoGrantResponse, input),
|
|
31584
|
+
githubToken: (input) => post("/legion/v1/gh-token", LegionGoGrantCredentialRequest, LegionGoGitHubTokenResponse, input),
|
|
31585
|
+
gitCredential: (input) => post("/legion/v1/git-credential", LegionGoGrantCredentialRequest, LegionGoGitCredentialResponse, input),
|
|
31586
|
+
provisioningCredential: (input) => post("/legion/v1/provisioning-credential", LegionGoGrantCredentialRequest, LegionGoGitHubTokenResponse, input),
|
|
31587
|
+
handoffComplete: (input) => post("/legion/v1/handoff/complete", LegionGoHandoffCompleteRequest, LegionGoEmptyResponse, input),
|
|
31588
|
+
issueStatus: (input) => post("/legion/v1/issues/status", LegionGoIssueStatusRequest, LegionGoEmptyResponse, input),
|
|
31589
|
+
gateRegister: (input) => post("/legion/v1/gates/register", LegionGoGateRegisterRequest, LegionGoEmptyResponse, input),
|
|
31590
|
+
waveRelease: (input) => post("/legion/v1/waves/release", LegionGoWaveReleaseRequest, LegionGoWaveReleaseResponse, input),
|
|
31591
|
+
phaseBackward: (input) => post("/legion/v1/phase/backward", LegionGoPhaseBackwardRequest, LegionGoEmptyResponse, input),
|
|
31592
|
+
phaseRetry: (input) => post("/legion/v1/phase/retry", LegionGoPhaseRetryRequest, LegionGoEmptyResponse, input),
|
|
31593
|
+
signOff: (input) => post("/legion/v1/signoff", LegionGoSignOffRequest, LegionGoEmptyResponse, input)
|
|
31491
31594
|
};
|
|
31492
31595
|
}
|
|
31493
31596
|
|
|
@@ -31592,7 +31695,7 @@ async function bootstrapGoClaim(context, state) {
|
|
|
31592
31695
|
if (existing !== undefined)
|
|
31593
31696
|
return existing;
|
|
31594
31697
|
const bootToken = requiredSecret(process.env, "LEGION_BOOT_TOKEN");
|
|
31595
|
-
const daemon =
|
|
31698
|
+
const daemon = state.daemon();
|
|
31596
31699
|
const bootstrap = (async () => {
|
|
31597
31700
|
const { sessionFile, agentId } = await state.persistedTranscript(context);
|
|
31598
31701
|
state.recordBootstrappedSession(sessionFile);
|
|
@@ -31610,7 +31713,9 @@ async function bootstrapGoClaim(context, state) {
|
|
|
31610
31713
|
generation: claim.generation
|
|
31611
31714
|
};
|
|
31612
31715
|
try {
|
|
31613
|
-
|
|
31716
|
+
const stateDir = requiredEnvironment(process.env, "LEGION_STATE_DIR");
|
|
31717
|
+
await exportJjSessionAttribution(sessionFile, stateDir);
|
|
31718
|
+
process.env.LEGION_GRANT_FILE = path3.join(stateDir, "secrets", `${claim.claimToken}-grant`);
|
|
31614
31719
|
state.setCapability({
|
|
31615
31720
|
kind: "phase-worker",
|
|
31616
31721
|
sessionID,
|
|
@@ -31621,6 +31726,7 @@ async function bootstrapGoClaim(context, state) {
|
|
|
31621
31726
|
secret: claim.secret
|
|
31622
31727
|
});
|
|
31623
31728
|
await claimEnvoyRole(sessionID, claim.claimToken, context);
|
|
31729
|
+
await subscribeLegionNotice(sessionID, legionNoticeSubject(requiredEnvironment(process.env, "LEGION_PROJECT"), claim.role === "architect" ? claim.tree : claim.issue), context);
|
|
31624
31730
|
await callGoReadyWithRetry("claims/ready", () => daemon.ready(ready));
|
|
31625
31731
|
onEnvoyRoleRegained(async (role, reason) => {
|
|
31626
31732
|
if (role !== claim.claimToken)
|
|
@@ -31646,9 +31752,6 @@ async function bootstrapGoClaim(context, state) {
|
|
|
31646
31752
|
}
|
|
31647
31753
|
}
|
|
31648
31754
|
|
|
31649
|
-
// src/legion/tools.ts
|
|
31650
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
31651
|
-
|
|
31652
31755
|
// src/tool-result.ts
|
|
31653
31756
|
function toolSuccess(text, details = {}) {
|
|
31654
31757
|
return { content: [{ type: "text", text }], details };
|
|
@@ -31657,8 +31760,167 @@ function toolFailure(error) {
|
|
|
31657
31760
|
return { content: [{ type: "text", text: messageFor(error) }], details: {}, isError: true };
|
|
31658
31761
|
}
|
|
31659
31762
|
|
|
31660
|
-
// src/legion/tools.ts
|
|
31763
|
+
// src/legion/go-tools.ts
|
|
31764
|
+
var OPERATIONS = {
|
|
31765
|
+
architect: [
|
|
31766
|
+
"register_gate",
|
|
31767
|
+
"release_children",
|
|
31768
|
+
"request_backward_move",
|
|
31769
|
+
"retry_or_escalate",
|
|
31770
|
+
"sign_off",
|
|
31771
|
+
"read_record"
|
|
31772
|
+
],
|
|
31773
|
+
"phase-worker": ["request_backward_move", "read_record"]
|
|
31774
|
+
};
|
|
31775
|
+
var OPERATION_FIELDS = {
|
|
31776
|
+
register_gate: ["issue", "artifactId", "version"],
|
|
31777
|
+
release_children: ["issues"],
|
|
31778
|
+
request_backward_move: ["to", "reason"],
|
|
31779
|
+
retry_or_escalate: ["issue", "decision"],
|
|
31780
|
+
sign_off: ["issue"],
|
|
31781
|
+
read_record: ["issue"]
|
|
31782
|
+
};
|
|
31661
31783
|
var jsonSuccess = (details) => toolSuccess(JSON.stringify(details), details);
|
|
31784
|
+
function toolSchema(pi) {
|
|
31785
|
+
const z = pi.zod;
|
|
31786
|
+
return z.object({
|
|
31787
|
+
op: z.enum([
|
|
31788
|
+
"register_gate",
|
|
31789
|
+
"release_children",
|
|
31790
|
+
"request_backward_move",
|
|
31791
|
+
"retry_or_escalate",
|
|
31792
|
+
"sign_off",
|
|
31793
|
+
"read_record"
|
|
31794
|
+
]),
|
|
31795
|
+
issue: z.string().optional(),
|
|
31796
|
+
artifactId: z.string().optional(),
|
|
31797
|
+
version: z.number().optional(),
|
|
31798
|
+
issues: z.array(z.string()).optional(),
|
|
31799
|
+
to: z.enum(LEGION_GO_PHASES).optional(),
|
|
31800
|
+
reason: z.string().optional(),
|
|
31801
|
+
decision: z.enum(["retry", "escalate"]).optional()
|
|
31802
|
+
});
|
|
31803
|
+
}
|
|
31804
|
+
function requiredString(parameters, operation, name) {
|
|
31805
|
+
const value = parameters[name];
|
|
31806
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
31807
|
+
throw new Error(`${operation} requires ${name}`);
|
|
31808
|
+
return value;
|
|
31809
|
+
}
|
|
31810
|
+
function assertOperationInput(parameters, operation) {
|
|
31811
|
+
const fields = OPERATION_FIELDS[operation];
|
|
31812
|
+
if (fields === undefined)
|
|
31813
|
+
throw new Error(`Unsupported Go Legion operation: ${operation}`);
|
|
31814
|
+
for (const name of Object.keys(parameters)) {
|
|
31815
|
+
if (name !== "op" && !fields.includes(name)) {
|
|
31816
|
+
throw new Error(`${operation} does not accept field "${name}"`);
|
|
31817
|
+
}
|
|
31818
|
+
}
|
|
31819
|
+
}
|
|
31820
|
+
async function grantFor(daemon, session) {
|
|
31821
|
+
if (session.secret === undefined) {
|
|
31822
|
+
throw new Error("Go Legion session has no registered claim capability");
|
|
31823
|
+
}
|
|
31824
|
+
return (await daemon.grant({
|
|
31825
|
+
sessionId: session.sessionId,
|
|
31826
|
+
secret: session.secret,
|
|
31827
|
+
tree: session.tree,
|
|
31828
|
+
issue: session.issue
|
|
31829
|
+
})).grantId;
|
|
31830
|
+
}
|
|
31831
|
+
function recordFrom(state, issue) {
|
|
31832
|
+
const record = state.issues[issue];
|
|
31833
|
+
if (record === undefined)
|
|
31834
|
+
throw new Error(`The Go daemon has no record for ${issue}`);
|
|
31835
|
+
return record;
|
|
31836
|
+
}
|
|
31837
|
+
function createGoLegionTool(deps) {
|
|
31838
|
+
const { pi, daemon, session } = deps;
|
|
31839
|
+
return {
|
|
31840
|
+
name: "legion",
|
|
31841
|
+
label: "legion",
|
|
31842
|
+
description: "Perform the workflow operation the Go Legion daemon assigned this role. The daemon advances phases; this tool cannot spawn workers.",
|
|
31843
|
+
defaultInactive: true,
|
|
31844
|
+
parameters: toolSchema(pi),
|
|
31845
|
+
execute: async (_id, parameters, _signal, _onUpdate, context) => {
|
|
31846
|
+
try {
|
|
31847
|
+
const active = session(context);
|
|
31848
|
+
const operation = requiredString(parameters, "legion", "op");
|
|
31849
|
+
if (!OPERATIONS[active.kind].includes(operation)) {
|
|
31850
|
+
throw new Error(`${operation} is not available to a ${active.kind} session`);
|
|
31851
|
+
}
|
|
31852
|
+
assertOperationInput(parameters, operation);
|
|
31853
|
+
const client = daemon();
|
|
31854
|
+
switch (operation) {
|
|
31855
|
+
case "read_record": {
|
|
31856
|
+
const issue = requiredString(parameters, operation, "issue");
|
|
31857
|
+
return jsonSuccess({ record: recordFrom(await client.state(), issue) });
|
|
31858
|
+
}
|
|
31859
|
+
case "register_gate": {
|
|
31860
|
+
const version = parameters.version;
|
|
31861
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) {
|
|
31862
|
+
throw new Error("register_gate requires a positive integer version");
|
|
31863
|
+
}
|
|
31864
|
+
const grantId = await grantFor(client, active);
|
|
31865
|
+
await client.gateRegister({
|
|
31866
|
+
grantId,
|
|
31867
|
+
issue: requiredString(parameters, operation, "issue"),
|
|
31868
|
+
artifactId: requiredString(parameters, operation, "artifactId"),
|
|
31869
|
+
version
|
|
31870
|
+
});
|
|
31871
|
+
return jsonSuccess({});
|
|
31872
|
+
}
|
|
31873
|
+
case "release_children": {
|
|
31874
|
+
const issues = parameters.issues;
|
|
31875
|
+
if (!Array.isArray(issues) || !issues.every((issue) => typeof issue === "string")) {
|
|
31876
|
+
throw new Error("release_children requires issues");
|
|
31877
|
+
}
|
|
31878
|
+
const grantId = await grantFor(client, active);
|
|
31879
|
+
return jsonSuccess(await client.waveRelease({ grantId, issues }));
|
|
31880
|
+
}
|
|
31881
|
+
case "request_backward_move": {
|
|
31882
|
+
const grantId = await grantFor(client, active);
|
|
31883
|
+
await client.phaseBackward({
|
|
31884
|
+
grantId,
|
|
31885
|
+
to: requiredString(parameters, operation, "to"),
|
|
31886
|
+
reason: requiredString(parameters, operation, "reason")
|
|
31887
|
+
});
|
|
31888
|
+
return jsonSuccess({});
|
|
31889
|
+
}
|
|
31890
|
+
case "retry_or_escalate": {
|
|
31891
|
+
const decision = parameters.decision;
|
|
31892
|
+
if (decision !== "retry" && decision !== "escalate") {
|
|
31893
|
+
throw new Error("retry_or_escalate requires decision retry or escalate");
|
|
31894
|
+
}
|
|
31895
|
+
const grantId = await grantFor(client, active);
|
|
31896
|
+
await client.phaseRetry({
|
|
31897
|
+
grantId,
|
|
31898
|
+
issue: requiredString(parameters, operation, "issue"),
|
|
31899
|
+
decision
|
|
31900
|
+
});
|
|
31901
|
+
return jsonSuccess({});
|
|
31902
|
+
}
|
|
31903
|
+
case "sign_off": {
|
|
31904
|
+
const grantId = await grantFor(client, active);
|
|
31905
|
+
await client.signOff({
|
|
31906
|
+
grantId,
|
|
31907
|
+
issue: requiredString(parameters, operation, "issue")
|
|
31908
|
+
});
|
|
31909
|
+
return jsonSuccess({});
|
|
31910
|
+
}
|
|
31911
|
+
default:
|
|
31912
|
+
throw new Error(`Unsupported Go Legion operation: ${operation}`);
|
|
31913
|
+
}
|
|
31914
|
+
} catch (error) {
|
|
31915
|
+
return toolFailure(error);
|
|
31916
|
+
}
|
|
31917
|
+
}
|
|
31918
|
+
};
|
|
31919
|
+
}
|
|
31920
|
+
|
|
31921
|
+
// src/legion/tools.ts
|
|
31922
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
31923
|
+
var jsonSuccess2 = (details) => toolSuccess(JSON.stringify(details), details);
|
|
31662
31924
|
function isDispatchArtifactId(value) {
|
|
31663
31925
|
return LegionDaemonApi.GatesRegister.request.shape.artifactId.safeParse(value).success;
|
|
31664
31926
|
}
|
|
@@ -31726,7 +31988,7 @@ function createLegionTool(deps) {
|
|
|
31726
31988
|
issue: stringInput("issue"),
|
|
31727
31989
|
status
|
|
31728
31990
|
});
|
|
31729
|
-
return
|
|
31991
|
+
return jsonSuccess2({});
|
|
31730
31992
|
}
|
|
31731
31993
|
case "register_gate": {
|
|
31732
31994
|
const version = parameters.version;
|
|
@@ -31745,14 +32007,14 @@ function createLegionTool(deps) {
|
|
|
31745
32007
|
artifactId,
|
|
31746
32008
|
version
|
|
31747
32009
|
});
|
|
31748
|
-
return
|
|
32010
|
+
return jsonSuccess2({});
|
|
31749
32011
|
}
|
|
31750
32012
|
case "release_wave": {
|
|
31751
32013
|
const issues = parameters.issues;
|
|
31752
32014
|
if (!Array.isArray(issues) || !issues.every((issue) => typeof issue === "string")) {
|
|
31753
32015
|
throw new Error("release_wave requires issues");
|
|
31754
32016
|
}
|
|
31755
|
-
return
|
|
32017
|
+
return jsonSuccess2(await daemon.releaseWave({
|
|
31756
32018
|
tree: architect.tree,
|
|
31757
32019
|
issues,
|
|
31758
32020
|
sessionId,
|
|
@@ -31773,7 +32035,7 @@ function createLegionTool(deps) {
|
|
|
31773
32035
|
sessionId,
|
|
31774
32036
|
secret: architect.secret
|
|
31775
32037
|
});
|
|
31776
|
-
return
|
|
32038
|
+
return jsonSuccess2({});
|
|
31777
32039
|
}
|
|
31778
32040
|
case "spawn_worker": {
|
|
31779
32041
|
const role = parameters.role;
|
|
@@ -31781,7 +32043,7 @@ function createLegionTool(deps) {
|
|
|
31781
32043
|
throw new Error("spawn_worker requires a valid Legion role");
|
|
31782
32044
|
}
|
|
31783
32045
|
const requestId = randomUUID2();
|
|
31784
|
-
return
|
|
32046
|
+
return jsonSuccess2(await daemon.spawnWorker({
|
|
31785
32047
|
tree: architect.tree,
|
|
31786
32048
|
sessionId,
|
|
31787
32049
|
secret: architect.secret,
|
|
@@ -31803,7 +32065,7 @@ function createLegionTool(deps) {
|
|
|
31803
32065
|
|
|
31804
32066
|
// src/subagent-session.ts
|
|
31805
32067
|
import fs from "fs";
|
|
31806
|
-
import
|
|
32068
|
+
import path4 from "path";
|
|
31807
32069
|
import * as host from "@oh-my-pi/pi-coding-agent";
|
|
31808
32070
|
var LEGION_BOOTSTRAPPED_SESSION = Symbol.for("legion.pi-envoy.bootstrapped-session");
|
|
31809
32071
|
var bootstrappedSessionStore = globalThis;
|
|
@@ -31814,7 +32076,7 @@ async function isSubagentSession(context) {
|
|
|
31814
32076
|
await context.sessionManager.ensureOnDisk();
|
|
31815
32077
|
const sessionFile = context.sessionManager.getSessionFile();
|
|
31816
32078
|
if (sessionFile !== undefined && fs.existsSync(sessionFile)) {
|
|
31817
|
-
return fs.existsSync(`${
|
|
32079
|
+
return fs.existsSync(`${path4.dirname(sessionFile)}.jsonl`);
|
|
31818
32080
|
}
|
|
31819
32081
|
const bootstrapped = bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
|
|
31820
32082
|
return bootstrapped !== undefined && bootstrapped !== sessionFile;
|
|
@@ -31900,7 +32162,7 @@ async function persistedTranscript(context) {
|
|
|
31900
32162
|
if (!sessionFile?.endsWith(".jsonl")) {
|
|
31901
32163
|
throw new Error("Legion session must have a persisted transcript");
|
|
31902
32164
|
}
|
|
31903
|
-
const agentId =
|
|
32165
|
+
const agentId = path5.basename(sessionFile, ".jsonl");
|
|
31904
32166
|
if (!agentId)
|
|
31905
32167
|
throw new Error("Legion session transcript has no agent id");
|
|
31906
32168
|
return { sessionFile, agentId };
|
|
@@ -32075,6 +32337,11 @@ function legionExtension(pi) {
|
|
|
32075
32337
|
});
|
|
32076
32338
|
return daemonClient;
|
|
32077
32339
|
};
|
|
32340
|
+
let goDaemonClient;
|
|
32341
|
+
const goRoleDaemon = () => {
|
|
32342
|
+
goDaemonClient ??= createLegionGoDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"));
|
|
32343
|
+
return goDaemonClient;
|
|
32344
|
+
};
|
|
32078
32345
|
const rerunReadyAfterRegain = async (endpoint, role, reason, call) => {
|
|
32079
32346
|
try {
|
|
32080
32347
|
await callReadyWithRetry(`${endpoint} after role regain`, call);
|
|
@@ -32286,7 +32553,7 @@ function legionExtension(pi) {
|
|
|
32286
32553
|
if (await checkSubagentSession(context))
|
|
32287
32554
|
return;
|
|
32288
32555
|
if (process.env.LEGION_DAEMON_API === "go") {
|
|
32289
|
-
|
|
32556
|
+
await bootstrapGoClaim(context, {
|
|
32290
32557
|
capability: () => capability,
|
|
32291
32558
|
setCapability: (next) => {
|
|
32292
32559
|
capability = next;
|
|
@@ -32295,10 +32562,14 @@ function legionExtension(pi) {
|
|
|
32295
32562
|
setBootstrap: (next) => {
|
|
32296
32563
|
bootstrap = next;
|
|
32297
32564
|
},
|
|
32565
|
+
daemon: goRoleDaemon,
|
|
32298
32566
|
exitProcess,
|
|
32299
32567
|
persistedTranscript,
|
|
32300
32568
|
recordBootstrappedSession
|
|
32301
32569
|
});
|
|
32570
|
+
registerGoTools();
|
|
32571
|
+
await activateLegionTool();
|
|
32572
|
+
return;
|
|
32302
32573
|
}
|
|
32303
32574
|
const classification = classifySession(process.env);
|
|
32304
32575
|
switch (classification.kind) {
|
|
@@ -32373,7 +32644,12 @@ function legionExtension(pi) {
|
|
|
32373
32644
|
}
|
|
32374
32645
|
return;
|
|
32375
32646
|
}
|
|
32376
|
-
return wrapBashWithGrant(() =>
|
|
32647
|
+
return wrapBashWithGrant(() => process.env.LEGION_DAEMON_API === "go" ? goRoleDaemon().grant({
|
|
32648
|
+
tree: active.tree,
|
|
32649
|
+
issue: active.issue,
|
|
32650
|
+
sessionId: sessionID,
|
|
32651
|
+
secret: active.secret
|
|
32652
|
+
}) : roleDaemon().grant({
|
|
32377
32653
|
tree: active.tree,
|
|
32378
32654
|
issue: active.issue,
|
|
32379
32655
|
sessionId: sessionID,
|
|
@@ -32424,6 +32700,30 @@ function legionExtension(pi) {
|
|
|
32424
32700
|
architectToolsRegistered = true;
|
|
32425
32701
|
pi.registerTool(createLegionTool({ pi, roleDaemon, architectSession }));
|
|
32426
32702
|
};
|
|
32703
|
+
let goToolsRegistered = false;
|
|
32704
|
+
const registerGoTools = () => {
|
|
32705
|
+
if (goToolsRegistered)
|
|
32706
|
+
return;
|
|
32707
|
+
goToolsRegistered = true;
|
|
32708
|
+
pi.registerTool(createGoLegionTool({
|
|
32709
|
+
pi,
|
|
32710
|
+
daemon: goRoleDaemon,
|
|
32711
|
+
session: (context) => {
|
|
32712
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
32713
|
+
const active = capability;
|
|
32714
|
+
if (active === undefined || active.sessionID !== sessionID) {
|
|
32715
|
+
throw new Error("Go Legion tool is available only to this session's registered claim");
|
|
32716
|
+
}
|
|
32717
|
+
return {
|
|
32718
|
+
kind: active.role === "architect" ? "architect" : "phase-worker",
|
|
32719
|
+
sessionId: active.sessionID,
|
|
32720
|
+
tree: active.tree,
|
|
32721
|
+
issue: active.issue,
|
|
32722
|
+
secret: active.secret
|
|
32723
|
+
};
|
|
32724
|
+
}
|
|
32725
|
+
}));
|
|
32726
|
+
};
|
|
32427
32727
|
pi.registerCommand("legion-claim-controller", {
|
|
32428
32728
|
description: "Claim the Legion controller role and register daemon authority for this session",
|
|
32429
32729
|
handler: async (_args, context) => controllerSession.claim(context)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion-envoy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.61.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"omp": {
|
|
6
6
|
"extensions": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
15
|
"daemonApiVersion": 8,
|
|
16
|
-
"goDaemonApiVersion":
|
|
16
|
+
"goDaemonApiVersion": 3
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|