github-router 0.3.25 → 0.3.27

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/main.js CHANGED
@@ -40,9 +40,30 @@ const PATHS = {
40
40
  return path.join(appDir(), "runtime");
41
41
  },
42
42
  get CLAUDE_CONFIG_DIR() {
43
- return path.join(appDir(), "claude-config");
43
+ return path.join(appDir(), "claude-config", claudeConfigDirSuffix());
44
44
  }
45
45
  };
46
+ /**
47
+ * Per-launch suffix for `PATHS.CLAUDE_CONFIG_DIR`. Lazily generated on
48
+ * first access and cached for the lifetime of the process so every
49
+ * caller (env-var injection in `getClaudeCodeEnvVars`,
50
+ * `ensureClaudeConfigMirror` provisioning, peer-agent `.md` writes
51
+ * under `<dir>/agents/`, the shutdown cleanup) resolves the same path.
52
+ *
53
+ * Shape: `<pid>-<8 hex>`. The PID prefix is what
54
+ * `sweepStaleClaudeConfigMirrors` keys off to drop orphans from
55
+ * crashed prior sessions; the 8-hex random suffix prevents collision
56
+ * if a future caller (tests, internal relaunch) ever clears the cache
57
+ * within a single PID lifetime.
58
+ *
59
+ * NOT exported — every consumer should go through `PATHS.CLAUDE_CONFIG_DIR`
60
+ * so the homedir-mock pattern used in the test suite keeps working.
61
+ */
62
+ let _claudeConfigDirSuffix;
63
+ function claudeConfigDirSuffix() {
64
+ if (_claudeConfigDirSuffix === void 0) _claudeConfigDirSuffix = `${process.pid}-${randomBytes(4).toString("hex")}`;
65
+ return _claudeConfigDirSuffix;
66
+ }
46
67
  async function ensurePaths() {
47
68
  await fs.mkdir(PATHS.APP_DIR, { recursive: true });
48
69
  await fs.mkdir(PATHS.CODEX_HOME, { recursive: true });
@@ -52,6 +73,9 @@ async function ensurePaths() {
52
73
  await sweepStaleRuntimeFiles().catch((err) => {
53
74
  consola.debug("Runtime sweep skipped:", err);
54
75
  });
76
+ await sweepStaleClaudeConfigMirrors().catch((err) => {
77
+ consola.debug("Per-launch claude-config sweep skipped:", err);
78
+ });
55
79
  await sweepStalePeerAgentMdFiles().catch((err) => {
56
80
  consola.debug("Peer-agent .md sweep skipped:", err);
57
81
  });
@@ -506,6 +530,73 @@ async function sweepStalePeerAgentMdFiles() {
506
530
  * new coordinator-style agent is added in `codex-mcp-config.ts`.
507
531
  */
508
532
  const PEER_AGENT_MD_FILENAME = /^peer-(\d+)-[0-9a-f]{8}-(?:codex-critic|codex-reviewer|gemini-critic|codex-implementer|peer-review-coordinator)\.md$/;
533
+ /**
534
+ * Strict regex matching only per-launch claude-config mirror dirs this
535
+ * proxy creates: `<pid>-<8 hex>`. Anchored to the entire entry name so
536
+ * user-authored siblings under `<appDir>/claude-config/` (if any) are
537
+ * untouchable. The PID prefix is what `sweepStaleClaudeConfigMirrors`
538
+ * keys off; the 8-hex random suffix matches `randomBytes(4)` exactly
539
+ * (no `?` — files created by a different shape are not ours).
540
+ */
541
+ const CLAUDE_CONFIG_MIRROR_DIR = /^(\d+)-[0-9a-f]{8}$/;
542
+ /**
543
+ * Sweep stale per-launch CLAUDE_CONFIG_DIR mirrors left behind by
544
+ * crashed prior proxy sessions. Symmetric to `sweepStalePeerAgentMdFiles`
545
+ * — same liveness rule (only delete when the embedded PID is dead),
546
+ * same strict regex (the dir-name allowlist is the load-bearing
547
+ * protection against deleting user-authored siblings).
548
+ *
549
+ * Scans `<appDir>/claude-config/` (the parent of the per-launch dirs).
550
+ * Each entry whose name matches `<pid>-<8 hex>` AND whose PID is no
551
+ * longer alive is removed recursively. `fs.rm({recursive: true})`
552
+ * walks the tree calling `unlink` on symlinks/junctions rather than
553
+ * following them, so the SHARED junctions back to `~/.claude/<X>`
554
+ * are removed without touching their targets.
555
+ *
556
+ * Tolerates missing parent dir (first-ever launch, or user wiped it).
557
+ */
558
+ async function sweepStaleClaudeConfigMirrors() {
559
+ const parent = path.join(appDir(), "claude-config");
560
+ let entries;
561
+ try {
562
+ entries = await fs.readdir(parent);
563
+ } catch (err) {
564
+ if (err.code === "ENOENT") return;
565
+ throw err;
566
+ }
567
+ for (const name$1 of entries) {
568
+ const match = CLAUDE_CONFIG_MIRROR_DIR.exec(name$1);
569
+ if (!match) continue;
570
+ if (isPidAlive(Number.parseInt(match[1], 10))) continue;
571
+ await fs.rm(path.join(parent, name$1), {
572
+ recursive: true,
573
+ force: true
574
+ }).catch((err) => {
575
+ consola.debug(`sweepStaleClaudeConfigMirrors: cannot rm ${name$1}:`, err);
576
+ });
577
+ }
578
+ }
579
+ /**
580
+ * Remove THIS launch's per-launch CLAUDE_CONFIG_DIR on shutdown.
581
+ * Best-effort: a failure here must not block process exit (the caller
582
+ * wraps this in a `.catch`-equivalent via `launchChild`'s onShutdown
583
+ * try/catch). Symmetric to `writePeerMcpRuntimeFiles`'s `cleanup()`:
584
+ * we own this dir for the lifetime of the proxy, so removing it on
585
+ * normal shutdown is correct; the boot-time sweep handles the
586
+ * abnormal-exit case.
587
+ *
588
+ * `fs.rm({recursive: true})` removes SHARED junctions via unlink
589
+ * (does NOT follow them into the user's real `~/.claude/<X>`).
590
+ */
591
+ async function removeOwnClaudeConfigMirror() {
592
+ const dir = PATHS.CLAUDE_CONFIG_DIR;
593
+ await fs.rm(dir, {
594
+ recursive: true,
595
+ force: true
596
+ }).catch((err) => {
597
+ consola.debug(`removeOwnClaudeConfigMirror: rm ${dir} skipped:`, err);
598
+ });
599
+ }
509
600
 
510
601
  //#endregion
511
602
  //#region src/lib/state.ts
@@ -903,6 +994,9 @@ function normalizeModelId(id) {
903
994
  * Resolve a model name to the best available variant in the Copilot model list.
904
995
  *
905
996
  * Resolution cascade:
997
+ * 0. `[1m]` literal-bracket suffix: strip, delegate, warn if downgraded.
998
+ * Bracketed slug must never reach Copilot (400s on it). See cc-backup
999
+ * `src/utils/context.ts:35-40` for Claude Code's 1M unlock mechanism.
906
1000
  * 1. Exact match
907
1001
  * 2. Case-insensitive match
908
1002
  * 3. Family preference (opus→1m, codex→highest version)
@@ -916,6 +1010,13 @@ function normalizeModelId(id) {
916
1010
  function resolveModel(modelId) {
917
1011
  const models = state.models?.data;
918
1012
  if (!models) return modelId;
1013
+ const oneMMatch = modelId.match(/^(.*)\[1m\]$/i);
1014
+ if (oneMMatch) {
1015
+ const stripped = oneMMatch[1];
1016
+ const resolved = resolveModel(stripped);
1017
+ if (!/-1m(?:$|-)/.test(resolved)) consola.warn(`Model "${modelId}" requested 1M context but no -1m backend is in Copilot's catalog for this tier/family; downgrading upstream to "${resolved}" (200K). Claude Code's local context accounting will still assume 1M — expect premature auto-compact. Drop the [1m] suffix (or unset CLAUDE_CODE_DISABLE_1M_CONTEXT if you set it) to silence.`);
1018
+ return resolved;
1019
+ }
919
1020
  if (models.some((m) => m.id === modelId)) return modelId;
920
1021
  const lower = modelId.toLowerCase();
921
1022
  const ciMatch = models.find((m) => m.id.toLowerCase() === lower);
@@ -1410,6 +1511,44 @@ const DEFAULT_PORT = 8787;
1410
1511
  const DEFAULT_CLAUDE_MODEL = "claude-opus-4-7";
1411
1512
  const DEFAULT_CLAUDE_MODEL_FALLBACKS = ["claude-opus-4-6", "claude-opus-4-5"];
1412
1513
  /**
1514
+ * Cap-aware default picker for `ANTHROPIC_MODEL` on the implicit-default
1515
+ * path. Returns `claude-opus-4-7[1m]` when the live Copilot catalog
1516
+ * contains a `*-opus-4.7-1m*` variant (enterprise tier), else
1517
+ * `DEFAULT_CLAUDE_MODEL` (the bare slug).
1518
+ *
1519
+ * The `[1m]` literal-bracket suffix is Claude Code's local 1M-context
1520
+ * unlock — cc-backup `src/utils/context.ts:35-40` matches `/\[1m\]/i`
1521
+ * to flip the context window from 200K to 1M, which drives compaction
1522
+ * triggers, the status-line context %, and token budgets. Without the
1523
+ * bracket Claude Code accounts against 200K regardless of how the
1524
+ * proxy routes the underlying request.
1525
+ *
1526
+ * Cap-awareness matters because on non-enterprise Copilot tiers there
1527
+ * is no `-1m` opus backend; sending `[1m]` there would either 400 at
1528
+ * Copilot or (with `resolveModel`'s graceful-degrade) silently
1529
+ * downgrade upstream while Claude Code still over-accounts context.
1530
+ * This helper detects the catalog state at launch and only opts in
1531
+ * when the backend can actually serve 1M.
1532
+ *
1533
+ * Sonnet/Haiku families are intentionally NOT given `[1m]` defaults
1534
+ * because Copilot has no `-1m` backend for them (and Anthropic-side
1535
+ * `modelSupports1M` doesn't list haiku at all). See
1536
+ * `src/lib/server-setup.ts:getClaudeCodeEnvVars` for the
1537
+ * `ANTHROPIC_DEFAULT_{SONNET,HAIKU,OPUS}_MODEL` tier defaults.
1538
+ *
1539
+ * Must be called AFTER `cacheModels()` has populated `state.models`.
1540
+ * Returns the bare slug if the catalog isn't populated (resolveModel
1541
+ * can't tell the difference between "no catalog yet" and "no 1M
1542
+ * variant" — defaulting safe-side preserves the pre-change behavior).
1543
+ */
1544
+ function pickClaudeDefault() {
1545
+ if (state.models?.data.some((m) => /opus-4[.-]7-1m(?:$|-)/i.test(m.id)) ?? false) {
1546
+ consola.info(`Catalog contains opus-4.7-1m variant; defaulting ANTHROPIC_MODEL to "${DEFAULT_CLAUDE_MODEL}[1m]" so Claude Code accounts for 1M context locally. Set CLAUDE_CODE_DISABLE_1M_CONTEXT=1 to opt out (HIPAA), or pass --model ${DEFAULT_CLAUDE_MODEL} to pin 200K.`);
1547
+ return `${DEFAULT_CLAUDE_MODEL}[1m]`;
1548
+ }
1549
+ return DEFAULT_CLAUDE_MODEL;
1550
+ }
1551
+ /**
1413
1552
  * Default model for `github-router codex`. `gpt-5.5` is the new flagship
1414
1553
  * `/responses` model; the fallback chain handles older Copilot tiers where
1415
1554
  * 5.5 hasn't rolled out yet. `resolveCodexModel` provides a final
@@ -2091,6 +2230,42 @@ function buildAgentPrompt(persona, opts) {
2091
2230
  "When the tool returns, surface its output to the lead verbatim. Do not summarize, paraphrase, or add your own commentary on top — the lead integrates the persona's reply directly."
2092
2231
  ].join("\n");
2093
2232
  }
2233
+ /**
2234
+ * Build the awareness snippet appended to the spawned `claude` session's
2235
+ * system prompt via `--append-system-prompt`. Non-prescriptive — Claude
2236
+ * sees that the peer tools and advisor exist; *when* to invoke is left
2237
+ * to Claude's judgment.
2238
+ *
2239
+ * Trimmed to <100 tokens by design. The per-tool descriptions are
2240
+ * already in Claude's context as MCP tool descriptions (loaded from
2241
+ * `tools/list`); the snippet's net-new value is:
2242
+ * - the `advisor` mention (built-in, not MCP-discoverable),
2243
+ * - the `peer-review-coordinator` fan-out hint,
2244
+ * - the "subagents you spawn inherit these" claim (the load-bearing
2245
+ * UX payoff of the holistic subagent-MCP-inheritance fix).
2246
+ *
2247
+ * Surface contract (regression-pinned in tests/peer-mcp-personas.test.ts):
2248
+ * - Always lists codex_critic, codex_reviewer, opus_critic, advisor,
2249
+ * peer-review-coordinator, and the subagent-inheritance fact.
2250
+ * - Conditionally lists gemini_critic only when `geminiAvailable`.
2251
+ * - Mentions `codex-cli` stdio bridge only when `codexCli`.
2252
+ *
2253
+ * The snippet is the awareness layer; the auto-invocation triggers
2254
+ * (CALL BEFORE / CALL AFTER) remain in each MCP tool's own `description`.
2255
+ * The two layers are intentionally complementary — keep the snippet
2256
+ * terse and never re-encode the prescriptive triggers here.
2257
+ */
2258
+ function buildPeerAwarenessSnippet(opts) {
2259
+ const criticList = ["`codex_critic` (gpt-5.5)", "`codex_reviewer` (gpt-5.3-codex)"];
2260
+ if (opts.geminiAvailable) criticList.push("`gemini_critic` (gemini-3.1-pro)");
2261
+ criticList.push("`opus_critic` (Opus 4.7)");
2262
+ const codexCliClause = opts.codexCli ? " The `mcp__codex-cli__codex` stdio bridge dispatches to `codex-implementer` for end-to-end coding tasks." : "";
2263
+ return [
2264
+ "## Peer review and advisor",
2265
+ "",
2266
+ `Cross-lab peer critics under \`mcp__gh-router-peers__*\` — ${criticList.join(", ")} — plus the \`peer-review-coordinator\` fan-out subagent, and Claude Code's built-in \`advisor\` tool, are available at your discretion for second opinions and adversarial review. Subagents you spawn inherit them.${codexCliClause}`
2267
+ ].join("\n");
2268
+ }
2094
2269
  /** Convenience: every persona that should be registered for the given mode. */
2095
2270
  function personasFor(opts) {
2096
2271
  const result = [];
@@ -2404,6 +2579,90 @@ async function writePeerAgentMdFiles(agents, opts) {
2404
2579
  };
2405
2580
  }
2406
2581
  /**
2582
+ * Mutate the mirrored `<CLAUDE_CONFIG_DIR>/.claude.json` to add the
2583
+ * `gh-router-peers` entry (and `codex-cli` when enabled) under
2584
+ * `mcpServers`. This is the load-bearing fix for subagent MCP visibility.
2585
+ *
2586
+ * Subagents — Agent-tool subagents, forks, and agent-teams subprocesses
2587
+ * — discover MCP servers from persistent scopes (`.claude.json` and
2588
+ * project-scope `.mcp.json`), NOT from the parent's `--mcp-config` CLI
2589
+ * flag. Writing into the per-launch mirror's `.claude.json` makes the
2590
+ * MCP entry visible to subagents transparently: they inherit
2591
+ * `CLAUDE_CONFIG_DIR` from the parent's env, so they read the same
2592
+ * config file we just mutated.
2593
+ *
2594
+ * Safety:
2595
+ * - Refuses to overwrite a same-named user-side entry (the snapshot
2596
+ * copied their `.claude.json` first, so an existing entry would
2597
+ * belong to the user). Returns `{ ok: false }` so the caller can
2598
+ * fall back to leaving `--mcp-config` in place for the parent.
2599
+ * - Preserves all other top-level fields and other `mcpServers`
2600
+ * entries.
2601
+ * - Atomic write: temp-file with `wx` (`O_CREAT | O_EXCL`) followed by
2602
+ * `rename`, mirroring the synthetic-credentials write pattern in
2603
+ * `ensureClaudeConfigMirror`. Mode 0o600. The per-launch
2604
+ * `CLAUDE_CONFIG_DIR` means there are no cross-launch racers.
2605
+ */
2606
+ async function injectPeerMcpIntoMirror(serverUrl, opts) {
2607
+ const dir = opts.claudeConfigDir ?? PATHS.CLAUDE_CONFIG_DIR;
2608
+ const target = path.join(dir, ".claude.json");
2609
+ let existing = {};
2610
+ try {
2611
+ const raw = await fs.readFile(target, "utf8");
2612
+ try {
2613
+ const parsed = JSON.parse(raw);
2614
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
2615
+ else consola.warn(`injectPeerMcpIntoMirror: ${target} parsed to non-object (typeof=${typeof parsed}); discarding contents and starting fresh.`);
2616
+ } catch (err) {
2617
+ consola.warn(`injectPeerMcpIntoMirror: cannot parse ${target} as JSON; starting fresh (existing contents will be overwritten):`, err);
2618
+ }
2619
+ } catch (err) {
2620
+ if (err.code !== "ENOENT") consola.debug(`injectPeerMcpIntoMirror: cannot read ${target}:`, err);
2621
+ }
2622
+ let mcpServers;
2623
+ const rawServers = existing.mcpServers;
2624
+ if (rawServers !== void 0 && rawServers !== null && typeof rawServers === "object" && !Array.isArray(rawServers)) mcpServers = rawServers;
2625
+ else {
2626
+ if (rawServers !== void 0 && rawServers !== null) consola.warn(`injectPeerMcpIntoMirror: mcpServers field in ${target} is not an object (typeof=${typeof rawServers}); replacing with our entry.`);
2627
+ mcpServers = {};
2628
+ }
2629
+ const peerConfig = buildPeerMcpConfig(serverUrl, {
2630
+ codexCli: opts.codexCli,
2631
+ geminiAvailable: opts.geminiAvailable,
2632
+ nonce: opts.nonce,
2633
+ codexHome: opts.codexHome ?? PATHS.CODEX_HOME
2634
+ });
2635
+ const conflicts = [];
2636
+ for (const name$1 of Object.keys(peerConfig.mcpServers)) if (mcpServers[name$1] !== void 0) conflicts.push(name$1);
2637
+ if (conflicts.length > 0) {
2638
+ consola.warn(`injectPeerMcpIntoMirror: your ~/.claude/.claude.json already has mcpServers entries named [${conflicts.join(", ")}]; refusing to overwrite. Subagents will not see the peer-MCP tools — only the parent session via --mcp-config fallback. To resolve, rename the user-side server(s) (e.g. via \`claude mcp remove\`) and relaunch.`);
2639
+ return {
2640
+ ok: false,
2641
+ reason: "user-has-conflicting-entry",
2642
+ conflictingServers: conflicts
2643
+ };
2644
+ }
2645
+ for (const [name$1, entry] of Object.entries(peerConfig.mcpServers)) mcpServers[name$1] = entry;
2646
+ existing.mcpServers = mcpServers;
2647
+ const desiredJson = JSON.stringify(existing, null, 2) + "\n";
2648
+ await fs.mkdir(dir, { recursive: true });
2649
+ const tempPath = `${target}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
2650
+ try {
2651
+ await fs.writeFile(tempPath, desiredJson, {
2652
+ mode: 384,
2653
+ flag: "wx"
2654
+ });
2655
+ await fs.rename(tempPath, target);
2656
+ } catch (err) {
2657
+ await fs.unlink(tempPath).catch(() => {});
2658
+ throw err;
2659
+ }
2660
+ return {
2661
+ ok: true,
2662
+ serversAdded: Object.keys(peerConfig.mcpServers)
2663
+ };
2664
+ }
2665
+ /**
2407
2666
  * Generate a per-launch nonce, write the MCP config + agents JSON
2408
2667
  * tempfiles under `CLAUDE_RUNTIME_DIR` with mode 0o600 and `O_EXCL`,
2409
2668
  * and return a `cleanup()` to unlink them on shutdown.
@@ -2655,7 +2914,7 @@ function initProxyFromEnv() {
2655
2914
  //#endregion
2656
2915
  //#region package.json
2657
2916
  var name = "github-router";
2658
- var version = "0.3.25";
2917
+ var version = "0.3.27";
2659
2918
 
2660
2919
  //#endregion
2661
2920
  //#region src/lib/approval.ts
@@ -6350,6 +6609,9 @@ function getClaudeCodeEnvVars(serverUrl, model) {
6350
6609
  };
6351
6610
  if (model) vars.ANTHROPIC_MODEL = model;
6352
6611
  if (process.env.ANTHROPIC_SMALL_FAST_MODEL === void 0) vars.ANTHROPIC_SMALL_FAST_MODEL = "claude-haiku-4-5";
6612
+ if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL === void 0) vars.ANTHROPIC_DEFAULT_SONNET_MODEL = "claude-sonnet-4-6";
6613
+ if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === void 0) vars.ANTHROPIC_DEFAULT_HAIKU_MODEL = "claude-haiku-4-5";
6614
+ if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL === void 0) vars.ANTHROPIC_DEFAULT_OPUS_MODEL = "claude-opus-4-7";
6353
6615
  for (const key of [
6354
6616
  "CLAUDE_CODE_ENABLE_EXPERIMENTAL_ADVISOR_TOOL",
6355
6617
  "CLAUDE_CODE_FORK_SUBAGENT",
@@ -6470,7 +6732,7 @@ const claude = defineCommand({
6470
6732
  }
6471
6733
  enableFileLogging();
6472
6734
  const usingDefault = !args.model;
6473
- let chosenSlug = args.model ?? DEFAULT_CLAUDE_MODEL;
6735
+ let chosenSlug = args.model ?? pickClaudeDefault();
6474
6736
  let resolvedSlug = resolveModel(chosenSlug);
6475
6737
  if (usingDefault && state.models) {
6476
6738
  const inCache = (slug) => state.models?.data.some((m) => m.id === resolveModel(slug)) ?? false;
@@ -6492,7 +6754,10 @@ const claude = defineCommand({
6492
6754
  process$1.stderr.write(`Server ready on ${serverUrl}, launching Claude Code (${banner})...\n`);
6493
6755
  const envVars = getClaudeCodeEnvVars(serverUrl, chosenSlug);
6494
6756
  const extraArgs = args._ ?? [];
6495
- let onShutdown;
6757
+ const baseShutdown = async () => {
6758
+ await removeOwnClaudeConfigMirror();
6759
+ };
6760
+ let onShutdown = baseShutdown;
6496
6761
  if (args["codex-mcp"] !== false) try {
6497
6762
  const requestedCli = args["codex-cli"] ?? false;
6498
6763
  const backend = resolveCodexCliBackend({
@@ -6506,11 +6771,27 @@ const claude = defineCommand({
6506
6771
  geminiAvailable: geminiAvailable$1
6507
6772
  });
6508
6773
  state.peerMcpNonce = runtime.nonce;
6509
- onShutdown = runtime.cleanup;
6510
- extraArgs.push("--mcp-config", runtime.mcpConfigPath);
6511
- if (args["codex-mcp-only"] === true) extraArgs.push("--strict-mcp-config");
6774
+ onShutdown = async () => {
6775
+ await runtime.cleanup();
6776
+ await baseShutdown();
6777
+ };
6778
+ const injected = await injectPeerMcpIntoMirror(serverUrl, {
6779
+ codexCli: backend === "cli",
6780
+ geminiAvailable: geminiAvailable$1,
6781
+ nonce: runtime.nonce
6782
+ });
6783
+ if (!injected.ok) {
6784
+ extraArgs.push("--mcp-config", runtime.mcpConfigPath);
6785
+ if (args["codex-mcp-only"] === true) extraArgs.push("--strict-mcp-config");
6786
+ } else if (args["codex-mcp-only"] === true) consola.warn("--codex-mcp-only has no effect when peer MCP is wired via the mirrored .claude.json (the user's existing user-scope MCPs in the snapshot are still visible). Pass --no-codex-mcp to skip peer-MCP wiring entirely.");
6512
6787
  const personaNames = runtime.personas.map((p) => p.agentName).join(", ");
6513
- process$1.stderr.write(`Peer MCP wired (backend=${backend}, personas=[${personaNames}], subagent .md files=${runtime.agentMdPaths.length}).\n`);
6788
+ const subagentVisibility = injected.ok ? `subagent-visible (mirrored mcpServers: [${injected.serversAdded.join(", ")}])` : `subagent-INVISIBLE (collision on user-side mcpServers: [${injected.conflictingServers.join(", ")}]; parent-only via --mcp-config)`;
6789
+ process$1.stderr.write(`Peer MCP wired (backend=${backend}, personas=[${personaNames}], subagent .md files=${runtime.agentMdPaths.length}, ${subagentVisibility}).\n`);
6790
+ const peerAwarenessOptOut = (process$1.env.GH_ROUTER_PEER_AWARENESS ?? "1").trim().toLowerCase();
6791
+ if (!(peerAwarenessOptOut === "" || peerAwarenessOptOut === "0" || peerAwarenessOptOut === "false" || peerAwarenessOptOut === "off" || peerAwarenessOptOut === "no")) extraArgs.push("--append-system-prompt", buildPeerAwarenessSnippet({
6792
+ codexCli: backend === "cli",
6793
+ geminiAvailable: geminiAvailable$1
6794
+ }));
6514
6795
  } catch (err) {
6515
6796
  consola.warn(`Peer MCP wiring failed (claude will launch without it): ${err instanceof Error ? err.message : String(err)}`);
6516
6797
  }