@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 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 path4 from "path";
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.60.2",
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: 1
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 = strictObject({
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
- throw new LegionGoDaemonApiError(method, path, response.status, "value" in refusal ? refusal.value.error : `${JSON.stringify(text)} is not the Go daemon's refusal shape (${refusal.problem})`);
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 = createLegionGoDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"));
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
- await exportJjSessionAttribution(sessionFile, requiredEnvironment(process.env, "LEGION_STATE_DIR"));
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 jsonSuccess({});
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 jsonSuccess({});
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 jsonSuccess(await daemon.releaseWave({
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 jsonSuccess({});
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 jsonSuccess(await daemon.spawnWorker({
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 path3 from "path";
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(`${path3.dirname(sessionFile)}.jsonl`);
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 = path4.basename(sessionFile, ".jsonl");
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
- return bootstrapGoClaim(context, {
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(() => roleDaemon().grant({
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.60.2",
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": 1
16
+ "goDaemonApiVersion": 3
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",