@sjawhar/pi-legion-envoy 1.27.1 → 1.28.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
@@ -29746,7 +29746,7 @@ var MessageEventPayloadSchema = object({
29746
29746
  });
29747
29747
  var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
29748
29748
  id: string2(),
29749
- issue_key: string2(),
29749
+ issue_key: string2().nullable(),
29750
29750
  author: object({ kind: string2(), id: string2() }),
29751
29751
  body: string2(),
29752
29752
  target: string2(),
@@ -29759,6 +29759,7 @@ var MessageDeliveryEventPayloadSchema = object({
29759
29759
  attempt: number2().int().positive().optional(),
29760
29760
  delivery: _enum2(["btw", "aside", "steer"]).optional(),
29761
29761
  session_id: string2().optional(),
29762
+ target: string2().optional(),
29762
29763
  title: string2().optional(),
29763
29764
  state: _enum2(["sent", "failed"]).optional(),
29764
29765
  error: string2().optional()
@@ -31095,7 +31096,7 @@ var DispatchTargetedFrameSchema = object({
31095
31096
  }).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
31096
31097
  var RecoverableDispatchDeliveryFailureSchema = object({
31097
31098
  event: object({
31098
- issue_key: string2(),
31099
+ issue_key: string2().nullable(),
31099
31100
  payload: object({ id: string2(), body: string2().optional() }).passthrough()
31100
31101
  }).passthrough(),
31101
31102
  delivery: DispatchDeliveryRequestSchema
@@ -31298,7 +31299,7 @@ function renderInbound(raw, sessionID, subject) {
31298
31299
  askQuestion = dispatchAskQuestion(frame.event);
31299
31300
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31300
31301
  dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
31301
- if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
31302
+ if (frame.event.type === "message.created") {
31302
31303
  const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31303
31304
  const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
31304
31305
  if (message.success && requested.success && message.data.id !== undefined) {
@@ -31309,7 +31310,9 @@ function renderInbound(raw, sessionID, subject) {
31309
31310
  issueKey: frame.event.issue_key,
31310
31311
  body: message.data.body ?? envelope.payload_summary ?? ""
31311
31312
  };
31312
- dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31313
+ if (frame.event.issue_key !== null) {
31314
+ dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31315
+ }
31313
31316
  }
31314
31317
  }
31315
31318
  dispatchEvent = {
@@ -32402,12 +32405,23 @@ async function executeDispatchTool(input) {
32402
32405
  if (!input.config.enabled || !configUrl || !configToken) {
32403
32406
  throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
32404
32407
  }
32408
+ const baseFetch = input.fetchImpl ?? fetch;
32409
+ const fetchImpl = Object.assign(async (...args) => {
32410
+ try {
32411
+ return await baseFetch(...args);
32412
+ } catch (error) {
32413
+ if (error instanceof TypeError) {
32414
+ throw new Error(`Dispatch at ${configUrl} is unreachable: ${error.message}. If the Dispatch URL changed, restart this agent process so it picks up the new configuration.`);
32415
+ }
32416
+ throw error;
32417
+ }
32418
+ }, { preconnect: baseFetch.preconnect });
32405
32419
  if (input.tool === "dispatch_open_asks") {
32406
32420
  const sessionId = input.sessionId?.trim();
32407
32421
  if (!sessionId)
32408
32422
  throw new Error("host session id is required for dispatch_open_asks");
32409
32423
  toolSchema(input.tool).parse(input.args);
32410
- const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
32424
+ const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
32411
32425
  const response = await client.openAsks(sessionId);
32412
32426
  return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
32413
32427
  }
@@ -32416,7 +32430,7 @@ async function executeDispatchTool(input) {
32416
32430
  const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
32417
32431
  const args = toolSchema(input.tool).parse(ownerArguments.args);
32418
32432
  const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
32419
- const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
32433
+ const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
32420
32434
  const owner = ownerArguments.owner?.kind === "issue" ? {
32421
32435
  kind: "issue",
32422
32436
  issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
@@ -32603,17 +32617,18 @@ async function executeDispatchTool(input) {
32603
32617
  }
32604
32618
  case "dispatch_message": {
32605
32619
  const inReplyTo = messageInReplyTo(args);
32606
- const message = await client.message(issue(), {
32620
+ const issueKey = issue();
32621
+ const message = await client.message(issueKey, {
32607
32622
  body: stringArg(args, "body"),
32608
32623
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
32609
32624
  actor
32610
32625
  });
32611
- const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
32626
+ const messageRef = `dispatch://${issueKey}/message/${message.id}`;
32612
32627
  return {
32613
32628
  text: `Posted message ${message.id} (${messageRef})`,
32614
32629
  details: {
32615
- issue: message.issue_key,
32616
- topic: dispatchIssueSubject(message.issue_key, ">"),
32630
+ issue: issueKey,
32631
+ topic: dispatchIssueSubject(issueKey, ">"),
32617
32632
  message: message.id
32618
32633
  }
32619
32634
  };
@@ -33255,59 +33270,7 @@ async function copySessionID(copyToClipboard, sessionID) {
33255
33270
  var codec2 = import_nats.StringCodec();
33256
33271
  var NATS_RETRY_INTERVAL_MS = 15000;
33257
33272
  var ROLE_CLAIM_ENTRY = "envoy-role-claim";
33258
- var ASK_AWARENESS_ENTRY = "dispatch-ask-awareness";
33259
- var LEGION_MANAGED_ENTRY = "legion-managed-session";
33260
33273
  var OPEN_ASKS_TIMEOUT_MS = 3000;
33261
- var OPEN_ASKS_REMINDER = "You have no unanswered asks in Dispatch. If you are waiting for human input, open an ask. Otherwise ignore this reminder and continue with any remaining work. Do not reply just to acknowledge this reminder.";
33262
- function isAskAwarenessEntry(entry) {
33263
- if (typeof entry !== "object" || entry === null)
33264
- return false;
33265
- if (!("type" in entry) || entry.type !== "custom")
33266
- return false;
33267
- if (!("customType" in entry) || entry.customType !== ASK_AWARENESS_ENTRY)
33268
- return false;
33269
- if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
33270
- return false;
33271
- const data = entry.data;
33272
- return "session_id" in data && typeof data.session_id === "string" && "period" in data && typeof data.period === "number" && "baseline_as_of" in data && (typeof data.baseline_as_of === "string" || data.baseline_as_of === null) && "checked" in data && typeof data.checked === "boolean" && "fired" in data && typeof data.fired === "boolean" && "saw_ask" in data && typeof data.saw_ask === "boolean";
33273
- }
33274
- function isLegionManagedEntry(entry) {
33275
- if (typeof entry !== "object" || entry === null)
33276
- return false;
33277
- if (!("type" in entry) || entry.type !== "custom")
33278
- return false;
33279
- if (!("customType" in entry) || entry.customType !== LEGION_MANAGED_ENTRY)
33280
- return false;
33281
- if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
33282
- return false;
33283
- return "session_id" in entry.data && typeof entry.data.session_id === "string";
33284
- }
33285
- function emptyAskAwareness(sessionID) {
33286
- return {
33287
- session_id: sessionID,
33288
- period: 0,
33289
- baseline_as_of: null,
33290
- checked: false,
33291
- fired: false,
33292
- saw_ask: false
33293
- };
33294
- }
33295
- function transcriptAskAwareness(branch, sessionID) {
33296
- let state;
33297
- for (const entry of branch) {
33298
- if (isAskAwarenessEntry(entry) && entry.data.session_id === sessionID)
33299
- state = entry.data;
33300
- }
33301
- return state;
33302
- }
33303
- function transcriptMarksLegionSession(branch, sessionID) {
33304
- return branch.some((entry) => isLegionManagedEntry(entry) && entry.data.session_id === sessionID);
33305
- }
33306
- function isHumanTurnInitiator(message) {
33307
- if (message.attribution !== "user")
33308
- return false;
33309
- return message.role === "user" || message.role === "custom" && (message.customType === "skill-prompt" || message.customType === "collab-prompt");
33310
- }
33311
33274
  function isRoleClaimEntry(entry) {
33312
33275
  if (typeof entry !== "object" || entry === null)
33313
33276
  return false;
@@ -33327,15 +33290,13 @@ function legionRoleClaimBridge() {
33327
33290
  let bridge = store[LEGION_ROLE_CLAIM_BRIDGE];
33328
33291
  if (bridge === undefined) {
33329
33292
  const ready = Promise.withResolvers();
33330
- bridge = { ready, managedSessions: new Set };
33293
+ bridge = { ready };
33331
33294
  store[LEGION_ROLE_CLAIM_BRIDGE] = bridge;
33332
33295
  }
33333
33296
  return bridge;
33334
33297
  }
33335
33298
  async function claimEnvoyRole(sessionID, role, context) {
33336
33299
  const bridge = legionRoleClaimBridge();
33337
- bridge.managedSessions.add(sessionID);
33338
- bridge.markManagedSession?.(sessionID, context);
33339
33300
  const claim = bridge.claim ?? await bridge.ready.promise;
33340
33301
  await claim(sessionID, role, context);
33341
33302
  }
@@ -33384,87 +33345,26 @@ function envoyExtension(pi) {
33384
33345
  let claimedRoleTopic;
33385
33346
  let activeSessionContext;
33386
33347
  const inbox = [];
33387
- let askAwareness = emptyAskAwareness("");
33388
- let candidateBaseline;
33389
- let awarenessGeneration = 0;
33390
- let availabilityWarningPeriod;
33391
- let agentRunning = false;
33392
- let agentWillContinue = false;
33393
- let legionManagedSessionID = "";
33394
- let terminalInputUnsubscribe;
33395
- const persistAskAwareness = (state) => {
33396
- askAwareness = state;
33397
- pi.appendEntry(ASK_AWARENESS_ENTRY, state);
33398
- };
33348
+ const availabilityWarningSessionIDs = new Set;
33399
33349
  const warnAskAvailability = (context, error) => {
33400
- const key = `${askAwareness.session_id}:${askAwareness.period}`;
33401
- if (availabilityWarningPeriod === key)
33350
+ const sessionID = context.sessionManager.getSessionId();
33351
+ if (availabilityWarningSessionIDs.has(sessionID))
33402
33352
  return;
33403
- availabilityWarningPeriod = key;
33404
- context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)}); reminder skipped`, "warning");
33353
+ availabilityWarningSessionIDs.add(sessionID);
33354
+ context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)}); summary unavailable`, "warning");
33405
33355
  };
33406
- const queryOpenAsks = async (requestedSessionID, since, signal) => {
33356
+ const queryOpenAsks = async (requestedSessionID) => {
33407
33357
  const config = activeDispatchConfig();
33408
33358
  if (config === null)
33409
33359
  return null;
33410
- const deadline = AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS);
33411
- const requestSignal = signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
33412
- return new DispatchClient(config.url, config.token, fetch, requestSignal).openAsks(requestedSessionID, since);
33413
- };
33414
- const setReminderBaseline = (requestedSessionID, period, asOf) => {
33415
- candidateBaseline = { sessionID: requestedSessionID, asOf };
33416
- if (askAwareness.session_id !== requestedSessionID || askAwareness.period !== period || askAwareness.checked || askAwareness.fired || askAwareness.baseline_as_of !== null) {
33417
- return;
33418
- }
33419
- persistAskAwareness({ ...askAwareness, baseline_as_of: asOf });
33420
- };
33421
- const refreshReminderBaseline = async (requestedSessionID, period, context) => {
33422
- try {
33423
- const snapshot = await queryOpenAsks(requestedSessionID);
33424
- if (snapshot === null)
33425
- return;
33426
- setReminderBaseline(requestedSessionID, period, snapshot.as_of);
33427
- } catch (error) {
33428
- warnAskAvailability(context, error);
33429
- }
33430
- };
33431
- const armAskReminder = async (context, refreshBaseline = false) => {
33432
- const id = context.sessionManager.getSessionId();
33433
- if (id === "" || activeDispatchConfig() === null)
33434
- return;
33435
- awarenessGeneration++;
33436
- availabilityWarningPeriod = undefined;
33437
- const baseline = !agentRunning && !refreshBaseline && candidateBaseline !== undefined && candidateBaseline.sessionID === id ? candidateBaseline.asOf : null;
33438
- const previousPeriod = askAwareness.session_id === id ? askAwareness.period : 0;
33439
- persistAskAwareness({
33440
- session_id: id,
33441
- period: previousPeriod + 1,
33442
- baseline_as_of: baseline,
33443
- checked: false,
33444
- fired: false,
33445
- saw_ask: false
33446
- });
33447
- if (agentRunning || refreshBaseline) {
33448
- await refreshReminderBaseline(id, askAwareness.period, context);
33449
- }
33450
- };
33451
- const cancelPendingAskReminder = () => {
33452
- awarenessGeneration++;
33453
- };
33454
- const installTerminalInputCancellation = (context) => {
33455
- terminalInputUnsubscribe?.();
33456
- terminalInputUnsubscribe = context.ui.onTerminalInput(cancelPendingAskReminder);
33360
+ const snapshot = await new DispatchClient(config.url, config.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
33361
+ availabilityWarningSessionIDs.delete(requestedSessionID);
33362
+ return snapshot;
33457
33363
  };
33458
33364
  const restoreLocalSessionState = (context) => {
33459
33365
  sessionDirectory = context.cwd;
33460
33366
  sessionID = context.sessionManager.getSessionId();
33461
33367
  activeSessionContext = context;
33462
- candidateBaseline = undefined;
33463
- const branch = context.sessionManager.getBranch?.() ?? [];
33464
- askAwareness = transcriptAskAwareness(branch, sessionID) ?? emptyAskAwareness(sessionID);
33465
- const persistedLegionSession = transcriptMarksLegionSession(branch, sessionID);
33466
- const managed = persistedLegionSession || legionRoleClaimBridge().managedSessions.has(sessionID);
33467
- legionManagedSessionID = managed ? sessionID : "";
33468
33368
  };
33469
33369
  pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
33470
33370
  registerEnvoyMessageRenderer(pi);
@@ -33505,13 +33405,6 @@ function envoyExtension(pi) {
33505
33405
  const dedupeKey = rendered.envelope?.dedupe_key;
33506
33406
  const duplicate = dedupeKey !== undefined && dedupeKeys.has(dedupeKey);
33507
33407
  if (!duplicate && !rendered.skip) {
33508
- if (rendered.dispatchActor?.kind === "user" && activeSessionContext !== undefined) {
33509
- try {
33510
- await armAskReminder(activeSessionContext, true);
33511
- } catch (error) {
33512
- warnAskAvailability(activeSessionContext, error);
33513
- }
33514
- }
33515
33408
  const envelope = rendered.envelope;
33516
33409
  if (envelope !== undefined) {
33517
33410
  inbox.unshift({
@@ -33831,17 +33724,6 @@ function envoyExtension(pi) {
33831
33724
  return run;
33832
33725
  };
33833
33726
  const bridge = legionRoleClaimBridge();
33834
- bridge.markManagedSession = (targetSessionID, context) => {
33835
- bridge.managedSessions.add(targetSessionID);
33836
- if (targetSessionID !== sessionID)
33837
- return;
33838
- if (legionManagedSessionID === targetSessionID)
33839
- return;
33840
- legionManagedSessionID = targetSessionID;
33841
- if (context !== undefined || activeSessionContext !== undefined) {
33842
- pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
33843
- }
33844
- };
33845
33727
  const claim = async (targetSessionID, role, callerContext) => {
33846
33728
  const context = callerContext ?? activeSessionContext;
33847
33729
  if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
@@ -33855,7 +33737,6 @@ function envoyExtension(pi) {
33855
33737
  bridge.claim = claim;
33856
33738
  bridge.ready.resolve(claim);
33857
33739
  pi.on("session_start", async (_event, context) => {
33858
- installTerminalInputCancellation(context);
33859
33740
  const previousSessionID = sessionID;
33860
33741
  restoreLocalSessionState(context);
33861
33742
  if (dispatchConfig.error !== null) {
@@ -33891,7 +33772,6 @@ function envoyExtension(pi) {
33891
33772
  }
33892
33773
  });
33893
33774
  const rebind = async (reason, context) => {
33894
- installTerminalInputCancellation(context);
33895
33775
  const previousID = sessionID;
33896
33776
  restoreLocalSessionState(context);
33897
33777
  if (defaults.natsUrls.length === 0)
@@ -33941,8 +33821,6 @@ function envoyExtension(pi) {
33941
33821
  ]);
33942
33822
  } finally {
33943
33823
  clearTimeout(timer);
33944
- terminalInputUnsubscribe?.();
33945
- terminalInputUnsubscribe = undefined;
33946
33824
  connection = undefined;
33947
33825
  activeSessionContext = undefined;
33948
33826
  subscriptions.clear();
@@ -33996,7 +33874,6 @@ function envoyExtension(pi) {
33996
33874
  const snapshot = await queryOpenAsks(id);
33997
33875
  if (snapshot === null)
33998
33876
  return;
33999
- setReminderBaseline(id, askAwareness.session_id === id ? askAwareness.period : 0, snapshot.as_of);
34000
33877
  return {
34001
33878
  message: {
34002
33879
  customType: "dispatch-open-asks",
@@ -34018,60 +33895,7 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
34018
33895
  };
34019
33896
  }
34020
33897
  });
34021
- pi.on("agent_start", async () => {
34022
- agentRunning = true;
34023
- agentWillContinue = false;
34024
- });
34025
- pi.on("agent_end", async (event) => {
34026
- agentRunning = false;
34027
- agentWillContinue = event.willContinue === true;
34028
- });
34029
- pi.on("input", async () => {
34030
- cancelPendingAskReminder();
34031
- });
34032
- pi.on("message_start", async (event, context) => {
34033
- if (!isHumanTurnInitiator(event.message))
34034
- return;
34035
- try {
34036
- await armAskReminder(context);
34037
- } catch (error) {
34038
- warnAskAvailability(context, error);
34039
- }
34040
- });
34041
- pi.on("session_stop", async (event, context) => {
34042
- if (event.stop_hook_active || event.signal.aborted || shuttingDown || agentWillContinue || event.last_assistant_message?.role !== "assistant" || event.session_id !== sessionID || legionManagedSessionID === event.session_id || askAwareness.session_id !== event.session_id || askAwareness.period === 0 || askAwareness.checked || askAwareness.fired) {
34043
- return;
34044
- }
34045
- const baseline = askAwareness.baseline_as_of;
34046
- if (baseline === null)
34047
- return;
34048
- if (context.ui.getEditorText().trim() !== "")
34049
- return;
34050
- const claimed = { ...askAwareness, checked: true };
34051
- const generation = awarenessGeneration;
34052
- persistAskAwareness(claimed);
34053
- let snapshot;
34054
- try {
34055
- snapshot = await queryOpenAsks(event.session_id, baseline, event.signal);
34056
- } catch (error) {
34057
- if (!event.signal.aborted && generation === awarenessGeneration) {
34058
- warnAskAvailability(context, error);
34059
- }
34060
- return;
34061
- }
34062
- if (snapshot === null || event.signal.aborted || generation !== awarenessGeneration || shuttingDown || agentWillContinue || context.sessionManager.getSessionId() !== event.session_id || sessionID !== event.session_id || legionManagedSessionID === event.session_id || context.ui.getEditorText().trim() !== "" || askAwareness.session_id !== claimed.session_id || askAwareness.period !== claimed.period || !askAwareness.checked) {
34063
- return;
34064
- }
34065
- if (snapshot.count > 0 || snapshot.opened_since || askAwareness.saw_ask) {
34066
- return;
34067
- }
34068
- persistAskAwareness({ ...askAwareness, fired: true });
34069
- return { continue: true, additionalContext: OPEN_ASKS_REMINDER };
34070
- });
34071
33898
  pi.on("tool_result", async (event) => {
34072
- if (!event.isError && event.toolName === "dispatch_ask" && typeof event.details === "object" && event.details !== null && "ask" in event.details && typeof event.details.ask === "string" && askAwareness.session_id === sessionID && askAwareness.period > 0 && !askAwareness.saw_ask) {
34073
- persistAskAwareness({ ...askAwareness, saw_ask: true });
34074
- }
34075
33899
  if (event.isError)
34076
33900
  return;
34077
33901
  const topic = dispatchSubscriptionTopic(event.details);
package/dist/legion.js CHANGED
@@ -29746,7 +29746,7 @@ var MessageEventPayloadSchema = object({
29746
29746
  });
29747
29747
  var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
29748
29748
  id: string2(),
29749
- issue_key: string2(),
29749
+ issue_key: string2().nullable(),
29750
29750
  author: object({ kind: string2(), id: string2() }),
29751
29751
  body: string2(),
29752
29752
  target: string2(),
@@ -29759,6 +29759,7 @@ var MessageDeliveryEventPayloadSchema = object({
29759
29759
  attempt: number2().int().positive().optional(),
29760
29760
  delivery: _enum2(["btw", "aside", "steer"]).optional(),
29761
29761
  session_id: string2().optional(),
29762
+ target: string2().optional(),
29762
29763
  title: string2().optional(),
29763
29764
  state: _enum2(["sent", "failed"]).optional(),
29764
29765
  error: string2().optional()
@@ -31548,7 +31549,7 @@ var DispatchTargetedFrameSchema = object({
31548
31549
  }).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
31549
31550
  var RecoverableDispatchDeliveryFailureSchema = object({
31550
31551
  event: object({
31551
- issue_key: string2(),
31552
+ issue_key: string2().nullable(),
31552
31553
  payload: object({ id: string2(), body: string2().optional() }).passthrough()
31553
31554
  }).passthrough(),
31554
31555
  delivery: DispatchDeliveryRequestSchema
@@ -31751,7 +31752,7 @@ function renderInbound(raw, sessionID, subject) {
31751
31752
  askQuestion = dispatchAskQuestion(frame.event);
31752
31753
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31753
31754
  dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
31754
- if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
31755
+ if (frame.event.type === "message.created") {
31755
31756
  const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31756
31757
  const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
31757
31758
  if (message.success && requested.success && message.data.id !== undefined) {
@@ -31762,7 +31763,9 @@ function renderInbound(raw, sessionID, subject) {
31762
31763
  issueKey: frame.event.issue_key,
31763
31764
  body: message.data.body ?? envelope.payload_summary ?? ""
31764
31765
  };
31765
- dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31766
+ if (frame.event.issue_key !== null) {
31767
+ dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31768
+ }
31766
31769
  }
31767
31770
  }
31768
31771
  dispatchEvent = {
@@ -32833,12 +32836,23 @@ async function executeDispatchTool(input) {
32833
32836
  if (!input.config.enabled || !configUrl || !configToken) {
32834
32837
  throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
32835
32838
  }
32839
+ const baseFetch = input.fetchImpl ?? fetch;
32840
+ const fetchImpl = Object.assign(async (...args) => {
32841
+ try {
32842
+ return await baseFetch(...args);
32843
+ } catch (error) {
32844
+ if (error instanceof TypeError) {
32845
+ throw new Error(`Dispatch at ${configUrl} is unreachable: ${error.message}. If the Dispatch URL changed, restart this agent process so it picks up the new configuration.`);
32846
+ }
32847
+ throw error;
32848
+ }
32849
+ }, { preconnect: baseFetch.preconnect });
32836
32850
  if (input.tool === "dispatch_open_asks") {
32837
32851
  const sessionId = input.sessionId?.trim();
32838
32852
  if (!sessionId)
32839
32853
  throw new Error("host session id is required for dispatch_open_asks");
32840
32854
  toolSchema(input.tool).parse(input.args);
32841
- const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
32855
+ const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
32842
32856
  const response = await client.openAsks(sessionId);
32843
32857
  return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
32844
32858
  }
@@ -32847,7 +32861,7 @@ async function executeDispatchTool(input) {
32847
32861
  const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
32848
32862
  const args = toolSchema(input.tool).parse(ownerArguments.args);
32849
32863
  const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
32850
- const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
32864
+ const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
32851
32865
  const owner = ownerArguments.owner?.kind === "issue" ? {
32852
32866
  kind: "issue",
32853
32867
  issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
@@ -33034,17 +33048,18 @@ async function executeDispatchTool(input) {
33034
33048
  }
33035
33049
  case "dispatch_message": {
33036
33050
  const inReplyTo = messageInReplyTo(args);
33037
- const message = await client.message(issue(), {
33051
+ const issueKey = issue();
33052
+ const message = await client.message(issueKey, {
33038
33053
  body: stringArg(args, "body"),
33039
33054
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
33040
33055
  actor
33041
33056
  });
33042
- const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
33057
+ const messageRef = `dispatch://${issueKey}/message/${message.id}`;
33043
33058
  return {
33044
33059
  text: `Posted message ${message.id} (${messageRef})`,
33045
33060
  details: {
33046
- issue: message.issue_key,
33047
- topic: dispatchIssueSubject(message.issue_key, ">"),
33061
+ issue: issueKey,
33062
+ topic: dispatchIssueSubject(issueKey, ">"),
33048
33063
  message: message.id
33049
33064
  }
33050
33065
  };
@@ -33678,59 +33693,7 @@ async function copySessionID(copyToClipboard, sessionID) {
33678
33693
  var codec2 = import_nats.StringCodec();
33679
33694
  var NATS_RETRY_INTERVAL_MS = 15000;
33680
33695
  var ROLE_CLAIM_ENTRY = "envoy-role-claim";
33681
- var ASK_AWARENESS_ENTRY = "dispatch-ask-awareness";
33682
- var LEGION_MANAGED_ENTRY = "legion-managed-session";
33683
33696
  var OPEN_ASKS_TIMEOUT_MS = 3000;
33684
- var OPEN_ASKS_REMINDER = "You have no unanswered asks in Dispatch. If you are waiting for human input, open an ask. Otherwise ignore this reminder and continue with any remaining work. Do not reply just to acknowledge this reminder.";
33685
- function isAskAwarenessEntry(entry) {
33686
- if (typeof entry !== "object" || entry === null)
33687
- return false;
33688
- if (!("type" in entry) || entry.type !== "custom")
33689
- return false;
33690
- if (!("customType" in entry) || entry.customType !== ASK_AWARENESS_ENTRY)
33691
- return false;
33692
- if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
33693
- return false;
33694
- const data = entry.data;
33695
- return "session_id" in data && typeof data.session_id === "string" && "period" in data && typeof data.period === "number" && "baseline_as_of" in data && (typeof data.baseline_as_of === "string" || data.baseline_as_of === null) && "checked" in data && typeof data.checked === "boolean" && "fired" in data && typeof data.fired === "boolean" && "saw_ask" in data && typeof data.saw_ask === "boolean";
33696
- }
33697
- function isLegionManagedEntry(entry) {
33698
- if (typeof entry !== "object" || entry === null)
33699
- return false;
33700
- if (!("type" in entry) || entry.type !== "custom")
33701
- return false;
33702
- if (!("customType" in entry) || entry.customType !== LEGION_MANAGED_ENTRY)
33703
- return false;
33704
- if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
33705
- return false;
33706
- return "session_id" in entry.data && typeof entry.data.session_id === "string";
33707
- }
33708
- function emptyAskAwareness(sessionID) {
33709
- return {
33710
- session_id: sessionID,
33711
- period: 0,
33712
- baseline_as_of: null,
33713
- checked: false,
33714
- fired: false,
33715
- saw_ask: false
33716
- };
33717
- }
33718
- function transcriptAskAwareness(branch, sessionID) {
33719
- let state;
33720
- for (const entry of branch) {
33721
- if (isAskAwarenessEntry(entry) && entry.data.session_id === sessionID)
33722
- state = entry.data;
33723
- }
33724
- return state;
33725
- }
33726
- function transcriptMarksLegionSession(branch, sessionID) {
33727
- return branch.some((entry) => isLegionManagedEntry(entry) && entry.data.session_id === sessionID);
33728
- }
33729
- function isHumanTurnInitiator(message) {
33730
- if (message.attribution !== "user")
33731
- return false;
33732
- return message.role === "user" || message.role === "custom" && (message.customType === "skill-prompt" || message.customType === "collab-prompt");
33733
- }
33734
33697
  function isRoleClaimEntry(entry) {
33735
33698
  if (typeof entry !== "object" || entry === null)
33736
33699
  return false;
@@ -33750,15 +33713,13 @@ function legionRoleClaimBridge() {
33750
33713
  let bridge = store[LEGION_ROLE_CLAIM_BRIDGE];
33751
33714
  if (bridge === undefined) {
33752
33715
  const ready = Promise.withResolvers();
33753
- bridge = { ready, managedSessions: new Set };
33716
+ bridge = { ready };
33754
33717
  store[LEGION_ROLE_CLAIM_BRIDGE] = bridge;
33755
33718
  }
33756
33719
  return bridge;
33757
33720
  }
33758
33721
  async function claimEnvoyRole(sessionID, role, context) {
33759
33722
  const bridge = legionRoleClaimBridge();
33760
- bridge.managedSessions.add(sessionID);
33761
- bridge.markManagedSession?.(sessionID, context);
33762
33723
  const claim = bridge.claim ?? await bridge.ready.promise;
33763
33724
  await claim(sessionID, role, context);
33764
33725
  }
@@ -33807,87 +33768,26 @@ function envoyExtension(pi) {
33807
33768
  let claimedRoleTopic;
33808
33769
  let activeSessionContext;
33809
33770
  const inbox = [];
33810
- let askAwareness = emptyAskAwareness("");
33811
- let candidateBaseline;
33812
- let awarenessGeneration = 0;
33813
- let availabilityWarningPeriod;
33814
- let agentRunning = false;
33815
- let agentWillContinue = false;
33816
- let legionManagedSessionID = "";
33817
- let terminalInputUnsubscribe;
33818
- const persistAskAwareness = (state) => {
33819
- askAwareness = state;
33820
- pi.appendEntry(ASK_AWARENESS_ENTRY, state);
33821
- };
33771
+ const availabilityWarningSessionIDs = new Set;
33822
33772
  const warnAskAvailability = (context, error) => {
33823
- const key = `${askAwareness.session_id}:${askAwareness.period}`;
33824
- if (availabilityWarningPeriod === key)
33773
+ const sessionID = context.sessionManager.getSessionId();
33774
+ if (availabilityWarningSessionIDs.has(sessionID))
33825
33775
  return;
33826
- availabilityWarningPeriod = key;
33827
- context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)}); reminder skipped`, "warning");
33776
+ availabilityWarningSessionIDs.add(sessionID);
33777
+ context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error)}); summary unavailable`, "warning");
33828
33778
  };
33829
- const queryOpenAsks = async (requestedSessionID, since, signal) => {
33779
+ const queryOpenAsks = async (requestedSessionID) => {
33830
33780
  const config = activeDispatchConfig();
33831
33781
  if (config === null)
33832
33782
  return null;
33833
- const deadline = AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS);
33834
- const requestSignal = signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
33835
- return new DispatchClient(config.url, config.token, fetch, requestSignal).openAsks(requestedSessionID, since);
33836
- };
33837
- const setReminderBaseline = (requestedSessionID, period, asOf) => {
33838
- candidateBaseline = { sessionID: requestedSessionID, asOf };
33839
- if (askAwareness.session_id !== requestedSessionID || askAwareness.period !== period || askAwareness.checked || askAwareness.fired || askAwareness.baseline_as_of !== null) {
33840
- return;
33841
- }
33842
- persistAskAwareness({ ...askAwareness, baseline_as_of: asOf });
33843
- };
33844
- const refreshReminderBaseline = async (requestedSessionID, period, context) => {
33845
- try {
33846
- const snapshot = await queryOpenAsks(requestedSessionID);
33847
- if (snapshot === null)
33848
- return;
33849
- setReminderBaseline(requestedSessionID, period, snapshot.as_of);
33850
- } catch (error) {
33851
- warnAskAvailability(context, error);
33852
- }
33853
- };
33854
- const armAskReminder = async (context, refreshBaseline = false) => {
33855
- const id = context.sessionManager.getSessionId();
33856
- if (id === "" || activeDispatchConfig() === null)
33857
- return;
33858
- awarenessGeneration++;
33859
- availabilityWarningPeriod = undefined;
33860
- const baseline = !agentRunning && !refreshBaseline && candidateBaseline !== undefined && candidateBaseline.sessionID === id ? candidateBaseline.asOf : null;
33861
- const previousPeriod = askAwareness.session_id === id ? askAwareness.period : 0;
33862
- persistAskAwareness({
33863
- session_id: id,
33864
- period: previousPeriod + 1,
33865
- baseline_as_of: baseline,
33866
- checked: false,
33867
- fired: false,
33868
- saw_ask: false
33869
- });
33870
- if (agentRunning || refreshBaseline) {
33871
- await refreshReminderBaseline(id, askAwareness.period, context);
33872
- }
33873
- };
33874
- const cancelPendingAskReminder = () => {
33875
- awarenessGeneration++;
33876
- };
33877
- const installTerminalInputCancellation = (context) => {
33878
- terminalInputUnsubscribe?.();
33879
- terminalInputUnsubscribe = context.ui.onTerminalInput(cancelPendingAskReminder);
33783
+ const snapshot = await new DispatchClient(config.url, config.token, fetch, AbortSignal.timeout(OPEN_ASKS_TIMEOUT_MS)).openAsks(requestedSessionID);
33784
+ availabilityWarningSessionIDs.delete(requestedSessionID);
33785
+ return snapshot;
33880
33786
  };
33881
33787
  const restoreLocalSessionState = (context) => {
33882
33788
  sessionDirectory = context.cwd;
33883
33789
  sessionID = context.sessionManager.getSessionId();
33884
33790
  activeSessionContext = context;
33885
- candidateBaseline = undefined;
33886
- const branch = context.sessionManager.getBranch?.() ?? [];
33887
- askAwareness = transcriptAskAwareness(branch, sessionID) ?? emptyAskAwareness(sessionID);
33888
- const persistedLegionSession = transcriptMarksLegionSession(branch, sessionID);
33889
- const managed = persistedLegionSession || legionRoleClaimBridge().managedSessions.has(sessionID);
33890
- legionManagedSessionID = managed ? sessionID : "";
33891
33791
  };
33892
33792
  pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
33893
33793
  registerEnvoyMessageRenderer(pi);
@@ -33928,13 +33828,6 @@ function envoyExtension(pi) {
33928
33828
  const dedupeKey = rendered.envelope?.dedupe_key;
33929
33829
  const duplicate = dedupeKey !== undefined && dedupeKeys.has(dedupeKey);
33930
33830
  if (!duplicate && !rendered.skip) {
33931
- if (rendered.dispatchActor?.kind === "user" && activeSessionContext !== undefined) {
33932
- try {
33933
- await armAskReminder(activeSessionContext, true);
33934
- } catch (error) {
33935
- warnAskAvailability(activeSessionContext, error);
33936
- }
33937
- }
33938
33831
  const envelope = rendered.envelope;
33939
33832
  if (envelope !== undefined) {
33940
33833
  inbox.unshift({
@@ -34254,17 +34147,6 @@ function envoyExtension(pi) {
34254
34147
  return run;
34255
34148
  };
34256
34149
  const bridge = legionRoleClaimBridge();
34257
- bridge.markManagedSession = (targetSessionID, context) => {
34258
- bridge.managedSessions.add(targetSessionID);
34259
- if (targetSessionID !== sessionID)
34260
- return;
34261
- if (legionManagedSessionID === targetSessionID)
34262
- return;
34263
- legionManagedSessionID = targetSessionID;
34264
- if (context !== undefined || activeSessionContext !== undefined) {
34265
- pi.appendEntry(LEGION_MANAGED_ENTRY, { session_id: targetSessionID });
34266
- }
34267
- };
34268
34150
  const claim = async (targetSessionID, role, callerContext) => {
34269
34151
  const context = callerContext ?? activeSessionContext;
34270
34152
  if (context === undefined || context.sessionManager.getSessionId() !== targetSessionID) {
@@ -34278,7 +34160,6 @@ function envoyExtension(pi) {
34278
34160
  bridge.claim = claim;
34279
34161
  bridge.ready.resolve(claim);
34280
34162
  pi.on("session_start", async (_event, context) => {
34281
- installTerminalInputCancellation(context);
34282
34163
  const previousSessionID = sessionID;
34283
34164
  restoreLocalSessionState(context);
34284
34165
  if (dispatchConfig.error !== null) {
@@ -34314,7 +34195,6 @@ function envoyExtension(pi) {
34314
34195
  }
34315
34196
  });
34316
34197
  const rebind = async (reason, context) => {
34317
- installTerminalInputCancellation(context);
34318
34198
  const previousID = sessionID;
34319
34199
  restoreLocalSessionState(context);
34320
34200
  if (defaults.natsUrls.length === 0)
@@ -34364,8 +34244,6 @@ function envoyExtension(pi) {
34364
34244
  ]);
34365
34245
  } finally {
34366
34246
  clearTimeout(timer);
34367
- terminalInputUnsubscribe?.();
34368
- terminalInputUnsubscribe = undefined;
34369
34247
  connection = undefined;
34370
34248
  activeSessionContext = undefined;
34371
34249
  subscriptions.clear();
@@ -34419,7 +34297,6 @@ function envoyExtension(pi) {
34419
34297
  const snapshot = await queryOpenAsks(id);
34420
34298
  if (snapshot === null)
34421
34299
  return;
34422
- setReminderBaseline(id, askAwareness.session_id === id ? askAwareness.period : 0, snapshot.as_of);
34423
34300
  return {
34424
34301
  message: {
34425
34302
  customType: "dispatch-open-asks",
@@ -34441,60 +34318,7 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
34441
34318
  };
34442
34319
  }
34443
34320
  });
34444
- pi.on("agent_start", async () => {
34445
- agentRunning = true;
34446
- agentWillContinue = false;
34447
- });
34448
- pi.on("agent_end", async (event) => {
34449
- agentRunning = false;
34450
- agentWillContinue = event.willContinue === true;
34451
- });
34452
- pi.on("input", async () => {
34453
- cancelPendingAskReminder();
34454
- });
34455
- pi.on("message_start", async (event, context) => {
34456
- if (!isHumanTurnInitiator(event.message))
34457
- return;
34458
- try {
34459
- await armAskReminder(context);
34460
- } catch (error) {
34461
- warnAskAvailability(context, error);
34462
- }
34463
- });
34464
- pi.on("session_stop", async (event, context) => {
34465
- if (event.stop_hook_active || event.signal.aborted || shuttingDown || agentWillContinue || event.last_assistant_message?.role !== "assistant" || event.session_id !== sessionID || legionManagedSessionID === event.session_id || askAwareness.session_id !== event.session_id || askAwareness.period === 0 || askAwareness.checked || askAwareness.fired) {
34466
- return;
34467
- }
34468
- const baseline = askAwareness.baseline_as_of;
34469
- if (baseline === null)
34470
- return;
34471
- if (context.ui.getEditorText().trim() !== "")
34472
- return;
34473
- const claimed = { ...askAwareness, checked: true };
34474
- const generation = awarenessGeneration;
34475
- persistAskAwareness(claimed);
34476
- let snapshot;
34477
- try {
34478
- snapshot = await queryOpenAsks(event.session_id, baseline, event.signal);
34479
- } catch (error) {
34480
- if (!event.signal.aborted && generation === awarenessGeneration) {
34481
- warnAskAvailability(context, error);
34482
- }
34483
- return;
34484
- }
34485
- if (snapshot === null || event.signal.aborted || generation !== awarenessGeneration || shuttingDown || agentWillContinue || context.sessionManager.getSessionId() !== event.session_id || sessionID !== event.session_id || legionManagedSessionID === event.session_id || context.ui.getEditorText().trim() !== "" || askAwareness.session_id !== claimed.session_id || askAwareness.period !== claimed.period || !askAwareness.checked) {
34486
- return;
34487
- }
34488
- if (snapshot.count > 0 || snapshot.opened_since || askAwareness.saw_ask) {
34489
- return;
34490
- }
34491
- persistAskAwareness({ ...askAwareness, fired: true });
34492
- return { continue: true, additionalContext: OPEN_ASKS_REMINDER };
34493
- });
34494
34321
  pi.on("tool_result", async (event) => {
34495
- if (!event.isError && event.toolName === "dispatch_ask" && typeof event.details === "object" && event.details !== null && "ask" in event.details && typeof event.details.ask === "string" && askAwareness.session_id === sessionID && askAwareness.period > 0 && !askAwareness.saw_ask) {
34496
- persistAskAwareness({ ...askAwareness, saw_ask: true });
34497
- }
34498
34322
  if (event.isError)
34499
34323
  return;
34500
34324
  const topic = dispatchSubscriptionTopic(event.details);
@@ -45,7 +45,7 @@ before step 3. The design gate is not a substitute for review and retro.
45
45
 
46
46
  1. Re-read the issue, its acceptance criteria, the PR, test evidence, and review evidence.
47
47
  Confirm the PR carries both proofs: the implementer's own `E2E (implementer)` line and the tester's `E2E (tester)` line,
48
- each naming a production-like surface (a scratch daemon, a smoke rig, a sandbox repository, a devN
48
+ each naming a production-like surface (the daemon's test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the operator's next daemon restart, recorded on the PR; a sandbox repository, a devN
49
49
  stack, staging, or a local stack with real migrations), a command or run id, an observation, a head
50
50
  SHA, and a negative control. If either is missing, or links only a unit suite, the retro's first
51
51
  durable learning is that gap and the issue goes back — to the implementer for its own proof, to the
@@ -251,7 +251,7 @@ PR opens, and every later phase keeps it current rather than replacing it:
251
251
  ```
252
252
  ## Verification
253
253
 
254
- **CI:** `Tests` run <run-id> — jobs lint, pr-title, typecheck, test all success at <head-sha>.
254
+ **CI:** `Tests` run <run-id> — jobs lint, typecheck, test all success at <head-sha>; `PR Title` run <run-id> — job pr-title success at <head-sha>.
255
255
 
256
256
  **Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
257
257
  - Thread <id>: fixed in <commit-sha> — <one line>.
@@ -276,6 +276,7 @@ Verified the implementer's proof by <re-running its command | driving the same s
276
276
  **Fast-follow:** <one named cleanup item and where it will land>, or "none".
277
277
 
278
278
  **Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
279
+ **Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, rebase onto the new base and push — the new head runs Tests against the new merge result — and cite that run in the PR body.
279
280
  ```
280
281
 
281
282
  - **Threads are dispositioned individually, never resolved in bulk.** Every open review
@@ -301,11 +302,11 @@ Verified the implementer's proof by <re-running its command | driving the same s
301
302
  changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
302
303
  Findings about naming, duplication, or wording are batched into the single `Fast-follow`
303
304
  line instead of iterating per push.
304
- - **Rebase only on a real conflict.** Sami, 2026-09-11, verbatim:
305
+ - **Rebase only on a real conflict, except after a base retarget.** Sami, 2026-09-11, verbatim:
305
306
  "Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
306
- The implementer rebases the issue branch only when GitHub reports it `CONFLICTING` or the
307
- controller asks because of a conflict — never to pick up `main` or to refresh CI. A single
308
- failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
307
+ The implementer rebases the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
308
+ because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never rebase to
309
+ pick up `main` or refresh CI. A single failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
309
310
  pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
310
311
  confirmation, not a new round (see *The unchanged-diff check* below). Before rebasing, record
311
312
  the fingerprint at the current tip; after pushing the rebased branch, record it at the new
@@ -317,9 +318,10 @@ Verified the implementer's proof by <re-running its command | driving the same s
317
318
  field names naming, duplication, or wording cleanup only; anything that changes behaviour,
318
319
  hides an error, or breaks a gate lands in this PR.
319
320
  - **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
320
- The proof is the changed behaviour exercised on the surface a user reaches it through — a
321
- scratch daemon, a smoke rig, a sandbox repository, a real browser, a devN stack, a local stack
322
- with real migrations — with the exact command or run id, what was observed, the head SHA, and
321
+ The proof is the changed behaviour exercised on the surface a user reaches it through — the daemon's
322
+ test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the
323
+ operator's next daemon restart, recorded on the PR; a sandbox repository, a real browser, a devN stack,
324
+ or a local stack with real migrations — with the exact command or run id, what was observed, the head SHA,
323
325
  one negative control. The same proof goes into `.legion/implement.json` as its required `proof`
324
326
  array (`legion handoff write --phase implement` refuses a payload without one and names the
325
327
  field), and into the PR body, because the reviewer and the merger verify facts on GitHub and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.27.1",
3
+ "version": "1.28.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [