@sjawhar/pi-legion-envoy 1.60.1 → 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
@@ -29849,6 +29849,29 @@ var AskFollowerEventPayloadSchema = object({
29849
29849
  session_id: string2().optional(),
29850
29850
  by: object({ kind: string2(), id: string2().optional() }).passthrough().optional()
29851
29851
  });
29852
+ // ../contracts/src/dispatch-href.ts
29853
+ function itemFromSearch(search) {
29854
+ const params = new URLSearchParams(search);
29855
+ const ask = params.get("ask");
29856
+ const comment = params.get("comment");
29857
+ if (ask === null && comment === null) {
29858
+ return;
29859
+ }
29860
+ if (ask !== null && comment !== null) {
29861
+ return null;
29862
+ }
29863
+ const raw = ask ?? comment ?? "";
29864
+ if (raw === "" || raw.includes("/")) {
29865
+ return null;
29866
+ }
29867
+ let id;
29868
+ try {
29869
+ id = decodeURIComponent(raw);
29870
+ } catch {
29871
+ return null;
29872
+ }
29873
+ return ask === null ? { id, kind: "comment" } : { id, kind: "ask" };
29874
+ }
29852
29875
  // ../contracts/src/dispatch-snippet.ts
29853
29876
  var HTML_ENTITIES = [
29854
29877
  ["&lt;", "<"],
@@ -30449,6 +30472,10 @@ var handoffMessageSchema = object({
30449
30472
  // ../contracts/src/subject.ts
30450
30473
  var AGENT_TOPIC_PREFIX = "notifications.agent.";
30451
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
+ }
30452
30479
  var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
30453
30480
  var DISPATCH_ISSUE_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}issue.`;
30454
30481
  var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
@@ -33004,6 +33031,13 @@ function dispatchRefFromUrl(value, serverUrl) {
33004
33031
  if (issuePage) {
33005
33032
  const [, key, page, artifact, ask, comment, message] = issuePage;
33006
33033
  const version = versionOf("v");
33034
+ if (page === "spec" || artifact !== undefined) {
33035
+ const item = itemFromSearch(url.search);
33036
+ if (item === null)
33037
+ return;
33038
+ if (item !== undefined)
33039
+ return `dispatch://${key}/${item.kind}/${item.id}`;
33040
+ }
33007
33041
  if (page === "spec" && version !== "")
33008
33042
  return `dispatch://${key}/artifact/spec${version}`;
33009
33043
  if (page !== undefined)
@@ -33024,13 +33058,10 @@ function dispatchRefFromUrl(value, serverUrl) {
33024
33058
  return;
33025
33059
  const [, project, slug] = documentPage;
33026
33060
  const document = `dispatch://${project}/artifact/${decodeURIComponent(slug ?? "")}${versionOf("version")}`;
33027
- const ask = url.searchParams.get("ask");
33028
- if (ask !== null)
33029
- return `${document}/ask/${ask}`;
33030
- const comment = url.searchParams.get("comment");
33031
- if (comment !== null)
33032
- return `${document}/comment/${comment}`;
33033
- return document;
33061
+ const item = itemFromSearch(url.search);
33062
+ if (item === null)
33063
+ return;
33064
+ return item === undefined ? document : `${document}/${item.kind}/${item.id}`;
33034
33065
  }
33035
33066
  var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, or " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename); " + "a dashboard URL on this Dispatch server is accepted too";
33036
33067
  var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
@@ -34629,6 +34660,13 @@ async function claimEnvoyRole(sessionID, role, context) {
34629
34660
  throw new Error("Envoy has no bound instance for a role claim");
34630
34661
  await instance.claim(sessionID, role, context);
34631
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
+ }
34632
34670
  function onEnvoyRoleRegained(listener) {
34633
34671
  legionRoleClaimBridge().regained = listener;
34634
34672
  }
@@ -35163,8 +35201,19 @@ function envoyExtension(pi) {
35163
35201
  await registerSession();
35164
35202
  await setEnvoyRole(role);
35165
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
+ };
35166
35214
  const claimInstance = {
35167
35215
  claim,
35216
+ subscribe: subscribeNotice,
35168
35217
  sessionID: () => activeSessionContext?.sessionManager.getSessionId() ?? sessionID
35169
35218
  };
35170
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.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: 1
16160
+ goDaemonApiVersion: 3
16161
16161
  },
16162
16162
  repository: {
16163
16163
  type: "git",
@@ -29921,6 +29921,29 @@ var AskFollowerEventPayloadSchema = object({
29921
29921
  session_id: string2().optional(),
29922
29922
  by: object({ kind: string2(), id: string2().optional() }).passthrough().optional()
29923
29923
  });
29924
+ // ../contracts/src/dispatch-href.ts
29925
+ function itemFromSearch(search) {
29926
+ const params = new URLSearchParams(search);
29927
+ const ask = params.get("ask");
29928
+ const comment = params.get("comment");
29929
+ if (ask === null && comment === null) {
29930
+ return;
29931
+ }
29932
+ if (ask !== null && comment !== null) {
29933
+ return null;
29934
+ }
29935
+ const raw = ask ?? comment ?? "";
29936
+ if (raw === "" || raw.includes("/")) {
29937
+ return null;
29938
+ }
29939
+ let id;
29940
+ try {
29941
+ id = decodeURIComponent(raw);
29942
+ } catch {
29943
+ return null;
29944
+ }
29945
+ return ask === null ? { id, kind: "comment" } : { id, kind: "ask" };
29946
+ }
29924
29947
  // ../contracts/src/dispatch-snippet.ts
29925
29948
  var HTML_ENTITIES = [
29926
29949
  ["&lt;", "<"],
@@ -30521,6 +30544,10 @@ var handoffMessageSchema = object({
30521
30544
  // ../contracts/src/subject.ts
30522
30545
  var AGENT_TOPIC_PREFIX = "notifications.agent.";
30523
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
+ }
30524
30551
  var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
30525
30552
  var DISPATCH_ISSUE_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}issue.`;
30526
30553
  var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
@@ -31160,6 +31187,13 @@ async function claimEnvoyRole(sessionID, role, context) {
31160
31187
  throw new Error("Envoy has no bound instance for a role claim");
31161
31188
  await instance.claim(sessionID, role, context);
31162
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
+ }
31163
31197
  function onEnvoyRoleRegained(listener) {
31164
31198
  legionRoleClaimBridge().regained = listener;
31165
31199
  }
@@ -31254,6 +31288,9 @@ async function writeGrantFile(file, grantId) {
31254
31288
  }
31255
31289
  }
31256
31290
 
31291
+ // src/legion/go-bootstrap.ts
31292
+ import path3 from "path";
31293
+
31257
31294
  // ../contracts/src/legion-go-api.ts
31258
31295
  var nonEmptyString2 = string2().min(1);
31259
31296
  var timestamp = exports_iso.datetime({ offset: true });
@@ -31323,10 +31360,18 @@ var legionGoSlotView = strictObject({
31323
31360
  index: number2().int().nonnegative(),
31324
31361
  admittedAt: timestamp
31325
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
+ });
31326
31370
  var legionGoIssue = strictObject({
31327
31371
  key: nonEmptyString2,
31328
31372
  generation: number2().int().nonnegative(),
31329
31373
  phase: _enum2(LEGION_GO_PHASES),
31374
+ status: nonEmptyString2,
31330
31375
  architect: legionGoClaimView.optional(),
31331
31376
  workers: partialRecord(_enum2(LEGION_ROLES), legionGoPhaseView),
31332
31377
  pullRequest: legionGoPullRequestView.optional(),
@@ -31348,7 +31393,8 @@ var legionGoAdmission = strictObject({
31348
31393
  var LegionGoStateResponse = strictObject({
31349
31394
  daemon: goDaemonInfo,
31350
31395
  admission: legionGoAdmission,
31351
- issues: record(string2(), legionGoIssue)
31396
+ issues: record(string2(), legionGoIssue),
31397
+ pendingStatusWrites: array(legionGoPendingStatusWrite)
31352
31398
  });
31353
31399
  var LegionGoRegisterResponse = strictObject({
31354
31400
  claimToken: nonEmptyString2,
@@ -31358,9 +31404,10 @@ var LegionGoRegisterResponse = strictObject({
31358
31404
  generation: number2().int().positive(),
31359
31405
  secret: nonEmptyString2
31360
31406
  });
31361
- var LegionGoErrorResponse = strictObject({
31362
- error: nonEmptyString2
31363
- });
31407
+ var LegionGoErrorResponse = union([
31408
+ strictObject({ error: nonEmptyString2 }),
31409
+ strictObject({ code: nonEmptyString2, error: nonEmptyString2 })
31410
+ ]);
31364
31411
  var legionGoDeliveryView = strictObject({
31365
31412
  id: nonEmptyString2,
31366
31413
  task: nonEmptyString2,
@@ -31389,18 +31436,81 @@ var LegionGoOperatorClaimResponse = strictObject({
31389
31436
  var LegionGoOperatorClaimsResponse = strictObject({
31390
31437
  claims: array(LegionGoOperatorClaimResponse)
31391
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
+ });
31392
31500
 
31393
31501
  // src/legion/go-daemon-client.ts
31394
31502
  class LegionGoDaemonApiError extends Error {
31395
31503
  method;
31396
31504
  path;
31397
31505
  status;
31506
+ code;
31398
31507
  detail;
31399
- constructor(method, path, status, detail) {
31400
- 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}`);
31401
31510
  this.method = method;
31402
31511
  this.path = path;
31403
31512
  this.status = status;
31513
+ this.code = code;
31404
31514
  this.detail = detail;
31405
31515
  this.name = "LegionGoDaemonApiError";
31406
31516
  }
@@ -31444,7 +31554,11 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
31444
31554
  if (response.ok)
31445
31555
  return { status: response.status, text };
31446
31556
  const refusal = parseStrictly(LegionGoErrorResponse, text);
31447
- 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})`);
31448
31562
  };
31449
31563
  const read = async (method, path, schema, body) => {
31450
31564
  const { status, text } = await call(method, path, body);
@@ -31454,6 +31568,7 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
31454
31568
  }
31455
31569
  return parsed.value;
31456
31570
  };
31571
+ const post = (path, request, response, body) => read("POST", path, response, request.parse(body));
31457
31572
  const acknowledge = async (path, body) => {
31458
31573
  const { status, text } = await call("POST", path, body);
31459
31574
  if (status !== 204 || text !== "") {
@@ -31464,7 +31579,18 @@ function createLegionGoDaemonClient(baseUrl, fetchFn = fetch) {
31464
31579
  state: () => read("GET", "/legion/v1/state", LegionGoStateResponse),
31465
31580
  register: (input) => read("POST", "/legion/v1/claims/register", LegionGoRegisterResponse, input),
31466
31581
  ready: (input) => acknowledge("/legion/v1/claims/ready", input),
31467
- 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)
31468
31594
  };
31469
31595
  }
31470
31596
 
@@ -31569,7 +31695,7 @@ async function bootstrapGoClaim(context, state) {
31569
31695
  if (existing !== undefined)
31570
31696
  return existing;
31571
31697
  const bootToken = requiredSecret(process.env, "LEGION_BOOT_TOKEN");
31572
- const daemon = createLegionGoDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"));
31698
+ const daemon = state.daemon();
31573
31699
  const bootstrap = (async () => {
31574
31700
  const { sessionFile, agentId } = await state.persistedTranscript(context);
31575
31701
  state.recordBootstrappedSession(sessionFile);
@@ -31587,7 +31713,9 @@ async function bootstrapGoClaim(context, state) {
31587
31713
  generation: claim.generation
31588
31714
  };
31589
31715
  try {
31590
- 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`);
31591
31719
  state.setCapability({
31592
31720
  kind: "phase-worker",
31593
31721
  sessionID,
@@ -31598,6 +31726,7 @@ async function bootstrapGoClaim(context, state) {
31598
31726
  secret: claim.secret
31599
31727
  });
31600
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);
31601
31730
  await callGoReadyWithRetry("claims/ready", () => daemon.ready(ready));
31602
31731
  onEnvoyRoleRegained(async (role, reason) => {
31603
31732
  if (role !== claim.claimToken)
@@ -31623,9 +31752,6 @@ async function bootstrapGoClaim(context, state) {
31623
31752
  }
31624
31753
  }
31625
31754
 
31626
- // src/legion/tools.ts
31627
- import { randomUUID as randomUUID2 } from "crypto";
31628
-
31629
31755
  // src/tool-result.ts
31630
31756
  function toolSuccess(text, details = {}) {
31631
31757
  return { content: [{ type: "text", text }], details };
@@ -31634,8 +31760,167 @@ function toolFailure(error) {
31634
31760
  return { content: [{ type: "text", text: messageFor(error) }], details: {}, isError: true };
31635
31761
  }
31636
31762
 
31637
- // 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
+ };
31638
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);
31639
31924
  function isDispatchArtifactId(value) {
31640
31925
  return LegionDaemonApi.GatesRegister.request.shape.artifactId.safeParse(value).success;
31641
31926
  }
@@ -31703,7 +31988,7 @@ function createLegionTool(deps) {
31703
31988
  issue: stringInput("issue"),
31704
31989
  status
31705
31990
  });
31706
- return jsonSuccess({});
31991
+ return jsonSuccess2({});
31707
31992
  }
31708
31993
  case "register_gate": {
31709
31994
  const version = parameters.version;
@@ -31722,14 +32007,14 @@ function createLegionTool(deps) {
31722
32007
  artifactId,
31723
32008
  version
31724
32009
  });
31725
- return jsonSuccess({});
32010
+ return jsonSuccess2({});
31726
32011
  }
