github-router 0.3.178 → 0.3.202

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.
@@ -1,3 +1,3 @@
1
- import { a as isUnderClaudeConfigMirror, c as sweepStalePeerAgentMdFiles, d as writeRuntimeFileSecure, i as ensurePaths, l as sweepStaleRuntimeFiles, n as PEER_AGENT_MD_FILENAME, o as removeOwnClaudeConfigMirror, r as ensureClaudeConfigMirror, s as sweepStaleClaudeConfigMirrors, t as PATHS, u as writeArtifactCredsToMirror } from "./paths-BZPgxKUl.js";
1
+ import { a as isUnderClaudeConfigMirror, c as sweepStalePeerAgentMdFiles, d as writeRuntimeFileSecure, i as ensurePaths, l as sweepStaleRuntimeFiles, n as PEER_AGENT_MD_FILENAME, o as removeOwnClaudeConfigMirror, r as ensureClaudeConfigMirror, s as sweepStaleClaudeConfigMirrors, t as PATHS, u as writeArtifactCredsToMirror } from "./paths-BO22pMUb.js";
2
2
 
3
3
  export { PATHS, PEER_AGENT_MD_FILENAME, ensureClaudeConfigMirror, ensurePaths, isUnderClaudeConfigMirror, removeOwnClaudeConfigMirror, sweepStaleClaudeConfigMirrors, sweepStalePeerAgentMdFiles, sweepStaleRuntimeFiles, writeArtifactCredsToMirror, writeRuntimeFileSecure };
@@ -30,6 +30,9 @@ const PATHS = {
30
30
  get CODEX_HOME() {
31
31
  return nodePath.join(appDir(), "codex-isolated");
32
32
  },
33
+ get CLOUDCLI_HOME() {
34
+ return nodePath.join(appDir(), "cloudcli");
35
+ },
33
36
  get CLAUDE_RUNTIME_DIR() {
34
37
  return nodePath.join(appDir(), "runtime");
35
38
  },
@@ -117,12 +120,12 @@ async function ensurePaths() {
117
120
  consola.debug("Peer-agent .md sweep skipped:", err);
118
121
  });
119
122
  await (async () => {
120
- await (await import("./lifecycle-D0n3hXUT.js")).sweepStaleWorktreesAtBoot();
123
+ await (await import("./lifecycle-Ci81s6eL.js")).sweepStaleWorktreesAtBoot();
121
124
  })().catch((err) => {
122
125
  consola.debug("Worker worktree boot sweep skipped:", err);
123
126
  });
124
127
  await (async () => {
125
- await (await import("./lifecycle-ClliSC-c.js")).sweepStaleColbertMetaAtBoot();
128
+ await (await import("./lifecycle-CJV1XBFP.js")).sweepStaleColbertMetaAtBoot();
126
129
  })().catch((err) => {
127
130
  consola.debug("ColBERT meta boot sweep skipped:", err);
128
131
  });
