@sjawhar/claude-envoy 1.12.1 → 1.14.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.
@@ -2,5 +2,5 @@
2
2
  "name": "claude-envoy",
3
3
  "description": "Deliver Envoy events as Claude Code channel notifications and expose Envoy MCP tools.",
4
4
  "mcpServers": "./.mcp.json",
5
- "version": "1.12.1"
5
+ "version": "1.14.0"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-envoy",
3
- "version": "1.12.1",
3
+ "version": "1.14.0",
4
4
  "description": "Deliver Envoy events as Claude Code channel notifications and expose Envoy MCP tools.",
5
5
  "mcpServers": {}
6
6
  }
package/dist/dispatch.js CHANGED
@@ -14662,7 +14662,8 @@ var IssueEventPayloadSchema = object({
14662
14662
  route: string2().nullish(),
14663
14663
  changed: array(string2()).optional(),
14664
14664
  ancestors: array(string2()).optional(),
14665
- thread_logins: array(string2()).optional()
14665
+ thread_logins: array(string2()).optional(),
14666
+ forced: object({ past: array(string2()) }).optional()
14666
14667
  });
14667
14668
  var ArtifactCreatedEventPayloadSchema = object({
14668
14669
  artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
@@ -15615,12 +15616,12 @@ var dispatchToolSpecs = [
15615
15616
  {
15616
15617
  name: "dispatch_related",
15617
15618
  example: { project: "CORE", title: "Retry failed webhook deliveries" },
15618
- description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issue leaves that issue's own items out, " + "as a check on that issue's own write does; keywords_only skips meaning search.",
15619
+ description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issues leaves each named issue's own " + "items out of the results, at most 50 of them, so a draft under a parent can leave out " + "that parent and its children without changing how rare the text's words are; " + "keywords_only skips meaning search.",
15619
15620
  arguments: (z) => ({
15620
15621
  project: z.string().describe("Project key to search within, such as CORE."),
15621
15622
  title: z.string({ max: ISSUE_TITLE_MAX }).describe("Optional title to check, as an issue title; at most 1,000 characters.").optional(),
15622
15623
  text: z.string().describe("Optional text to check, such as a draft spec or a question.").optional(),
15623
- exclude_issue: z.string().describe("Optional issue key in the project whose own items are left out.").optional(),
15624
+ exclude_issues: z.array(z.string()).describe("Optional issue keys in the project whose own items are left out of the results; at most 50.").optional(),
15624
15625
  keywords_only: z.boolean().describe("Search by keywords only, skipping meaning search.").optional()
15625
15626
  }),
15626
15627
  validation: {
@@ -17565,9 +17566,12 @@ async function tryExec(exec, file, args, cwd, limitMs) {
17565
17566
  exec(file, args, { cwd, signal: controller.signal }),
17566
17567
  expired.promise
17567
17568
  ]);
17568
- return answer?.stdout ?? null;
17569
+ if (answer === null) {
17570
+ return { failure: "timedOut", ok: false };
17571
+ }
17572
+ return { ok: true, stdout: answer.stdout };
17569
17573
  } catch {
17570
- return null;
17574
+ return { failure: "refused", ok: false };
17571
17575
  } finally {
17572
17576
  clearTimeout(timer);
17573
17577
  }
@@ -17608,13 +17612,13 @@ function parseGitHubRemoteUrl(url) {
17608
17612
  return null;
17609
17613
  }
17610
17614
  async function resolveCwdRepo(cwd, exec) {
17611
- const jjOutput = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
17612
- if (jjOutput !== null) {
17613
- const url = selectRemoteUrl(parseRemoteList(jjOutput));
17615
+ const remoteList = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
17616
+ if (remoteList.ok) {
17617
+ const url = selectRemoteUrl(parseRemoteList(remoteList.stdout));
17614
17618
  return url ? parseGitHubRemoteUrl(url) : null;
17615
17619
  }
17616
- const originUrl = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
17617
- return originUrl ? parseGitHubRemoteUrl(originUrl) : null;
17620
+ const originRemote = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
17621
+ return originRemote.ok ? parseGitHubRemoteUrl(originRemote.stdout) : null;
17618
17622
  }
17619
17623
  async function resolveOrigin(env, exec, cwd) {
17620
17624
  const origin = {
@@ -17623,8 +17627,8 @@ async function resolveOrigin(env, exec, cwd) {
17623
17627
  };
17624
17628
  const pane = env.TMUX_PANE;
17625
17629
  if (pane) {
17626
- const output = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
17627
- const [target, paneId] = output?.trim().split(" ") ?? [];
17630
+ const answer = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
17631
+ const [target, paneId] = answer.ok ? answer.stdout.trim().split(" ") : [];
17628
17632
  if (target)
17629
17633
  origin.tmux = target;
17630
17634
  if (paneId)
@@ -20540,11 +20544,19 @@ function relatedCheckLines(suggestions, context) {
20540
20544
  lines.push(`A past decision may already answer this: ${suggestionLabel(decision)}, answered by ${decision.answered_by}${when} \u2192 ${absoluteHref(decision.href, context.configUrl)}`);
20541
20545
  lines.push(...askLines(decision.ask));
20542
20546
  }
20547
+ const likely = suggestions.likely_duplicate;
20548
+ if (likely !== undefined && likely !== null) {
20549
+ lines.push(`Likely duplicate: ${likely.key} [${likely.status}] ${likely.title} \u2192 ${absoluteHref(likely.href, context.configUrl)}`);
20550
+ lines.push("Read it. If it is the same work, fold this issue into it by the duplicate rule of skill://dispatch-first.");
20551
+ }
20552
+ if (suggestions.near_copy_check !== undefined && suggestions.near_copy_check !== "") {
20553
+ lines.push(`No near-copy check was made: ${suggestions.near_copy_check}.`);
20554
+ }
20543
20555
  for (const match of suggestions.title_matches ?? []) {
20544
- lines.push(`Title overlaps you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
20556
+ lines.push(`Near copy you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
20545
20557
  }
20546
20558
  } catch (error) {
20547
- lines.push(`The check's past decision or title overlaps could not be shown: ${messageFor(error)}`);
20559
+ lines.push(`The check's past decision or near copies could not be shown: ${messageFor(error)}`);
20548
20560
  }
20549
20561
  return lines;
20550
20562
  }
@@ -22427,7 +22439,13 @@ function eventHead(event) {
22427
22439
  return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
22428
22440
  case "artifact.human_edited":
22429
22441
  return textHead(humanEditHead(event.payload));
22430
- case "issue.created":
22442
+ case "issue.created": {
22443
+ const forced = event.payload.forced;
22444
+ const head = `status ${event.payload.status}`;
22445
+ if (forced === undefined)
22446
+ return head;
22447
+ return forced.past.length === 0 ? `${head} \xB7 forced, the near-copy check did not answer` : `${head} \xB7 forced past ${forced.past.join(", ")}`;
22448
+ }
22431
22449
  case "issue.updated":
22432
22450
  case "issue.closed":
22433
22451
  return `status ${event.payload.status}`;
@@ -22443,10 +22461,14 @@ function eventRef(event) {
22443
22461
  const kind = event.type.startsWith("message.") ? "message" : event.type.startsWith("comment.") || event.type.startsWith("suggestion.") ? "comment" : event.type.startsWith("ask.") ? "ask" : undefined;
22444
22462
  return kind === undefined ? undefined : `dispatch://${event.issue_key}/${kind}/${id}`;
22445
22463
  }
22464
+ function eventActorText(actor) {
22465
+ const title = actor.kind === "session" ? actor.origin?.session_title : undefined;
22466
+ return title === undefined || title === "" ? actorText(actor) : `${actorText(actor)} (${title})`;
22467
+ }
22446
22468
  function eventLine(event) {
22447
22469
  const head = eventHead(event);
22448
22470
  const ref = eventRef(event);
22449
- return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${actorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
22471
+ return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${eventActorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
22450
22472
  }
22451
22473
  function childrenSummary(issue) {
22452
22474
  return [
@@ -23263,15 +23285,17 @@ async function executeDispatchTool(input) {
23263
23285
  const candidates = duplicateCandidates(error);
23264
23286
  return {
23265
23287
  text: [
23266
- `Not created: "${title}" looks like a duplicate.`,
23288
+ `Not created: "${title}" is a near copy of an open issue.`,
23267
23289
  ...candidates.map((candidate) => {
23268
23290
  const href = new URL(candidate.href, linkBase).toString();
23269
23291
  return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
23270
23292
  }),
23271
- "Reference the existing issue, or run dispatch issue again with --force after reading it."
23293
+ "Read each issue above. If one is the same work, fold this into it by the duplicate rule of skill://dispatch-first.",
23294
+ "If none is, run dispatch issue again with --force, and write in the new issue's spec why it is not each issue named here."
23272
23295
  ].join(`
23273
23296
  `),
23274
- details: { duplicates: candidates }
23297
+ details: { duplicates: candidates },
23298
+ refused: true
23275
23299
  };
23276
23300
  }
23277
23301
  }
@@ -23439,12 +23463,12 @@ async function executeDispatchTool(input) {
23439
23463
  const project = stringArg(args, "project");
23440
23464
  const title = optionalString(args, "title");
23441
23465
  const text = optionalString(args, "text");
23442
- const excludeIssue = optionalString(args, "exclude_issue");
23466
+ const excludeIssues = optionalStringArray(args, "exclude_issues");
23443
23467
  const keywordsOnly = optionalBoolean(args, "keywords_only");
23444
23468
  const suggestions = await client.related(project, {
23445
23469
  ...title === undefined ? {} : { title },
23446
23470
  ...text === undefined ? {} : { text },
23447
- ...excludeIssue === undefined ? {} : { exclude_issue: excludeIssue },
23471
+ ...excludeIssues === undefined ? {} : { exclude_issues: excludeIssues },
23448
23472
  ...keywordsOnly === undefined ? {} : { keywords_only: keywordsOnly }
23449
23473
  });
23450
23474
  return {
@@ -27532,6 +27556,7 @@ function dispatchTarget(env, config) {
27532
27556
  var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
27533
27557
  var DEFAULT_HEARTBEAT_MS = SESSION_HEARTBEAT_MS;
27534
27558
  var MIN_PASS_DEADLINE_MS = 30000;
27559
+ var DEFAULT_TMUX_PLACE_CACHE_MS = 30000;
27535
27560
  var MIN_INTERVAL_MS = 25;
27536
27561
  var MAX_INTERVAL_MS = 2147483647;
27537
27562
  var REGISTRATION_SETTLE_MS = 5000;
@@ -27545,6 +27570,10 @@ function namedInterval(raw) {
27545
27570
  const parsed = Number(raw);
27546
27571
  return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
27547
27572
  }
27573
+ function resolvedInterval(raw, fallback) {
27574
+ const named = namedInterval(raw);
27575
+ return named === undefined ? fallback : Math.min(Math.max(named, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
27576
+ }
27548
27577
  function natsSeedFromEnvironment(env) {
27549
27578
  const { NATS_NKEY_SEED_FILE: file, NATS_NKEY_SEED: plain } = env;
27550
27579
  if (file !== undefined) {
@@ -27635,8 +27664,8 @@ function envoySettings(env, config, options = {}) {
27635
27664
  inbound = { off: messageFor(error) };
27636
27665
  }
27637
27666
  }
27638
- const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
27639
- const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
27667
+ const heartbeatMs = resolvedInterval(env.ENVOY_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
27668
+ const tmuxPlaceCacheMs = resolvedInterval(env.ENVOY_TMUX_PLACE_CACHE_MS, DEFAULT_TMUX_PLACE_CACHE_MS);
27640
27669
  const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
27641
27670
  return {
27642
27671
  enabled: true,
@@ -27644,6 +27673,7 @@ function envoySettings(env, config, options = {}) {
27644
27673
  credential,
27645
27674
  inbound,
27646
27675
  heartbeatMs,
27676
+ tmuxPlaceCacheMs,
27647
27677
  passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
27648
27678
  passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
27649
27679
  };
@@ -28063,6 +28093,7 @@ function createEnvoyClient(config) {
28063
28093
  ...input.delivery === undefined ? {} : { delivery: input.delivery },
28064
28094
  ...input.clientInstanceID === undefined ? {} : { client_instance_id: input.clientInstanceID },
28065
28095
  host: machineID(),
28096
+ ...input.place === undefined ? {} : { tmux_place: input.place },
28066
28097
  plugin: input.plugin.name,
28067
28098
  plugin_version: input.plugin.version
28068
28099
  }, { actingSession: input.sessionID }))),
@@ -28283,7 +28314,7 @@ async function runDispatchCli(argv, rawEnv, io) {
28283
28314
  if (parsed.kind === "refused") {
28284
28315
  return parsed.tool === undefined ? refuse([`dispatch: ${parsed.problems.join(`
28285
28316
  `)}`, "Run dispatch --help."].join(`
28286
- `), USAGE) : refuse(new ToolInputError(parsed.tool, parsed.problems).message, REFUSED, parsed.tool);
28317
+ `), USAGE) : refuse(new ToolInputError(parsed.tool, parsed.problems).message, USAGE, parsed.tool);
28287
28318
  }
28288
28319
  const hostName = env.DISPATCH_HOST ?? (env.CLAUDE_CODE_CHILD_SESSION === "1" ? "claude" : undefined);
28289
28320
  const host = HOSTS.find((name) => name === hostName);
@@ -28311,7 +28342,7 @@ async function runDispatchCli(argv, rawEnv, io) {
28311
28342
  serverUrls: settings?.enabled === true ? dispatchServerUrls(settings) : []
28312
28343
  });
28313
28344
  if (problems.length > 0) {
28314
- return refuse(new ToolInputError(parsed.tool, problems).message, REFUSED, parsed.tool);
28345
+ return refuse(new ToolInputError(parsed.tool, problems).message, USAGE, parsed.tool);
28315
28346
  }
28316
28347
  print(JSON.stringify(parsed.args, null, 2));
28317
28348
  return OK;
@@ -28448,7 +28479,7 @@ async function runDispatchCli(argv, rawEnv, io) {
28448
28479
  notes.push(followNoticeText(followed, await subscriptions()));
28449
28480
  }
28450
28481
  entry = { tool, details: result.details };
28451
- code = OK;
28482
+ code = result.refused === true ? REFUSED : OK;
28452
28483
  } catch (error) {
28453
28484
  text = messageFor(error);
28454
28485
  if (error instanceof DispatchWriteLandedError) {
@@ -28456,7 +28487,7 @@ async function runDispatchCli(argv, rawEnv, io) {
28456
28487
  code = OK;
28457
28488
  } else {
28458
28489
  entry = { tool, error: text };
28459
- code = REFUSED;
28490
+ code = error instanceof ToolInputError ? USAGE : REFUSED;
28460
28491
  }
28461
28492
  }
28462
28493
  try {
@@ -36311,7 +36311,8 @@ var IssueEventPayloadSchema = object({
36311
36311
  route: string2().nullish(),
36312
36312
  changed: array(string2()).optional(),
36313
36313
  ancestors: array(string2()).optional(),
36314
- thread_logins: array(string2()).optional()
36314
+ thread_logins: array(string2()).optional(),
36315
+ forced: object({ past: array(string2()) }).optional()
36315
36316
  });
36316
36317
  var ArtifactCreatedEventPayloadSchema = object({
36317
36318
  artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
@@ -37264,12 +37265,12 @@ var dispatchToolSpecs = [
37264
37265
  {
37265
37266
  name: "dispatch_related",
37266
37267
  example: { project: "CORE", title: "Retry failed webhook deliveries" },
37267
- description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issue leaves that issue's own items out, " + "as a check on that issue's own write does; keywords_only skips meaning search.",
37268
+ description: "Run the related-item check that dispatch issue, dispatch ask and a new decision block in " + "dispatch doc-edit run on their own write, and write nothing: it lists the three items of " + "one project most like a title and a text (at most one per issue or document, open ones " + "first), each ask with whether it is answered, and a past decision that already answers " + "the same question. Use it to check a draft before filing it. It searches by meaning and by " + "the text's rarest words, and says how it searched; when meaning search cannot run it " + "searches by keywords only and says why. When it could not search at all it says why and " + "names the dispatch search to run instead. exclude_issues leaves each named issue's own " + "items out of the results, at most 50 of them, so a draft under a parent can leave out " + "that parent and its children without changing how rare the text's words are; " + "keywords_only skips meaning search.",
37268
37269
  arguments: (z) => ({
37269
37270
  project: z.string().describe("Project key to search within, such as CORE."),
37270
37271
  title: z.string({ max: ISSUE_TITLE_MAX }).describe("Optional title to check, as an issue title; at most 1,000 characters.").optional(),
37271
37272
  text: z.string().describe("Optional text to check, such as a draft spec or a question.").optional(),
37272
- exclude_issue: z.string().describe("Optional issue key in the project whose own items are left out.").optional(),
37273
+ exclude_issues: z.array(z.string()).describe("Optional issue keys in the project whose own items are left out of the results; at most 50.").optional(),
37273
37274
  keywords_only: z.boolean().describe("Search by keywords only, skipping meaning search.").optional()
37274
37275
  }),
37275
37276
  validation: {
@@ -44626,6 +44627,7 @@ function fileBearer(bearer, url, section, env, commands = "run") {
44626
44627
  var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
44627
44628
  var DEFAULT_HEARTBEAT_MS = SESSION_HEARTBEAT_MS;
44628
44629
  var MIN_PASS_DEADLINE_MS = 30000;
44630
+ var DEFAULT_TMUX_PLACE_CACHE_MS = 30000;
44629
44631
  var MIN_INTERVAL_MS = 25;
44630
44632
  var MAX_INTERVAL_MS = 2147483647;
44631
44633
  var REGISTRATION_SETTLE_MS = 5000;
@@ -44639,6 +44641,10 @@ function namedInterval(raw) {
44639
44641
  const parsed = Number(raw);
44640
44642
  return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
44641
44643
  }
44644
+ function resolvedInterval(raw, fallback) {
44645
+ const named = namedInterval(raw);
44646
+ return named === undefined ? fallback : Math.min(Math.max(named, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
44647
+ }
44642
44648
  function natsSeedFromEnvironment(env) {
44643
44649
  const { NATS_NKEY_SEED_FILE: file, NATS_NKEY_SEED: plain } = env;
44644
44650
  if (file !== undefined) {
@@ -44729,8 +44735,8 @@ function envoySettings(env, config, options = {}) {
44729
44735
  inbound = { off: messageFor(error) };
44730
44736
  }
44731
44737
  }
44732
- const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
44733
- const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
44738
+ const heartbeatMs = resolvedInterval(env.ENVOY_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
44739
+ const tmuxPlaceCacheMs = resolvedInterval(env.ENVOY_TMUX_PLACE_CACHE_MS, DEFAULT_TMUX_PLACE_CACHE_MS);
44734
44740
  const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
44735
44741
  return {
44736
44742
  enabled: true,
@@ -44738,6 +44744,7 @@ function envoySettings(env, config, options = {}) {
44738
44744
  credential,
44739
44745
  inbound,
44740
44746
  heartbeatMs,
44747
+ tmuxPlaceCacheMs,
44741
44748
  passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
44742
44749
  passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
44743
44750
  };
@@ -45064,6 +45071,7 @@ function createEnvoyClient(config) {
45064
45071
  ...input.delivery === undefined ? {} : { delivery: input.delivery },
45065
45072
  ...input.clientInstanceID === undefined ? {} : { client_instance_id: input.clientInstanceID },
45066
45073
  host: machineID(),
45074
+ ...input.place === undefined ? {} : { tmux_place: input.place },
45067
45075
  plugin: input.plugin.name,
45068
45076
  plugin_version: input.plugin.version
45069
45077
  }, { actingSession: input.sessionID }))),
@@ -46869,9 +46877,12 @@ async function tryExec(exec, file, args, cwd, limitMs) {
46869
46877
  exec(file, args, { cwd, signal: controller.signal }),
46870
46878
  expired.promise
46871
46879
  ]);
46872
- return answer?.stdout ?? null;
46880
+ if (answer === null) {
46881
+ return { failure: "timedOut", ok: false };
46882
+ }
46883
+ return { ok: true, stdout: answer.stdout };
46873
46884
  } catch {
46874
- return null;
46885
+ return { failure: "refused", ok: false };
46875
46886
  } finally {
46876
46887
  clearTimeout(timer);
46877
46888
  }
@@ -46912,13 +46923,13 @@ function parseGitHubRemoteUrl(url) {
46912
46923
  return null;
46913
46924
  }
46914
46925
  async function resolveCwdRepo(cwd, exec) {
46915
- const jjOutput = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
46916
- if (jjOutput !== null) {
46917
- const url = selectRemoteUrl(parseRemoteList(jjOutput));
46926
+ const remoteList = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
46927
+ if (remoteList.ok) {
46928
+ const url = selectRemoteUrl(parseRemoteList(remoteList.stdout));
46918
46929
  return url ? parseGitHubRemoteUrl(url) : null;
46919
46930
  }
46920
- const originUrl = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
46921
- return originUrl ? parseGitHubRemoteUrl(originUrl) : null;
46931
+ const originRemote = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
46932
+ return originRemote.ok ? parseGitHubRemoteUrl(originRemote.stdout) : null;
46922
46933
  }
46923
46934
  async function resolveOrigin(env, exec, cwd) {
46924
46935
  const origin = {
@@ -46927,8 +46938,8 @@ async function resolveOrigin(env, exec, cwd) {
46927
46938
  };
46928
46939
  const pane = env.TMUX_PANE;
46929
46940
  if (pane) {
46930
- const output = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
46931
- const [target, paneId] = output?.trim().split(" ") ?? [];
46941
+ const answer = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
46942
+ const [target, paneId] = answer.ok ? answer.stdout.trim().split(" ") : [];
46932
46943
  if (target)
46933
46944
  origin.tmux = target;
46934
46945
  if (paneId)
@@ -47315,11 +47326,19 @@ function relatedCheckLines(suggestions, context) {
47315
47326
  lines.push(`A past decision may already answer this: ${suggestionLabel(decision)}, answered by ${decision.answered_by}${when} \u2192 ${absoluteHref(decision.href, context.configUrl)}`);
47316
47327
  lines.push(...askLines(decision.ask));
47317
47328
  }
47329
+ const likely = suggestions.likely_duplicate;
47330
+ if (likely !== undefined && likely !== null) {
47331
+ lines.push(`Likely duplicate: ${likely.key} [${likely.status}] ${likely.title} \u2192 ${absoluteHref(likely.href, context.configUrl)}`);
47332
+ lines.push("Read it. If it is the same work, fold this issue into it by the duplicate rule of skill://dispatch-first.");
47333
+ }
47334
+ if (suggestions.near_copy_check !== undefined && suggestions.near_copy_check !== "") {
47335
+ lines.push(`No near-copy check was made: ${suggestions.near_copy_check}.`);
47336
+ }
47318
47337
  for (const match of suggestions.title_matches ?? []) {
47319
- lines.push(`Title overlaps you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
47338
+ lines.push(`Near copy you forced past: ${match.key} [${match.status}] ${match.title} \u2192 ${absoluteHref(match.href, context.configUrl)}`);
47320
47339
  }
47321
47340
  } catch (error) {
47322
- lines.push(`The check's past decision or title overlaps could not be shown: ${messageFor(error)}`);
47341
+ lines.push(`The check's past decision or near copies could not be shown: ${messageFor(error)}`);
47323
47342
  }
47324
47343
  return lines;
47325
47344
  }
@@ -49170,7 +49189,13 @@ function eventHead(event) {
49170
49189
  return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
49171
49190
  case "artifact.human_edited":
49172
49191
  return textHead(humanEditHead(event.payload));
49173
- case "issue.created":
49192
+ case "issue.created": {
49193
+ const forced = event.payload.forced;
49194
+ const head = `status ${event.payload.status}`;
49195
+ if (forced === undefined)
49196
+ return head;
49197
+ return forced.past.length === 0 ? `${head} \xB7 forced, the near-copy check did not answer` : `${head} \xB7 forced past ${forced.past.join(", ")}`;
49198
+ }
49174
49199
  case "issue.updated":
49175
49200
  case "issue.closed":
49176
49201
  return `status ${event.payload.status}`;
@@ -49186,10 +49211,14 @@ function eventRef(event) {
49186
49211
  const kind = event.type.startsWith("message.") ? "message" : event.type.startsWith("comment.") || event.type.startsWith("suggestion.") ? "comment" : event.type.startsWith("ask.") ? "ask" : undefined;
49187
49212
  return kind === undefined ? undefined : `dispatch://${event.issue_key}/${kind}/${id}`;
49188
49213
  }
49214
+ function eventActorText(actor) {
49215
+ const title = actor.kind === "session" ? actor.origin?.session_title : undefined;
49216
+ return title === undefined || title === "" ? actorText(actor) : `${actorText(actor)} (${title})`;
49217
+ }
49189
49218
  function eventLine(event) {
49190
49219
  const head = eventHead(event);
49191
49220
  const ref = eventRef(event);
49192
- return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${actorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
49221
+ return `- #${event.seq} ${event.type}${ref === undefined ? "" : ` ${ref}`} \xB7 ${eventActorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
49193
49222
  }
49194
49223
  function childrenSummary(issue) {
49195
49224
  return [
@@ -50006,15 +50035,17 @@ async function executeDispatchTool(input) {
50006
50035
  const candidates = duplicateCandidates(error);
50007
50036
  return {
50008
50037
  text: [
50009
- `Not created: "${title}" looks like a duplicate.`,
50038
+ `Not created: "${title}" is a near copy of an open issue.`,
50010
50039
  ...candidates.map((candidate) => {
50011
50040
  const href = new URL(candidate.href, linkBase).toString();
50012
50041
  return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
50013
50042
  }),
50014
- "Reference the existing issue, or run dispatch issue again with --force after reading it."
50043
+ "Read each issue above. If one is the same work, fold this into it by the duplicate rule of skill://dispatch-first.",
50044
+ "If none is, run dispatch issue again with --force, and write in the new issue's spec why it is not each issue named here."
50015
50045
  ].join(`
50016
50046
  `),
50017
- details: { duplicates: candidates }
50047
+ details: { duplicates: candidates },
50048
+ refused: true
50018
50049
  };
50019
50050
  }
50020
50051
  }
@@ -50182,12 +50213,12 @@ async function executeDispatchTool(input) {
50182
50213
  const project = stringArg(args, "project");
50183
50214
  const title = optionalString(args, "title");
50184
50215
  const text = optionalString(args, "text");
50185
- const excludeIssue = optionalString(args, "exclude_issue");
50216
+ const excludeIssues = optionalStringArray(args, "exclude_issues");
50186
50217
  const keywordsOnly = optionalBoolean(args, "keywords_only");
50187
50218
  const suggestions = await client.related(project, {
50188
50219
  ...title === undefined ? {} : { title },
50189
50220
  ...text === undefined ? {} : { text },
50190
- ...excludeIssue === undefined ? {} : { exclude_issue: excludeIssue },
50221
+ ...excludeIssues === undefined ? {} : { exclude_issues: excludeIssues },
50191
50222
  ...keywordsOnly === undefined ? {} : { keywords_only: keywordsOnly }
50192
50223
  });
50193
50224
  return {
@@ -55145,7 +55176,7 @@ var import_nats2 = __toESM(require_mod4(), 1);
55145
55176
 
55146
55177
  // package.json
55147
55178
  var name = "@sjawhar/claude-envoy";
55148
- var version2 = "1.12.1";
55179
+ var version2 = "1.14.0";
55149
55180
  var legion = {
55150
55181
  daemonApiVersion: 55
55151
55182
  };
@@ -57218,9 +57249,63 @@ function replayStateWriter(file, restored) {
57218
57249
  }
57219
57250
 
57220
57251
  // src/session-title.ts
57252
+ import { basename as basename4 } from "path";
57253
+
57254
+ // ../envoy-client/src/process-ancestry.ts
57221
57255
  import { execFileSync } from "child_process";
57222
57256
  import { readFileSync as readFileSync9 } from "fs";
57223
- import { basename as basename4 } from "path";
57257
+ var MAX_ANCESTORS = 64;
57258
+ var PARENT_LOOKUP_MS = 1000;
57259
+ function procParentPid(pid) {
57260
+ try {
57261
+ const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
57262
+ const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
57263
+ return Number.isInteger(parent) ? parent : undefined;
57264
+ } catch {
57265
+ return;
57266
+ }
57267
+ }
57268
+ function processTable() {
57269
+ const table = new Map;
57270
+ try {
57271
+ const listing = execFileSync("ps", ["-Ao", "pid=,ppid="], {
57272
+ encoding: "utf8",
57273
+ killSignal: "SIGKILL",
57274
+ timeout: PARENT_LOOKUP_MS
57275
+ });
57276
+ for (const line of listing.split(`
57277
+ `)) {
57278
+ const [, pid = "", parent = ""] = line.match(/^\s*(\d+)\s+(\d+)\s*$/) ?? [];
57279
+ if (pid !== "")
57280
+ table.set(Number(pid), Number(parent));
57281
+ }
57282
+ } catch {}
57283
+ return table;
57284
+ }
57285
+ function isAncestor(candidate, pid = process.pid) {
57286
+ let table;
57287
+ const parentPid = (child) => {
57288
+ if (process.platform === "linux")
57289
+ return procParentPid(child);
57290
+ table ??= processTable();
57291
+ return table.get(child);
57292
+ };
57293
+ let current = pid;
57294
+ for (let step = 0;current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
57295
+ if (String(current) === candidate)
57296
+ return true;
57297
+ current = parentPid(current);
57298
+ }
57299
+ return false;
57300
+ }
57301
+
57302
+ // ../envoy-client/src/tmux-server.ts
57303
+ function tmuxServer(value) {
57304
+ const parts = (value ?? "").split(",");
57305
+ return { serverPid: parts.at(-2) ?? "", socket: parts.slice(0, -2).join(",") };
57306
+ }
57307
+
57308
+ // src/session-title.ts
57224
57309
  var TMUX_PANE_FORMAT = [
57225
57310
  "#{session_name}",
57226
57311
  "#{window_name}",
@@ -57231,7 +57316,6 @@ var TMUX_PANE_FORMAT = [
57231
57316
  ].join("\t");
57232
57317
  var TMUX_TIMEOUT_MS = 1000;
57233
57318
  var SHORT_ID_LENGTH = 8;
57234
- var MAX_ANCESTORS = 64;
57235
57319
  var CSI_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "gu");
57236
57320
  function oneLine(value) {
57237
57321
  return value.replace(CSI_SEQUENCE, "").replace(/[\u202a-\u202e\u2066-\u2069]/gu, "").replace(/[\s\p{Cc}]+/gu, " ").trim();
@@ -57239,32 +57323,6 @@ function oneLine(value) {
57239
57323
  function visible(value) {
57240
57324
  return value.replace(/[\s\p{Cc}\p{Cf}\p{Cs}\p{Cn}\p{M}\p{Default_Ignorable_Code_Point}\u2800]/gu, "") !== "";
57241
57325
  }
57242
- function parentPid(pid) {
57243
- try {
57244
- if (process.platform === "linux") {
57245
- const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
57246
- const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
57247
- return Number.isInteger(parent) ? parent : undefined;
57248
- }
57249
- const parent = Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
57250
- encoding: "utf8",
57251
- timeout: TMUX_TIMEOUT_MS,
57252
- killSignal: "SIGKILL"
57253
- }).trim());
57254
- return Number.isInteger(parent) ? parent : undefined;
57255
- } catch {
57256
- return;
57257
- }
57258
- }
57259
- function isAncestor(candidate, pid = process.pid) {
57260
- let current = pid;
57261
- for (let step = 0;current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
57262
- if (String(current) === candidate)
57263
- return true;
57264
- current = parentPid(current);
57265
- }
57266
- return false;
57267
- }
57268
57326
  async function queryTmux(socket, pane, format) {
57269
57327
  let child;
57270
57328
  try {
@@ -57328,9 +57386,7 @@ async function sessionTitle(environment, directory, sources = {}) {
57328
57386
  const explicit = oneLine(configuredValue(environment.ENVOY_SESSION_TITLE) ?? "");
57329
57387
  if (visible(explicit))
57330
57388
  return () => explicit;
57331
- const parts = configuredValue(environment.TMUX)?.split(",") ?? [];
57332
- const socket = parts.slice(0, -2).join(",");
57333
- const serverPid = parts.at(-2) ?? "";
57389
+ const { serverPid, socket } = tmuxServer(configuredValue(environment.TMUX));
57334
57390
  const pane = configuredValue(environment.TMUX_PANE)?.trim();
57335
57391
  const directoryName = oneLine(basename4(directory) || directory);
57336
57392
  const name = (socket === "" || serverPid === "" || pane === undefined ? undefined : await tmuxPaneTitle(socket, serverPid, pane, ownPane, query)) ?? (visible(directoryName) ? directoryName : "session");