@sjawhar/pi-legion-envoy 1.33.0 → 1.35.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
@@ -29649,6 +29649,7 @@ function date4(params) {
29649
29649
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
29650
29650
  config(en_default());
29651
29651
  // ../contracts/src/dispatch-api.ts
29652
+ var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
29652
29653
  function searchOwnerOf(result) {
29653
29654
  return result.owner ?? { kind: "issue", key: result.issue.key };
29654
29655
  }
@@ -29731,6 +29732,8 @@ var CommentEventPayloadSchema = object({
29731
29732
  ask_id: string2().nullish(),
29732
29733
  ask_question: string2().optional(),
29733
29734
  ask_state: _enum2(["open", "answered", "resolved"]).optional(),
29735
+ ask_waiting_on: _enum2(["human", "agent"]).optional(),
29736
+ turn: _enum2(["human", "agent"]).nullish(),
29734
29737
  anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
29735
29738
  suggestion: object({ replace_with: string2().optional() }).nullish(),
29736
29739
  author: object({ kind: string2(), id: string2() }).optional(),
@@ -29757,7 +29760,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
29757
29760
  var MessageDeliveryEventPayloadSchema = object({
29758
29761
  message_id: string2().optional(),
29759
29762
  attempt: number2().int().positive().optional(),
29760
- delivery: _enum2(["btw", "aside", "steer"]).optional(),
29763
+ delivery: _enum2(DELIVERY_CAPABILITIES).optional(),
29761
29764
  session_id: string2().optional(),
29762
29765
  target: string2().optional(),
29763
29766
  title: string2().optional(),
@@ -29848,6 +29851,16 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
29848
29851
  message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
29849
29852
  };
29850
29853
  }
29854
+ var commentValidation = (() => {
29855
+ const owner = documentOwnerValidation(true);
29856
+ return {
29857
+ check: (value) => {
29858
+ const input = value;
29859
+ return owner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
29860
+ },
29861
+ message: `${owner.message} turn requires reply_to_ask.`
29862
+ };
29863
+ })();
29851
29864
  var SPEC_SECTIONS = [
29852
29865
  "Summary",
29853
29866
  "Decisions needed",
@@ -29943,9 +29956,10 @@ var dispatchToolSpecs = [
29943
29956
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
29944
29957
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
29945
29958
  reply_to: z.string().describe("Full id of a comment to reply to; replying to any comment in a thread continues that " + "thread (an ask's clarification thread included).").optional(),
29946
- reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional()
29959
+ reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
29960
+ turn: z.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
29947
29961
  }),
29948
- validation: documentOwnerValidation(true)
29962
+ validation: commentValidation
29949
29963
  },
29950
29964
  {
29951
29965
  name: "dispatch_suggest",
@@ -30334,7 +30348,7 @@ var LegionDaemonApi = {
30334
30348
  queue: array(nonEmptyString)
30335
30349
  }),
30336
30350
  gates: record(string2(), stateGate),
30337
- controllerLocator: stateLocator.optional(),
30351
+ controllerLocator: stateTreeLocator.optional(),
30338
30352
  roles: record(string2(), stateRole),
30339
30353
  controllerPendingNotices: number2().int().nonnegative(),
30340
30354
  pendingStatusWrites: array(nonEmptyString),
@@ -30342,7 +30356,11 @@ var LegionDaemonApi = {
30342
30356
  })
30343
30357
  },
30344
30358
  ControllerReady: {
30345
- request: strictObject({ secret: nonEmptyString, sessionId: nonEmptyString }),
30359
+ request: strictObject({
30360
+ secret: nonEmptyString,
30361
+ sessionId: nonEmptyString,
30362
+ ompSessionFile: nonEmptyString.optional()
30363
+ }),
30346
30364
  response: object({})
30347
30365
  },
30348
30366
  ProcessStarted: {
@@ -30462,17 +30480,23 @@ var LegionDaemonApi = {
30462
30480
  response: object({})
30463
30481
  },
30464
30482
  Grant: {
30465
- request: strictObject({
30466
- tree: nonEmptyString,
30467
- issue: nonEmptyString,
30468
- sessionId: nonEmptyString,
30469
- secret: nonEmptyString
30470
- }),
30483
+ request: union([
30484
+ strictObject({
30485
+ sessionId: nonEmptyString,
30486
+ secret: nonEmptyString,
30487
+ tree: nonEmptyString,
30488
+ issue: nonEmptyString
30489
+ }),
30490
+ strictObject({ sessionId: nonEmptyString, secret: nonEmptyString })
30491
+ ]),
30471
30492
  response: object({ grantId: nonEmptyString, expiresAt: nonEmptyString })
30472
30493
  },
30473
30494
  GitHubToken: {
30474
- request: strictObject({ grantId: nonEmptyString }),
30495
+ request: strictObject({ grantId: nonEmptyString, merge: literal(true).optional() }),
30475
30496
  response: object({ token: nonEmptyString, appLogin: string2().endsWith("[bot]") })
30497
+ },
30498
+ GitCredential: {
30499
+ request: strictObject({ grantId: nonEmptyString })
30476
30500
  }
30477
30501
  };
30478
30502
  // ../contracts/src/repo.ts
@@ -31103,7 +31127,7 @@ var TolerantInboundEnvelopeSchema = object({
31103
31127
  }).passthrough();
31104
31128
  var DispatchDeliveryRequestSchema = object({
31105
31129
  attempt: number2().int().positive(),
31106
- mode: _enum2(["btw", "aside", "steer"])
31130
+ mode: _enum2(DELIVERY_CAPABILITIES)
31107
31131
  });
31108
31132
  var DispatchTargetedFrameSchema = object({
31109
31133
  event: DispatchEventSchema,
@@ -32595,21 +32619,34 @@ async function executeDispatchTool(input) {
32595
32619
  if (replyTo !== undefined && replyToAsk !== undefined) {
32596
32620
  throw new Error("reply_to and reply_to_ask cannot both be set");
32597
32621
  }
32622
+ const requestedTurn = optionalString(args, "turn");
32623
+ if (requestedTurn !== undefined && replyToAsk === undefined) {
32624
+ throw new Error("turn requires reply_to_ask");
32625
+ }
32626
+ if (requestedTurn !== undefined && requestedTurn !== "agent" && requestedTurn !== "human") {
32627
+ throw new Error("turn must be agent or human");
32628
+ }
32598
32629
  const commentInput = {
32599
32630
  body: stringArg(args, "body"),
32600
32631
  ...anchored === undefined ? {} : { anchor: anchored },
32601
32632
  ...replyTo === undefined ? {} : { reply_to: replyTo },
32602
32633
  ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
32634
+ ...requestedTurn === undefined ? {} : { turn: requestedTurn },
32603
32635
  actor
32604
32636
  };
32605
32637
  const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
32638
+ const askState = comment.turn === null ? "" : ` (ask now waiting on ${comment.turn})`;
32606
32639
  return {
32607
- text: `Posted comment ${comment.id}`,
32640
+ text: `Posted comment ${comment.id}${askState}`,
32608
32641
  details: resolved === undefined ? {
32609
32642
  issue: comment.issue_key,
32610
32643
  topic: dispatchIssueSubject(issue(), ">"),
32611
- comment: comment.id
32612
- } : writeResultDetails(resolved, { comment: comment.id })
32644
+ comment: comment.id,
32645
+ ...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
32646
+ } : writeResultDetails(resolved, {
32647
+ comment: comment.id,
32648
+ ...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
32649
+ })
32613
32650
  };
32614
32651
  }
32615
32652
  case "dispatch_suggest": {
@@ -33226,6 +33263,31 @@ function messageMetadata(input) {
33226
33263
  import { logger } from "@oh-my-pi/pi-utils";
33227
33264
  var import_nats = __toESM(require_mod4(), 1);
33228
33265
 
33266
+ // src/legion/role-claim-bridge.ts
33267
+ var LEGION_ROLE_CLAIM_BRIDGE = Symbol.for("legion.pi-envoy.role-claim-bridge");
33268
+ function legionRoleClaimBridge() {
33269
+ const store = globalThis;
33270
+ const bridge = store[LEGION_ROLE_CLAIM_BRIDGE];
33271
+ if (bridge)
33272
+ return bridge;
33273
+ const createdBridge = {
33274
+ instances: [],
33275
+ regained: undefined
33276
+ };
33277
+ store[LEGION_ROLE_CLAIM_BRIDGE] = createdBridge;
33278
+ return createdBridge;
33279
+ }
33280
+ async function claimEnvoyRole(sessionID, role, context) {
33281
+ const bridge = legionRoleClaimBridge();
33282
+ const instance = bridge.instances.findLast((candidate) => candidate.sessionID() === sessionID) ?? bridge.instances.at(-1);
33283
+ if (instance === undefined)
33284
+ throw new Error("Envoy has no bound instance for a role claim");
33285
+ await instance.claim(sessionID, role, context);
33286
+ }
33287
+ function onEnvoyRoleRegained(listener) {
33288
+ legionRoleClaimBridge().regained = listener;
33289
+ }
33290
+
33229
33291
  // src/tool-result.ts
33230
33292
  function toolSuccess(text, details = {}) {
33231
33293
  return { content: [{ type: "text", text }], details };
@@ -33290,6 +33352,8 @@ async function copySessionID(copyToClipboard, sessionID) {
33290
33352
  // extensions/envoy.ts
33291
33353
  var codec2 = import_nats.StringCodec();
33292
33354
  var NATS_RETRY_INTERVAL_MS = 15000;
33355
+ var ALL_CAPABILITIES = DELIVERY_CAPABILITIES;
33356
+ var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
33293
33357
  var ROLE_CLAIM_ENTRY = "envoy-role-claim";
33294
33358
  var OPEN_ASKS_TIMEOUT_MS = 3000;
33295
33359
  function isRoleClaimEntry(entry) {
@@ -33305,25 +33369,6 @@ function isRoleClaimEntry(entry) {
33305
33369
  return false;
33306
33370
  return entry.data.role === null || typeof entry.data.role === "string";
33307
33371
  }
33308
- var LEGION_ROLE_CLAIM_BRIDGE = Symbol.for("legion.pi-envoy.role-claim-bridge");
33309
- function legionRoleClaimBridge() {
33310
- const store = globalThis;
33311
- let bridge = store[LEGION_ROLE_CLAIM_BRIDGE];
33312
- if (bridge === undefined) {
33313
- const ready = Promise.withResolvers();
33314
- bridge = { ready };
33315
- store[LEGION_ROLE_CLAIM_BRIDGE] = bridge;
33316
- }
33317
- return bridge;
33318
- }
33319
- async function claimEnvoyRole(sessionID, role, context) {
33320
- const bridge = legionRoleClaimBridge();
33321
- const claim = bridge.claim ?? await bridge.ready.promise;
33322
- await claim(sessionID, role, context);
33323
- }
33324
- function onEnvoyRoleRegained(listener) {
33325
- legionRoleClaimBridge().regained = listener;
33326
- }
33327
33372
  function resolveSkillsDirectory() {
33328
33373
  const moduleDirectory = dirname(fileURLToPath(import.meta.url));
33329
33374
  const candidates = [
@@ -33548,7 +33593,7 @@ function envoyExtension(pi) {
33548
33593
  topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
33549
33594
  port: 0,
33550
33595
  title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
33551
- capabilities: typeof pi.askEphemeral === "function" ? ["aside", "btw"] : ["aside"],
33596
+ capabilities: typeof pi.askEphemeral === "function" ? ALL_CAPABILITIES : CAPABILITIES_WITHOUT_BTW,
33552
33597
  driving: false,
33553
33598
  selfSubscribed: true
33554
33599
  });
@@ -33759,8 +33804,11 @@ function envoyExtension(pi) {
33759
33804
  await registerSession();
33760
33805
  await setEnvoyRole(role);
33761
33806
  };
33762
- bridge.claim = claim;
33763
- bridge.ready.resolve(claim);
33807
+ const claimInstance = {
33808
+ claim,
33809
+ sessionID: () => activeSessionContext?.sessionManager.getSessionId() ?? sessionID
33810
+ };
33811
+ bridge.instances.push(claimInstance);
33764
33812
  pi.on("session_start", async (_event, context) => {
33765
33813
  const previousSessionID = sessionID;
33766
33814
  restoreLocalSessionState(context);
@@ -33829,6 +33877,9 @@ function envoyExtension(pi) {
33829
33877
  pi.on("session_tree", (_event, context) => rebind(undefined, context));
33830
33878
  pi.on("session_shutdown", async () => {
33831
33879
  shuttingDown = true;
33880
+ const bound = bridge.instances.indexOf(claimInstance);
33881
+ if (bound !== -1)
33882
+ bridge.instances.splice(bound, 1);
33832
33883
  const deadline = Promise.withResolvers();
33833
33884
  const timer = setTimeout(deadline.resolve, 1000);
33834
33885
  try {
@@ -34097,7 +34148,5 @@ function topicsFor(parameters, fallback = []) {
34097
34148
  return expandSubscriptionTopics(value);
34098
34149
  }
34099
34150
  export {
34100
- claimEnvoyRole,
34101
- envoyExtension as default,
34102
- onEnvoyRoleRegained
34151
+ envoyExtension as default
34103
34152
  };