@@ -980,4 +983,4 @@ async function removeOwnClaudeConfigMirror() {
980
983
 
981
984
  //#endregion
982
985
  export { isUnderClaudeConfigMirror as a, sweepStalePeerAgentMdFiles as c, writeRuntimeFileSecure as d, ensurePaths as i, sweepStaleRuntimeFiles as l, PEER_AGENT_MD_FILENAME as n, removeOwnClaudeConfigMirror as o, ensureClaudeConfigMirror as r, sweepStaleClaudeConfigMirrors as s, PATHS as t, writeArtifactCredsToMirror as u };
983
- //# sourceMappingURL=paths-BZPgxKUl.js.map
986
+ //# sourceMappingURL=paths-BO22pMUb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths-BO22pMUb.js","names":["path","_claudeConfigDirSuffix: string | undefined","CLAUDE_HOME_POLICY: ReadonlyMap<string, MirrorPolicy>","SHARED_TOPLEVEL_NAMES: ReadonlyArray<string>","existing: Record<string, unknown>","selectedSource: string | null","sourceCandidates: Array<string | null>","stat","merged: Record<string, unknown>","lst: import(\"node:fs\").Stats","raw: string","parsed: Record<string, unknown>","entries: Array<string>","stats: Awaited<ReturnType<typeof fs.lstat>>","existing: Awaited<ReturnType<typeof fs.lstat>> | null"],"sources":["../src/lib/paths.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport consola from \"consola\"\n\nfunction appDir(): string {\n return path.join(os.homedir(), \".local\", \"share\", \"github-router\")\n}\n\nexport const PATHS = {\n get APP_DIR() {\n return appDir()\n },\n get GITHUB_TOKEN_PATH() {\n return path.join(appDir(), \"github_token\")\n },\n /**\n * Second, WRITE-capable GitHub token for the first-mate\n * agent-orchestration surface (`--agents`), stored apart from\n * `github_token` (the Copilot App token). Minted by\n * `setupGitHubAgentToken()` via a device-flow login against the GitHub\n * CLI OAuth client. Mode 0o600.\n */\n get GITHUB_AGENT_TOKEN_PATH() {\n return path.join(appDir(), \"github_agent_token\")\n },\n /**\n * Root of the durable first-mate fleet registry (`--agents`): the\n * global `missions.json` mission index + per-repo `<owner>__<repo>.json`\n * unit ledgers + the decisions/approvals ledger. Cross-launch and\n * OUTSIDE the per-launch CLAUDE_CONFIG_DIR mirror (which is swept each\n * launch) — the first mate must survive proxy restarts / context\n * clears. Modeled on the worker-agent lifecycle ledger.\n */\n get FIRST_MATE_DIR() {\n return (\n process.env.GH_ROUTER_FIRST_MATE_DIR ?? path.join(appDir(), \"first-mate\")\n )\n },\n get ERROR_LOG_PATH() {\n return path.join(appDir(), \"error.log\")\n },\n /**\n * Durable, router-owned directory for OVERFLOW worker diffs. When a\n * `worker_implement`/`worker_test` worktree run produces a unified diff\n * larger than the inline cap (`DIFF_CAP_BYTES` in worker-agent/worktree.ts),\n * the FULL `git diff --binary --full-index` patch is written here BEFORE the\n * worktree is removed, and its absolute path is returned to the caller so\n * the (possibly binary) change stays recoverable / `git apply`-able instead\n * of being destroyed with the worktree. Cross-launch cache (like\n * TOOLBELT_BIN_DIR) — deliberately OUTSIDE the per-launch CLAUDE_CONFIG_DIR\n * mirror and OUTSIDE the user's repo.\n */\n get WORKER_DIFFS_DIR() {\n return path.join(appDir(), \"worker-diffs\")\n },\n /**\n * Isolated CODEX_HOME for the spawned Codex CLI. Masks any cached\n * ChatGPT subscription login (openai/codex#2733 — cached login can\n * override OPENAI_API_KEY) so the proxy's dummy key is authoritative.\n */\n get CODEX_HOME() {\n return path.join(appDir(), \"codex-isolated\")\n },\n /**\n * Router-owned install + data dir for CloudCLI (the `serve` browser control\n * plane). Holds the pinned local `npm install` (node_modules/), an isolated\n * `auth.db` (so we never collide with a user's standalone CloudCLI DB), and\n * the persisted `.serve-secret.json` login. See docs/serve-control-plane.md.\n */\n get CLOUDCLI_HOME() {\n return path.join(appDir(), \"cloudcli\")\n },\n /**\n * Runtime tempfiles for the per-launch peer-MCP wiring (the\n * `--mcp-config` JSON and `--agents` JSON written before spawning\n * Claude Code). Mode 0o700 to match the security review's mandate;\n * cleaned on shutdown via the per-launch `cleanup()`, plus a\n * boot-time sweep of stale files (dead PIDs, >24h old).\n */\n get CLAUDE_RUNTIME_DIR() {\n return path.join(appDir(), \"runtime\")\n },\n /**\n * Router-owned CLAUDE_CONFIG_DIR. The spawned Claude Code (and any\n * teammates it spawns via the agent-teams primitive) reads its\n * config — including `.credentials.json` — from this dir. We\n * snapshot-copy the user's `~/.claude/` here at startup (excluding\n * `.credentials.json` and volatile state), then write our own\n * synthetic Console OAuth credential. The teammate-spawn allowlist\n * propagates `CLAUDE_CONFIG_DIR` to children, so teammates find the\n * synthetic credential and authenticate instead of falling into the\n * \"Not logged in · Run /login\" gate that would otherwise leave\n * them mute. See `ensureClaudeConfigMirror` below.\n *\n * Per-launch dir: `<appDir>/claude-config/<pid>-<8 hex>`. Two\n * concurrent `github-router claude` launches each get their own\n * isolated mirror, so per-launch state (synthetic credential,\n * snapshot copy of `~/.claude/`, future per-launch `.claude.json`\n * mutation with the peer-MCP entry) cannot cross-talk. The\n * per-launch suffix is cached on first access (see\n * `claudeConfigDirSuffix()`) so all callers within a single proxy\n * lifetime see the same value. Boot-time `sweepStaleClaudeConfigMirrors`\n * reaps mirrors from crashed prior PIDs.\n */\n get CLAUDE_CONFIG_DIR() {\n return path.join(appDir(), \"claude-config\", claudeConfigDirSuffix())\n },\n /**\n * Durable router-owned `bin/` for the LLM toolbelt — curated CLI\n * tools (rg/fd/jq/sd/sg/yq) materialized here and prepended to the\n * spawned agent's PATH. Intentionally OUTSIDE the per-launch\n * CLAUDE_CONFIG_DIR mirror (which is swept per launch): the toolbelt\n * is a cross-launch cache, like `last-update-check`.\n */\n get TOOLBELT_BIN_DIR() {\n return path.join(appDir(), \"bin\")\n },\n /**\n * Root of the github-router-managed ColBERT semantic-search sidecar\n * (the `--semantic-search` opt-in). Everything heavy that the\n * `colgrep` child needs is provisioned, SHA-verified, and stored here\n * by the router — never in the user's repo, never in colgrep's own\n * global `%APPDATA%\\colgrep` / `~/.config/colgrep`. Cross-launch cache\n * (like TOOLBELT_BIN_DIR), deliberately OUTSIDE the per-launch\n * CLAUDE_CONFIG_DIR mirror.\n *\n * Layout (see docs/research/colbert-sidecar-design.md §4):\n * colbert/\n * bin/colgrep[.exe] # provisioned binary\n * models/LateOn-Code-edge/<rev>/ # 5 model files\n * onnxruntime/1.23.0/cpu/<libname> # ORT dylib (ORT_DYLIB_PATH)\n * indices/ # === COLGREP_DATA_DIR ===\n * <project>-<xxh3(path|model)>/ # colgrep-owned per-(path,model)\n * .gh-router-meta/<hash>.json # router-owned sidecar metadata\n */\n get COLBERT_DIR() {\n return path.join(appDir(), \"colbert\")\n },\n get COLBERT_BIN_DIR() {\n return path.join(appDir(), \"colbert\", \"bin\")\n },\n get COLBERT_MODELS_DIR() {\n return path.join(appDir(), \"colbert\", \"models\")\n },\n get COLBERT_ORT_DIR() {\n return path.join(appDir(), \"colbert\", \"onnxruntime\")\n },\n /**\n * Value passed to colgrep as `COLGREP_DATA_DIR`. colgrep keys the\n * physical index dir by `xxh3(canonical_project_path | model)` under\n * here, so every distinct workspace gets its own index dir for free.\n * The router-owned freshness sidecar lives at\n * `<COLBERT_INDICES_DIR>/.gh-router-meta/<hash>.json`.\n */\n get COLBERT_INDICES_DIR() {\n return path.join(appDir(), \"colbert\", \"indices\")\n },\n get COLBERT_META_DIR() {\n return path.join(appDir(), \"colbert\", \"indices\", \".gh-router-meta\")\n },\n}\n\n/**\n * Per-launch suffix for `PATHS.CLAUDE_CONFIG_DIR`. Lazily generated on\n * first access and cached for the lifetime of the process so every\n * caller (env-var injection in `getClaudeCodeEnvVars`,\n * `ensureClaudeConfigMirror` provisioning, peer-agent `.md` writes\n * under `<dir>/agents/`, the shutdown cleanup) resolves the same path.\n *\n * Shape: `<pid>-<8 hex>`. The PID prefix is what\n * `sweepStaleClaudeConfigMirrors` keys off to drop orphans from\n * crashed prior sessions; the 8-hex random suffix prevents collision\n * if a future caller (tests, internal relaunch) ever clears the cache\n * within a single PID lifetime.\n *\n * NOT exported — every consumer should go through `PATHS.CLAUDE_CONFIG_DIR`\n * so the homedir-mock pattern used in the test suite keeps working.\n */\nlet _claudeConfigDirSuffix: string | undefined\nfunction claudeConfigDirSuffix(): string {\n if (_claudeConfigDirSuffix === undefined) {\n _claudeConfigDirSuffix = `${process.pid}-${randomBytes(4).toString(\"hex\")}`\n }\n return _claudeConfigDirSuffix\n}\n\n/**\n * Predicate: is `target` inside the current launch's `CLAUDE_CONFIG_DIR`?\n *\n * Used as a defence-in-depth safety guard by helpers that mutate files\n * under the mirror dir (e.g. `appendPeerAwarenessToMirroredClaudeMd`).\n * If `PATHS.CLAUDE_CONFIG_DIR` has somehow been pointed at the user's\n * real `~/.claude/` (configuration drift, future code regression), the\n * helper refuses to write — the proxy should never mutate user files.\n *\n * Pure-prefix `startsWith` is unsafe (`/a/foo` matches `/a/foobar`), so\n * this requires either equality or `<root><sep>...` strictly.\n * Synchronous because we don't need real-path resolution; symlink\n * refusal lives at the consuming helper.\n */\nexport function isUnderClaudeConfigMirror(target: string): boolean {\n const resolvedRoot = path.resolve(PATHS.CLAUDE_CONFIG_DIR)\n const resolvedTarget = path.resolve(target)\n if (resolvedTarget === resolvedRoot) return true\n return resolvedTarget.startsWith(resolvedRoot + path.sep)\n}\n\nexport async function ensurePaths(): Promise<void> {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n await fs.mkdir(PATHS.CODEX_HOME, { recursive: true })\n await fs.mkdir(PATHS.CLAUDE_RUNTIME_DIR, { recursive: true })\n // mkdir({recursive: true}) does NOT chmod an existing directory, so\n // explicitly tighten in case the dir was created by an older version.\n await chmodIfPossible(PATHS.CLAUDE_RUNTIME_DIR, 0o700)\n await ensureFile(PATHS.GITHUB_TOKEN_PATH)\n // Second (write-capable) GitHub token file for `--agents`. Always\n // ensured so the path exists; only populated by a device-flow login\n // when the agent surface is opted in. Tightened to 0o600 (it holds a\n // repo/workflow-scoped credential).\n await ensureFile(PATHS.GITHUB_AGENT_TOKEN_PATH)\n await chmodIfPossible(PATHS.GITHUB_AGENT_TOKEN_PATH, 0o600)\n await sweepStaleRuntimeFiles().catch((err) => {\n consola.debug(\"Runtime sweep skipped:\", err)\n })\n // Sweep stale per-launch CLAUDE_CONFIG_DIR mirrors left behind by\n // crashed prior proxy sessions BEFORE peer-agent .md sweep, since\n // the .md sweep is scoped to THIS launch's mirror and the per-launch\n // dir sweep is the parent cleanup for the same orphan class.\n await sweepStaleClaudeConfigMirrors().catch((err) => {\n consola.debug(\"Per-launch claude-config sweep skipped:\", err)\n })\n // Phase 2.5: also sweep stale peer-* subagent .md files from this\n // launch's CLAUDE_CONFIG_DIR/agents/ (defense-in-depth — should be\n // a no-op since the per-launch dir didn't exist before this PID\n // started; keeps the safety net in case a future change ever shares\n // an agents/ dir across launches).\n await sweepStalePeerAgentMdFiles().catch((err) => {\n consola.debug(\"Peer-agent .md sweep skipped:\", err)\n })\n // Worker-agent boot-time PID+instance safety net. Walks the\n // worker-repos.json ledger and removes any worktree dir whose\n // <pid> is dead OR whose <instance> UUID doesn't match this proxy.\n // Catches SIGKILL/OOM/host-crash escapees from prior sessions.\n // Lazy-imported so the worker-agent module doesn't get loaded by\n // every consumer of `paths.ts`.\n await (async () => {\n const mod = await import(\"./worker-agent/lifecycle\")\n await mod.sweepStaleWorktreesAtBoot()\n })().catch((err) => {\n consola.debug(\"Worker worktree boot sweep skipped:\", err)\n })\n // ColBERT sidecar boot sweep: reclassify any `.gh-router-meta/*.json`\n // entry stuck in `building` with a dead `buildPid` → `failed`, so the\n // next `code` semantic-mode call re-kicks a build instead of routing to\n // a never-finishing one. NEVER kills a PID from a prior boot. Lazy-\n // imported so paths.ts doesn't drag the colbert module into every\n // consumer.\n await (async () => {\n const mod = await import(\"./colbert/lifecycle\")\n await mod.sweepStaleColbertMetaAtBoot()\n })().catch((err) => {\n consola.debug(\"ColBERT meta boot sweep skipped:\", err)\n })\n}\n\n/**\n * Per-entry mirror policy. Every top-level entry under `~/.claude/` falls\n * into exactly one bucket; unlisted names default to `MIRRORED` so a future\n * Claude-Code-side addition flows through as a snapshot copy rather than\n * being silently lost.\n *\n * Three policies:\n *\n * - `ISOLATED` — not present in the mirror at all. The proxy owns its\n * own copy (synthetic `.credentials.json`, the `.github-router-managed`\n * marker) or the entry has no place in a proxy session\n * (`.credentials.json.lock`, `.oauth_refresh.lock` couple refresh loops\n * across sessions; `statsig/` is write-heavy and would constantly\n * re-copy; `cache/` and `logs/` are ephemeral; `paste-cache/` holds\n * sensitive clipboard extracts and shouldn't leak across sessions —\n * gemini-critic finding).\n *\n * - `SHARED` — symlink `<mirror>/<X>` → `~/.claude/<X>` so writes made\n * during the proxy session land in the user's real `~/.claude/` and\n * chat history is visible in both proxy and plain-`claude` sessions.\n * **Directories only.** Never use this for individual files: Claude\n * Code's atomic-write pattern (`fs.writeFile(temp); fs.rename(temp,\n * target)`) does NOT follow symlinks — a `rename` over the symlink\n * replaces it with a regular file, silently severing the connection\n * to `~/.claude/<X>`. Gemini-critic finding from the 3-lab review.\n *\n * - `MIRRORED` (default) — snapshot-copy with mtime skip. Use for static\n * or settings-shaped state where proxy-session writes should NOT flow\n * back to `~/.claude/` (e.g. `settings.json`, `.claude.json`,\n * `teams/`, `session-env/`) and for `agents/` — the proxy itself\n * writes per-launch `peer-<pid>-*.md` files into the mirror's `agents/`\n * and `sweepStalePeerAgentMdFiles` deletes them; a symlink would route\n * those writes/deletes into the user's real `~/.claude/agents/` and\n * destroy the user's own subagent files. **Hard regression test**:\n * `policyFor(\"agents\") === \"MIRRORED\"` is asserted in\n * `tests/lib-paths.test.ts` to prevent accidental reclassification.\n *\n * Sub-paths within MIRRORED dirs cascade recursively (existing behavior).\n */\ntype MirrorPolicy = \"ISOLATED\" | \"SHARED\" | \"MIRRORED\"\n\nconst CLAUDE_HOME_POLICY: ReadonlyMap<string, MirrorPolicy> = new Map<\n string,\n MirrorPolicy\n>([\n // ISOLATED\n [\".credentials.json\", \"ISOLATED\"],\n [\".credentials.json.lock\", \"ISOLATED\"],\n [\".oauth_refresh.lock\", \"ISOLATED\"],\n // Defense-in-depth: don't let a user-side file/symlink with the same\n // name as our marker collide with what we write. The marker write\n // logic also lstat-checks before writing (refuses if a non-regular\n // file exists at the path), but excluding it here removes the\n // attack vector entirely.\n [\".github-router-managed\", \"ISOLATED\"],\n [\"statsig\", \"ISOLATED\"],\n [\"cache\", \"ISOLATED\"],\n [\"logs\", \"ISOLATED\"],\n [\"paste-cache\", \"ISOLATED\"],\n [\"jobs\", \"ISOLATED\"],\n [\"daemon\", \"ISOLATED\"],\n [\"daemon.log\", \"ISOLATED\"],\n // SHARED — directories only (see policy doc above)\n [\"projects\", \"SHARED\"],\n [\"sessions\", \"SHARED\"],\n [\"tasks\", \"SHARED\"],\n [\"todos\", \"SHARED\"],\n [\"transcripts\", \"SHARED\"],\n [\"shell-snapshots\", \"SHARED\"],\n // The underscored variant is the historical exclude-list name; some\n // Claude Code versions may still use it. Classify SHARED so either\n // spelling resolves correctly.\n [\"shell_snapshots\", \"SHARED\"],\n [\"plans\", \"SHARED\"],\n [\"file-history\", \"SHARED\"],\n [\"backups\", \"SHARED\"],\n])\n\nfunction policyFor(name: string): MirrorPolicy {\n return CLAUDE_HOME_POLICY.get(name) ?? \"MIRRORED\"\n}\n\n/**\n * Test-only export: lets the test suite assert hard regression guards\n * such as `policyFor(\"agents\") === \"MIRRORED\"` (preventing accidental\n * reclassification that would let `sweepStalePeerAgentMdFiles` delete\n * files in the user's real `~/.claude/agents/`).\n */\nexport const __testing = { policyFor, ensureSharedSymlink }\n\n/**\n * Names with `SHARED` policy, materialized once for iteration in\n * `ensureClaudeConfigMirror`'s post-copy phase.\n */\nconst SHARED_TOPLEVEL_NAMES: ReadonlyArray<string> = Array.from(\n CLAUDE_HOME_POLICY.entries(),\n)\n .filter(([, kind]) => kind === \"SHARED\")\n .map(([name]) => name)\n\n/**\n * Marker file written into the router-owned CLAUDE_CONFIG_DIR so users\n * (and our own future sweeps) can identify that the dir is managed by\n * github-router. Content is informational only; no logic depends on\n * its presence.\n */\nconst MANAGED_MARKER_FILENAME = \".github-router-managed\"\n\n/**\n * Synthetic Console OAuth credential the router writes into its own\n * `CLAUDE_CONFIG_DIR/.credentials.json` so spawned Claude Code (and\n * any teammates it spawns) can authenticate without a real user\n * `/login`.\n *\n * Schema verified verbatim from `claude` v2.1.140 binary, function\n * `guH` (the credentials-save mutation). Fields:\n * - `accessToken` — sent as `Authorization: Bearer ...` to the\n * proxy. Proxy accepts any bearer (per CLAUDE.md \"doesn't enforce\n * auth\").\n * - `refreshToken` — only used by Claude Code's reactive refresh\n * path (function `nH8`), which fires on 401 from upstream. The\n * proxy maintains the no-401 invariant on the Anthropic-shape\n * boundary, so this is never invoked. Synthetic value is fine.\n * - `expiresAt` — far-future (2099-01-01 ms epoch). Sidesteps the\n * proactive refresh path (`R8H(expiresAt)` returns false).\n * - `scopes` — claude-ai-shaped so `tB(scopes)` returns true,\n * making `Hq()` true (full feature surface, not \"inference only\").\n * - `subscriptionType` — `\"max\"`. Pure client-side label\n * (`e7()` / `Zc_()` / `CZ1()`); no server validation since\n * `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` suppresses\n * subscription-validation calls. Picks the most-permissive gating.\n * - `rateLimitTier` — `\"default_claude_max_20x\"`. Paired with\n * `subscriptionType:\"max\"` this is the real Max-20x tier, so the\n * credential is internally consistent (vs the prior odd `max`+`null`).\n * Verified live (claude v2.1.158) call-sites are cosmetic billing /\n * upsell-suppression UI plus the `getPlanModeV2AgentCount` (`bGK`)\n * `max && 20x → 3` branch — which `CLAUDE_CODE_PLAN_V2_AGENT_COUNT`\n * (set to 7 in server-setup) already overrides, so this is\n * belt-and-suspenders for the natural code path. No client-side quota\n * enforcement keys off the tier (rate-limit UI reads server\n * `x-ratelimit-*` headers; the proxy holds the no-429 invariant).\n */\nconst SYNTHETIC_CREDENTIAL = {\n claudeAiOauth: {\n accessToken: \"github-router-synthetic\",\n refreshToken: \"github-router-synthetic\",\n expiresAt: 4_070_908_800_000,\n scopes: [\"user:inference\", \"user:profile\"],\n subscriptionType: \"max\",\n rateLimitTier: \"default_claude_max_20x\",\n clientId: \"github-router\",\n },\n} as const\n\n/**\n * Router-owned fields injected into `<CLAUDE_CONFIG_DIR>/.claude.json` so\n * Claude Code skips its first-launch onboarding wizard. The synthetic\n * `.credentials.json` covers the OAuth *token*, but on a fresh machine\n * the wizard runs anyway — and one of its steps is the browser-OAuth\n * \"Sign in to your Anthropic account\" prompt that the user sees today.\n *\n * Schema verified against `claude` v2.1.158 binary strings:\n *\n * - `hasCompletedOnboarding: true` — single load-bearing gate.\n * Binary code path: `if (!E_().hasCompletedOnboarding) { ... await\n * Onboarding.default(...) }`. Setting true causes the wizard\n * (including the OAuth login step) to be skipped entirely.\n *\n * - `bypassPermissionsModeAccepted: true` — skips the\n * `--dangerously-skip-permissions` trust disclaimer. Binary gate:\n * `K = Ip() || Boolean(E_().bypassPermissionsModeAccepted)`. The\n * proxy passes `--dangerously-skip-permissions` so `Ip()` typically\n * suffices, but pinning the field covers edge code paths (e.g.\n * `--bg --dangerously-skip-permissions`) that still consult it\n * directly and would otherwise throw \"requires accepting the\n * disclaimer first.\"\n *\n * - `oauthAccount` — nice-to-have for UI consistency. Claude Code\n * reads sub-fields with optional chaining (`oauthAccount?.accountUuid`,\n * `oauthAccount?.organizationUuid`, `oauthAccount?.organizationRole`)\n * so undefined doesn't trigger login — but populating gives any\n * \"logged in as X\" / status-line UI something deterministic and\n * obviously-synthetic to display. Values are intentionally\n * non-credible (`github-router@local`, all-zero UUIDs) so any\n * leak into logs is self-documenting, not impersonation.\n *\n * Merge semantics: the two boolean gates are **force-overridden** —\n * even an existing `false` is flipped to `true`. (A user logging out\n * via the Claude Code UI cannot defeat the proxy-session bypass.)\n * `oauthAccount` is overwritten only when the existing value isn't a\n * *structurally usable* one (presence-only would preserve `{}` or\n * `{foo:1}` shapes that defeat the fix); a real OAuth identity with\n * non-empty `accountUuid` and `organizationUuid` is preserved as-is.\n * Every OTHER top-level field the user brought in via the mirror walk\n * (settings, project history, user-side MCP entries, etc.) is preserved\n * verbatim. The mirror is a router-controlled view, not a faithful\n * copy — same trade-off the synthetic credential already makes.\n */\nconst SYNTHETIC_CLAUDE_JSON_FIELDS = {\n hasCompletedOnboarding: true,\n bypassPermissionsModeAccepted: true,\n oauthAccount: {\n accountUuid: \"00000000-0000-0000-0000-000000000000\",\n organizationUuid: \"00000000-0000-0000-0000-000000000000\",\n organizationRole: \"member\",\n emailAddress: \"github-router@local\",\n organizationName: \"github-router\",\n },\n} as const\n\n/**\n * Snapshot-copy the user's `~/.claude/` into the router-owned\n * CLAUDE_CONFIG_DIR (real files, not symlinks — symlinks don't isolate\n * writes), classifying each top-level entry per `CLAUDE_HOME_POLICY`:\n * ISOLATED entries are skipped, MIRRORED entries are copied, and\n * SHARED entries become directory symlinks back to `~/.claude/<X>` so\n * chat history (in `projects/<cwd-hash>/<session-uuid>.jsonl`) and\n * other durable user state flow between proxy and plain-`claude`\n * sessions. Then writes the synthetic `.credentials.json` so spawned\n * Claude Code (and teammates that inherit `CLAUDE_CONFIG_DIR`)\n * authenticate.\n *\n * Idempotent: only re-copies files whose source `mtime` is newer than\n * target; SHARED-symlink creation no-ops when the symlink already\n * points at the right target. Concurrent-safe: `mkdir({recursive:true})`\n * is idempotent; symlinks are created via atomic temp+rename so two\n * parallel github-router-claude startups can't race to EEXIST; the\n * credentials write uses temp-file + atomic rename so Claude Code's\n * `EZ1()` mtime watcher never sees a partial write.\n *\n * Walks with `lstat` (does NOT follow symlinks during traversal — a\n * symlink-into-`/` would otherwise let the walk escape). Symlink leaves\n * in the source tree are skipped during the MIRRORED copy walk (per the\n * symlink-confused-deputy security finding); SHARED symlinks are\n * created on the mirror side only, pointing at predetermined targets\n * inside the user's real `~/.claude/`.\n *\n * Caller is expected to invoke this after `ensurePaths()` and before\n * spawning Claude Code (`launchChild`). The mirror must exist before\n * the child reads it. Currently called from the `claude` subcommand\n * entry point only; `start` and `codex` subcommands don't need it.\n */\nexport async function ensureClaudeConfigMirror(opts: {\n realHome?: string\n} = {}): Promise<void> {\n const realHome = opts.realHome ?? os.homedir()\n const sourceDir = path.join(realHome, \".claude\")\n const targetDir = PATHS.CLAUDE_CONFIG_DIR\n\n // 1. Create our config dir (idempotent, mode 0o700)\n await fs.mkdir(targetDir, { recursive: true, mode: 0o700 })\n await chmodIfPossible(targetDir, 0o700)\n\n // 2. Snapshot-copy from ~/.claude if it exists. Only MIRRORED entries\n // flow through this walk; ISOLATED and SHARED entries are filtered\n // in `mirrorDirRecursive` and handled separately.\n let sourceExists = false\n try {\n const sourceStat = await fs.stat(sourceDir)\n sourceExists = sourceStat.isDirectory()\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot stat ${sourceDir}:`, err)\n }\n }\n if (sourceExists) {\n await mirrorDirRecursive(sourceDir, targetDir, \"\")\n }\n\n // 3. Always ensure agents/ exists (even if user has none) so the\n // peer-agent .md emission has a place to write. Empty dir is fine.\n // agents/ is MIRRORED, not SHARED — the proxy writes per-launch\n // `peer-<pid>-*.md` files here and `sweepStalePeerAgentMdFiles`\n // deletes them; routing those operations into the user's real\n // `~/.claude/agents/` would destroy their custom subagent files.\n await fs.mkdir(path.join(targetDir, \"agents\"), { recursive: true })\n\n // 4. Create symlinks for SHARED entries so chat history (and other\n // durable user state) is visible in both proxy and plain-`claude`.\n for (const name of SHARED_TOPLEVEL_NAMES) {\n await ensureSharedSymlink(name, sourceDir, targetDir).catch((err) => {\n consola.debug(\n `ensureClaudeConfigMirror: SHARED symlink for ${name} skipped:`,\n err,\n )\n })\n }\n\n // 5. Write synthetic .credentials.json (only if content differs)\n const credentialsPath = path.join(targetDir, \".credentials.json\")\n const desiredJson = JSON.stringify(SYNTHETIC_CREDENTIAL, null, 2)\n let needsWrite = true\n try {\n const existing = await fs.readFile(credentialsPath, \"utf8\")\n needsWrite = existing.trim() !== desiredJson.trim()\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot read existing credentials:`, err)\n }\n }\n if (needsWrite) {\n // Atomic temp-file + rename so EZ1()'s mtime watcher doesn't see\n // a partial write. wx flag ensures we don't clobber a concurrent\n // writer's tempfile.\n const tempPath = `${credentialsPath}.${process.pid}.tmp`\n try {\n await fs.writeFile(tempPath, desiredJson + \"\\n\", { mode: 0o600, flag: \"wx\" })\n await fs.rename(tempPath, credentialsPath)\n } catch (err) {\n // EEXIST on the tempfile means another concurrent startup is\n // mid-write. Best-effort: skip — the other writer will produce\n // identical content (deterministic constant blob).\n if ((err as NodeJS.ErrnoException).code === \"EEXIST\") {\n consola.debug(\n \"ensureClaudeConfigMirror: concurrent credentials-write detected, skipping\",\n )\n } else {\n await fs.unlink(tempPath).catch(() => {})\n throw err\n }\n }\n }\n await chmodIfPossible(credentialsPath, 0o600)\n\n // 6. Inject onboarding-skip fields into <CLAUDE_CONFIG_DIR>/.claude.json.\n // Runs AFTER the mirror walk (step 2) so a user's existing\n // .claude.json content (settings, projects, user-side mcpServers)\n // is preserved, and BEFORE `injectPeerMcpIntoMirror` (called later\n // from `src/claude.ts`) which read-merge-writes the same file and\n // will preserve our fields naturally.\n //\n // Without this step, on a brand-new machine where ~/.claude/.claude.json\n // doesn't exist, nothing lands in the mirror, Claude Code's\n // `hasCompletedOnboarding` gate defaults to falsy, and the\n // first-launch wizard runs — including the \"Sign in to your\n // Anthropic account\" OAuth step that defeats the synthetic\n // credential. See SYNTHETIC_CLAUDE_JSON_FIELDS for the gate\n // rationale + binary-verified field shape.\n await injectSyntheticClaudeJsonFields(targetDir, sourceDir, realHome)\n\n // 6a. Postcondition: verify the gate fields actually landed in the\n // mirror's .claude.json. The helper has several \"warn and\n // return\" branches (non-regular target, non-ENOENT read failure)\n // where falling through silently would let Claude Code's\n // first-launch wizard fire — the exact bug this whole\n // subsystem exists to prevent. Re-reading the file and\n // asserting the booleans converts every silent-fail path into a\n // fail-loud launch error, which `src/claude.ts` surfaces to the\n // user with `process.exit(1)`. Cheap (one extra readFile +\n // JSON.parse on a small file we just wrote) and gives the\n // no-silent-degradation invariant teeth even if a future\n // contributor adds a new bail-out branch.\n await assertOnboardingGateInjected(targetDir)\n\n // 7. Write/refresh marker file. Use lstat (not access) to detect\n // symlinks at the marker path — a previously-mirrored or\n // user-placed symlink could otherwise let our `fs.writeFile`\n // follow through to an arbitrary target. With the symlink-skip\n // policy in `mirrorDirRecursive` this is defense-in-depth, but\n // cheap and definitive.\n const markerPath = path.join(targetDir, MANAGED_MARKER_FILENAME)\n let markerExists = false\n try {\n const markerStat = await fs.lstat(markerPath)\n if (markerStat.isFile()) {\n markerExists = true\n } else {\n // Anything non-regular (symlink, dir, special file) is a red flag —\n // refuse to overwrite, log loudly. The user can investigate.\n consola.warn(\n `ensureClaudeConfigMirror: ${markerPath} exists but is not a regular file (mode=${markerStat.mode.toString(8)}); refusing to overwrite. Inspect and remove manually if safe.`,\n )\n markerExists = true\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot lstat marker:`, err)\n markerExists = true\n }\n }\n if (!markerExists) {\n const body = `Managed by github-router. Created ${new Date().toISOString()}. Safe to delete (will be recreated).\\n`\n // wx flag (O_CREAT | O_EXCL) refuses to clobber an existing\n // file or symlink (POSIX O_EXCL behavior) — additional protection\n // against the marker-symlink confused-deputy vector.\n await fs\n .writeFile(markerPath, body, { mode: 0o600, flag: \"wx\" })\n .catch((err) => {\n consola.debug(`ensureClaudeConfigMirror: marker write skipped:`, err)\n })\n }\n}\n\n/**\n * Read-merge-atomic-write the router-required onboarding-skip fields\n * (see `SYNTHETIC_CLAUDE_JSON_FIELDS`) into `<targetDir>/.claude.json`.\n *\n * Inputs:\n * - `targetDir` — the per-launch mirror dir we own.\n * - `sourceDir` — the user's `~/.claude/` (subdir-level legacy path).\n * - `realHome` — the user's `$HOME`. Used to locate the **canonical**\n * `~/.claude.json` (HOME level), which is what Claude Code's path\n * resolver `qG` actually reads when `CLAUDE_CONFIG_DIR` is unset:\n * `path.join(process.env.CLAUDE_CONFIG_DIR || homedir(),\n * \".claude.json\")`. The mirror walk operates on `~/.claude/` and\n * thus can only see the subdir-level `~/.claude/.claude.json`\n * (often a stale shadow); the canonical HOME-level file — where\n * `claude mcp add` writes and Claude Code reads on default\n * invocations — would otherwise be invisible to the proxy session.\n *\n * Behaviour:\n * - Mirror's `.claude.json` is a non-regular file (symlink, dir,\n * etc.) → log warn, refuse to touch. The postcondition check in\n * `ensureClaudeConfigMirror` then throws, blocking launch — the\n * user investigates the warn-flagged path rather than landing in\n * the OAuth wizard.\n * - Read existing content from the first VALID source, in priority\n * order:\n * (a) `~/.claude.json` HOME-level (canonical Claude Code path)\n * (b) The mirror file itself (snapshot-walk's product; only\n * consulted if it's a regular file per step 1)\n * (c) `~/.claude/.claude.json` subdir-level (legacy)\n * A candidate that's missing (ENOENT), too large (> 50 MiB cap),\n * or fails to parse as a non-empty object is **skipped** — the\n * loop falls through to the next candidate, so a malformed\n * higher-priority source can't drop valid lower-priority content\n * on the floor. HOME-level non-ENOENT read failures warn\n * (user-visible — corp perms / OneDrive). All other failures\n * debug-log. `fs.readFile` follows symlinks; reading user-owned\n * content into a per-launch user-owned dir is not a privilege\n * escalation (same uid).\n * - Merge: **force-override** all three synthetic fields\n * unconditionally — `hasCompletedOnboarding`,\n * `bypassPermissionsModeAccepted`, AND `oauthAccount`. A\n * within-proxy \"log out\" is impossible by design. `oauthAccount`\n * is overwritten even when the user has a real one, because\n * pairing the synthetic OAuth token (from `.credentials.json`)\n * with a real-user identity blob creates a split-brain auth\n * state that Claude Code's identity-vs-token cross-checks may\n * treat as session corruption (triggering re-login → defeats\n * the fix). The user's real identity remains intact in their\n * own `~/.claude.json`; only the proxy session sees synthetic.\n * `Object.assign(Object.create(null), existing)` instead of spread\n * defuses the `__proto__` prototype-pollution sink from JSON.parse.\n * - Idempotency: skip the write IFF the source we read FROM is the\n * mirror itself AND the current mirror content is byte-equivalent\n * to what we'd write. Bytes-on-disk comparison is robust against\n * filesystem mtime resolution (FAT/exFAT 2 s buckets) and the\n * source-presence-vs-mirror-presence conflation that broke\n * round-2's `needsWrite = !hadExisting` heuristic.\n *\n * Atomic write: tempfile (per-PID + 4-byte random suffix matching\n * `injectPeerMcpIntoMirror`'s pattern, so collision is effectively\n * impossible) + rename, mode 0o600. Pre-chmod the destination before\n * rename to defuse Windows read-only attribute issues. Any write\n * error throws to the caller — `src/claude.ts` fails the launch\n * loudly. Combined with the postcondition check, every silent-\n * degradation path is converted to a fail-loud launch error.\n */\nasync function injectSyntheticClaudeJsonFields(\n targetDir: string,\n sourceDir: string,\n realHome: string,\n): Promise<void> {\n const claudeJsonPath = path.join(targetDir, \".claude.json\")\n\n // 1. lstat the mirror path before touching it (defense-in-depth,\n // matching the marker-write at step 7). If something other than a\n // regular file occupies the slot, refuse — `fs.readFile` /\n // `chmodIfPossible` / `fs.rename` would otherwise follow / clobber\n // in surprising ways. The postcondition then throws.\n let mirrorIsRegularFile = false\n try {\n const mirrorStat = await fs.lstat(claudeJsonPath)\n if (mirrorStat.isFile()) {\n mirrorIsRegularFile = true\n } else {\n consola.warn(\n `ensureClaudeConfigMirror: ${claudeJsonPath} exists but is not a regular file (mode=${mirrorStat.mode.toString(8)}); refusing to inject synthetic fields. Inspect and remove manually if safe.`,\n )\n return\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.warn(\n `ensureClaudeConfigMirror: cannot lstat ${claudeJsonPath}; refusing to inject synthetic fields (overwriting an unreadable file would risk destroying user content):`,\n err,\n )\n return\n }\n // ENOENT: mirror file doesn't exist yet — fine; step 2 skips the\n // mirror candidate and step 4 creates a new file.\n }\n\n // 2. Read existing content from the first VALID source. Priority:\n // (a) `~/.claude.json` HOME-level (canonical Claude Code path)\n // (b) Mirror file (the snapshot-walk's product; only if regular)\n // (c) `~/.claude/.claude.json` subdir-level (legacy)\n // A candidate that's missing (ENOENT) or fails to parse as a\n // non-empty object is SKIPPED — the loop falls through to the\n // next candidate. This is the load-bearing fix for \"corrupt HOME\n // blocks valid subdir\": a zero-byte or malformed HOME file no\n // longer drops the user's good subdir content on the floor.\n // Size cap (`MAX_CLAUDE_JSON_BYTES`) skips absurdly large files\n // that would risk OOM. HOME read failures (non-ENOENT) warn\n // loudly — the canonical path failing is user-visible. Mirror /\n // subdir read failures debug-log and fall through.\n // `fs.readFile` follows symlinks; reading user-owned content\n // into a per-launch user-owned dir is not a privilege escalation.\n let existing: Record<string, unknown> = {}\n let selectedSource: string | null = null\n const homeLevelClaudeJson = path.join(realHome, \".claude.json\")\n const subdirClaudeJson = path.join(sourceDir, \".claude.json\")\n const sourceCandidates: Array<string | null> = [\n homeLevelClaudeJson,\n mirrorIsRegularFile ? claudeJsonPath : null,\n subdirClaudeJson,\n ]\n for (const sourceCandidate of sourceCandidates) {\n if (sourceCandidate === null) continue\n try {\n const stat = await fs.stat(sourceCandidate)\n if (stat.size > MAX_CLAUDE_JSON_BYTES) {\n consola.warn(\n `ensureClaudeConfigMirror: ${sourceCandidate} is ${stat.size} bytes (> ${MAX_CLAUDE_JSON_BYTES} cap); skipping this source to avoid OOM. Inspect for accidental log/state accumulation.`,\n )\n continue\n }\n const raw = await fs.readFile(sourceCandidate, \"utf8\")\n const parsed = parseExistingClaudeJson(raw, sourceCandidate)\n if (parsed === null) {\n // Malformed (parse failure or non-object). Skip; try next candidate.\n continue\n }\n existing = parsed\n selectedSource = sourceCandidate\n consola.debug(\n `ensureClaudeConfigMirror: read .claude.json content from ${sourceCandidate}`,\n )\n break\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === \"ENOENT\") {\n // Normal case — skip silently.\n continue\n }\n // Non-ENOENT read/stat failure. Severity depends on which source:\n // - HOME-level (canonical): warn (user-visible — corp perms,\n // OneDrive cloud-only, EACCES, etc.). Fall through, since\n // the gate-injection still needs to happen.\n // - Mirror / subdir: debug. Mirror failures used to abort,\n // but that locked out the still-valid subdir-level fallback;\n // we can always re-create the mirror from any valid source.\n if (sourceCandidate === homeLevelClaudeJson) {\n consola.warn(\n `ensureClaudeConfigMirror: cannot read canonical ${homeLevelClaudeJson}; falling back to other sources. User-scope MCPs added via 'claude mcp add' may be invisible to the proxy session:`,\n err,\n )\n } else {\n consola.debug(\n `ensureClaudeConfigMirror: cannot read source ${sourceCandidate}:`,\n err,\n )\n }\n // Fall through to next candidate. If all miss/fail, proceed\n // with synthetic-only content (existing stays {}).\n }\n }\n\n // 3. Build merged content. `Object.assign(Object.create(null), ...)`\n // instead of `{...existing}` avoids the `__proto__` prototype-\n // pollution sink (`JSON.parse('{\"__proto__\":{...}}')` sets the\n // object's prototype via defineProperty; spreading then triggers\n // the standard `__proto__` setter and mutates merged's prototype\n // chain. Cheap defense, prevents the entire class.\n //\n // All three gate fields are FORCE-OVERRIDDEN regardless of\n // existing value:\n // - `hasCompletedOnboarding` and `bypassPermissionsModeAccepted`\n // are the load-bearing gates we exist to set; a within-proxy\n // \"log out\" is impossible by design.\n // - `oauthAccount` is overwritten unconditionally with the\n // synthetic identity (NOT preserved even when real). Pairing\n // the synthetic OAuth token (from `.credentials.json`) with\n // a real-user `accountUuid` / `organizationUuid` creates a\n // split-brain auth state where Claude Code's identity-vs-\n // token cross-checks (binary `oauthAccount?.accountUuid`\n // consumers) may treat the mismatch as session corruption\n // and trigger re-login — the exact bug we're preventing.\n // The user's real identity remains intact in their own\n // `~/.claude.json`; only the proxy session sees synthetic.\n const merged: Record<string, unknown> = Object.assign(\n Object.create(null) as Record<string, unknown>,\n existing,\n )\n merged.hasCompletedOnboarding = SYNTHETIC_CLAUDE_JSON_FIELDS.hasCompletedOnboarding\n merged.bypassPermissionsModeAccepted = SYNTHETIC_CLAUDE_JSON_FIELDS.bypassPermissionsModeAccepted\n merged.oauthAccount = SYNTHETIC_CLAUDE_JSON_FIELDS.oauthAccount\n\n // 4. Decide if a write is needed. Idempotency contract: skip the\n // write IFF we read FROM the mirror itself AND the current mirror\n // content is byte-equivalent to what we'd write. Bytes-on-disk\n // comparison (vs. inferring from source state) is robust against:\n // - source-presence ≠ mirror-presence (a fully-onboarded HOME\n // file with no mirror yet must still trigger a mirror write)\n // - HOME content newer than mirror (HOME wins → mirror is\n // always rewritten when source was HOME)\n // - filesystem mtime resolution (FAT/exFAT 2s buckets)\n const desiredJson = JSON.stringify(merged, null, 2) + \"\\n\"\n if (selectedSource === claudeJsonPath) {\n try {\n const currentMirrorJson = await fs.readFile(claudeJsonPath, \"utf8\")\n if (currentMirrorJson === desiredJson) {\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n return\n }\n } catch {\n // Mirror read failed between step 2's success and now — fall\n // through to write. Worst case we rewrite identical content.\n }\n }\n\n // 5. Atomic temp + rename. Mode 0o600. Per-PID + 4-byte random\n // suffix matches `injectPeerMcpIntoMirror`'s pattern. Any write\n // error throws to the caller — `src/claude.ts` exits the launch\n // with a loud error message rather than silently degrading to\n // the OAuth wizard.\n //\n // Pre-chmod the destination before rename to defuse Windows\n // read-only attribute issues (FAT/exFAT volumes, corp-managed\n // perms): `fs.rename` over a read-only file can fail with EPERM\n // on Windows even though POSIX would silently succeed.\n //\n // Concurrent-racer tolerance: when multiple `ensureClaudeConfigMirror()`\n // calls run in parallel (e.g., from tests, or a future caller),\n // Windows `fs.rename` can fail with EBUSY/EPERM/EACCES if\n // another racer has the destination open or mid-write. The\n // write content is fully deterministic (same SYNTHETIC fields,\n // same force-override merge of the same source-priority list),\n // so a successful rename by ANY racer produces the same bytes\n // we'd produce. On rename failure, verify the on-disk content\n // matches `desiredJson` — if so, accept silently (another\n // racer won the race; the file is correct).\n const tempPath = `${claudeJsonPath}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`\n try {\n await fs.writeFile(tempPath, desiredJson, { mode: 0o600, flag: \"wx\" })\n if (mirrorIsRegularFile) {\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n }\n await fs.rename(tempPath, claudeJsonPath)\n } catch (err) {\n await fs.unlink(tempPath).catch(() => {})\n try {\n const observed = await fs.readFile(claudeJsonPath, \"utf8\")\n if (observed === desiredJson) {\n consola.debug(\n `ensureClaudeConfigMirror: rename failed but mirror already holds expected content (concurrent racer won the race):`,\n err,\n )\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n return\n }\n } catch {\n // Fall through to rethrow the original error.\n }\n throw err\n }\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n}\n\n/**\n * Size cap on candidate `.claude.json` reads in\n * `injectSyntheticClaudeJsonFields`. Real-world files are KB-MB scale;\n * if a `.claude.json` has grown to hundreds of MB (corruption, runaway\n * project history accumulation, accidental log redirect) reading it\n * blocks the event loop and risks OOM. 50 MiB is comfortably above\n * any legitimate Claude Code state and well below typical RSS budgets.\n */\nconst MAX_CLAUDE_JSON_BYTES = 50 * 1024 * 1024\n\n/**\n * Structural check for `oauthAccount`: must be a non-null, non-array\n * object whose `accountUuid` AND `organizationUuid` are non-empty\n * strings. Empty objects / partial blobs (missing UUIDs) do NOT count\n * as \"usable\" — preserving them would let Claude Code's various\n * \"logged in as X\" UIs read undefined sub-fields and (in some flows)\n * fall back to a re-login prompt that defeats the synthetic-credential\n * fix. Field names match the binary's optional-chain reads\n * (`oauthAccount?.accountUuid`, `oauthAccount?.organizationUuid`).\n */\nfunction isUsableOauthAccount(value: unknown): boolean {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false\n const obj = value as Record<string, unknown>\n const accountUuid = obj.accountUuid\n const organizationUuid = obj.organizationUuid\n return (\n typeof accountUuid === \"string\"\n && accountUuid.length > 0\n && typeof organizationUuid === \"string\"\n && organizationUuid.length > 0\n )\n}\n\n/**\n * Parse a JSON blob expected to be an object. Returns `null` on parse\n * failure or non-object/array value (so callers can SKIP this source\n * rather than treating an empty object as a successful read — a\n * round-1 finding from gemini-critic). Logs a warn so the user can\n * investigate. Used in `injectSyntheticClaudeJsonFields`'s source-\n * priority loop: a malformed HOME-level file falls through to the\n * mirror or subdir candidate instead of dropping valid lower-priority\n * content on the floor.\n */\nfunction parseExistingClaudeJson(\n raw: string,\n contextPath: string,\n): Record<string, unknown> | null {\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n consola.warn(\n `ensureClaudeConfigMirror: ${contextPath} parsed to non-object `\n + `(typeof=${typeof parsed}); skipping this source.`,\n )\n return null\n } catch (err) {\n consola.warn(\n `ensureClaudeConfigMirror: cannot parse ${contextPath} as JSON; `\n + `skipping this source:`,\n err,\n )\n return null\n }\n}\n\n/**\n * Postcondition for `ensureClaudeConfigMirror`: re-reads the mirror's\n * `.claude.json` and verifies that both boolean gate fields actually\n * landed. Converts every \"warn and return\" branch in\n * `injectSyntheticClaudeJsonFields` (non-regular target, non-ENOENT\n * read failure, postcondition lstat race) into a launch-failing\n * throw — `src/claude.ts:197`'s catch path turns it into a clear\n * `process.exit(1)` rather than silently spawning Claude Code into\n * the OAuth wizard. The whole point of this subsystem is preventing\n * that silent degradation; a warn that scrolls by in the launch log\n * is not enough.\n *\n * Cheap (one extra readFile + JSON.parse on a small file we just\n * wrote) and load-bearing: every future contributor who adds a new\n * bail-out branch in the helper above automatically gets fail-loud\n * for free instead of having to remember to thread the failure\n * upward by hand.\n */\nasync function assertOnboardingGateInjected(targetDir: string): Promise<void> {\n const claudeJsonPath = path.join(targetDir, \".claude.json\")\n\n // 1. lstat first — never follow a symlink. A planted symlink at the\n // mirror path pointing at an attacker-controlled file with valid\n // gate fields would otherwise pass a plain readFile check and let\n // the helper's \"non-regular target\" warn-and-return slip through.\n let lst: import(\"node:fs\").Stats\n try {\n lst = await fs.lstat(claudeJsonPath)\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — cannot lstat ${claudeJsonPath} after injection (synthetic onboarding-skip fields are required to prevent Claude Code's first-launch wizard, which would defeat the synthetic credential): ${(err as Error).message}`,\n )\n }\n if (!lst.isFile()) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} exists but is not a regular file (mode=${lst.mode.toString(8)}). Check the preceding warn-level log lines for the underlying cause; without a regular file holding the gate fields, Claude Code's first-launch wizard fires and defeats the synthetic credential.`,\n )\n }\n\n let raw: string\n try {\n raw = await fs.readFile(claudeJsonPath, \"utf8\")\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — cannot read ${claudeJsonPath} after injection: ${(err as Error).message}`,\n )\n }\n let parsed: Record<string, unknown>\n try {\n const json = JSON.parse(raw) as unknown\n if (!json || typeof json !== \"object\" || Array.isArray(json)) {\n throw new Error(`parsed to non-object (typeof=${typeof json})`)\n }\n parsed = json as Record<string, unknown>\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} is not a valid JSON object after injection: ${(err as Error).message}`,\n )\n }\n if (\n parsed.hasCompletedOnboarding !== true\n || parsed.bypassPermissionsModeAccepted !== true\n ) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} is missing required gate fields after injection (hasCompletedOnboarding=${String(parsed.hasCompletedOnboarding)}, bypassPermissionsModeAccepted=${String(parsed.bypassPermissionsModeAccepted)}). Check the preceding warn-level log lines for the underlying cause; without these fields Claude Code's first-launch wizard fires and defeats the synthetic credential.`,\n )\n }\n // Reuse `isUsableOauthAccount` so a future change to the structural\n // definition stays consistent across helper write-decision and\n // postcondition. The synthetic blob we always write satisfies this\n // by construction; a regression that drops oauthAccount during the\n // merge would otherwise slip past (per the round-2 codex/opus/\n // gemini-3-lab finding).\n if (!isUsableOauthAccount(parsed.oauthAccount)) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} has invalid oauthAccount after injection (got ${JSON.stringify(parsed.oauthAccount)}). The synthetic identity blob is required so Claude Code's identity-vs-token cross-checks don't trigger a re-login that defeats the synthetic credential.`,\n )\n }\n}\n\n/**\n * Recursive snapshot-copy helper for `ensureClaudeConfigMirror`. Walks\n * `sourceDir/relPath` and mirrors each entry into `targetDir/relPath`.\n * - Top-level entries are dispatched on `policyFor(name)`:\n * - `ISOLATED` → skipped entirely (no presence in mirror).\n * - `SHARED` → skipped from the copy walk; handled by\n * `ensureSharedSymlink` in the post-copy phase.\n * - `MIRRORED` → copied as today.\n * - Symlinks are skipped (not recreated) so the walk never follows out\n * of `sourceDir` and we don't reintroduce a confused-deputy vector.\n * - Files copy only if source mtime > target mtime (idempotent).\n */\nasync function mirrorDirRecursive(\n sourceDir: string,\n targetDir: string,\n relPath: string,\n): Promise<void> {\n const sourcePath = path.join(sourceDir, relPath)\n let entries: Array<string>\n try {\n entries = await fs.readdir(sourcePath)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n consola.debug(`mirrorDirRecursive: cannot readdir ${sourcePath}:`, err)\n return\n }\n for (const name of entries) {\n // Policy dispatch at top-level only. Sub-paths within MIRRORED\n // dirs always cascade as MIRRORED.\n if (relPath === \"\") {\n const policy = policyFor(name)\n if (policy === \"ISOLATED\" || policy === \"SHARED\") continue\n }\n const childRel = relPath === \"\" ? name : path.join(relPath, name)\n const childSource = path.join(sourceDir, childRel)\n const childTarget = path.join(targetDir, childRel)\n let stats: Awaited<ReturnType<typeof fs.lstat>>\n try {\n stats = await fs.lstat(childSource)\n } catch (err) {\n consola.debug(`mirrorDirRecursive: cannot lstat ${childSource}:`, err)\n continue\n }\n if (stats.isSymbolicLink()) {\n // Skip symlinks during mirror copy. gemini-critic security finding:\n // recreating user symlinks in our mirror creates a confused-deputy\n // vector — a previously prompt-injected process could place\n // `~/.claude/<X>` → `/some/sensitive/file`, our walker would mirror\n // it, and any subsequent write to `<mirror>/<X>` (by us or by\n // Claude Code) would follow the symlink and overwrite the target.\n // Snapshot-copy semantics make symlink preservation moot anyway:\n // a snapshot is a point-in-time content copy, and a symlink\n // recreated in the mirror points at exactly the same target as\n // the original would have — the user-side symlink is sufficient.\n // If a user has a legitimate need for a symlink to be visible\n // through the proxy session, they can create the equivalent\n // symlink in their `~/.claude/` directly and it'll be reachable\n // — they just won't see it in our mirror dir.\n consola.debug(`mirrorDirRecursive: skipping symlink ${childSource} (security policy)`)\n continue\n }\n if (stats.isDirectory()) {\n await fs.mkdir(childTarget, { recursive: true })\n await mirrorDirRecursive(sourceDir, targetDir, childRel)\n continue\n }\n if (stats.isFile()) {\n // mtime-based skip — only copy if source is newer than target.\n let needsCopy = true\n try {\n const targetStat = await fs.lstat(childTarget)\n if (targetStat.isFile() && targetStat.mtimeMs >= stats.mtimeMs) {\n needsCopy = false\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`mirrorDirRecursive: lstat target ${childTarget}:`, err)\n }\n }\n if (!needsCopy) continue\n try {\n await fs.copyFile(childSource, childTarget, fs.constants.COPYFILE_FICLONE)\n } catch (err) {\n consola.debug(`mirrorDirRecursive: copy ${childSource} → ${childTarget}:`, err)\n }\n continue\n }\n // Skip other inode types (sockets, devices, fifos) silently.\n }\n}\n\n/**\n * Create or refresh a directory symlink `<mirrorDir>/<name>` →\n * `<sourceDir>/<name>` (i.e. `~/.local/share/github-router/claude-config/<X>`\n * → `~/.claude/<X>`). Idempotent and concurrent-safe.\n *\n * Behavior depending on what's already at `<mirrorDir>/<name>`:\n * - Symlink with the correct target → no-op.\n * - Symlink with the wrong target → replace atomically.\n * - Empty real directory (legacy mirror leftover with no proxy-session\n * writes accumulated yet) → `rmdir` and replace with the symlink.\n * Safe by definition: `fs.rmdir` only succeeds on empty dirs (POSIX),\n * so there is nothing to lose. Smooths the upgrade path for users\n * whose legacy mirror dirs were never written to.\n * - Non-empty real directory or regular file → loud-warn and skip.\n * Auto-deleting would destroy proxy-session writes from the prior\n * version. The user is told the exact path and remediation.\n * - ENOENT → create symlink atomically.\n *\n * Atomic-creation: symlinks are first written at a unique side-path\n * (`<mirrorDir>/<name>.tmp.<pid>.<8 hex>`) and then `fs.rename()`d into\n * place. POSIX `rename` is atomic and replaces an existing symlink in\n * a single step, so two concurrent `github-router claude` startups can't\n * race to `EEXIST` — the loser's rename just overwrites the winner's\n * symlink with an identical one. Gemini-critic 3-lab-review finding.\n *\n * Pre-creates `~/.claude/<name>/` as a real directory if missing so\n * Claude Code's writes through the symlink don't fail with ENOENT.\n */\nasync function ensureSharedSymlink(\n name: string,\n sourceDir: string,\n mirrorDir: string,\n): Promise<void> {\n const sourcePath = path.join(sourceDir, name)\n const mirrorPath = path.join(mirrorDir, name)\n\n // 1. Ensure the source directory exists. Without this, Claude Code's\n // writes through the symlink (e.g. `projects/<hash>/foo.jsonl`)\n // fail with ENOENT on the parent dir.\n try {\n await fs.mkdir(sourcePath, { recursive: true })\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the symlink and rename catches below):\n // if the source dir cannot be created (e.g. a stray regular file\n // sitting at `~/.claude/projects`, perms blocking mkdir on a\n // corp-managed Windows box, OneDrive cloud-only reparse point),\n // ensureSharedSymlink returns without creating a junction. The\n // spawned Claude Code child then writes to the REAL `~/.claude`\n // while the proxy reads from the mirror — exactly the split-brain\n // pattern this whole function exists to prevent. Silent debug-log\n // hid this from us once already; warn so the user sees the cause.\n consola.warn(\n `ensureSharedSymlink(${name}): cannot mkdir source ${sourcePath}:`,\n err,\n )\n return\n }\n\n // 2. Inspect the mirror-side slot.\n let existing: Awaited<ReturnType<typeof fs.lstat>> | null = null\n try {\n existing = await fs.lstat(mirrorPath)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the other fs catches in this function):\n // ENOENT is the only expected-and-benign failure mode here\n // (slot doesn't exist yet — falls through to create). Any other\n // lstat failure (EACCES, ELOOP, EIO from a sketchy reparse\n // point) means we bail without creating the junction, which\n // silently leaves the proxy and child diverged. A visible warn\n // surfaces the root cause instead of a mysteriously missing\n // junction.\n consola.warn(\n `ensureSharedSymlink(${name}): cannot lstat ${mirrorPath}:`,\n err,\n )\n return\n }\n }\n\n if (existing?.isSymbolicLink()) {\n // Resolve both sides to their canonical absolute paths and compare.\n // We use `fs.realpath` rather than the raw `fs.readlink()` output\n // because Windows junctions resolve via readlink to `\\\\?\\`-prefixed\n // device-namespace paths (e.g. `\\\\?\\C:\\Users\\foo\\.claude\\projects`)\n // while we wrote the plain absolute `sourcePath` (e.g.\n // `C:\\Users\\foo\\.claude\\projects`) with `fs.symlink`. A literal\n // `===` on the raw readlink output never matched on Windows, so\n // the fast path silently failed and every startup tore down +\n // recreated all 9 SHARED junctions — masked locally because NTFS\n // File System Tunneling forges the creation timestamp for a name\n // deleted and recreated within 15 s (the per-startup churn was\n // real, the ctime-stable assertion was a false negative). The\n // realpath comparison canonicalizes both forms to the same string\n // on POSIX and Windows alike, and as a bonus handles drive-letter\n // casing / trailing-slash differences too. The extra two syscalls\n // per slot are negligible at proxy startup (runs once per launch).\n //\n // CRITICAL: sourceReal and currentReal are NOT treated symmetrically.\n // If `sourceReal` is null (we just mkdir'd it above, but realpath\n // failed — OneDrive cloud-only reparse point, EACCES on parent,\n // EXDEV mount oddity), we WARN AND RETURN rather than fall through.\n // Falling through would do unlink+symlink+rename with the same\n // failing realpath next launch — silent every-startup churn, the\n // exact bug class round-3 G2 fixed in a different code path.\n // `currentReal === null` is benign (broken/wrong slot — replace).\n const sourceReal = await fs.realpath(sourcePath).catch(() => null)\n if (sourceReal === null) {\n consola.warn(\n `ensureSharedSymlink(${name}): cannot resolve source ${sourcePath} ` +\n `— skipping junction creation to avoid silent every-startup churn. ` +\n `Inspect the source dir's permissions / OneDrive sync state and re-launch.`,\n )\n return\n }\n const currentReal = await fs.realpath(mirrorPath).catch(() => null)\n if (currentReal !== null && currentReal === sourceReal) {\n return\n }\n // Wrong target (or unresolvable mirror) — fall through to the\n // atomic-rename replace path.\n } else if (existing?.isDirectory()) {\n // Legacy real directory at the slot. Try `fs.rmdir` — on POSIX it\n // succeeds ONLY if the directory is empty, so there's nothing to\n // lose. If it's non-empty (ENOTEMPTY) or any other failure occurs,\n // fall back to the warn-and-skip path so we never auto-clobber\n // user data.\n try {\n await fs.rmdir(mirrorPath)\n // Empty dir reaped — fall through to the atomic-rename create path.\n } catch (err) {\n consola.warn(\n `ensureClaudeConfigMirror: ${mirrorPath} is a non-empty real directory ` +\n `from an older github-router version; refusing to clobber. ` +\n `If you want chat-history continuity for \"${name}\", move its ` +\n `contents into ${sourcePath}/ then delete ${mirrorPath}; the ` +\n `mirror will create a symlink (junction on Windows) on next launch. ` +\n `(rmdir error: ${(err as NodeJS.ErrnoException).code ?? \"unknown\"})`,\n )\n return\n }\n } else if (existing) {\n // Regular file (or special inode like a socket) — never auto-clobber.\n consola.warn(\n `ensureClaudeConfigMirror: ${mirrorPath} is a regular file at a ` +\n `SHARED symlink slot; refusing to clobber. Inspect and remove ` +\n `manually if safe; the mirror will create a symlink on next launch.`,\n )\n return\n }\n\n // 3. Atomic-rename creation: symlink to a unique temp path, then\n // rename over the slot. `fs.rename` replaces existing symlinks\n // atomically on POSIX and is safe against concurrent racers.\n // On Windows, MoveFileEx with MOVEFILE_REPLACE_EXISTING does NOT\n // replace an existing directory or junction destination\n // (npm/cli#9021), so when the slot already holds a wrong-target\n // junction we must explicitly unlink it first. The sub-millisecond\n // window of no-link is acceptable: ensureClaudeConfigMirror is\n // idempotent under concurrency and only runs at proxy startup,\n // before any spawned Claude Code child has been launched.\n const tempPath = `${mirrorPath}.tmp.${process.pid}.${randomBytes(4).toString(\"hex\")}`\n try {\n await fs.symlink(\n sourcePath,\n tempPath,\n process.platform === \"win32\" ? \"junction\" : \"dir\",\n )\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\" rule:\n // the rule applies to ALL fs catches in this function, not just the\n // rename one. The temp path is per-pid + 8-hex random so EEXIST is\n // essentially impossible — any failure here (EPERM on Windows\n // without DevMode, EXDEV cross-volume, ENOSPC, …) is a real\n // operational problem the user needs to see.\n consola.warn(\n `ensureSharedSymlink(${name}): symlink ${tempPath} failed:`,\n err,\n )\n return\n }\n if (process.platform === \"win32\" && existing?.isSymbolicLink()) {\n // Windows-only: clear the wrong-target junction so the rename\n // below can land. Best-effort — if a concurrent racer already\n // unlinked it, the rename succeeds as a CREATE; if a concurrent\n // racer already replaced it with a fresh junction, the rename\n // hits the catch below and we surface a warn.\n await fs.unlink(mirrorPath).catch(() => {})\n }\n try {\n await fs.rename(tempPath, mirrorPath)\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the fs.symlink catch above): a silent\n // debug log here previously hid the Windows rename-replace bug\n // (junction-over-junction MoveFileEx EPERM). Post-fix, rename\n // failures should be rare and visible.\n consola.warn(\n `ensureSharedSymlink(${name}): rename ${tempPath} → ${mirrorPath} failed:`,\n err,\n )\n await fs.unlink(tempPath).catch(() => {})\n }\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath, fs.constants.W_OK)\n } catch {\n await fs.writeFile(filePath, \"\")\n await fs.chmod(filePath, 0o600)\n }\n}\n\nasync function chmodIfPossible(target: string, mode: number): Promise<void> {\n if (process.platform === \"win32\") return // Windows chmod is no-op-ish\n try {\n await fs.chmod(target, mode)\n } catch (err) {\n consola.debug(`chmod ${target} ${mode.toString(8)} failed:`, err)\n }\n}\n\n/**\n * Write a runtime tempfile securely.\n *\n * - Mode `0o600` so other local users (multi-tenant boxes, shared\n * dev containers) can't read the per-launch nonce or runtime URL.\n * - `flag: \"wx\"` (O_CREAT | O_EXCL | O_WRONLY) refuses to overwrite\n * an existing path. POSIX open(2) with O_EXCL also rejects\n * pre-placed symlinks, killing the symlink-clobber attack vector.\n * - The caller's responsibility to pick a path NOT yet in use.\n * We intentionally do NOT pre-unlink: an `lstat` + `unlink` +\n * `open(O_EXCL)` sequence still has a TOCTOU window where an\n * attacker can drop a symlink between unlink and open. Letting\n * `wx` fail is the safer behavior — surfaces the conflict\n * instead of silently following.\n */\nexport async function writeRuntimeFileSecure(\n filePath: string,\n content: string,\n): Promise<void> {\n await fs.writeFile(filePath, content, { mode: 0o600, flag: \"wx\" })\n}\n\n/**\n * Write the ai-or-die artifact creds (base/token/sessionId/insecureTLS) to a\n * mode-600 file in the per-launch mirror so the `internal-artifact-open` hook can\n * authenticate: AIORDIE_TOKEN is stripped from the spawned child env, and passing\n * a token via argv would leak it to `ps`. Insecure-TLS mirrors the artifact\n * client's loopback-only fail-closed policy. Best-effort; atomic temp+rename.\n */\nexport async function writeArtifactCredsToMirror(\n insecureTLS: boolean,\n env: NodeJS.ProcessEnv = process.env,\n): Promise<void> {\n const baseUrl = env.AIORDIE_BASE_URL\n const token = env.AIORDIE_TOKEN\n const sessionId = env.AIORDIE_SESSION_ID\n if (!baseUrl || !token || !sessionId) return\n const target = path.join(PATHS.CLAUDE_CONFIG_DIR, \".aiordie-artifact.json\")\n const tempPath = `${target}.${process.pid}.tmp`\n const body = JSON.stringify({ baseUrl, token, sessionId, insecureTLS }) + \"\\n\"\n await fs.writeFile(tempPath, body, { mode: 0o600, flag: \"wx\" }).catch(async (err) => {\n if ((err as NodeJS.ErrnoException).code === \"EEXIST\") return\n throw err\n })\n await fs.rename(tempPath, target).catch(() => {})\n await chmodIfPossible(target, 0o600)\n}\n\n/**\n * Sweep stale runtime tempfiles. Removes files whose embedded PID is no\n * longer a live process. A proxy crash (`kill -9`, OS reboot) leaves\n * orphans that would otherwise accumulate forever — and worse, a stale\n * config pointing at a now-recycled port could route MCP traffic to\n * whatever process bound that port next.\n *\n * Naming convention: `peer-mcp-<pid>.json` and `peer-agents-<pid>.json`.\n * Files not matching either pattern are left alone — this directory\n * is shared with future runtime artifacts.\n *\n * We deliberately do NOT age-prune files whose PID is alive. A\n * legitimately long-running proxy can have a tempfile older than any\n * arbitrary threshold; deleting it out from under the live process\n * breaks the spawned Claude Code child's MCP/agent wiring with no clean\n * recovery. PID-wraparound risk is mitigated by (a) PID reuse on Linux\n * being slow under typical loads, and (b) the file is only consulted by\n * github-router itself — an unrelated process that inherits the PID\n * never reads it.\n */\nexport async function sweepStaleRuntimeFiles(): Promise<void> {\n const dir = PATHS.CLAUDE_RUNTIME_DIR\n let entries: Array<string>\n try {\n entries = await fs.readdir(dir)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n\n for (const name of entries) {\n // Match both legacy `peer-mcp-<pid>.json` and current\n // `peer-mcp-<pid>-<rand>.json` filenames so we can clean up either.\n const match = /^peer-(?:mcp|agents)-(\\d+)(?:-[0-9a-f]+)?\\.json$/.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n const filePath = path.join(dir, name)\n\n if (isPidAlive(pid)) continue\n\n await fs.unlink(filePath).catch(() => {\n // already gone or unreadable, fine\n })\n }\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n // signal 0 = check existence without delivering a signal. EPERM\n // means the process exists but we can't signal it (which is still\n // \"alive\" for our purposes); ESRCH means it's gone.\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Sweep stale peer-* subagent .md files from the router-owned\n * `CLAUDE_CONFIG_DIR/agents/`. Phase 2.5 writes one .md per peer agent\n * into Claude Code's agents directory (now our config dir's `agents/`\n * subdir, since `getClaudeCodeEnvVars` points `CLAUDE_CONFIG_DIR` at\n * `PATHS.CLAUDE_CONFIG_DIR`) so they appear in Claude Code's Task\n * `subagent_type` enum. Files are named `peer-<pid>-<rand>-<agentName>.md`\n * so this sweep can drop orphans from crashed prior proxy sessions\n * without touching the user's own .md files (which were copied into\n * the same dir during `ensureClaudeConfigMirror`).\n *\n * Same liveness rule as `sweepStaleRuntimeFiles`: only delete when the\n * file's embedded PID is no longer alive. Live PIDs keep their files —\n * a long-running proxy doesn't lose its agent registrations.\n *\n * Regex tightening (Phase 2.6, codex-critic + gemini-critic 2-lab finding):\n * the original sweep regex `^peer-(\\d+)(?:-[0-9a-f]+)?-.+\\.md$` was too\n * permissive — a user-authored `peer-12345-meeting-notes.md` matches\n * (`12345` = \"PID\", `-meeting-notes` = trailing `.+`) and would be\n * silently unlinked when 12345 happens to be a dead PID (overwhelmingly\n * likely). Tightened to require BOTH the 8-hex-char random suffix AND\n * an exact-match persona name suffix, eliminating the risk for any\n * realistic user filename.\n */\nexport async function sweepStalePeerAgentMdFiles(): Promise<void> {\n const dir = path.join(PATHS.CLAUDE_CONFIG_DIR, \"agents\")\n let entries: Array<string>\n try {\n entries = await fs.readdir(dir)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n for (const name of entries) {\n const match = PEER_AGENT_MD_FILENAME.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n if (isPidAlive(pid)) continue\n await fs.unlink(path.join(dir, name)).catch(() => {\n // already gone or unreadable, fine\n })\n }\n}\n\n/**\n * Strict regex matching only files this proxy writes:\n * peer-<pid>-<8 hex>-<exact persona/coordinator/native/dispatcher name>.md\n * The name allowlist is the load-bearing protection against deleting user\n * files. Update this list whenever a new persona is added to `PERSONAS_READ` /\n * `PERSONAS_WRITE` in `peer-mcp-personas.ts`, a new coordinator-style or native\n * subagent is added in `codex-mcp-config.ts`, or a new `worker-*` dispatcher mode\n * is added in `worker-dispatch.ts` (a drift test in tests/worker-dispatch pins\n * that the `ALL_DISPATCHER_AGENT_NAMES` are all covered here).\n */\nexport const PEER_AGENT_MD_FILENAME =\n /^peer-(\\d+)-[0-9a-f]{8}-(?:codex-critic|codex-reviewer|gemini-critic|gemini-reviewer|opus-critic|codex-implementer|peer-review-coordinator|implementer|debugger|qa-engineer|worker-explore|worker-implement|worker-review|worker-plan|worker-test|worker-browse)\\.md$/\n\n/**\n * Strict regex matching only per-launch claude-config mirror dirs this\n * proxy creates: `<pid>-<8 hex>`. Anchored to the entire entry name so\n * user-authored siblings under `<appDir>/claude-config/` (if any) are\n * untouchable. The PID prefix is what `sweepStaleClaudeConfigMirrors`\n * keys off; the 8-hex random suffix matches `randomBytes(4)` exactly\n * (no `?` — files created by a different shape are not ours).\n */\nconst CLAUDE_CONFIG_MIRROR_DIR = /^(\\d+)-[0-9a-f]{8}$/\n\n/**\n * Sweep stale per-launch CLAUDE_CONFIG_DIR mirrors left behind by\n * crashed prior proxy sessions. Symmetric to `sweepStalePeerAgentMdFiles`\n * — same liveness rule (only delete when the embedded PID is dead),\n * same strict regex (the dir-name allowlist is the load-bearing\n * protection against deleting user-authored siblings).\n *\n * Scans `<appDir>/claude-config/` (the parent of the per-launch dirs).\n * Each entry whose name matches `<pid>-<8 hex>` AND whose PID is no\n * longer alive is removed recursively. `fs.rm({recursive: true})`\n * walks the tree calling `unlink` on symlinks/junctions rather than\n * following them, so the SHARED junctions back to `~/.claude/<X>`\n * are removed without touching their targets.\n *\n * Tolerates missing parent dir (first-ever launch, or user wiped it).\n */\nexport async function sweepStaleClaudeConfigMirrors(): Promise<void> {\n const parent = path.join(appDir(), \"claude-config\")\n let entries: Array<string>\n try {\n entries = await fs.readdir(parent)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n for (const name of entries) {\n const match = CLAUDE_CONFIG_MIRROR_DIR.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n if (isPidAlive(pid)) continue\n await fs\n .rm(path.join(parent, name), { recursive: true, force: true })\n .catch((err) => {\n // Best-effort: stale-dir cleanup must never block startup.\n // Common failure modes (worth surviving silently): an EBUSY/EPERM\n // on Windows if a leftover handle is still open, or a stray\n // root-owned file inside the dir from a previous run with\n // different permissions.\n consola.debug(\n `sweepStaleClaudeConfigMirrors: cannot rm ${name}:`,\n err,\n )\n })\n }\n}\n\n/**\n * Remove THIS launch's per-launch CLAUDE_CONFIG_DIR on shutdown.\n * Best-effort: a failure here must not block process exit (the caller\n * wraps this in a `.catch`-equivalent via `launchChild`'s onShutdown\n * try/catch). Symmetric to `writePeerMcpRuntimeFiles`'s `cleanup()`:\n * we own this dir for the lifetime of the proxy, so removing it on\n * normal shutdown is correct; the boot-time sweep handles the\n * abnormal-exit case.\n *\n * `fs.rm({recursive: true})` removes SHARED junctions via unlink\n * (does NOT follow them into the user's real `~/.claude/<X>`).\n */\nexport async function removeOwnClaudeConfigMirror(): Promise<void> {\n const dir = PATHS.CLAUDE_CONFIG_DIR\n await fs.rm(dir, { recursive: true, force: true }).catch((err) => {\n consola.debug(`removeOwnClaudeConfigMirror: rm ${dir} skipped:`, err)\n })\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import { t as PATHS } from "./paths-BZPgxKUl.js";
2
- import { d as runCommandCapture, l as parseBoolEnv, n as isPidAlive, o as trackChild, p as runManagedExeCapture, r as registerColbertExitHandlers, t as getColbertInstanceUuid, u as resolveExecutable } from "./lifecycle-DrDEjx_c.js";
3
- import { i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-Dvr3pGGG.js";
1
+ import { t as PATHS } from "./paths-BO22pMUb.js";
2
+ import { d as runCommandCapture, l as parseBoolEnv, n as isPidAlive, o as trackChild, p as runManagedExeCapture, r as registerColbertExitHandlers, t as getColbertInstanceUuid, u as resolveExecutable } from "./lifecycle-D-1CYr1Y.js";
3
+ import { i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-bPdiXjYB.js";
4
4
  import { createRequire } from "node:module";
5
5
  import consola from "consola";
6
6
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -623,6 +623,13 @@ function normalizeModelId$1(id) {
623
623
  * version pinning (a dated catalog id matched at Step 1) always wins.
624
624
  * 6. Return as-is with a warning
625
625
  */
626
+ /**
627
+ * `[1m]`-downgrade warnings we've already emitted this process. `resolveModel`
628
+ * is called many times per launch (env setup, model validation, the banner,
629
+ * and — under `serve` — on every model-picker `/models` poll), so the downgrade
630
+ * notice is deduped to once per model id to avoid log spam.
631
+ */
632
+ const warnedOneMDowngrade = /* @__PURE__ */ new Set();
626
633
  function resolveModel(modelId) {
627
634
  const models = state.models?.data;
628
635
  if (!models) return modelId;
@@ -630,7 +637,10 @@ function resolveModel(modelId) {
630
637
  if (oneMMatch) {
631
638
  const stripped = oneMMatch[1];
632
639
  const resolved = resolveModel(stripped);
633
- 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.`);
640
+ if (!/-1m(?:$|-)/.test(resolved) && !warnedOneMDowngrade.has(modelId)) {
641
+ warnedOneMDowngrade.add(modelId);
642
+ 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.`);
643
+ }
634
644
  return resolved;
635
645
  }
636
646
  if (models.some((m) => m.id === modelId)) return modelId;
@@ -1145,6 +1155,25 @@ function collapsePathKeys(env) {
1145
1155
  return env;
1146
1156
  }
1147
1157
 
1158
+ //#endregion
1159
+ //#region src/lib/mcp-workspace-header.ts
1160
+ /** Header the per-session MCP headersHelper emits, carrying the calling Claude
1161
+ * session's working directory. The /mcp handler uses it as the default
1162
+ * `workspace` for repo-scoped tools so a machine-wide `serve` targets the
1163
+ * active repo, not the proxy launch cwd. */
1164
+ const MCP_WORKSPACE_HEADER = "X-GH-Workspace";
1165
+ /** stdout JSON the headersHelper prints; Claude merges it into the connection
1166
+ * headers. `cwd` is the helper's own process cwd = the session's project dir. */
1167
+ function buildWorkspaceHeaderJson(cwd) {
1168
+ return JSON.stringify({ [MCP_WORKSPACE_HEADER]: cwd });
1169
+ }
1170
+ /** Command string Claude runs as the headersHelper. Mirrors
1171
+ * buildPromptSubmitHookCommand's quoting (src/lib/orchestration/prompt-submit-hook.ts). */
1172
+ function buildWorkspaceHeaderHelperCommand(execPath, scriptPath) {
1173
+ const q = (s) => `"${s}"`;
1174
+ return scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)} internal-workspace-header` : `${q(execPath)} internal-workspace-header`;
1175
+ }
1176
+
1148
1177
  //#endregion
1149
1178
  //#region src/lib/insecure-tls.ts
1150
1179
  const IS_BUN$1 = typeof globalThis.Bun !== "undefined";
@@ -15582,6 +15611,7 @@ async function provisionAndIndexColbert(opts = {}) {
15582
15611
  return;
15583
15612
  }
15584
15613
  if (!provisioned) return;
15614
+ if (opts.skipCwdIndex) return;
15585
15615
  const cwd = opts.cwd ?? process$1.cwd();
15586
15616
  try {
15587
15617
  if ((await gitState(cwd)).isRepo && await startupKickAllowed(cwd)) kickBackgroundInit(cwd);
@@ -16901,7 +16931,7 @@ function logAudit$1(record) {
16901
16931
  try {
16902
16932
  const fs$2 = await import("node:fs/promises");
16903
16933
  const path$1 = await import("node:path");
16904
- const { PATHS: PATHS$1 } = await import("./paths-CDYvyApX.js");
16934
+ const { PATHS: PATHS$1 } = await import("./paths-B5k78n0d.js");
16905
16935
  const dir = path$1.join(PATHS$1.APP_DIR, "browser-mcp");
16906
16936
  await fs$2.mkdir(dir, { recursive: true });
16907
16937
  const line = JSON.stringify({
@@ -24233,7 +24263,13 @@ function logTelemetry(t) {
24233
24263
  if (t.errorMessage) parts.push(`error=${JSON.stringify(t.errorMessage)}`);
24234
24264
  process.stderr.write(parts.join(" ") + "\n");
24235
24265
  }
24236
- async function handleToolsCall(body, scope) {
24266
+ function toolAcceptsWorkspace(tool$1) {
24267
+ return tool$1.capability === "worker" || tool$1.toolNameHttp === "code" || tool$1.toolNameHttp === "run_workflow";
24268
+ }
24269
+ function applySessionWorkspace(args, sessionWorkspace, tool$1) {
24270
+ if ((!tool$1 || toolAcceptsWorkspace(tool$1)) && typeof sessionWorkspace === "string" && sessionWorkspace.length > 0 && nodePath.isAbsolute(sessionWorkspace) && (args.workspace === void 0 || args.workspace === "")) args.workspace = sessionWorkspace;
24271
+ }
24272
+ async function handleToolsCall(body, scope, sessionWorkspace) {
24237
24273
  const params = body.params ?? {};
24238
24274
  const name = typeof params.name === "string" ? params.name : "";
24239
24275
  const args = params.arguments ?? {};
@@ -24296,6 +24332,7 @@ async function handleToolsCall(body, scope) {
24296
24332
  const telemetryName = persona ? persona.agentName : nonPersonaTool.toolNameHttp;
24297
24333
  const telemetryModel = persona ? persona.model : "(non-persona)";
24298
24334
  try {
24335
+ if (nonPersonaTool) applySessionWorkspace(args, sessionWorkspace, nonPersonaTool);
24299
24336
  const result = persona ? await callPersona(persona, personaPrompt, personaContext, personaEffort, aborter?.signal) : await nonPersonaTool.handler(args, aborter?.signal);
24300
24337
  logTelemetry({
24301
24338
  name: telemetryName,
@@ -24341,7 +24378,7 @@ function handleCancelledNotification(body) {
24341
24378
  }
24342
24379
  cancelInflight(requestId, "client requested cancellation");
24343
24380
  }
24344
- async function handleRpc(_c, body, scope) {
24381
+ async function handleRpc(_c, body, scope, sessionWorkspace) {
24345
24382
  if (body === null || typeof body !== "object" || Array.isArray(body)) return {
24346
24383
  status: 200,
24347
24384
  body: rpcError(null, RPC_INVALID_REQUEST, "jsonrpc 2.0 envelope required")
@@ -24392,7 +24429,7 @@ async function handleRpc(_c, body, scope) {
24392
24429
  };
24393
24430
  return {
24394
24431
  status: 200,
24395
- body: await handleToolsCall(body, scope)
24432
+ body: await handleToolsCall(body, scope, sessionWorkspace)
24396
24433
  };
24397
24434
  case "resources/list":
24398
24435
  if (isNotification) return {
@@ -24483,17 +24520,18 @@ async function handleMcpPost(c, scopeArg = "all") {
24483
24520
  consola.debug("/mcp parse error:", err);
24484
24521
  return c.json(rpcError(null, RPC_PARSE_ERROR, "request body is not valid JSON"), 200);
24485
24522
  }
24523
+ const sessionWorkspace = c.req.header(MCP_WORKSPACE_HEADER);
24486
24524
  if (process.env.GH_ROUTER_LOG_PEER_MCP === "1" && typeof body === "object" && body !== null && !Array.isArray(body) && body.method === "tools/call") {
24487
24525
  const nm = typeof body.params?.name === "string" ? body.params.name : "?";
24488
24526
  process.stderr.write(`[peer-mcp] recv t=${Date.now()} name=${nm} scope=${scope} inflight=${currentInFlight()}\n`);
24489
24527
  }
24490
- if (typeof body === "object" && body !== null && !Array.isArray(body) && body.method === "tools/call" && acceptsEventStream(c.req.header("accept"))) return handleToolsCallSSE(body, scope);
24528
+ if (typeof body === "object" && body !== null && !Array.isArray(body) && body.method === "tools/call" && acceptsEventStream(c.req.header("accept"))) return handleToolsCallSSE(body, scope, sessionWorkspace);
24491
24529
  if (typeof body === "object" && body !== null && !Array.isArray(body) && body.method === "tools/call") {
24492
24530
  const preflight = jsonPathPreflightCap(body, scope);
24493
24531
  if (preflight) return c.json(preflight, 200);
24494
24532
  }
24495
24533
  try {
24496
- const { status, body: respBody } = await handleRpc(c, body, scope);
24534
+ const { status, body: respBody } = await handleRpc(c, body, scope, sessionWorkspace);
24497
24535
  if (respBody === null) return c.body(null, status);
24498
24536
  return c.json(respBody, status);
24499
24537
  } catch (err) {
@@ -24546,9 +24584,9 @@ function acceptsEventStream(accept) {
24546
24584
  * "Invalid state: Controller is already closed" race without warning.
24547
24585
  */
24548
24586
  const SSE_HEARTBEAT_INTERVAL_MS = 5e3;
24549
- async function handleToolsCallSSE(body, scope) {
24587
+ async function handleToolsCallSSE(body, scope, sessionWorkspace) {
24550
24588
  const encoder = new TextEncoder();
24551
- const callPromise = handleToolsCall(body, scope);
24589
+ const callPromise = handleToolsCall(body, scope, sessionWorkspace);
24552
24590
  let heartbeatHandle;
24553
24591
  const stream = new ReadableStream({
24554
24592
  async start(controller) {
@@ -31459,8 +31497,9 @@ function assertMcpToolSurfaceConsistent() {
31459
31497
  /**
31460
31498
  * Shared closure body for the two worker MCP tools. Validates the
31461
31499
  * minimal arg shape (prompt required + optional knobs typed), then
31462
- * forwards to `runWorkerAgent`. `workspace` defaults to the proxy's
31463
- * launch cwd; callers can override via the optional `workspace` arg
31500
+ * forwards to `runWorkerAgent`. Outside serve mode, `workspace` defaults
31501
+ * to the proxy's launch cwd; serve mode requires an explicit/header-derived
31502
+ * workspace. Callers can override via the optional `workspace` arg
31464
31503
  * (absolute paths only — enforced here). The engine performs every
31465
31504
  * deeper validation (model existence, thinking clamp, worktree
31466
31505
  * provisioning, semaphore acquisition, workspace realpath +
@@ -31522,7 +31561,7 @@ async function runWorkerToolCall(call) {
31522
31561
  };
31523
31562
  worktree = args.worktree;
31524
31563
  }
31525
- let workspace = process.cwd();
31564
+ let workspace;
31526
31565
  if (args.workspace !== void 0) {
31527
31566
  if (typeof args.workspace !== "string" || args.workspace.length === 0) return {
31528
31567
  content: [{
@@ -31539,7 +31578,14 @@ async function runWorkerToolCall(call) {
31539
31578
  isError: true
31540
31579
  };
31541
31580
  workspace = args.workspace;
31542
- }
31581
+ } else if (state.serveMode) return {
31582
+ content: [{
31583
+ type: "text",
31584
+ text: `worker_${mode}: a workspace is required. This is a machine-wide github-router serve; pass the absolute path of the project you are working in as \`workspace\`.`
31585
+ }],
31586
+ isError: true
31587
+ };
31588
+ else workspace = process.cwd();
31543
31589
  let maxWallClockMs;
31544
31590
  let clampNote = "";
31545
31591
  if (args.maxWallClockMs !== void 0) {
@@ -31770,7 +31816,34 @@ async function runStandInToolCall(args, signal) {
31770
31816
  text: JSON.stringify(result)
31771
31817
  }] };
31772
31818
  }
31819
+ /**
31820
+ * Every exact `mcp__<key>__<tool>` name github-router injects, for the given
31821
+ * resolved group keys (`peers`/`search`/… → their collision-resolved mcpServers
31822
+ * key). A SUPERSET — it ignores per-tool capability gates because an allow-list
31823
+ * entry for a tool that isn't actually served is inert, which keeps it correct
31824
+ * as gates change and as tools are added.
31825
+ *
31826
+ * Used to seed CloudCLI's `localStorage['claude-settings'].allowedTools` so its
31827
+ * Agent-SDK `canUseTool` auto-approves our MCP tools in PLAN mode. `canUseTool`
31828
+ * does EXACT tool-name matching (no `mcp__<server>` wildcard — see
31829
+ * `matchesToolPermission` in CloudCLI's `claude-sdk.js`), so bare `mcp__peers`
31830
+ * would NOT cover `mcp__peers__gemini_critic`; the exact names are required.
31831
+ * This is the ONLY lever for plan mode: bypass mode skips `canUseTool`, and the
31832
+ * mirror `settings.json permissions.allow` is NOT consulted by `canUseTool`
31833
+ * (which reads `sdkOptions.allowedTools`, seeded from this localStorage key).
31834
+ */
31835
+ function enumerateInjectedMcpToolNames(groupKeys, opts = {}) {
31836
+ const names = [];
31837
+ const peersKey = groupKeys.peers;
31838
+ if (peersKey) for (const p of [...PERSONAS_READ, ...PERSONAS_WRITE]) names.push(`mcp__${peersKey}__${p.toolNameHttp}`);
31839
+ for (const t of NON_PERSONA_MCP_TOOLS) {
31840
+ const key = groupKeys[t.group];
31841
+ if (key) names.push(`mcp__${key}__${t.toolNameHttp}`);
31842
+ }
31843
+ if (opts.codexCli) names.push("mcp__codex-cli__codex");
31844
+ return [...new Set(names)];
31845
+ }
31773
31846
 