31727
32012
  case "release_wave": {
31728
32013
  const issues = parameters.issues;
31729
32014
  if (!Array.isArray(issues) || !issues.every((issue) => typeof issue === "string")) {
31730
32015
  throw new Error("release_wave requires issues");
31731
32016
  }
31732
- return jsonSuccess(await daemon.releaseWave({
32017
+ return jsonSuccess2(await daemon.releaseWave({
31733
32018
  tree: architect.tree,
31734
32019
  issues,
31735
32020
  sessionId,
@@ -31750,7 +32035,7 @@ function createLegionTool(deps) {
31750
32035
  sessionId,
31751
32036
  secret: architect.secret
31752
32037
  });
31753
- return jsonSuccess({});
32038
+ return jsonSuccess2({});
31754
32039
  }
31755
32040
  case "spawn_worker": {
31756
32041
  const role = parameters.role;
@@ -31758,7 +32043,7 @@ function createLegionTool(deps) {
31758
32043
  throw new Error("spawn_worker requires a valid Legion role");
31759
32044
  }
31760
32045
  const requestId = randomUUID2();
31761
- return jsonSuccess(await daemon.spawnWorker({
32046
+ return jsonSuccess2(await daemon.spawnWorker({
31762
32047
  tree: architect.tree,
31763
32048
  sessionId,
31764
32049
  secret: architect.secret,
@@ -31780,7 +32065,7 @@ function createLegionTool(deps) {
31780
32065
 
31781
32066
  // src/subagent-session.ts
31782
32067
  import fs from "fs";
31783
- import path3 from "path";
32068
+ import path4 from "path";
31784
32069
  import * as host from "@oh-my-pi/pi-coding-agent";
31785
32070
  var LEGION_BOOTSTRAPPED_SESSION = Symbol.for("legion.pi-envoy.bootstrapped-session");
31786
32071
  var bootstrappedSessionStore = globalThis;
@@ -31791,7 +32076,7 @@ async function isSubagentSession(context) {
31791
32076
  await context.sessionManager.ensureOnDisk();
31792
32077
  const sessionFile = context.sessionManager.getSessionFile();
31793
32078
  if (sessionFile !== undefined && fs.existsSync(sessionFile)) {
31794
- return fs.existsSync(`${path3.dirname(sessionFile)}.jsonl`);
32079
+ return fs.existsSync(`${path4.dirname(sessionFile)}.jsonl`);
31795
32080
  }
31796
32081
  const bootstrapped = bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
31797
32082
  return bootstrapped !== undefined && bootstrapped !== sessionFile;
@@ -31877,7 +32162,7 @@ async function persistedTranscript(context) {
31877
32162
  if (!sessionFile?.endsWith(".jsonl")) {
31878
32163
  throw new Error("Legion session must have a persisted transcript");
31879
32164
  }
31880
- const agentId = path4.basename(sessionFile, ".jsonl");
32165
+ const agentId = path5.basename(sessionFile, ".jsonl");
31881
32166
  if (!agentId)
31882
32167
  throw new Error("Legion session transcript has no agent id");
31883
32168
  return { sessionFile, agentId };
@@ -32052,6 +32337,11 @@ function legionExtension(pi) {
32052
32337
  });
32053
32338
  return daemonClient;
32054
32339
  };
32340
+ let goDaemonClient;
32341
+ const goRoleDaemon = () => {
32342
+ goDaemonClient ??= createLegionGoDaemonClient(requiredEnvironment(process.env, "LEGION_DAEMON_URL"));
32343
+ return goDaemonClient;
32344
+ };
32055
32345
  const rerunReadyAfterRegain = async (endpoint, role, reason, call) => {
32056
32346
  try {
32057
32347
  await callReadyWithRetry(`${endpoint} after role regain`, call);
@@ -32263,7 +32553,7 @@ function legionExtension(pi) {
32263
32553
  if (await checkSubagentSession(context))
32264
32554
  return;
32265
32555
  if (process.env.LEGION_DAEMON_API === "go") {
32266
- return bootstrapGoClaim(context, {
32556
+ await bootstrapGoClaim(context, {
32267
32557
  capability: () => capability,
32268
32558
  setCapability: (next) => {
32269
32559
  capability = next;
@@ -32272,10 +32562,14 @@ function legionExtension(pi) {
32272
32562
  setBootstrap: (next) => {
32273
32563
  bootstrap = next;
32274
32564
  },
32565
+ daemon: goRoleDaemon,
32275
32566
  exitProcess,
32276
32567
  persistedTranscript,
32277
32568
  recordBootstrappedSession
32278
32569
  });
32570
+ registerGoTools();
32571
+ await activateLegionTool();
32572
+ return;
32279
32573
  }
32280
32574
  const classification = classifySession(process.env);
32281
32575
  switch (classification.kind) {
@@ -32350,7 +32644,12 @@ function legionExtension(pi) {
32350
32644
  }
32351
32645
  return;
32352
32646
  }
32353
- 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({
32354
32653
  tree: active.tree,
32355
32654
  issue: active.issue,
32356
32655
  sessionId: sessionID,
@@ -32401,6 +32700,30 @@ function legionExtension(pi) {
32401
32700
  architectToolsRegistered = true;
32402
32701
  pi.registerTool(createLegionTool({ pi, roleDaemon, architectSession }));
32403
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
+ };
32404
32727
  pi.registerCommand("legion-claim-controller", {
32405
32728
  description: "Claim the Legion controller role and register daemon authority for this session",
32406
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.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": 1
16
+ "goDaemonApiVersion": 3
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",