31774
31847
  //#endregion
31775
- export { isAdvisorRequested as $, cacheCopilotVersion as $t, liveExec as A, provisionBrowserAssets as At, withNoOutputRetry as B, DEFAULT_CODEX_MODEL as Bt, fileReviewDebounce as C, resolveMcpToolTimeoutMs as Ct, stopGateEnabledForRepo as D, MAX_RESPONSE_BODY_BYTES as Dt, repoRoot as E, createChatCompletions as Et, IMPLEMENT_DEFAULT_MODEL as F, shouldUseInsecureTls as Ft, vscodeRipgrepPath as G, generateRandomPort as Gt, buildToolbeltAwareness as H, DEFAULT_PORT as Ht, PLAN_DEFAULT_MODEL as I, ArtifactClient as It, searchWeb as J, withInstallLock as Jt, TOOLBELT_TOOLS$1 as K, pickClaudeDefault as Kt, REVIEW_DEFAULT_MODEL as L, collapsePathKeys as Lt, BROWSE_DEFAULT_MODEL as M, provisionAndIndexColbert as Mt, DEFAULT_MODEL as N, extractTarGzMember as Nt, stopReviewStateDir as O, readResponseBodyCapped as Ot, EXPLORE_DEFAULT_MODEL as P, extractZipMember as Pt, injectAdvisorTool as Q, tryRefreshAndRetry as Qt, appendPlanReminder as R, toolbeltPathOverride as Rt, fileLastPromptStore as S, assembleResponsesPayload as St, repoFingerprint as T, createResponses as Tt, toolbeltEnabled as U, UPSTREAM_FETCH_TIMEOUT_MS as Ut, availableToolCommands as V, DEFAULT_CODEX_MODEL_FALLBACKS as Vt, toolbeltSkipSet as W, UPSTREAM_INACTIVITY_TIMEOUT_MS as Wt, ADVISOR_TOOL_INSTRUCTIONS as X, setupGitHubAgentToken as Xt, ADVISOR_INTERNAL_TOOL_NAME as Y, setupCopilotToken as Yt, buildAdvisorStream as Z, setupGitHubToken as Zt, stopGateId as _, workerToolsEnabled as _t, buildPeerAwarenessSnippet as a, resolveModel as an, relayAnthropicStream as at, fileBaselineStore as b, createMessages as bt, buildArtifactOpenHookCommand as c, fetchWithTransientRetry as cn, agentToolsEnabled as ct, captureLaunchBaseline as d, GITHUB_API_BASE_URL as dn, browserCompoundToolsEnabled as dt, cacheModels as en, buildAnthropicErrorEvent as et, decideStopHook as f, copilotBaseUrl as fn, browserToolsEnabled as ft, stopGateDisabled as g, standInToolEnabled as gt, launchBaselineKey as h, state as hn, nativeSubagentModel as ht, buildAgentPrompt as i, resolveCodexModel as in, readIteratorWithTimeout as it, resolveSealedGate as j, hasSupportedBrowserInstalled as jt, trustRepo as k, parseJsonOrDiagnose as kt, buildSessionBindHookCommand as l, HTTPError as ln, artifactToolsEnabled as lt, injectStopHookIntoSettingsFile as m, githubHeaders as mn, geminiAvailable as mt, MCP_GROUPS as n, filterBetaHeader as nn, isControllerClosedError as nt, buildPeerAwarenessSummary as o, sleep as on, handleMcpDelete as ot, fileBlockBudget as p, copilotHeaders as pn, fleetToolsEnabled as pt, assetFor as q, getPackageVersion as qt, assertMcpToolSurfaceConsistent as r, isNullish as rn, logStreamError as rt, personasFor as s, getModels as sn, handleMcpPost as st, GROUP_META as t, cacheVSCodeVersion as tn, buildOpenAIErrorEvent as tt, buildStopHookCommand as u, forwardError as un, browseAgentEnabled as ut, stopGatePlanMode as v, shimDefaultsToXhigh as vt, isSubagentContext as w, pickEndpoint as wt, fileFindingsStore as x, getTokenCount as xt, stopReviewEnabled as y, countTokens as yt, runWorkerAgent as z, DEFAULT_CLAUDE_MODEL_FALLBACKS as zt };
31776
- //# sourceMappingURL=peer-mcp-personas-B1-1mVPB.js.map
31848
+ export { buildAdvisorStream as $, setupCopilotToken as $t, trustRepo as A, readResponseBodyCapped as At, runWorkerAgent as B, buildWorkspaceHeaderJson as Bt, fileLastPromptStore as C, getTokenCount as Ct, repoRoot as D, createResponses as Dt, repoFingerprint as E, pickEndpoint as Et, EXPLORE_DEFAULT_MODEL as F, extractTarGzMember as Ft, toolbeltEnabled as G, DEFAULT_CODEX_MODEL_FALLBACKS as Gt, buildEnv as H, toolbeltPathOverride as Ht, IMPLEMENT_DEFAULT_MODEL as I, extractZipMember as It, TOOLBELT_TOOLS$1 as J, UPSTREAM_INACTIVITY_TIMEOUT_MS as Jt, toolbeltSkipSet as K, DEFAULT_PORT as Kt, PLAN_DEFAULT_MODEL as L, shouldUseInsecureTls as Lt, resolveSealedGate as M, provisionBrowserAssets as Mt, BROWSE_DEFAULT_MODEL as N, hasSupportedBrowserInstalled as Nt, stopGateEnabledForRepo as O, createChatCompletions as Ot, DEFAULT_MODEL as P, provisionAndIndexColbert as Pt, ADVISOR_TOOL_INSTRUCTIONS as Q, withInstallLock as Qt, REVIEW_DEFAULT_MODEL as R, ArtifactClient as Rt, fileFindingsStore as S, createMessages as St, isSubagentContext as T, resolveMcpToolTimeoutMs as Tt, availableToolCommands as U, DEFAULT_CLAUDE_MODEL_FALLBACKS as Ut, withNoOutputRetry as V, collapsePathKeys as Vt, buildToolbeltAwareness as W, DEFAULT_CODEX_MODEL as Wt, searchWeb as X, pickClaudeDefault as Xt, assetFor as Y, generateRandomPort as Yt, ADVISOR_INTERNAL_TOOL_NAME as Z, getPackageVersion as Zt, stopGateDisabled as _, copilotBaseUrl as _n, nativeSubagentModel as _t, buildPeerAwarenessSnippet as a, cacheVSCodeVersion as an, logStreamError as at, stopReviewEnabled as b, state as bn, shimDefaultsToXhigh as bt, personasFor as c, resolveCodexModel as cn, handleMcpDelete as ct, buildStopHookCommand as d, getModels as dn, artifactToolsEnabled as dt, setupGitHubAgentToken as en, injectAdvisorTool as et, captureLaunchBaseline as f, getGitHubUser as fn, browseAgentEnabled as ft, launchBaselineKey as g, GITHUB_API_BASE_URL as gn, geminiAvailable as gt, injectStopHookIntoSettingsFile as h, forwardError as hn, fleetToolsEnabled as ht, buildAgentPrompt as i, cacheModels as in, isControllerClosedError as it, liveExec as j, parseJsonOrDiagnose as jt, stopReviewStateDir as k, MAX_RESPONSE_BODY_BYTES as kt, buildArtifactOpenHookCommand as l, resolveModel as ln, handleMcpPost as lt, fileBlockBudget as m, HTTPError as mn, browserToolsEnabled as mt, MCP_GROUPS as n, tryRefreshAndRetry as nn, buildAnthropicErrorEvent as nt, buildPeerAwarenessSummary as o, filterBetaHeader as on, readIteratorWithTimeout as ot, decideStopHook as p, fetchWithTransientRetry as pn, browserCompoundToolsEnabled as pt, vscodeRipgrepPath as q, UPSTREAM_FETCH_TIMEOUT_MS as qt, assertMcpToolSurfaceConsistent as r, cacheCopilotVersion as rn, buildOpenAIErrorEvent as rt, enumerateInjectedMcpToolNames as s, isNullish as sn, relayAnthropicStream as st, GROUP_META as t, setupGitHubToken as tn, isAdvisorRequested as tt, buildSessionBindHookCommand as u, sleep as un, agentToolsEnabled as ut, stopGateId as v, copilotHeaders as vn, standInToolEnabled as vt, fileReviewDebounce as w, assembleResponsesPayload as wt, fileBaselineStore as x, countTokens as xt, stopGatePlanMode as y, githubHeaders as yn, workerToolsEnabled as yt, appendPlanReminder as z, buildWorkspaceHeaderHelperCommand as zt };
31849
+ //# sourceMappingURL=peer-mcp-personas-rik0SIrg.js.map