github-router 0.3.240 → 0.3.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { $ as searchWeb, $t as DEFAULT_CLAUDE_MODEL_FALLBACKS, A as trustRepo, An as state, At as getTokenCount, Bt as hasSupportedBrowserInstalled, C as fileLastPromptStore, Cn as fetchWithTransientRetry, Ct as scoutModel, D as repoRoot, Dn as copilotBaseUrl, Dt as shimDefaultsToXhigh, E as repoFingerprint, En as GITHUB_API_BASE_URL, Et as workerToolsEnabled, Ft as createChatCompletions, G as buildEnv, Gt as CONDENSED_OPERATING_SEQUENCE, Ht as extractTarGzMember, It as MAX_RESPONSE_BODY_BYTES, J as toolbeltEnabled, Jt as ArtifactClient, K as availableToolCommands, Kt as DEFINITION_OF_GREATNESS, Lt as readResponseBodyCapped, M as resolveSealedGate, Mt as resolveMcpToolTimeoutMs, Nt as pickEndpoint, O as stopGateEnabledForRepo, On as copilotHeaders, Ot as countTokens, Pt as createResponses, Q as assetFor, Qt as toolbeltPathOverride, Rt as parseJsonOrDiagnose, S as fileFindingsStore, Sn as getGitHubUser, St as reviewerModel, T as isSubagentContext, Tn as forwardError, Tt as standInToolEnabled, Ut as extractZipMember, Vt as provisionAndIndexColbert, Wt as warmTreeSitterPool, X as vscodeRipgrepPath, Xt as buildWorkspaceHeaderJson, Y as toolbeltSkipSet, Yt as buildWorkspaceHeaderHelperCommand, Z as TOOLBELT_TOOLS, Zt as collapsePathKeys, _ as stopGateDisabled, _n as isNullish, _t as browserCompoundToolsEnabled, a as buildPeerAwarenessSnippet, an as generateRandomPort, at as buildAnthropicErrorEvent, b as stopReviewEnabled, bn as sleep$1, bt as geminiAvailable, c as personasFor, cn as withInstallLock, ct as logStreamError, d as buildStopHookCommand, dn as setupGitHubToken, dt as handleMcpDelete, en as DEFAULT_CODEX_MODEL, et as ADVISOR_INTERNAL_TOOL_NAME, f as captureLaunchBaseline, fn as tryRefreshAndRetry, ft as handleMcpPost, g as launchBaselineKey, gn as filterBetaHeader, gt as browseAgentEnabled, h as injectStopHookIntoSettingsFile, hn as cacheVSCodeVersion, ht as brainstormModel, i as buildAgentPrompt, in as UPSTREAM_INACTIVITY_TIMEOUT_MS, it as isAdvisorRequested, j as liveExec, jt as assembleResponsesPayload, k as stopReviewStateDir, kn as githubHeaders, kt as createMessages, l as buildArtifactOpenHookCommand, ln as setupCopilotToken, lt as readIteratorWithTimeout, m as fileBlockBudget, mn as cacheModels, mt as artifactToolsEnabled, n as MCP_GROUPS, nn as DEFAULT_PORT, nt as buildAdvisorStream, o as buildPeerAwarenessSummary, on as pickClaudeDefault, ot as buildOpenAIErrorEvent, p as decideStopHook, pn as cacheCopilotVersion, pt as agentToolsEnabled, q as buildToolbeltAwareness, qt as shouldUseInsecureTls, r as assertMcpToolSurfaceConsistent, rn as UPSTREAM_FETCH_TIMEOUT_MS, rt as injectAdvisorTool, s as enumerateInjectedMcpToolNames, sn as getPackageVersion, st as isControllerClosedError, t as GROUP_META, tn as DEFAULT_CODEX_MODEL_FALLBACKS, tt as ADVISOR_TOOL_INSTRUCTIONS, u as buildSessionBindHookCommand, un as setupGitHubAgentToken, ut as relayAnthropicStream, v as stopGateId, vn as resolveCodexModel, vt as browserToolsEnabled, w as fileReviewDebounce, wn as HTTPError, wt as scribeModel, x as fileBaselineStore, xn as getModels, xt as nativeSubagentModel, y as stopGatePlanMode, yn as resolveModel, yt as fleetToolsEnabled, zt as provisionBrowserAssets } from "./peer-mcp-personas-CDja7arT.js";
3
- import { a as isUnderClaudeConfigMirror, d as writeRuntimeFileSecure, i as ensurePaths, o as removeOwnClaudeConfigMirror, r as ensureClaudeConfigMirror, t as PATHS, u as writeArtifactCredsToMirror } from "./paths-Srxs-aR-.js";
4
- import { c as killManagedTree, d as resolveExecutable, f as runCommandCapture, l as parseBoolEnv, p as runCommandVoid, s as killChildProcessTree, u as parseIntEnv } from "./lifecycle-C-9_vhyM.js";
5
- import { a as sweepRegistry } from "./lifecycle-Cuuj40CW.js";
2
+ import { $ as searchWeb, $t as DEFAULT_CLAUDE_MODEL_FALLBACKS, A as trustRepo, An as state, At as getTokenCount, Bt as hasSupportedBrowserInstalled, C as fileLastPromptStore, Cn as fetchWithTransientRetry, Ct as scoutModel, D as repoRoot, Dn as copilotBaseUrl, Dt as shimDefaultsToXhigh, E as repoFingerprint, En as GITHUB_API_BASE_URL, Et as workerToolsEnabled, Ft as createChatCompletions, G as buildEnv, Gt as CONDENSED_OPERATING_SEQUENCE, Ht as extractTarGzMember, It as MAX_RESPONSE_BODY_BYTES, J as toolbeltEnabled, Jt as ArtifactClient, K as availableToolCommands, Kt as DEFINITION_OF_GREATNESS, Lt as readResponseBodyCapped, M as resolveSealedGate, Mt as resolveMcpToolTimeoutMs, Nt as pickEndpoint, O as stopGateEnabledForRepo, On as copilotHeaders, Ot as countTokens, Pt as createResponses, Q as assetFor, Qt as toolbeltPathOverride, Rt as parseJsonOrDiagnose, S as fileFindingsStore, Sn as getGitHubUser, St as reviewerModel, T as isSubagentContext, Tn as forwardError, Tt as standInToolEnabled, Ut as extractZipMember, Vt as provisionAndIndexColbert, Wt as warmTreeSitterPool, X as vscodeRipgrepPath, Xt as buildWorkspaceHeaderJson, Y as toolbeltSkipSet, Yt as buildWorkspaceHeaderHelperCommand, Z as TOOLBELT_TOOLS, Zt as collapsePathKeys, _ as stopGateDisabled, _n as isNullish, _t as browserCompoundToolsEnabled, a as buildPeerAwarenessSnippet, an as generateRandomPort, at as buildAnthropicErrorEvent, b as stopReviewEnabled, bn as sleep$1, bt as geminiAvailable, c as personasFor, cn as withInstallLock, ct as logStreamError, d as buildStopHookCommand, dn as setupGitHubToken, dt as handleMcpDelete, en as DEFAULT_CODEX_MODEL, et as ADVISOR_INTERNAL_TOOL_NAME, f as captureLaunchBaseline, fn as tryRefreshAndRetry, ft as handleMcpPost, g as launchBaselineKey, gn as filterBetaHeader, gt as browseAgentEnabled, h as injectStopHookIntoSettingsFile, hn as cacheVSCodeVersion, ht as brainstormModel, i as buildAgentPrompt, in as UPSTREAM_INACTIVITY_TIMEOUT_MS, it as isAdvisorRequested, j as liveExec, jt as assembleResponsesPayload, k as stopReviewStateDir, kn as githubHeaders, kt as createMessages, l as buildArtifactOpenHookCommand, ln as setupCopilotToken, lt as readIteratorWithTimeout, m as fileBlockBudget, mn as cacheModels, mt as artifactToolsEnabled, n as MCP_GROUPS, nt as buildAdvisorStream, o as buildPeerAwarenessSummary, on as pickClaudeDefault, ot as buildOpenAIErrorEvent, p as decideStopHook, pn as cacheCopilotVersion, pt as agentToolsEnabled, q as buildToolbeltAwareness, qt as shouldUseInsecureTls, r as assertMcpToolSurfaceConsistent, rn as UPSTREAM_FETCH_TIMEOUT_MS, rt as injectAdvisorTool, s as enumerateInjectedMcpToolNames, sn as getPackageVersion$1, st as isControllerClosedError, t as GROUP_META, tn as DEFAULT_CODEX_MODEL_FALLBACKS, tt as ADVISOR_TOOL_INSTRUCTIONS, u as buildSessionBindHookCommand, un as setupGitHubAgentToken, ut as relayAnthropicStream, v as stopGateId, vn as resolveCodexModel, vt as browserToolsEnabled, w as fileReviewDebounce, wn as HTTPError, wt as scribeModel, x as fileBaselineStore, xn as getModels, xt as nativeSubagentModel, y as stopGatePlanMode, yn as resolveModel, yt as fleetToolsEnabled, zt as provisionBrowserAssets } from "./peer-mcp-personas-r_UmLBkJ.js";
3
+ import { a as isUnderClaudeConfigMirror, d as writeRuntimeFileSecure, i as ensurePaths, o as removeOwnClaudeConfigMirror, r as ensureClaudeConfigMirror, t as PATHS, u as writeArtifactCredsToMirror } from "./paths-BjTMI_xK.js";
4
+ import { c as killManagedTree, d as resolveExecutable, f as runCommandCapture, l as parseBoolEnv, p as runCommandVoid, s as killChildProcessTree, u as parseIntEnv } from "./lifecycle-DHZsVN8j.js";
5
+ import { a as sweepRegistry } from "./lifecycle-CUf1UDrH.js";
6
6
  import { defineCommand, runMain } from "citty";
7
7
  import consola from "consola";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
9
9
  import fs, { chmod, copyFile, link, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
10
10
  import os, { homedir, tmpdir } from "node:os";
11
- import * as nodePath$1 from "node:path";
12
- import nodePath from "node:path";
11
+ import * as nodePath from "node:path";
12
+ import path from "node:path";
13
13
  import process$1 from "node:process";
14
14
  import { execFileSync, spawn } from "node:child_process";
15
15
  import * as fs$2 from "node:fs";
@@ -24,7 +24,6 @@ import { cors } from "hono/cors";
24
24
  import { Marked } from "marked";
25
25
  import http from "node:http";
26
26
  import clipboard from "clipboardy";
27
-
28
27
  //#region src/auth.ts
29
28
  async function runAuth(options) {
30
29
  if (options.verbose) {
@@ -61,7 +60,6 @@ const auth = defineCommand({
61
60
  });
62
61
  }
63
62
  });
64
-
65
63
  //#endregion
66
64
  //#region src/services/github/get-copilot-usage.ts
67
65
  const getCopilotUsage = async () => {
@@ -69,7 +67,6 @@ const getCopilotUsage = async () => {
69
67
  if (!response.ok) throw new HTTPError("Failed to get Copilot usage", response);
70
68
  return await response.json();
71
69
  };
72
-
73
70
  //#endregion
74
71
  //#region src/check-usage.ts
75
72
  const checkUsage = defineCommand({
@@ -87,13 +84,13 @@ const checkUsage = defineCommand({
87
84
  const premiumUsed = premiumTotal - premium.remaining;
88
85
  const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
89
86
  const premiumPercentRemaining = premium.percent_remaining;
90
- function summarizeQuota(name$1, snap) {
91
- if (!snap) return `${name$1}: N/A`;
87
+ function summarizeQuota(name, snap) {
88
+ if (!snap) return `${name}: N/A`;
92
89
  const total = snap.entitlement;
93
90
  const used = total - snap.remaining;
94
91
  const percentUsed = total > 0 ? used / total * 100 : 0;
95
92
  const percentRemaining = snap.percent_remaining;
96
- return `${name$1}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
93
+ return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
97
94
  }
98
95
  const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
99
96
  const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
@@ -105,7 +102,6 @@ const checkUsage = defineCommand({
105
102
  }
106
103
  }
107
104
  });
108
-
109
105
  //#endregion
110
106
  //#region src/lib/claude-version-check.ts
111
107
  const NPM_PACKAGE$1 = "@anthropic-ai/claude-code";
@@ -115,7 +111,7 @@ const CLAUDE_VERSION_TIMEOUT_MS = 3e3;
115
111
  const NPM_INSTALL_TIMEOUT_MS = 12e4;
116
112
  /** Path to the throttle cache. Created on demand. */
117
113
  function cacheFilePath$1() {
118
- return nodePath.join(os.homedir(), ".local", "share", "github-router", "last-update-check");
114
+ return path.join(os.homedir(), ".local", "share", "github-router", "last-update-check");
119
115
  }
120
116
  /**
121
117
  * Read the throttle cache. Returns null on missing/corrupt file —
@@ -133,7 +129,7 @@ async function readCache$1() {
133
129
  }
134
130
  async function writeCache$1(cache) {
135
131
  try {
136
- await fs.mkdir(nodePath.dirname(cacheFilePath$1()), { recursive: true });
132
+ await fs.mkdir(path.dirname(cacheFilePath$1()), { recursive: true });
137
133
  await fs.writeFile(cacheFilePath$1(), JSON.stringify(cache), { mode: 384 });
138
134
  } catch (err) {
139
135
  consola.debug("Failed to write claude version-check cache:", err);
@@ -309,14 +305,29 @@ async function updateClaude(latestVersion, deps = {}) {
309
305
  consola.success(`${NPM_PACKAGE$1} updated to ${latestVersion}`);
310
306
  })) consola.debug("Claude Code update already in progress in another process; skipping.");
311
307
  }
312
-
313
308
  //#endregion
314
309
  //#region src/lib/self-update.ts
310
+ /**
311
+ * Self-update for the `github-router` proxy.
312
+ *
313
+ * The proxy is a *running* process, so updating its own global install
314
+ * in place (`npm install -g github-router@latest`) deletes/overwrites
315
+ * files the OS holds open — on Windows that fails deterministically
316
+ * (EPERM/EBUSY) and can corrupt the install. So we never install in
317
+ * place: when a newer version exists we spawn a **detached updater that
318
+ * waits for this process to exit**, then installs. The update takes
319
+ * effect on the next launch — no re-exec, no restart prompt.
320
+ *
321
+ * Best-effort throughout: every failure is swallowed to debug/warn and
322
+ * never blocks startup. The startup probe is bounded and the install is
323
+ * off the critical path (detached), so even a blackholed network can't
324
+ * delay the proxy from serving.
325
+ */
315
326
  const NPM_PACKAGE = "github-router";
316
327
  const THROTTLE_HOURS = 1;
317
328
  const NPM_VIEW_TIMEOUT_MS = 5e3;
318
329
  function cacheFilePath() {
319
- return nodePath.join(os.homedir(), ".local", "share", "github-router", "last-self-update-check");
330
+ return path.join(os.homedir(), ".local", "share", "github-router", "last-self-update-check");
320
331
  }
321
332
  async function readCache() {
322
333
  try {
@@ -329,7 +340,7 @@ async function readCache() {
329
340
  }
330
341
  async function writeCache(cache) {
331
342
  try {
332
- await fs.mkdir(nodePath.dirname(cacheFilePath()), { recursive: true });
343
+ await fs.mkdir(path.dirname(cacheFilePath()), { recursive: true });
333
344
  await fs.writeFile(cacheFilePath(), JSON.stringify(cache), { mode: 384 });
334
345
  } catch (err) {
335
346
  consola.debug("Failed to write self-update cache:", err);
@@ -406,7 +417,7 @@ async function runSelfUpdate(opts) {
406
417
  try {
407
418
  const cache = await readCache();
408
419
  if (!opts.force && !shouldCheckNow(cache)) return;
409
- const installed = getPackageVersion();
420
+ const installed = getPackageVersion$1();
410
421
  if (installed === "unknown") return;
411
422
  const npmPath = resolveExecutable("npm");
412
423
  if (!npmPath) return;
@@ -424,10 +435,40 @@ async function runSelfUpdate(opts) {
424
435
  consola.debug("Self-update check failed:", err);
425
436
  }
426
437
  }
427
-
428
438
  //#endregion
429
439
  //#region src/lib/process-guard/index.ts
430
440
  /**
441
+ * Crash-safe process guard (POSIX only).
442
+ *
443
+ * If the proxy dies WITHOUT running its own shutdown handlers (hard crash,
444
+ * SIGKILL, OOM), nothing in `cleanup()` runs to reap the launched CLI's
445
+ * process tree. This guard is the last-resort net for that case.
446
+ *
447
+ * Platform split:
448
+ * - **Windows: no guard needed.** Node assigns each child to a Job
449
+ * Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, so when the proxy
450
+ * dies (any cause) the OS tears down the whole descendant tree —
451
+ * verified: force-killing a Node parent kills child AND grandchild.
452
+ * `startProcessGuard` is a no-op on win32.
453
+ * - **POSIX: a detached `node -e` reaper.** There is no job-object
454
+ * equivalent (a killed parent's children reparent to init), so we
455
+ * spawn a detached watchdog that learns of proxy death via an inherited
456
+ * stdin PIPE EOF (the proxy holds the write end; the OS closes it on
457
+ * proxy death) and reaps the CLI's process group.
458
+ *
459
+ * Never kill the WRONG process: if the child's PID/PGID was recycled after
460
+ * the child exited, a blind `kill(-pid)` could hit an innocent group. The
461
+ * reaper snapshots the child's process start-time at startup and, on proxy
462
+ * death, kills ONLY if the live PID's start-time still matches. A mismatch
463
+ * / unreadable probe → skip (fail-safe). We never trade an orphan-leak for
464
+ * killing the user's work.
465
+ *
466
+ * The reaper is detached + unref'd so it survives the proxy's SIGKILL, and
467
+ * self-exits once it has acted (or after a 24h cap) so it can never become
468
+ * the orphan it exists to prevent. Opt out with
469
+ * `GH_ROUTER_DISABLE_PROCESS_GUARD=1`.
470
+ */
471
+ /**
431
472
  * Live reaper children, held so the GC can't collect the `ChildProcess`
432
473
  * (and with it the stdin write-end that is the proxy-death signal) while
433
474
  * the proxy runs. Entries self-remove on the reaper's exit. We do NOT
@@ -538,7 +579,6 @@ function startProcessGuard(child) {
538
579
  if (process$1.platform === "win32") return;
539
580
  spawnNodeReaper(pid, true);
540
581
  }
541
-
542
582
  //#endregion
543
583
  //#region src/lib/stripped-env-keys.ts
544
584
  /**
@@ -589,7 +629,6 @@ const STRIPPED_AUTH_ROUTING_ENV_KEYS = [
589
629
  "AIORDIE_TOKEN",
590
630
  "AIORDIE_INSECURE_TLS"
591
631
  ];
592
-
593
632
  //#endregion
594
633
  //#region src/lib/launch.ts
595
634
  /**
@@ -616,9 +655,9 @@ function sanitizeParentEnv(parent) {
616
655
  for (const key of STRIPPED_PARENT_ENV_KEYS) delete sanitized[key];
617
656
  return sanitized;
618
657
  }
619
- function commandExists(name$1) {
658
+ function commandExists(name) {
620
659
  try {
621
- execFileSync(process$1.platform === "win32" ? "where.exe" : "which", [name$1], { stdio: "ignore" });
660
+ execFileSync(process$1.platform === "win32" ? "where.exe" : "which", [name], { stdio: "ignore" });
622
661
  return true;
623
662
  } catch {
624
663
  return false;
@@ -637,7 +676,7 @@ function commandExists(name$1) {
637
676
  * installed fails with a spurious "not found on PATH".
638
677
  */
639
678
  function isExecutableAvailable(executable) {
640
- if (nodePath.isAbsolute(executable)) return existsSync(executable);
679
+ if (path.isAbsolute(executable)) return existsSync(executable);
641
680
  return commandExists(executable);
642
681
  }
643
682
  /**
@@ -683,10 +722,10 @@ function getCodexVersion() {
683
722
  };
684
723
  const major = Number.parseInt(m[1], 10);
685
724
  const minor = Number.parseInt(m[2], 10);
686
- const version$2 = `${m[1]}.${m[2]}.${m[3]}`;
725
+ const version = `${m[1]}.${m[2]}.${m[3]}`;
687
726
  return {
688
727
  ok: major > 0 || major === 0 && minor >= 129,
689
- version: version$2
728
+ version
690
729
  };
691
730
  }
692
731
  /**
@@ -704,7 +743,7 @@ function getCodexVersion() {
704
743
  function buildCodexCmd(target) {
705
744
  const cmd = ["codex"];
706
745
  if (target.serverUrl) cmd.push(...buildCodexProviderConfigFlags(target.serverUrl));
707
- cmd.push("--sandbox", "workspace-write", "--ask-for-approval", "on-request", "-m", target.model ?? DEFAULT_CODEX_MODEL, ...target.extraArgs);
746
+ cmd.push("--sandbox", "workspace-write", "--ask-for-approval", "on-request", "-m", target.model ?? "gpt-5.6-sol", ...target.extraArgs);
708
747
  return cmd;
709
748
  }
710
749
  function buildLaunchCommand(target) {
@@ -733,10 +772,10 @@ function buildLaunchCommand(target) {
733
772
  * intermediary to orphan its node/MCP grandchildren on a kill.
734
773
  */
735
774
  function windowsLaunchNeedsShell(executable) {
736
- const ext = nodePath.extname(executable).toLowerCase();
775
+ const ext = path.extname(executable).toLowerCase();
737
776
  return ext === ".cmd" || ext === ".bat";
738
777
  }
739
- function launchChild(target, server$1, options = {}) {
778
+ function launchChild(target, server, options = {}) {
740
779
  const { cmd, env } = buildLaunchCommand(target);
741
780
  const executable = cmd[0];
742
781
  if (!isExecutableAvailable(executable)) {
@@ -747,12 +786,14 @@ function launchChild(target, server$1, options = {}) {
747
786
  }
748
787
  let child;
749
788
  try {
750
- if (process$1.platform === "win32") if (windowsLaunchNeedsShell(cmd[0])) child = spawn(cmd.map((a) => a.includes(" ") ? `"${a}"` : a).join(" "), [], {
751
- env,
752
- stdio: "inherit",
753
- shell: true
754
- });
755
- else child = spawn(cmd[0], cmd.slice(1), {
789
+ if (process$1.platform === "win32") if (windowsLaunchNeedsShell(cmd[0])) {
790
+ const quoted = cmd.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ");
791
+ child = spawn(quoted, [], {
792
+ env,
793
+ stdio: "inherit",
794
+ shell: true
795
+ });
796
+ } else child = spawn(cmd[0], cmd.slice(1), {
756
797
  env,
757
798
  stdio: "inherit"
758
799
  });
@@ -765,7 +806,7 @@ function launchChild(target, server$1, options = {}) {
765
806
  const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`;
766
807
  consola.error(msg);
767
808
  process$1.stderr.write(msg + "\n");
768
- server$1.close(true).catch(() => {});
809
+ server.close(true).catch(() => {});
769
810
  if (options.onShutdown) Promise.resolve(options.onShutdown()).catch(() => {});
770
811
  process$1.exit(1);
771
812
  }
@@ -780,7 +821,7 @@ function launchChild(target, server$1, options = {}) {
780
821
  killChildProcessTree(child, { detachedGroup: process$1.platform !== "win32" });
781
822
  } catch {}
782
823
  try {
783
- await server$1.close(true);
824
+ await server.close(true);
784
825
  } catch {}
785
826
  if (options.onShutdown) try {
786
827
  await options.onShutdown();
@@ -830,7 +871,6 @@ function launchChild(target, server$1, options = {}) {
830
871
  cleanup().then(() => exit(1)).catch(() => exit(1));
831
872
  });
832
873
  }
833
-
834
874
  //#endregion
835
875
  //#region src/lib/worker-dispatch.ts
836
876
  /**
@@ -883,8 +923,7 @@ const ALL_WORKER_DISPATCH_MODES = [...CORE_WORKER_MODES, BROWSE_WORKER_MODE];
883
923
  function dispatcherAgentName(mode) {
884
924
  return `worker-${mode}`;
885
925
  }
886
- /** Every possible dispatcher subagent name (used by the stale-`.md` sweep). */
887
- const ALL_DISPATCHER_AGENT_NAMES = ALL_WORKER_DISPATCH_MODES.map(dispatcherAgentName);
926
+ ALL_WORKER_DISPATCH_MODES.map(dispatcherAgentName);
888
927
  /** The MCP tool name the workers server exposes for a mode under the resolved key. */
889
928
  function workerToolName(workersKey, mode) {
890
929
  return `mcp__${workersKey}__${mode}`;
@@ -1051,7 +1090,6 @@ function parseModesCsv(csv) {
1051
1090
  const out = csv.split(",").map((s) => s.trim()).filter((s) => known.has(s));
1052
1091
  return out.length > 0 ? out : [...CORE_WORKER_MODES];
1053
1092
  }
1054
-
1055
1093
  //#endregion
1056
1094
  //#region src/lib/codex-mcp-config.ts
1057
1095
  /** The `peers` server is always enabled, so its resolved key always exists;
@@ -1345,10 +1383,10 @@ function buildPeerAgentDefinitions(opts) {
1345
1383
  ...peersMcp
1346
1384
  };
1347
1385
  const nativeModel = nonEmptyModel(opts.nativeSubagentModel);
1348
- const reviewerModel$1 = nonEmptyModel(opts.reviewerModel);
1349
- const brainstormModel$1 = nonEmptyModel(opts.brainstormModel);
1350
- const scoutModel$1 = nonEmptyModel(opts.scoutModel);
1351
- const scribeModel$1 = nonEmptyModel(opts.scribeModel);
1386
+ const reviewerModel = nonEmptyModel(opts.reviewerModel);
1387
+ const brainstormModel = nonEmptyModel(opts.brainstormModel);
1388
+ const scoutModel = nonEmptyModel(opts.scoutModel);
1389
+ const scribeModel = nonEmptyModel(opts.scribeModel);
1352
1390
  const modelField = nativeModel ? { model: nativeModel } : {};
1353
1391
  const searchKey = opts.groupKeys.search ?? GROUP_META.search.preferredKey;
1354
1392
  const searchMcp = opts.serverUrl ? { mcpServers: { [searchKey]: httpEntryFor(opts.serverUrl, "search", opts.nonce, opts.workspaceHeaderCmd) } } : {};
@@ -1358,28 +1396,28 @@ function buildPeerAgentDefinitions(opts) {
1358
1396
  ...modelField
1359
1397
  };
1360
1398
  out.reviewer = {
1361
- description: reviewerModel$1 ? `Feedback subagent running ${reviewerModel$1}, a DIFFERENT lab from both the lead and the implementer, so its blind spots are decorrelated from whoever produced the work. Use proactively when something already exists and you want it assessed: a diff, a plan, a document, a failing test. Unlike the stateless peer critics, it reads the repo and can RUN things, so prefer it whenever the assessment needs execution or repo context (reproduce a failure, run the suite, bisect); prefer a peer critic when you already hold the artifact and want a fresh-context opinion on it. Model is overridable at spawn.` : `Feedback subagent (native tools, runs on the lead's model in its own context). Use proactively when something already exists and you want it assessed: a diff, a plan, a document, a failing test. Unlike the stateless peer critics, it reads the repo and can RUN things, so prefer it whenever the assessment needs execution or repo context; prefer a peer critic when you already hold the artifact and want a fresh-context opinion on it. Model is overridable at spawn.`,
1399
+ description: reviewerModel ? `Feedback subagent running ${reviewerModel}, a DIFFERENT lab from both the lead and the implementer, so its blind spots are decorrelated from whoever produced the work. Use proactively when something already exists and you want it assessed: a diff, a plan, a document, a failing test. Unlike the stateless peer critics, it reads the repo and can RUN things, so prefer it whenever the assessment needs execution or repo context (reproduce a failure, run the suite, bisect); prefer a peer critic when you already hold the artifact and want a fresh-context opinion on it. Model is overridable at spawn.` : `Feedback subagent (native tools, runs on the lead's model in its own context). Use proactively when something already exists and you want it assessed: a diff, a plan, a document, a failing test. Unlike the stateless peer critics, it reads the repo and can RUN things, so prefer it whenever the assessment needs execution or repo context; prefer a peer critic when you already hold the artifact and want a fresh-context opinion on it. Model is overridable at spawn.`,
1362
1400
  prompt: "You are a feedback subagent. Your job is to tell the caller what is actually true about the artifact you are given — code, a plan, a document, a failure report — and what is wrong with it. Verify against the ACTUAL code by reading it; never assume. Do whatever the assessment requires: reproduce a failure end to end as close to how a real user hits it as you can, form hypotheses and test them against the code and runtime, and isolate the true root cause rather than a symptom. Where the change warrants it, author tests that try to BREAK the implementation (edge cases, error paths, and the acceptance criteria as executable checks), run them, and report which pass and which fail; do NOT modify production code just to make tests pass. " + fileToolSteer("builds") + " Do the work yourself — do not spawn further subagents. Report severity-ranked findings with `file:line` citations, the evidence behind each, and end with a clear go/no-go.",
1363
- ...reviewerModel$1 ? { model: reviewerModel$1 } : {}
1401
+ ...reviewerModel ? { model: reviewerModel } : {}
1364
1402
  };
1365
1403
  out.brainstorm = {
1366
- description: brainstormModel$1 ? `Divergent-options subagent running ${brainstormModel$1} (third lab, for approaches the lead would not generate). Use proactively BEFORE an approach is chosen. Pass the decision, the constraints, what you have already ruled out, and the cost of being wrong; pass your current leading approach too if you have one, and it will try to beat it rather than restate it. Read-only; it proposes, then hands off to implementer. Model is overridable at spawn.` : `Divergent-options subagent (runs on the lead's model in its own context). Use proactively BEFORE an approach is chosen. Pass the decision, the constraints, what you have already ruled out, and the cost of being wrong; pass your current leading approach too if you have one, and it will try to beat it rather than restate it. Read-only; it proposes, then hands off to implementer. Model is overridable at spawn.`,
1404
+ description: brainstormModel ? `Divergent-options subagent running ${brainstormModel} (third lab, for approaches the lead would not generate). Use proactively BEFORE an approach is chosen. Pass the decision, the constraints, what you have already ruled out, and the cost of being wrong; pass your current leading approach too if you have one, and it will try to beat it rather than restate it. Read-only; it proposes, then hands off to implementer. Model is overridable at spawn.` : `Divergent-options subagent (runs on the lead's model in its own context). Use proactively BEFORE an approach is chosen. Pass the decision, the constraints, what you have already ruled out, and the cost of being wrong; pass your current leading approach too if you have one, and it will try to beat it rather than restate it. Read-only; it proposes, then hands off to implementer. Model is overridable at spawn.`,
1367
1405
  prompt: "You are a divergent-options subagent: the lead's sounding board while an approach is still open. Your job is to surface the option the lead would not have reached on its own. If the caller states its current leading approach, your job is to try to beat it, and you close with one verdict: `replace`, `retain`, or `insufficient evidence`. `replace` requires naming a concrete alternative that dominates it and the evidence that decides between them. `retain` is a real answer and a useful one: say it plainly when the approach survives a genuine attempt to beat it. Manufactured disagreement is as useless as agreement; do neither. If the caller states no leading approach, generate independently and let the lead compare. Return 3 to 5 approaches that differ in MECHANISM, not in phrasing. For each: how it works, what it costs, what would have to be true for it to be the right answer, and the failure mode that would kill it. Near-duplicate options are the failure mode to avoid — if only one real approach exists, say so plainly and explain why the alternatives are dead, because one honest option beats four padded ones. Screen EVERY candidate against this repository and this environment before you rank them, then verify the one you are about to recommend can actually run here: read the code path it depends on, the guard that would refuse it, the artifact it assumes exists. A recommendation that cannot execute is worse than no recommendation. If checking kills your front-runner, rerank and say so. Ground every option in what the repository actually contains, and prefer reusing what is already there over inventing something new. " + readOnlyToolSteer() + " Do the work yourself — do not spawn further subagents.",
1368
1406
  tools: readOnlyToolAllowlist(searchKey),
1369
- ...brainstormModel$1 ? { model: brainstormModel$1 } : {},
1407
+ ...brainstormModel ? { model: brainstormModel } : {},
1370
1408
  ...searchMcp
1371
1409
  };
1372
- if (scoutModel$1) out.scout = {
1373
- description: `Read-only exploration subagent running ${scoutModel$1} (fast and cheap, so repository lookups do not run at the lead's model rates). Use proactively to find or understand something in the codebase: it sweeps widely and returns conclusions with file:line references rather than file dumps. Model is overridable at spawn.`,
1410
+ if (scoutModel) out.scout = {
1411
+ description: `Read-only exploration subagent running ${scoutModel} (fast and cheap, so repository lookups do not run at the lead's model rates). Use proactively to find or understand something in the codebase: it sweeps widely and returns conclusions with file:line references rather than file dumps. Model is overridable at spawn.`,
1374
1412
  prompt: "You are a read-only exploration subagent. Answer the question by investigating the repository: cast a wide net, then narrow. Return the conclusion, not the raw material — cite `file:line` for anything load-bearing and quote only the lines that matter. If the answer is that something does not exist, say so explicitly and describe where you looked. " + readOnlyToolSteer() + " Do the work yourself — do not spawn further subagents.",
1375
1413
  tools: readOnlyToolAllowlist(searchKey),
1376
- model: scoutModel$1,
1414
+ model: scoutModel,
1377
1415
  ...searchMcp
1378
1416
  };
1379
1417
  out.scribe = {
1380
- description: scribeModel$1 ? `Documentation subagent running ${scribeModel$1}. Use proactively for prose that trails the code: docs, ADRs, CLAUDE.md sections, changelog entries, and README updates that have gone stale. Keeps low-glamour upkeep off the lead's context. Model is overridable at spawn.` : `Documentation subagent (runs on the lead's model in its own context). Use proactively for prose that trails the code: docs, ADRs, CLAUDE.md sections, changelog entries, and README updates that have gone stale. Model is overridable at spawn.`,
1418
+ description: scribeModel ? `Documentation subagent running ${scribeModel}. Use proactively for prose that trails the code: docs, ADRs, CLAUDE.md sections, changelog entries, and README updates that have gone stale. Keeps low-glamour upkeep off the lead's context. Model is overridable at spawn.` : `Documentation subagent (runs on the lead's model in its own context). Use proactively for prose that trails the code: docs, ADRs, CLAUDE.md sections, changelog entries, and README updates that have gone stale. Model is overridable at spawn.`,
1381
1419
  prompt: "You are a documentation subagent. Write and maintain the prose that trails the code: docs, ADRs, CLAUDE.md sections, changelog entries, README rows. Read the code before describing it — every claim you write must be checkable against the repository as it is now, not as a summary said it was. Match the surrounding document's voice, structure, and level of detail. Prefer updating an existing document over adding a new one, and delete what has become false rather than layering a correction on top of it. " + fileToolSteer("builds") + " Do the work yourself — do not spawn further subagents. Report which documents changed and any claim you could not verify.",
1382
- ...scribeModel$1 ? { model: scribeModel$1 } : {}
1420
+ ...scribeModel ? { model: scribeModel } : {}
1383
1421
  };
1384
1422
  if (opts.workerToolsAvailable) {
1385
1423
  const workersKey = workersKeyOf(opts.groupKeys);
@@ -1406,7 +1444,7 @@ function buildPeerAgentDefinitions(opts) {
1406
1444
  * sweep is scoped to peer-* names only via the persona-name allowlist.
1407
1445
  */
1408
1446
  function defaultAgentsDir() {
1409
- return nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "agents");
1447
+ return path.join(PATHS.CLAUDE_CONFIG_DIR, "agents");
1410
1448
  }
1411
1449
  /**
1412
1450
  * YAML frontmatter string-escape — sufficient for our use case where
@@ -1506,16 +1544,16 @@ function buildAgentMd(spec) {
1506
1544
  * Returns the file paths plus a cleanup() that unlinks them.
1507
1545
  */
1508
1546
  async function writePeerAgentMdFiles(agents, opts) {
1509
- for (const name$1 of Object.keys(agents)) if (!VALID_AGENT_NAME.test(name$1)) throw new Error(`writePeerAgentMdFiles: invalid agent name ${JSON.stringify(name$1)} — must match ${VALID_AGENT_NAME.source}`);
1547
+ for (const name of Object.keys(agents)) if (!VALID_AGENT_NAME.test(name)) throw new Error(`writePeerAgentMdFiles: invalid agent name ${JSON.stringify(name)} — must match ${VALID_AGENT_NAME.source}`);
1510
1548
  const dir = opts.agentsDir ?? defaultAgentsDir();
1511
1549
  await fs.mkdir(dir, { recursive: true });
1512
1550
  const paths = [];
1513
1551
  try {
1514
- for (const [name$1, def] of Object.entries(agents)) {
1515
- const filePath = nodePath.join(dir, `peer-${opts.fileSuffix}-${name$1}.md`);
1552
+ for (const [name, def] of Object.entries(agents)) {
1553
+ const filePath = path.join(dir, `peer-${opts.fileSuffix}-${name}.md`);
1516
1554
  await fs.unlink(filePath).catch(() => {});
1517
1555
  await writeRuntimeFileSecure(filePath, buildAgentMd({
1518
- name: name$1,
1556
+ name,
1519
1557
  description: def.description,
1520
1558
  prompt: def.prompt,
1521
1559
  model: def.model,
@@ -1574,7 +1612,7 @@ async function readMcpServersSnapshot(target) {
1574
1612
  */
1575
1613
  async function resolveGroupKeysFromMirror(enabledGroups, claudeConfigDir) {
1576
1614
  const dir = claudeConfigDir ?? PATHS.CLAUDE_CONFIG_DIR;
1577
- const existing = await readMcpServersSnapshot(nodePath.join(dir, ".claude.json"));
1615
+ const existing = await readMcpServersSnapshot(path.join(dir, ".claude.json"));
1578
1616
  const keys = {};
1579
1617
  for (const group of enabledGroups) {
1580
1618
  const bare = GROUP_META[group].preferredKey;
@@ -1622,7 +1660,7 @@ async function resolveGroupKeysFromMirror(enabledGroups, claudeConfigDir) {
1622
1660
  */
1623
1661
  async function injectPeerMcpIntoMirror(serverUrl, opts) {
1624
1662
  const dir = opts.claudeConfigDir ?? PATHS.CLAUDE_CONFIG_DIR;
1625
- const target = nodePath.join(dir, ".claude.json");
1663
+ const target = path.join(dir, ".claude.json");
1626
1664
  let existing = {};
1627
1665
  try {
1628
1666
  const raw = await fs.readFile(target, "utf8");
@@ -1652,7 +1690,7 @@ async function injectPeerMcpIntoMirror(serverUrl, opts) {
1652
1690
  workspaceHeaderCmd: buildWorkspaceHeaderHelperCommand(process.execPath, process.argv[1])
1653
1691
  });
1654
1692
  const conflicts = [];
1655
- for (const name$1 of Object.keys(peerConfig.mcpServers)) if (mcpServers[name$1] !== void 0) conflicts.push(name$1);
1693
+ for (const name of Object.keys(peerConfig.mcpServers)) if (mcpServers[name] !== void 0) conflicts.push(name);
1656
1694
  if (conflicts.length > 0) {
1657
1695
  consola.warn(`injectPeerMcpIntoMirror: your ~/.claude/.claude.json already has mcpServers entries named [${conflicts.join(", ")}]; refusing to overwrite. Subagents will not see those tools — only the parent session via --mcp-config fallback. To resolve, rename the user-side server(s) (e.g. via \`claude mcp remove\`) and relaunch.`);
1658
1696
  return {
@@ -1661,7 +1699,7 @@ async function injectPeerMcpIntoMirror(serverUrl, opts) {
1661
1699
  conflictingServers: conflicts
1662
1700
  };
1663
1701
  }
1664
- for (const [name$1, entry] of Object.entries(peerConfig.mcpServers)) mcpServers[name$1] = entry;
1702
+ for (const [name, entry] of Object.entries(peerConfig.mcpServers)) mcpServers[name] = entry;
1665
1703
  existing.mcpServers = mcpServers;
1666
1704
  const desiredJson = JSON.stringify(existing, null, 2) + "\n";
1667
1705
  await fs.mkdir(dir, { recursive: true });
@@ -1700,8 +1738,8 @@ async function writePeerMcpRuntimeFiles(serverUrl, opts) {
1700
1738
  await fs.mkdir(runtimeDir, { recursive: true });
1701
1739
  if (process.platform !== "win32") await fs.chmod(runtimeDir, 448).catch(() => {});
1702
1740
  const fileSuffix = `${process.pid}-${randomBytes(4).toString("hex")}`;
1703
- const mcpConfigPath = nodePath.join(runtimeDir, `peer-mcp-${fileSuffix}.json`);
1704
- const agentsPath = nodePath.join(runtimeDir, `peer-agents-${fileSuffix}.json`);
1741
+ const mcpConfigPath = path.join(runtimeDir, `peer-mcp-${fileSuffix}.json`);
1742
+ const agentsPath = path.join(runtimeDir, `peer-agents-${fileSuffix}.json`);
1705
1743
  const mcpConfig = buildPeerMcpConfig(serverUrl, {
1706
1744
  codexCli: opts.codexCli,
1707
1745
  geminiAvailable: opts.geminiAvailable,
@@ -1757,15 +1795,14 @@ async function writePeerMcpRuntimeFiles(serverUrl, opts) {
1757
1795
  cleanup
1758
1796
  };
1759
1797
  }
1760
-
1761
1798
  //#endregion
1762
1799
  //#region src/lib/file-log-reporter.ts
1763
- const MAX_LOG_BYTES = 1024 * 1024;
1800
+ const MAX_LOG_BYTES = 1048576;
1764
1801
  const DEDUP_MAX = 1e3;
1765
1802
  const ARG_MAX_LEN = 2048;
1766
1803
  const DEDUP_KEY_MAX_LEN = 200;
1767
1804
  const CREDENTIAL_RE = /\b(eyJ[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]+){0,2}|gh[opsu]_[A-Za-z0-9_]{20,}|Bearer\s+\S{20,})\b/g;
1768
- const ALLOWED_TYPES = new Set([
1805
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set([
1769
1806
  "fatal",
1770
1807
  "error",
1771
1808
  "warn"
@@ -1909,7 +1946,6 @@ function enableFileLogging() {
1909
1946
  consola.options.stdout = nullStream;
1910
1947
  consola.options.stderr = nullStream;
1911
1948
  }
1912
-
1913
1949
  //#endregion
1914
1950
  //#region src/lib/mcp-permissions-settings.ts
1915
1951
  /**
@@ -2121,7 +2157,6 @@ async function configureServeDefaultPermissionMode(settingsPath) {
2121
2157
  }, "servemode");
2122
2158
  return { written: true };
2123
2159
  }
2124
-
2125
2160
  //#endregion
2126
2161
  //#region src/lib/model-validation.ts
2127
2162
  const ENDPOINT_ALIASES = {
@@ -2169,10 +2204,34 @@ function listModelsForEndpoint(path) {
2169
2204
  return supported.includes(endpoint);
2170
2205
  }).map((m) => m.id);
2171
2206
  }
2172
-
2173
2207
  //#endregion
2174
2208
  //#region src/lib/orchestration/hook-mcp-client.ts
2175
2209
  /**
2210
+ * Tiny MCP-over-HTTP + inference client for the advisory-review hooks (hook V2).
2211
+ *
2212
+ * The hook subcommands (`internal-stop-review`, `internal-prompt-submit`) run as
2213
+ * SEPARATE short-lived processes spawned by Claude Code — they have no in-process
2214
+ * proxy `state`. They reach the running github-router proxy over loopback HTTP:
2215
+ *
2216
+ * - `callMcpTool` POSTs a JSON-RPC `tools/call` to `${serverUrl}/mcp/<group>`
2217
+ * with the per-launch `Authorization: Bearer <nonce>` (the same nonce the
2218
+ * proxy minted in `writePeerMcpRuntimeFiles` and validates in the /mcp
2219
+ * handler). Used for `workers/review` (the background reviewer) and
2220
+ * `search/code` (the prompt-hook grounding search). Forces the JSON response
2221
+ * path (`Accept: application/json`) — the SSE path's 60s-ceiling workaround
2222
+ * is a Claude-Code-MCP-client concern; our own fetch has no such cap, so the
2223
+ * simpler single-body JSON path is correct here.
2224
+ *
2225
+ * - `callInference` POSTs to `${serverUrl}/v1/responses` (no nonce — the /v1/*
2226
+ * passthrough authenticates upstream with the proxy's own Copilot token) for
2227
+ * the single gpt-5.6-sol scope/goal call the prompt hook makes.
2228
+ *
2229
+ * Everything here is best-effort and caller-fail-open: a transport error,
2230
+ * non-2xx, timeout, or malformed body throws, and every caller wraps the call so
2231
+ * the hook degrades gracefully (no findings / the regex-heuristic goal) rather
2232
+ * than disrupting the session.
2233
+ */
2234
+ /**
2176
2235
  * Read the proxy URL + nonce the launcher injected into the spawned child env
2177
2236
  * (`GH_ROUTER_HOOK_MCP_URL` / `GH_ROUTER_HOOK_NONCE`). Returns undefined when
2178
2237
  * either is absent — the hook then skips its LLM layer and falls back to its
@@ -2277,22 +2336,15 @@ async function postJson(url, payload, opts) {
2277
2336
  if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
2278
2337
  }
2279
2338
  }
2280
-
2281
- //#endregion
2282
- //#region src/lib/orchestration/plan-review-hook.ts
2283
- /** Minimum finalized-plan size worth spending a model review on. */
2284
- const PLAN_REVIEW_MIN_CHARS = 220;
2285
- /** Hard wall-clock for the advisory plan critic. */
2286
- const PLAN_REVIEW_TIMEOUT_MS = 25e3;
2287
2339
  /** Cap the plan embedded in the critic brief so a huge plan file cannot dominate. */
2288
- const MAX_REVIEWED_PLAN_CHARS = 80 * 1024;
2340
+ const MAX_REVIEWED_PLAN_CHARS = 81920;
2289
2341
  /** Default-on advisory plan review. Opt out with GH_ROUTER_DISABLE_PLAN_REVIEW=1. */
2290
2342
  function planReviewEnabled(env = process.env) {
2291
2343
  return parseBoolEnv(env.GH_ROUTER_DISABLE_PLAN_REVIEW) !== true;
2292
2344
  }
2293
2345
  /** File-backed per-session debounce for finalized plans, separate from Stop diff hashes. */
2294
2346
  function filePlanReviewDebounce(stateDir) {
2295
- const fileFor = (sid) => nodePath.join(stateDir, `plan-review-hash-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
2347
+ const fileFor = (sid) => path.join(stateDir, `plan-review-hash-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
2296
2348
  const readLast = async (sid) => {
2297
2349
  try {
2298
2350
  return (await promises.readFile(fileFor(sid), "utf8")).trim();
@@ -2317,12 +2369,12 @@ function parseObject(raw) {
2317
2369
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
2318
2370
  } catch {}
2319
2371
  }
2320
- async function resolvePlanText(toolInput, readFile$1) {
2372
+ async function resolvePlanText(toolInput, readFile) {
2321
2373
  if (!toolInput || typeof toolInput !== "object") return "";
2322
2374
  const ti = toolInput;
2323
2375
  const planFilePath = typeof ti.planFilePath === "string" && ti.planFilePath.trim().length > 0 ? ti.planFilePath.trim() : "";
2324
2376
  if (planFilePath.length > 0) try {
2325
- const fromFile = await readFile$1(planFilePath);
2377
+ const fromFile = await readFile(planFilePath);
2326
2378
  if (fromFile.trim().length > 0) return fromFile;
2327
2379
  } catch {}
2328
2380
  return typeof ti.plan === "string" ? ti.plan : "";
@@ -2355,7 +2407,7 @@ async function decidePlanReviewHook(input) {
2355
2407
  };
2356
2408
  const cwd = typeof payload.cwd === "string" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd;
2357
2409
  const trimmed = (await resolvePlanText(payload.tool_input, input.readFile ?? ((filePath) => promises.readFile(filePath, "utf8")))).trim();
2358
- if (trimmed.length < (input.minChars ?? PLAN_REVIEW_MIN_CHARS)) return {
2410
+ if (trimmed.length < (input.minChars ?? 220)) return {
2359
2411
  kind: "skip",
2360
2412
  reason: "trivial-plan"
2361
2413
  };
@@ -2397,7 +2449,7 @@ function framePlanFindings(text) {
2397
2449
  }
2398
2450
  /** Run the bounded critic call and write material findings to the shared findings store. */
2399
2451
  async function runPlanReview(input) {
2400
- const timeoutMs = input.timeoutMs ?? PLAN_REVIEW_TIMEOUT_MS;
2452
+ const timeoutMs = input.timeoutMs ?? 25e3;
2401
2453
  let timer;
2402
2454
  const controller = new AbortController();
2403
2455
  try {
@@ -2433,12 +2485,39 @@ function buildPlanReviewHookCommand(execPath, scriptPath) {
2433
2485
  if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-plan-review`;
2434
2486
  return `${q(execPath)} internal-plan-review`;
2435
2487
  }
2436
-
2437
2488
  //#endregion
2438
2489
  //#region src/lib/orchestration/harness-parse.ts
2490
+ /**
2491
+ * Deterministic, language-agnostic harness PARSER for the structural Stop-gate.
2492
+ *
2493
+ * The gate runs a repo's own checks (typecheck/lint/test/build) when the agent
2494
+ * tries to finish. Historically it only auto-enabled for Bun/TS repos via a
2495
+ * hard-coded sealed gate. This module generalizes detection by PARSING the
2496
+ * project's own authoritative config — `package.json` scripts, CI `run:` steps,
2497
+ * Make/just/task targets, and language manifests — and emitting the canonical
2498
+ * check commands it finds.
2499
+ *
2500
+ * Design properties (a cross-lab panel chose a parser over a model for these):
2501
+ * - EVIDENCE-PINNED BY CONSTRUCTION: every emitted command is either lifted
2502
+ * verbatim from a parsed source file (a package.json script, a CI `run:`
2503
+ * line, a Make target invocation) or is a fixed canonical manifest command
2504
+ * for a detected ecosystem (`cargo check`, `go vet ./...`). The parser never
2505
+ * invents a command, so it has no hallucination / prompt-injection surface.
2506
+ * - PURE + DETERMINISTIC: a function of the repo's files + the live PATH. The
2507
+ * runtime Stop hook re-derives it at each stop (no cache), so it always
2508
+ * reflects the current tree and fails open for free when a tool vanished.
2509
+ * - SHELL-SAFE: `liveExec` runs `command.trim().split(/\s+/)` (naive argv
2510
+ * split, no shell), so every emitted command is validated to be plain
2511
+ * space-separated tokens with no shell operators and no `%` (cmd.exe quoting
2512
+ * throws on `%`). See `isSafeCommand`.
2513
+ *
2514
+ * This is consumed ONLY by the local Stop hook. The sealed-gate kernel
2515
+ * (`run_workflow`) is never fed a parsed command — `GateDescriptor.kind` is
2516
+ * `"parsed"`, never a sealed id, and `sealedGateIds()` is unchanged.
2517
+ */
2439
2518
  /** The fast static checks that are always-on; `test` is opt-in (it runs project
2440
2519
  * code and can be slow on every stop). */
2441
- const STATIC_IDS = new Set(["typecheck", "lint"]);
2520
+ const STATIC_IDS = /* @__PURE__ */ new Set(["typecheck", "lint"]);
2442
2521
  /** Reject anything that is not a plain, shell-free, `%`-free argv line. This is
2443
2522
  * the load-bearing guard for `liveExec`'s naive whitespace split. */
2444
2523
  function isSafeCommand(command) {
@@ -2486,7 +2565,7 @@ async function readTextFile(file) {
2486
2565
  }
2487
2566
  /** package.json `scripts` as a plain string→string map (best-effort). */
2488
2567
  async function readScripts(root) {
2489
- const pkg = await readJsonFile(nodePath.join(root, "package.json"));
2568
+ const pkg = await readJsonFile(path.join(root, "package.json"));
2490
2569
  const scripts = pkg && typeof pkg === "object" ? pkg.scripts : void 0;
2491
2570
  const out = {};
2492
2571
  if (scripts && typeof scripts === "object") {
@@ -2496,31 +2575,31 @@ async function readScripts(root) {
2496
2575
  }
2497
2576
  /** Pick the JS package runner from the lockfile present at the root. */
2498
2577
  function nodeRunner(root) {
2499
- if (existsSync(nodePath.join(root, "bun.lockb")) || existsSync(nodePath.join(root, "bun.lock"))) return "bun";
2500
- if (existsSync(nodePath.join(root, "pnpm-lock.yaml"))) return "pnpm";
2501
- if (existsSync(nodePath.join(root, "yarn.lock"))) return "yarn";
2578
+ if (existsSync(path.join(root, "bun.lockb")) || existsSync(path.join(root, "bun.lock"))) return "bun";
2579
+ if (existsSync(path.join(root, "pnpm-lock.yaml"))) return "pnpm";
2580
+ if (existsSync(path.join(root, "yarn.lock"))) return "yarn";
2502
2581
  return "npm";
2503
2582
  }
2504
2583
  /** Map a package.json script NAME to a canonical id (read-only checks only — a
2505
2584
  * `:fix`/`:write` variant mutates the tree and is never a gate). */
2506
- function scriptNameToId(name$1) {
2507
- if (/:(fix|write)$/i.test(name$1)) return null;
2508
- if (/^(typecheck|type-check|check-types|tsc|types)$/i.test(name$1)) return "typecheck";
2509
- if (/^(lint|lint:check|eslint)$/i.test(name$1)) return "lint";
2510
- if (/^test$/i.test(name$1)) return "test";
2585
+ function scriptNameToId(name) {
2586
+ if (/:(fix|write)$/i.test(name)) return null;
2587
+ if (/^(typecheck|type-check|check-types|tsc|types)$/i.test(name)) return "typecheck";
2588
+ if (/^(lint|lint:check|eslint)$/i.test(name)) return "lint";
2589
+ if (/^test$/i.test(name)) return "test";
2511
2590
  return null;
2512
2591
  }
2513
2592
  async function collectNodeChecks(root, includeTests) {
2514
- if (!existsSync(nodePath.join(root, "package.json"))) return [];
2593
+ if (!existsSync(path.join(root, "package.json"))) return [];
2515
2594
  const runner = nodeRunner(root);
2516
2595
  if (resolveExecutable(runner, { env: process.env }) === null) return [];
2517
2596
  const scripts = await readScripts(root);
2518
2597
  const out = [];
2519
- for (const [name$1] of Object.entries(scripts)) {
2520
- const id = scriptNameToId(name$1);
2598
+ for (const [name] of Object.entries(scripts)) {
2599
+ const id = scriptNameToId(name);
2521
2600
  if (!id) continue;
2522
2601
  if (id === "test" && !includeTests) continue;
2523
- const command = `${runner} run ${name$1}`;
2602
+ const command = `${runner} run ${name}`;
2524
2603
  if (isSafeCommand(command)) out.push({
2525
2604
  id,
2526
2605
  command,
@@ -2538,11 +2617,11 @@ async function collectNodeChecks(root, includeTests) {
2538
2617
  */
2539
2618
  async function collectCiChecks(root) {
2540
2619
  const files = [];
2541
- const wfDir = nodePath.join(root, ".github", "workflows");
2620
+ const wfDir = path.join(root, ".github", "workflows");
2542
2621
  try {
2543
- for (const name$1 of await promises.readdir(wfDir)) if (/\.ya?ml$/i.test(name$1)) files.push(nodePath.join(wfDir, name$1));
2622
+ for (const name of await promises.readdir(wfDir)) if (/\.ya?ml$/i.test(name)) files.push(path.join(wfDir, name));
2544
2623
  } catch {}
2545
- const gitlab = nodePath.join(root, ".gitlab-ci.yml");
2624
+ const gitlab = path.join(root, ".gitlab-ci.yml");
2546
2625
  if (existsSync(gitlab)) files.push(gitlab);
2547
2626
  if (files.length === 0) return [];
2548
2627
  let parseYaml;
@@ -2588,7 +2667,7 @@ async function collectCiChecks(root) {
2588
2667
  const text = await readTextFile(f);
2589
2668
  if (text === void 0) continue;
2590
2669
  try {
2591
- walk(parseYaml(text), nodePath.relative(root, f) || nodePath.basename(f));
2670
+ walk(parseYaml(text), path.relative(root, f) || path.basename(f));
2592
2671
  } catch {}
2593
2672
  }
2594
2673
  return out;
@@ -2603,7 +2682,7 @@ async function collectTaskChecks(root, includeTests) {
2603
2682
  ["justfile", "just"],
2604
2683
  [".justfile", "just"]
2605
2684
  ]) {
2606
- const text = await readTextFile(nodePath.join(root, file));
2685
+ const text = await readTextFile(path.join(root, file));
2607
2686
  if (text === void 0) continue;
2608
2687
  if (resolveExecutable(tool, { env: process.env }) === null) continue;
2609
2688
  for (const line of text.split(/\r?\n/)) {
@@ -2619,20 +2698,20 @@ async function collectTaskChecks(root, includeTests) {
2619
2698
  });
2620
2699
  }
2621
2700
  }
2622
- const taskfile = ["Taskfile.yml", "Taskfile.yaml"].map((n) => nodePath.join(root, n)).find((p) => existsSync(p));
2701
+ const taskfile = ["Taskfile.yml", "Taskfile.yaml"].map((n) => path.join(root, n)).find((p) => existsSync(p));
2623
2702
  if (taskfile && resolveExecutable("task", { env: process.env }) !== null) {
2624
2703
  const text = await readTextFile(taskfile);
2625
2704
  if (text !== void 0) try {
2626
2705
  const { parse } = await import("yaml");
2627
2706
  const doc = parse(text);
2628
- for (const name$1 of Object.keys(doc?.tasks ?? {})) {
2629
- const id = wantId(name$1);
2707
+ for (const name of Object.keys(doc?.tasks ?? {})) {
2708
+ const id = wantId(name);
2630
2709
  if (!id || id === "test" && !includeTests) continue;
2631
- const command = `task ${name$1}`;
2710
+ const command = `task ${name}`;
2632
2711
  if (isSafeCommand(command)) out.push({
2633
2712
  id,
2634
2713
  command,
2635
- source: nodePath.basename(taskfile)
2714
+ source: path.basename(taskfile)
2636
2715
  });
2637
2716
  }
2638
2717
  } catch {}
@@ -2640,7 +2719,7 @@ async function collectTaskChecks(root, includeTests) {
2640
2719
  return out;
2641
2720
  }
2642
2721
  async function collectRustChecks(root, includeTests) {
2643
- if (!existsSync(nodePath.join(root, "Cargo.toml"))) return [];
2722
+ if (!existsSync(path.join(root, "Cargo.toml"))) return [];
2644
2723
  if (resolveExecutable("cargo", { env: process.env }) === null) return [];
2645
2724
  const out = [{
2646
2725
  id: "typecheck",
@@ -2660,7 +2739,7 @@ async function collectRustChecks(root, includeTests) {
2660
2739
  return out;
2661
2740
  }
2662
2741
  async function collectGoChecks(root, includeTests) {
2663
- if (!existsSync(nodePath.join(root, "go.mod"))) return [];
2742
+ if (!existsSync(path.join(root, "go.mod"))) return [];
2664
2743
  if (resolveExecutable("go", { env: process.env }) === null) return [];
2665
2744
  const out = [{
2666
2745
  id: "typecheck",
@@ -2682,11 +2761,11 @@ async function collectPythonChecks(root, includeTests) {
2682
2761
  "tox.ini",
2683
2762
  "ruff.toml",
2684
2763
  "mypy.ini"
2685
- ].filter((f) => existsSync(nodePath.join(root, f)));
2764
+ ].filter((f) => existsSync(path.join(root, f)));
2686
2765
  if (present.length === 0) return [];
2687
2766
  const evidence = present[0];
2688
2767
  const out = [];
2689
- const configText = await readTextFile(nodePath.join(root, present[0])) ?? "";
2768
+ const configText = await readTextFile(path.join(root, present[0])) ?? "";
2690
2769
  const mentions = (tool) => present.some((f) => f.startsWith(tool)) || configText.includes(tool);
2691
2770
  if (mentions("mypy") && resolveExecutable("mypy", { env: process.env }) !== null) out.push({
2692
2771
  id: "typecheck",
@@ -2698,7 +2777,7 @@ async function collectPythonChecks(root, includeTests) {
2698
2777
  command: "ruff check .",
2699
2778
  source: evidence
2700
2779
  });
2701
- if (includeTests && (mentions("pytest") || existsSync(nodePath.join(root, "pytest.ini")))) {
2780
+ if (includeTests && (mentions("pytest") || existsSync(path.join(root, "pytest.ini")))) {
2702
2781
  if (resolveExecutable("pytest", { env: process.env }) !== null) out.push({
2703
2782
  id: "test",
2704
2783
  command: "pytest -q",
@@ -2798,9 +2877,29 @@ function descriptorHash(d) {
2798
2877
  const canon = [...d.checks].map((c) => `${c.id}${c.command.trim().replace(/\s+/g, " ")}`).sort().join("");
2799
2878
  return `${d.kind}:${createHash("sha256").update(canon).digest("hex").slice(0, 32)}`;
2800
2879
  }
2801
-
2802
2880
  //#endregion
2803
2881
  //#region src/lib/orchestration/gate-discovery.ts
2882
+ /**
2883
+ * Evidence-pinned model FALLBACK for the structural Stop-gate — the last resort
2884
+ * when the deterministic parser (`harness-parse`) finds no runnable checks.
2885
+ *
2886
+ * A read-only worker reads the repo's own config/docs and proposes the canonical
2887
+ * check commands. Two guards keep this safe despite being model-authored:
2888
+ * 1. SANITIZE — `sanitizeDiscoveredCheck` rejects shell metacharacters (so a
2889
+ * command can never chain/redirect/expand), destructive/stateful verbs,
2890
+ * interactive/watch shapes, and an executable that isn't on PATH. What
2891
+ * survives is a single plain argv line, safe for `liveExec`'s naive split.
2892
+ * 2. EVIDENCE-PIN — a surviving command must appear (whitespace-normalized)
2893
+ * VERBATIM in one of the collected source files. The model cannot invent a
2894
+ * command or be prompt-injected into emitting one that isn't already a real
2895
+ * command in the repo (and a real command in a user-trusted repo is the
2896
+ * same authority the existing gate already runs).
2897
+ *
2898
+ * Discovery runs ONCE at launch and the result is cached per (repoFingerprint,
2899
+ * sourcesHash) in a human-readable record. The runtime Stop hook only READS the
2900
+ * cached record (no model call at stop). This is consumed ONLY by the local Stop
2901
+ * hook; the sealed-gate kernel never sees a discovered command.
2902
+ */
2804
2903
  /** The allowlist of files the discovery worker is steered to read — config +
2805
2904
  * docs that legitimately describe how to check a project. Secret files are
2806
2905
  * additionally blocked at the worker IO layer (`.env*`/`*.pem`/`id_*`/…). */
@@ -2829,11 +2928,11 @@ const SIGNAL_FILES = [
2829
2928
  "composer.json"
2830
2929
  ];
2831
2930
  const SIGNAL_DIRS = [".github/workflows"];
2832
- const MAX_SIGNAL_BYTES = 64 * 1024;
2931
+ const MAX_SIGNAL_BYTES = 65536;
2833
2932
  /** Global caps so a repo with many workflow files can't inflate discovery /
2834
2933
  * hashing cost: at most this many files and this much total text. */
2835
2934
  const MAX_SIGNAL_FILES = 40;
2836
- const MAX_TOTAL_SIGNAL_BYTES = 512 * 1024;
2935
+ const MAX_TOTAL_SIGNAL_BYTES = 524288;
2837
2936
  /** Words that mark a command as destructive, stateful, or non-terminating — a
2838
2937
  * "check" must never do any of these. (Shell operators are already rejected by
2839
2938
  * `isSafeCommand`, so chaining can't smuggle them past this word scan.) */
@@ -2850,7 +2949,8 @@ function sanitizeDiscoveredCheck(command) {
2850
2949
  const norm = normalizeWs(command);
2851
2950
  if (DENY_WORD.test(norm)) return false;
2852
2951
  if (isMutatingCommand(norm)) return false;
2853
- return resolveExecutable(norm.split(" ")[0] ?? "", { env: process.env }) !== null;
2952
+ const first = norm.split(" ")[0] ?? "";
2953
+ return resolveExecutable(first, { env: process.env }) !== null;
2854
2954
  }
2855
2955
  function normalizeWs(s) {
2856
2956
  return s.replace(/\s+/g, " ").trim();
@@ -2872,13 +2972,13 @@ async function collectSignals(root) {
2872
2972
  } catch {}
2873
2973
  };
2874
2974
  for (const f of SIGNAL_FILES) {
2875
- const abs = nodePath.join(root, f);
2975
+ const abs = path.join(root, f);
2876
2976
  if (existsSync(abs)) await readCapped(abs, f);
2877
2977
  }
2878
2978
  for (const d of SIGNAL_DIRS) {
2879
- const dir = nodePath.join(root, d);
2979
+ const dir = path.join(root, d);
2880
2980
  try {
2881
- for (const name$1 of await promises.readdir(dir)) if (/\.(ya?ml)$/i.test(name$1)) await readCapped(nodePath.join(dir, name$1), nodePath.join(d, name$1));
2981
+ for (const name of await promises.readdir(dir)) if (/\.(ya?ml)$/i.test(name)) await readCapped(path.join(dir, name), path.join(d, name));
2882
2982
  } catch {}
2883
2983
  }
2884
2984
  return {
@@ -2893,10 +2993,10 @@ async function sourcesHash(root) {
2893
2993
  return createHash("sha256").update(files.sort().join("\n")).update("\0").update(text).digest("hex");
2894
2994
  }
2895
2995
  function discoveredDir() {
2896
- return nodePath.join(PATHS.APP_DIR, "stop-gate", "discovered");
2996
+ return path.join(PATHS.APP_DIR, "stop-gate", "discovered");
2897
2997
  }
2898
2998
  function recordPathFor(root) {
2899
- return nodePath.join(discoveredDir(), createHash("sha256").update(nodePath.resolve(root)).digest("hex").slice(0, 32));
2999
+ return path.join(discoveredDir(), createHash("sha256").update(path.resolve(root)).digest("hex").slice(0, 32));
2900
3000
  }
2901
3001
  /** Read the cached discovered record for `root`, verifying it still matches the
2902
3002
  * live repo identity AND the live sources hash. Any mismatch / unreadable /
@@ -2931,16 +3031,16 @@ const DISCOVERY_PROMPT = (files) => `You are configuring an automated pre-finish
2931
3031
  function extractJson(text) {
2932
3032
  const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
2933
3033
  const body = fenced ? fenced[1] : text;
2934
- const start$1 = body.indexOf("{");
3034
+ const start = body.indexOf("{");
2935
3035
  const end = body.lastIndexOf("}");
2936
- if (start$1 < 0 || end <= start$1) return void 0;
3036
+ if (start < 0 || end <= start) return void 0;
2937
3037
  try {
2938
- return JSON.parse(body.slice(start$1, end + 1));
3038
+ return JSON.parse(body.slice(start, end + 1));
2939
3039
  } catch {
2940
3040
  return;
2941
3041
  }
2942
3042
  }
2943
- const VALID_IDS = new Set([
3043
+ const VALID_IDS = /* @__PURE__ */ new Set([
2944
3044
  "typecheck",
2945
3045
  "lint",
2946
3046
  "test"
@@ -2987,7 +3087,7 @@ async function discoverGateCommands(cwd, opts) {
2987
3087
  if (files.length === 0) return null;
2988
3088
  let result;
2989
3089
  try {
2990
- const { runWorkerAgent } = await import("./engine-5VxiBAdY.js");
3090
+ const { runWorkerAgent } = await import("./engine-DL15-epb.js");
2991
3091
  result = await runWorkerAgent({
2992
3092
  mode: "explore",
2993
3093
  workspace: root,
@@ -3010,10 +3110,25 @@ async function discoverGateCommands(cwd, opts) {
3010
3110
  evidence: files
3011
3111
  };
3012
3112
  }
3013
-
3014
3113
  //#endregion
3015
3114
  //#region src/lib/orchestration/prompt-submit-hook.ts
3016
3115
  /**
3116
+ * Pure decision logic for the `UserPromptSubmit` hook (the front-end of the
3117
+ * floor-raising surface). It serves TWO duties on the one event:
3118
+ *
3119
+ * 1. Per-prompt budget reset — clears the Stop-gate's block counter for this
3120
+ * session so `maxBlocks` is "per user prompt", not "per session lifetime"
3121
+ * (the per-prompt scoping the Stop-gate relies on).
3122
+ * 2. Goal steer — for a NON-trivial prompt, inject an advisory GOAL directive
3123
+ * (additive context, never blocking) pointing the model at `/gh-research`
3124
+ * then `/gh-orchestrate`. It raises the prior; it does not force execution.
3125
+ *
3126
+ * Top-level only: like the Stop-gate, it stands down in a subagent/teammate
3127
+ * context (the payload carries `agent_type`/`agent_id` there) so it never
3128
+ * recurses into spawned workers. Fail-open and never blocking: any error or a
3129
+ * trivial prompt yields an empty injection.
3130
+ */
3131
+ /**
3017
3132
  * The advisory goal injected for a non-trivial prompt. Uses the skills' slash
3018
3133
  * invocation form. The model still decides whether to follow it; the Stop-gate
3019
3134
  * backstops correctness at the output end.
@@ -3057,7 +3172,7 @@ const PROMPT_SEARCH_TIP = "TIP (advisory): when this task needs code context, se
3057
3172
  * user-derived (not invented) advisory note grounded in the search results. */
3058
3173
  const PROMPT_SCOPE_SYSTEM = "You are a scoping assistant for a coding agent about to act on a user's request. You are given the user's request and the results of a lexical + semantic code search over the relevant repository. Produce a SHORT advisory note (<= 120 words), plain text only:\n1. SCOPE: one line — is this trivial, focused (one area), or large/cross-cutting — grounded in what the search surfaced (reference the most relevant file(s) by name).\n2. GOAL: restate the user's OWN ask as a single measurable objective, in THEIR terms. Do NOT invent new requirements or acceptance criteria beyond what they asked.\n3. Only if the task is large/cross-cutting, add a final line: \"Consider /gh-research first to saturate understanding, then /gh-orchestrate to compose a floor-raising workflow.\" Omit it for a focused or trivial task.\nThis is advisory — the agent decides whether to follow it. Be concrete and concise; no preamble.";
3059
3174
  /** Max chars of each search-result blob fed into the scope inference. */
3060
- const SEARCH_CONTEXT_CAP = 6 * 1024;
3175
+ const SEARCH_CONTEXT_CAP = 6144;
3061
3176
  /** Wrap the prior-turn review findings in an explicitly NON-AUTHORITATIVE frame. */
3062
3177
  function framePendingFindings(findings) {
3063
3178
  return "ADVISORY — independent review of your PREVIOUS change (NON-AUTHORITATIVE): an independent gpt-5.6-sol reviewer flagged the following. Evaluate each on its merits — fix the real ones, and ignore any wrong one with a one-line reason. You are NOT obligated to act on these.\n" + findings.trim();
@@ -3144,9 +3259,22 @@ function buildPromptSubmitHookCommand(execPath, scriptPath) {
3144
3259
  if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-prompt-submit`;
3145
3260
  return `${q(execPath)} internal-prompt-submit`;
3146
3261
  }
3147
-
3148
3262
  //#endregion
3149
3263
  //#region src/lib/first-mate/operator-shaping.ts
3264
+ /**
3265
+ * Operator-mode steering for github-router `--agents` sessions.
3266
+ *
3267
+ * In first-mate/operator mode the spawned Claude is the cloud-agent operator, not
3268
+ * the product implementer. Product implementation is steered to GitHub cloud
3269
+ * agents by first-mate guidance and the gh-first-mate skill. This module no
3270
+ * longer hard-blocks local file writes or shell commands: Edit/Write/NotebookEdit
3271
+ * and Bash remain available as escape hatches when needed.
3272
+ *
3273
+ * The only enforced main-operator boundary is that local worker/orchestrate MCP
3274
+ * tools stay subagent-only. The main operator should invoke the worker-* Agent
3275
+ * subagents or delegate to GitHub cloud agents rather than calling
3276
+ * `mcp__workers__*` / `mcp__orchestrate__*` directly.
3277
+ */
3150
3278
  /** MCP prefixes denied to the main operator; these remain subagent-only. */
3151
3279
  const OPERATOR_DENIED_MCP_PREFIXES = ["mcp__workers__", "mcp__orchestrate__"];
3152
3280
  /**
@@ -3193,9 +3321,19 @@ function operatorPreToolUse(toolName, operatorMode, input) {
3193
3321
  };
3194
3322
  return { block: false };
3195
3323
  }
3196
-
3197
3324
  //#endregion
3198
3325
  //#region src/internal-first-mate-guard.ts
3326
+ /**
3327
+ * The internal `internal-first-mate-guard` subcommand: a PreToolUse hook that
3328
+ * keeps local worker/orchestrate MCP tools subagent-only in cloud-agent OPERATOR
3329
+ * mode. It is injected into the spawned Claude session's settings ONLY in
3330
+ * `--agents`/operator mode, with a matcher scoped to those MCP tool prefixes.
3331
+ *
3332
+ * Reads the PreToolUse payload from stdin (`{tool_name, tool_input, ...}`) and,
3333
+ * if the tool is denied in operator mode, blocks via exit code 2 with a reason
3334
+ * on stderr (the repo's hook convention). Bash and file-authoring tools are not
3335
+ * matched by this guard and are not blocked.
3336
+ */
3199
3337
  function readStdinSync() {
3200
3338
  try {
3201
3339
  return readFileSync(0, "utf8");
@@ -3211,7 +3349,7 @@ const internalFirstMateGuard = defineCommand({
3211
3349
  run() {
3212
3350
  let toolName = "";
3213
3351
  let hookInput;
3214
- let parsed = false;
3352
+ let parsed;
3215
3353
  try {
3216
3354
  const payload = JSON.parse(readStdinSync());
3217
3355
  if (typeof payload.tool_name === "string") toolName = payload.tool_name;
@@ -3241,7 +3379,6 @@ const FIRST_MATE_GUARD_MATCHER = "mcp__workers__.*|mcp__orchestrate__.*";
3241
3379
  function buildFirstMateGuardHookCommand(execPath, entry) {
3242
3380
  return `${JSON.stringify(execPath)} ${JSON.stringify(entry)} internal-first-mate-guard`;
3243
3381
  }
3244
-
3245
3382
  //#endregion
3246
3383
  //#region src/lib/injected-skills/first-mate-conduct-skill.ts
3247
3384
  const FIRST_MATE_CONDUCT_SKILL = {
@@ -3294,7 +3431,6 @@ The ledger is durable memory for unit state; the strategy store is durable memor
3294
3431
  ${DEFINITION_OF_GREATNESS}
3295
3432
  `
3296
3433
  };
3297
-
3298
3434
  //#endregion
3299
3435
  //#region src/lib/injected-skills/first-mate-operate-skill.ts
3300
3436
  const FIRST_MATE_OPERATE_SKILL = {
@@ -3396,14 +3532,12 @@ Hard authority limits: launching to real external channels, any spend or paid ac
3396
3532
  Report the current phase, its checkpoint and whether it is met with reproducible evidence, the active mission(s) and their phase-appropriate acceptance criteria, and the next decision or escalation.
3397
3533
  `
3398
3534
  };
3399
-
3400
3535
  //#endregion
3401
3536
  //#region src/lib/injected-skills/first-mate-setup-skill.ts
3402
3537
  const FIRST_MATE_SETUP_SKILL = {
3403
3538
  name: "gh-first-mate-scaffold",
3404
3539
  md: "---\nname: gh-first-mate-scaffold\ndescription: Scaffolds a repo-geared agentic-dev foundation through first-mate: seeds guidance files, role agents, ADRs, changelog, learnings, PR template, test instructions, Copilot setup, and CI through a scaffold branch and PR. Use before the first build wave on an owned repository.\nuser-invocable: true\n---\n\n# gh-first-mate-scaffold\n\nInvoke the `scaffold_repo` MCP tool (`mcp__first-mate__scaffold_repo`) before the first build wave on an owned repository. The goal is not generic TODO stubs; it is a repo-geared foundation that GitHub agents, local agents, reviewers, and CI can read.\n\n## What it seeds\n\n- `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` / `.github/copilot-instructions.md` — identical guidance with overview, detected stack, commands, hard DoD gate, primary OS, conventions, structure, decisions/memory, handoff, testing, and gotchas.\n- `.github/agents/{ceo,cto,cpo,planner,implementer,reviewer,researcher,tester}.md` mirrored into `.claude/agents/` — C-suite operator hats plus execution roles, each with frontmatter, cold-start contract, method, quality bar, output contract, and self-reminder.\n- `docs/playbook/README.md` — the autonomous DISCOVER → NICHE → POSITION → SCOPE → BUILD → LAUNCH → MEASURE → ITERATE → GROW protocol, externally verifiable phase gates, OODA / Build-Measure-Learn governance, and authority limits.\n- `docs/adrs/0000-template.md` plus `docs/adr/0001-record-architecture-decisions.md` — Nygard-style decision record foundation.\n- `LEARNINGS.md`, `CHANGELOG.md`, `docs/history/0000-template.md`, `docs/plans/README.md`, and `docs/research/README.md` — durable memory, history, plans, and research conventions.\n- `.github/pull_request_template.md` — summary, type, failure-modes-considered-and-tested, and DoD checklist.\n- `.github/instructions/tests.instructions.md` — path-scoped test guidance filled from detected framework/dir/glob where possible.\n- `.github/workflows/copilot-setup-steps.yml` and starter `.github/workflows/ci.yml` — detected toolchain setup with stable quality-gate job names.\n\nIt does not seed factory-protocol or `docs/factory/` files. Orchestration remains outside the product repo in first-mate.\n\n## Usage\n\n```\nmcp__first-mate__scaffold_repo({ repo: \"owner/repo\" })\nmcp__first-mate__scaffold_repo({ repo: \"owner/repo\", mode: \"enhance\" })\nmcp__first-mate__scaffold_repo({\n repo: \"owner/repo\",\n mode: \"add-missing-only\",\n detection_overrides: { primary_os: \"windows-latest\", test_command: \"npm test\" }\n})\n```\n\nModes:\n\n- `add-missing-only` (default): seed absent files and skip present files.\n- `enhance`: for guidance files, product playbook, ADR index, changelog, and learnings, append only missing `##` sections; never rewrite existing prose. Other present files are skipped.\n- `overwrite-approved`: replace existing files only when explicitly approved.\n\nAlways inspect the returned per-file report and PR. A no-op result means the repo already has the foundation or has no missing enhanceable sections.\n"
3405
3540
  };
3406
-
3407
3541
  //#endregion
3408
3542
  //#region src/lib/injected-skills/first-mate-skill.ts
3409
3543
  const FIRST_MATE_SKILL = {
@@ -3565,7 +3699,6 @@ When reporting progress, return:
3565
3699
  - Next wake: the heartbeat state — armed and next check in nextWakeSeconds seconds, or idle (disarmed).
3566
3700
  `
3567
3701
  };
3568
-
3569
3702
  //#endregion
3570
3703
  //#region src/lib/injected-skills/floor-keeper-skill.ts
3571
3704
  const FLOOR_KEEPER_SKILL = {
@@ -3716,7 +3849,6 @@ Return a compact final checkpoint:
3716
3849
  - Do not bury cap-hit or unknown states in a green-sounding summary.
3717
3850
  `
3718
3851
  };
3719
-
3720
3852
  //#endregion
3721
3853
  //#region src/lib/injected-skills/orchestrate-skill.ts
3722
3854
  const ORCHESTRATE_SKILL = {
@@ -3855,7 +3987,6 @@ Return:
3855
3987
  - Final residual risks and next action.
3856
3988
  `
3857
3989
  };
3858
-
3859
3990
  //#endregion
3860
3991
  //#region src/lib/injected-skills/research-skill.ts
3861
3992
  const RESEARCH_SKILL = {
@@ -3967,7 +4098,6 @@ Return a compact brief, not the whole research dump:
3967
4098
  - Do not paste the entire persisted brief into later turns unless the user asks.
3968
4099
  `
3969
4100
  };
3970
-
3971
4101
  //#endregion
3972
4102
  //#region src/lib/injected-skills/worker-skill.ts
3973
4103
  /**
@@ -4028,7 +4158,6 @@ The dispatcher calls the worker once and relays its result verbatim.
4028
4158
  for long worker fan-out.
4029
4159
  `
4030
4160
  };
4031
-
4032
4161
  //#endregion
4033
4162
  //#region src/lib/injected-skills/artifact-review-skill.ts
4034
4163
  const ARTIFACT_REVIEW_SKILL = {
@@ -4102,7 +4231,6 @@ To let the human act on the artifact (not just comment), emit declarative contro
4102
4231
  - The panel is a review surface, not an approver: outward/irreversible actions still need explicit user confirmation.
4103
4232
  `
4104
4233
  };
4105
-
4106
4234
  //#endregion
4107
4235
  //#region src/lib/claude-md-injection.ts
4108
4236
  /**
@@ -4189,7 +4317,7 @@ const OPERATING_DEFAULTS_DIGEST = "## Operating defaults (the user's explicit di
4189
4317
  * the awareness via `--append-system-prompt`, so skipping here only
4190
4318
  * loses descendant-reach.
4191
4319
  */
4192
- const MAX_CLAUDE_MD_BYTES = 1 * 1024 * 1024;
4320
+ const MAX_CLAUDE_MD_BYTES = 1048576;
4193
4321
  /**
4194
4322
  * Bounded retry budget for the temp → rename step on Windows where
4195
4323
  * `fs.rename` can transiently fail with EBUSY / EPERM / EACCES when
@@ -4326,7 +4454,7 @@ async function isUnderClaudeConfigMirrorRealpath(target) {
4326
4454
  consola.warn(`${ERROR_CODE$1}: realpath failed on mirror root ${mirrorRoot}: ${err instanceof Error ? err.message : String(err)}`);
4327
4455
  return false;
4328
4456
  }
4329
- const targetParent = nodePath.dirname(target);
4457
+ const targetParent = path.dirname(target);
4330
4458
  let resolvedTargetParent;
4331
4459
  try {
4332
4460
  resolvedTargetParent = await fs.realpath(targetParent);
@@ -4335,7 +4463,7 @@ async function isUnderClaudeConfigMirrorRealpath(target) {
4335
4463
  return false;
4336
4464
  }
4337
4465
  if (resolvedTargetParent === resolvedRoot) return true;
4338
- return resolvedTargetParent.startsWith(resolvedRoot + nodePath.sep);
4466
+ return resolvedTargetParent.startsWith(resolvedRoot + path.sep);
4339
4467
  }
4340
4468
  /**
4341
4469
  * Try `fs.rename(temp, target)` with bounded retry + verify-on-fail.
@@ -4385,13 +4513,13 @@ async function injectMarkerBlock(opts) {
4385
4513
  consola.warn(`${ERROR_CODE$1}: refusing to inject ${label} snippet that contains marker literal; this would corrupt idempotency on the next launch`);
4386
4514
  return;
4387
4515
  }
4388
- const target = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "CLAUDE.md");
4516
+ const target = path.join(PATHS.CLAUDE_CONFIG_DIR, "CLAUDE.md");
4389
4517
  if (!await isUnderClaudeConfigMirrorRealpath(target)) {
4390
4518
  consola.warn(`${ERROR_CODE$1}: refusing to write outside resolved mirror dir (target=${target}, mirror=${PATHS.CLAUDE_CONFIG_DIR}) [${label}]`);
4391
4519
  return;
4392
4520
  }
4393
- let existingContent = "";
4394
- let targetExists = false;
4521
+ let existingContent;
4522
+ let targetExists;
4395
4523
  try {
4396
4524
  const linkStat = await fs.lstat(target);
4397
4525
  if (linkStat.isSymbolicLink()) {
@@ -4568,9 +4696,25 @@ async function prependArtifactPanelDirectiveToMirroredClaudeMd(peersKey = "peers
4568
4696
  label: "artifact-panel-directive"
4569
4697
  });
4570
4698
  }
4571
-
4572
4699
  //#endregion
4573
4700
  //#region src/lib/injected-skills/write.ts
4701
+ /**
4702
+ * Writer for injected skills: materializes a `SKILL.md` into the per-launch
4703
+ * `CLAUDE_CONFIG_DIR` mirror so the spawned Claude Code session discovers it as a
4704
+ * user-scope skill (`<CLAUDE_CONFIG_DIR>/skills/<name>/SKILL.md`, where the folder
4705
+ * name MUST equal the frontmatter `name` — the loader enforces this).
4706
+ *
4707
+ * Safety mirrors `claude-md-injection.ts`:
4708
+ * - mirror-only write guard (symlink-resolving) so we never touch the user's
4709
+ * real `~/.claude/skills/`;
4710
+ * - ATOMIC temp+rename so a concurrent child process can never read a
4711
+ * half-written `SKILL.md` (Claude Code watches the skills dir);
4712
+ * - warn-and-continue on every failure — an injected skill is an enhancement,
4713
+ * never a launch blocker.
4714
+ *
4715
+ * No per-skill stale sweep is needed: the whole per-launch mirror dir is GC'd by
4716
+ * `removeOwnClaudeConfigMirror` / `sweepStaleClaudeConfigMirrors`.
4717
+ */
4574
4718
  /** Grep-able prefix on every warn path (mirrors the CLAUDE_MD_WRITE convention). */
4575
4719
  const ERROR_CODE = "INJECTED_SKILL_WRITE";
4576
4720
  /**
@@ -4586,13 +4730,13 @@ const VALID_SKILL_NAME = /^[a-z][a-z0-9-]*$/;
4586
4730
  * then the body). Idempotent across launches (overwrite); the per-launch mirror
4587
4731
  * dir is disposable.
4588
4732
  */
4589
- async function writeInjectedSkill(name$1, md) {
4590
- if (!VALID_SKILL_NAME.test(name$1)) {
4591
- consola.warn(`${ERROR_CODE}: invalid skill name "${name$1}" (need lowercase kebab); skipping`);
4733
+ async function writeInjectedSkill(name, md) {
4734
+ if (!VALID_SKILL_NAME.test(name)) {
4735
+ consola.warn(`${ERROR_CODE}: invalid skill name "${name}" (need lowercase kebab); skipping`);
4592
4736
  return { written: false };
4593
4737
  }
4594
- const dir = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "skills", name$1);
4595
- const target = nodePath.join(dir, "SKILL.md");
4738
+ const dir = path.join(PATHS.CLAUDE_CONFIG_DIR, "skills", name);
4739
+ const target = path.join(dir, "SKILL.md");
4596
4740
  try {
4597
4741
  await fs.mkdir(dir, { recursive: true });
4598
4742
  } catch (err) {
@@ -4621,9 +4765,15 @@ async function writeInjectedSkill(name$1, md) {
4621
4765
  path: target
4622
4766
  };
4623
4767
  }
4624
-
4625
4768
  //#endregion
4626
4769
  //#region src/lib/injected-skills/index.ts
4770
+ /**
4771
+ * Injected-skill registry: the floor-raising / controller skills the `claude`
4772
+ * launcher materializes into the per-launch `CLAUDE_CONFIG_DIR` mirror so the
4773
+ * spawned Claude Code session discovers them (`/gh-research`,
4774
+ * `/gh-orchestrate`, `/gh-floor-keeper`, `/gh-first-mate`). See
4775
+ * `docs/floor-raising-agent-surface.md`.
4776
+ */
4627
4777
  /** All injected skills, in dependency order (research underpins the others). */
4628
4778
  const INJECTED_SKILLS = [
4629
4779
  RESEARCH_SKILL,
@@ -4635,7 +4785,6 @@ const INJECTED_SKILLS = [
4635
4785
  FIRST_MATE_OPERATE_SKILL,
4636
4786
  FIRST_MATE_CONDUCT_SKILL
4637
4787
  ];
4638
-
4639
4788
  //#endregion
4640
4789
  //#region src/lib/attribution-settings.ts
4641
4790
  /**
@@ -4700,11 +4849,30 @@ async function injectAttributionSuppressionIntoSettingsFile(settingsPath) {
4700
4849
  await fs.rename(tmp, settingsPath);
4701
4850
  return { written: true };
4702
4851
  }
4703
-
4704
4852
  //#endregion
4705
4853
  //#region src/lib/toolbelt/provision.ts
4854
+ /**
4855
+ * Toolbelt provisioner: materialize curated CLI tools into the durable
4856
+ * router `bin/` dir so the spawned agent can call them on PATH.
4857
+ *
4858
+ * Properties (all load-bearing):
4859
+ * - **Best-effort.** Every tool is isolated in try/catch; a failure
4860
+ * skips that one tool and never throws to the launcher.
4861
+ * - **Gap-fill.** A tool already on the user's PATH is NOT
4862
+ * materialized (and a stale copy is removed) so we never shadow the
4863
+ * user's own `jq`/`yq`/etc.
4864
+ * - **Verified.** The downloaded archive's SHA256 must match the
4865
+ * hardcoded manifest digest BEFORE extraction.
4866
+ * - **Safe extraction.** Only the expected regular-file binary is
4867
+ * pulled out; symlink/hardlink/device entries are rejected.
4868
+ * - **Concurrency-safe.** A cross-process lock serializes writers;
4869
+ * unique temp names + atomic rename avoid half-written binaries.
4870
+ * - **Integrity-pruned.** Unexpected files in `bin/` are removed each
4871
+ * launch so a prompt-injected agent can't plant a `git.cmd` that
4872
+ * shadows real tools in future sessions.
4873
+ */
4706
4874
  /** Per-download cap (bytes) — these binaries are a few MB at most. */
4707
- const MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024;
4875
+ const MAX_DOWNLOAD_BYTES = 67108864;
4708
4876
  const DOWNLOAD_TIMEOUT_MS = 3e4;
4709
4877
  const EXE_EXT = process$1.platform === "win32" ? ".exe" : "";
4710
4878
  /**
@@ -4751,13 +4919,13 @@ async function pruneUnexpected(binDir) {
4751
4919
  } catch {
4752
4920
  return;
4753
4921
  }
4754
- for (const name$1 of entries) {
4755
- if (name$1.endsWith(".tmp")) continue;
4756
- if (!expected.has(name$1)) await rm(nodePath.join(binDir, name$1), { force: true }).catch(() => {});
4922
+ for (const name of entries) {
4923
+ if (name.endsWith(".tmp")) continue;
4924
+ if (!expected.has(name)) await rm(path.join(binDir, name), { force: true }).catch(() => {});
4757
4925
  }
4758
4926
  }
4759
4927
  async function provisionRg(binDir, skip) {
4760
- const dest = nodePath.join(binDir, "rg" + EXE_EXT);
4928
+ const dest = path.join(binDir, "rg" + EXE_EXT);
4761
4929
  if (skip.has("rg") || resolveExecutable("rg")) {
4762
4930
  await removeBin(dest);
4763
4931
  return;
@@ -4775,7 +4943,7 @@ async function provisionRg(binDir, skip) {
4775
4943
  await commit(tmp, dest);
4776
4944
  }
4777
4945
  async function provisionTool(spec, binDir, skip) {
4778
- const dest = nodePath.join(binDir, spec.binBasename + EXE_EXT);
4946
+ const dest = path.join(binDir, spec.binBasename + EXE_EXT);
4779
4947
  const sidecar = `${dest}.sha256`;
4780
4948
  const asset = assetFor(spec);
4781
4949
  if (skip.has(spec.command) || !asset) {
@@ -4843,7 +5011,7 @@ async function commit(tmp, dest) {
4843
5011
  }
4844
5012
  async function ensureAliases(spec, binDir, dest) {
4845
5013
  for (const alias of spec.aliases ?? []) {
4846
- const ap = nodePath.join(binDir, alias + EXE_EXT);
5014
+ const ap = path.join(binDir, alias + EXE_EXT);
4847
5015
  if (existsSync(ap)) continue;
4848
5016
  const tmp = tempName(ap);
4849
5017
  try {
@@ -4856,8 +5024,8 @@ async function ensureAliases(spec, binDir, dest) {
4856
5024
  }
4857
5025
  }
4858
5026
  async function removeTool(spec, binDir) {
4859
- await removeBin(nodePath.join(binDir, spec.binBasename + EXE_EXT));
4860
- for (const alias of spec.aliases ?? []) await removeBin(nodePath.join(binDir, alias + EXE_EXT));
5027
+ await removeBin(path.join(binDir, spec.binBasename + EXE_EXT));
5028
+ for (const alias of spec.aliases ?? []) await removeBin(path.join(binDir, alias + EXE_EXT));
4861
5029
  }
4862
5030
  async function removeBin(dest) {
4863
5031
  await rm(dest, { force: true }).catch(() => {});
@@ -4887,10 +5055,14 @@ async function exposedCommands(binDir) {
4887
5055
  for (const spec of TOOLBELT_TOOLS) if (present(spec.binBasename)) out.push(spec.command);
4888
5056
  return out;
4889
5057
  }
4890
-
4891
5058
  //#endregion
4892
5059
  //#region src/lib/keep-awake/flags.ts
4893
5060
  /**
5061
+ * Env-flag parsing for the Windows keep-awake feature. Pure +
5062
+ * unit-testable (no spawn, no platform branch — the platform gate lives
5063
+ * in `keepAwakeEnabled()`).
5064
+ */
5065
+ /**
4894
5066
  * True unless the operator opted out via `GH_ROUTER_DISABLE_KEEP_AWAKE`.
4895
5067
  * Keep-awake is ON BY DEFAULT (the win32-only platform gate is applied
4896
5068
  * separately in `keepAwakeEnabled()`). Mirrors the colbert opt-out idiom
@@ -4907,11 +5079,26 @@ function keepAwakeOptedIn() {
4907
5079
  function keepDisplayOn() {
4908
5080
  return parseBoolEnv(process$1.env.GH_ROUTER_KEEP_DISPLAY_ON) === true;
4909
5081
  }
4910
-
4911
5082
  //#endregion
4912
5083
  //#region src/lib/keep-awake/helper.ts
5084
+ /**
5085
+ * The persistent PowerShell helper that holds a Win32
5086
+ * `SetThreadExecutionState` assertion for the proxy's lifetime.
5087
+ *
5088
+ * `SetThreadExecutionState` is THREAD-scoped: a one-shot process that
5089
+ * sets it and exits releases the assertion immediately. So a long-lived
5090
+ * helper holds it on its own main thread and blocks on stdin; when the
5091
+ * proxy dies (clean exit, signal, SIGKILL/taskkill, OOM) the stdin pipe
5092
+ * closes, the helper hits EOF, clears the assertion, and exits — and
5093
+ * Windows releases the thread's assertion on process death regardless.
5094
+ * The helper therefore cannot outlive the proxy, so no orphan sweep is
5095
+ * needed (unlike colbert/worktrees, which leave on-disk artifacts).
5096
+ *
5097
+ * Best-effort: every failure path (no powershell, Constrained Language
5098
+ * Mode blocking `Add-Type`, spawn error) degrades to a clean no-op. We
5099
+ * NEVER attempt to install, enable, or bypass language-mode/WDAC policy.
5100
+ */
4913
5101
  const ES_CONTINUOUS = 2147483648;
4914
- const ES_SYSTEM_REQUIRED = 1;
4915
5102
  const ES_DISPLAY_REQUIRED = 2;
4916
5103
  /** Default time to wait for the helper's `OK` readiness line.
4917
5104
  *
@@ -4941,7 +5128,7 @@ const DEFAULT_READY_TIMEOUT_MS = 2e4;
4941
5128
  * 32-bit value so the hex literal handed to PowerShell is positive.
4942
5129
  */
4943
5130
  function executionStateFlags(displayRequired) {
4944
- let flags = ES_CONTINUOUS | ES_SYSTEM_REQUIRED;
5131
+ let flags = -2147483647;
4945
5132
  if (displayRequired) flags |= ES_DISPLAY_REQUIRED;
4946
5133
  return flags >>> 0;
4947
5134
  }
@@ -5057,17 +5244,37 @@ function killHelper(handle) {
5057
5244
  killManagedTree(child);
5058
5245
  } catch {}
5059
5246
  }
5060
-
5061
5247
  //#endregion
5062
5248
  //#region src/lib/keep-awake/index.ts
5063
5249
  /**
5250
+ * Windows keep-awake — public entry points.
5251
+ *
5252
+ * `startKeepAwake()` is the fire-and-forget call the `start` / `claude` /
5253
+ * `codex` launchers invoke after `setupAndServe` (alongside
5254
+ * `provisionAndIndexColbert()` / `runSelfUpdate()`). On Windows, by
5255
+ * default, it spawns a persistent PowerShell helper that holds a
5256
+ * `SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)`
5257
+ * assertion so the machine doesn't idle-sleep mid-session, and registers
5258
+ * SIGINT/SIGTERM/exit handlers to release it.
5259
+ *
5260
+ * Best-effort and model-agnostic: non-win32 is a total no-op; any
5261
+ * failure (no powershell, Constrained Language Mode blocking `Add-Type`)
5262
+ * degrades to a clean debug-logged no-op. We NEVER install, enable, or
5263
+ * bypass language-mode/WDAC policy, and never block or crash launch.
5264
+ *
5265
+ * Crash safety: the helper blocks on stdin, so if the proxy is hard-
5266
+ * killed the pipe closes, the helper hits EOF and exits, and Windows
5267
+ * releases the assertion on process death — no orphan is possible, so no
5268
+ * boot-time sweep is needed.
5269
+ */
5270
+ /**
5064
5271
  * True iff keep-awake should run THIS launch: win32 AND not opted out.
5065
5272
  * Non-win32 short-circuits before anything else (no spawn, no flags read
5066
5273
  * beyond the opt-out, no handler registration). `platform` is injectable
5067
5274
  * for tests; production callers use the default `process.platform`.
5068
5275
  */
5069
- function keepAwakeEnabled(platform$1 = process$1.platform) {
5070
- return platform$1 === "win32" && keepAwakeOptedIn();
5276
+ function keepAwakeEnabled(platform = process$1.platform) {
5277
+ return platform === "win32" && keepAwakeOptedIn();
5071
5278
  }
5072
5279
  let _handle = null;
5073
5280
  let _started = false;
@@ -5149,7 +5356,6 @@ function startKeepAwake() {
5149
5356
  async function stopKeepAwake() {
5150
5357
  releaseSync();
5151
5358
  }
5152
-
5153
5359
  //#endregion
5154
5360
  //#region src/lib/first-mate/scheduler/autospawn.ts
5155
5361
  /**
@@ -5215,7 +5421,7 @@ function nodeDaemonSpawn(cmd, spawnOpts, spawnImpl = spawn) {
5215
5421
  function maybeSpawnDaemon(opts) {
5216
5422
  const env = opts.env ?? process.env;
5217
5423
  if (!shouldAutoSpawnDaemon(env, opts.agentsEnabled)) return void 0;
5218
- const script = nodePath.join(opts.repoRoot ?? process.cwd(), "scripts/first-mate-daemon.ts");
5424
+ const script = path.join(opts.repoRoot ?? process.cwd(), "scripts/first-mate-daemon.ts");
5219
5425
  if (opts.spawn === void 0 && !existsSync(script)) {
5220
5426
  consola.debug(`first-mate daemon entry not found (${script}); auto-spawn skipped.`);
5221
5427
  return;
@@ -5284,7 +5490,6 @@ function wireDaemonTeardown(handle, opts = {}) {
5284
5490
  kill();
5285
5491
  });
5286
5492
  }
5287
-
5288
5493
  //#endregion
5289
5494
  //#region src/lib/proxy.ts
5290
5495
  function initProxyFromEnv() {
@@ -5330,26 +5535,23 @@ function initProxyFromEnv() {
5330
5535
  consola.debug("Proxy setup skipped:", err);
5331
5536
  }
5332
5537
  }
5333
-
5334
5538
  //#endregion
5335
5539
  //#region package.json
5336
5540
  var name = "github-router";
5337
- var version$1 = "0.3.240";
5338
-
5541
+ var version$1 = "0.3.241";
5339
5542
  //#endregion
5340
5543
  //#region src/lib/approval.ts
5341
5544
  const awaitApproval = async () => {
5342
5545
  if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected by user", Response.json({ message: "Request rejected by user" }, { status: 403 }));
5343
5546
  };
5344
-
5345
5547
  //#endregion
5346
5548
  //#region src/lib/rate-limit.ts
5347
5549
  const RATE_LIMIT_QUEUE_TIMEOUT_MS = 5e3;
5348
5550
  let rateLimitChain = Promise.resolve();
5349
- async function checkRateLimit(state$1) {
5350
- if (state$1.rateLimitSeconds === void 0) return;
5551
+ async function checkRateLimit(state) {
5552
+ if (state.rateLimitSeconds === void 0) return;
5351
5553
  const ticket = { aborted: false };
5352
- const myTurn = rateLimitChain.then(() => doCheck(state$1, ticket));
5554
+ const myTurn = rateLimitChain.then(() => doCheck(state, ticket));
5353
5555
  rateLimitChain = myTurn.catch(() => {});
5354
5556
  return Promise.race([myTurn, sleep$1(RATE_LIMIT_QUEUE_TIMEOUT_MS).then(() => {
5355
5557
  ticket.aborted = true;
@@ -5362,21 +5564,21 @@ async function checkRateLimit(state$1) {
5362
5564
  }, { status: 429 }));
5363
5565
  })]);
5364
5566
  }
5365
- async function doCheck(state$1, ticket) {
5366
- if (state$1.rateLimitSeconds === void 0) return;
5567
+ async function doCheck(state, ticket) {
5568
+ if (state.rateLimitSeconds === void 0) return;
5367
5569
  if (ticket.aborted) return;
5368
5570
  const now = Date.now();
5369
- if (!state$1.lastRequestTimestamp) {
5370
- state$1.lastRequestTimestamp = now;
5571
+ if (!state.lastRequestTimestamp) {
5572
+ state.lastRequestTimestamp = now;
5371
5573
  return;
5372
5574
  }
5373
- const elapsedSeconds = (now - state$1.lastRequestTimestamp) / 1e3;
5374
- if (elapsedSeconds > state$1.rateLimitSeconds) {
5375
- state$1.lastRequestTimestamp = now;
5575
+ const elapsedSeconds = (now - state.lastRequestTimestamp) / 1e3;
5576
+ if (elapsedSeconds > state.rateLimitSeconds) {
5577
+ state.lastRequestTimestamp = now;
5376
5578
  return;
5377
5579
  }
5378
- const waitTimeSeconds = Math.ceil(state$1.rateLimitSeconds - elapsedSeconds);
5379
- if (!state$1.rateLimitWait) {
5580
+ const waitTimeSeconds = Math.ceil(state.rateLimitSeconds - elapsedSeconds);
5581
+ if (!state.rateLimitWait) {
5380
5582
  consola.warn(`Rate limit exceeded. Need to wait ${waitTimeSeconds} more seconds.`);
5381
5583
  throw new HTTPError("Rate limit exceeded", Response.json({ message: "Rate limit exceeded" }, { status: 429 }));
5382
5584
  }
@@ -5384,10 +5586,9 @@ async function doCheck(state$1, ticket) {
5384
5586
  consola.warn(`Rate limit reached. Waiting ${waitTimeSeconds} seconds before proceeding...`);
5385
5587
  await sleep$1(waitTimeMs);
5386
5588
  if (ticket.aborted) return;
5387
- state$1.lastRequestTimestamp = Date.now();
5589
+ state.lastRequestTimestamp = Date.now();
5388
5590
  consola.info("Rate limit wait completed, proceeding with request");
5389
5591
  }
5390
-
5391
5592
  //#endregion
5392
5593
  //#region src/lib/request-log.ts
5393
5594
  /**
@@ -5495,7 +5696,6 @@ function collectToolFieldKeys(body) {
5495
5696
  for (const tool of tools) if (tool && typeof tool === "object" && !Array.isArray(tool)) for (const k of Object.keys(tool)) seen.add(k);
5496
5697
  return [...seen].sort();
5497
5698
  }
5498
-
5499
5699
  //#endregion
5500
5700
  //#region src/routes/chat-completions/handler.ts
5501
5701
  const ENCODER$1 = new TextEncoder();
@@ -5684,7 +5884,6 @@ function extractUserQuery$2(messages) {
5684
5884
  }
5685
5885
  }
5686
5886
  }
5687
-
5688
5887
  //#endregion
5689
5888
  //#region src/routes/chat-completions/route.ts
5690
5889
  const completionRoutes = new Hono();
@@ -5695,7 +5894,6 @@ completionRoutes.post("/", async (c) => {
5695
5894
  return await forwardError(c, error);
5696
5895
  }
5697
5896
  });
5698
-
5699
5897
  //#endregion
5700
5898
  //#region src/services/copilot/create-embeddings.ts
5701
5899
  const createEmbeddings = async (payload) => {
@@ -5708,19 +5906,18 @@ const createEmbeddings = async (payload) => {
5708
5906
  if (!response.ok) throw new HTTPError("Failed to create embeddings", response);
5709
5907
  return await response.json();
5710
5908
  };
5711
-
5712
5909
  //#endregion
5713
5910
  //#region src/routes/embeddings/route.ts
5714
5911
  const embeddingRoutes = new Hono();
5715
5912
  embeddingRoutes.post("/", async (c) => {
5716
5913
  try {
5717
- const response = await createEmbeddings(await c.req.json());
5914
+ const payload = await c.req.json();
5915
+ const response = await createEmbeddings(payload);
5718
5916
  return c.json(response);
5719
5917
  } catch (error) {
5720
5918
  return await forwardError(c, error);
5721
5919
  }
5722
5920
  });
5723
-
5724
5921
  //#endregion
5725
5922
  //#region src/routes/mcp/route.ts
5726
5923
  const mcpRoutes = new Hono();
@@ -5752,10 +5949,43 @@ mcpRoutes.delete("/:group", (c) => {
5752
5949
  return c.body(null, 500);
5753
5950
  }
5754
5951
  });
5755
-
5756
5952
  //#endregion
5757
5953
  //#region src/lib/sanitize-anthropic-body.ts
5758
5954
  /**
5955
+ * Inbound /v1/messages body sanitizer.
5956
+ *
5957
+ * Today this only handles ADVISOR-related corruption — the
5958
+ * proxy-generated `server_tool_use{name:"advisor"}` and paired
5959
+ * `advisor_tool_result` blocks may travel through Claude Code's
5960
+ * persisted conversation state with a malformed `id`/`tool_use_id`
5961
+ * (e.g., a leftover `toolu_*` value from before the round-5 fix).
5962
+ * Every replay of that history through `/v1/messages` would 400 at
5963
+ * Copilot's spec validator without rewriting them on inbound. This
5964
+ * module performs the rewrite.
5965
+ *
5966
+ * **Out of scope** (deliberate, per gemini-critic round 6 — the "ID
5967
+ * round-trip trap"): generic `tool_use.id` and `tool_result.tool_use_id`
5968
+ * are stateful references between requests; rewriting them statelessly
5969
+ * would break Claude Code's client-side tool tracker. Only advisor
5970
+ * blocks are touched here, because both sides of an advisor pair
5971
+ * (`server_tool_use` + `advisor_tool_result`) are proxy-generated and
5972
+ * round-trip together in the same request body — so a per-request
5973
+ * deterministic transformation preserves pairing without cross-request
5974
+ * state.
5975
+ *
5976
+ * **Round-7 holistic fix**: Copilot rejects `server_tool_use{name:
5977
+ * "advisor"}` outright (spec validator says only `web_search`,
5978
+ * `tool_search_tool_regex`, `tool_search_tool_bm25` are allowed), so
5979
+ * fixing the id format alone isn't enough. The sanitizer also
5980
+ * **translates** historical advisor pairs to the
5981
+ * `tool_use{__anthropic_advisor}` + `tool_result` shape Copilot
5982
+ * accepts (per user direction "Option C"). Multi-turn split is
5983
+ * required because `tool_result` must appear in a `user` role per
5984
+ * Anthropic spec. The `__anthropic_advisor` tool definition is
5985
+ * re-injected into `tools[]` so the `tool_use.name` reference
5986
+ * resolves.
5987
+ */
5988
+ /**
5759
5989
  * Convert a `srvtoolu_*` id to the matching `toolu_*` id used in the
5760
5990
  * Copilot-replay shape (`tool_use.id` must match `^toolu_*$`). For
5761
5991
  * any other input shape, fall back to a synthesized `toolu_advisor_N`
@@ -5798,7 +6028,7 @@ function splitAssistantTurnAtAdvisorPairs(originalContent, syntheticIndexRef) {
5798
6028
  while (i < originalContent.length) {
5799
6029
  const block = originalContent[i];
5800
6030
  const b = typeof block === "object" && block !== null ? block : null;
5801
- if (b && b.type === "server_tool_use" && b.name === ADVISOR_INTERNAL_TOOL_NAME.replace(/^__anthropic_/, "")) {
6031
+ if (b && b.type === "server_tool_use" && b.name === "__anthropic_advisor".replace(/^__anthropic_/, "")) {
5802
6032
  const stuId = typeof b.id === "string" ? b.id : "";
5803
6033
  const nextBlock = originalContent[i + 1];
5804
6034
  const next = typeof nextBlock === "object" && nextBlock !== null ? nextBlock : null;
@@ -5902,8 +6132,8 @@ function sanitizeAnthropicBody(rawBody) {
5902
6132
  if (!content.some((b) => {
5903
6133
  if (typeof b !== "object" || b === null) return false;
5904
6134
  const type = b.type;
5905
- const name$1 = b.name;
5906
- return type === "server_tool_use" && name$1 === "advisor" || type === "advisor_tool_result";
6135
+ const name = b.name;
6136
+ return type === "server_tool_use" && name === "advisor" || type === "advisor_tool_result";
5907
6137
  })) {
5908
6138
  rebuilt.push(msg);
5909
6139
  continue;
@@ -5920,7 +6150,7 @@ function sanitizeAnthropicBody(rawBody) {
5920
6150
  const existingTools = Array.isArray(parsed.tools) ? parsed.tools : [];
5921
6151
  if (!existingTools.some((t) => {
5922
6152
  if (typeof t !== "object" || t === null) return false;
5923
- return t.name === ADVISOR_INTERNAL_TOOL_NAME;
6153
+ return t.name === "__anthropic_advisor";
5924
6154
  })) parsed.tools = [...existingTools, {
5925
6155
  name: ADVISOR_INTERNAL_TOOL_NAME,
5926
6156
  description: ADVISOR_TOOL_INSTRUCTIONS,
@@ -5935,9 +6165,24 @@ function sanitizeAnthropicBody(rawBody) {
5935
6165
  if (!mutated) return rawBody;
5936
6166
  return JSON.stringify(parsed);
5937
6167
  }
5938
-
5939
6168
  //#endregion
5940
6169
  //#region src/lib/anthropic-translate/anthropic-sse.ts
6170
+ /**
6171
+ * Anthropic Messages SSE egress: frame builders + a Bun-safe ReadableStream
6172
+ * adapter that serializes a generator of Anthropic stream events to wire bytes.
6173
+ *
6174
+ * The synthesizer (see `responses-egress.ts`) is source-specific and yields
6175
+ * plain event objects; these builders + adapter own the Anthropic wire shape
6176
+ * and the stream lifecycle so the synthesizer stays a pure state machine.
6177
+ *
6178
+ * Stream-lifecycle contract (repo mandate for any synthesized SSE surface):
6179
+ * the adapter is pull-based (backpressure-respecting), guards every
6180
+ * `controller.enqueue`/`close` against the consumer-cancel race
6181
+ * (`isControllerClosedError`), and on a mid-stream upstream error emits a
6182
+ * terminal Anthropic `event: error` frame before closing — mirroring
6183
+ * `relayAnthropicStream`. Verified against Bun's
6184
+ * `TypeError: Invalid state: Controller is already closed`.
6185
+ */
5941
6186
  function makeMessageId() {
5942
6187
  return `msg_${randomUUID().replace(/-/g, "")}`;
5943
6188
  }
@@ -6067,11 +6312,11 @@ function anthropicSseStreamFromEvents(events, opts) {
6067
6312
  safeClose(controller);
6068
6313
  return;
6069
6314
  }
6070
- const name$1 = err instanceof Error ? err.name : "Error";
6315
+ const name = err instanceof Error ? err.name : "Error";
6071
6316
  const message = err instanceof Error ? err.message : String(err);
6072
- consola.error(`Anthropic-translate stream interrupted at ${opts.routePath}: ${name$1}: ${JSON.stringify(message)}`);
6317
+ consola.error(`Anthropic-translate stream interrupted at ${opts.routePath}: ${name}: ${JSON.stringify(message)}`);
6073
6318
  try {
6074
- controller.enqueue(enc.encode(buildAnthropicErrorEvent(name$1, message)));
6319
+ controller.enqueue(enc.encode(buildAnthropicErrorEvent(name, message)));
6075
6320
  } catch (enqueueError) {
6076
6321
  if (!isControllerClosedError(enqueueError)) consola.warn(`Could not deliver error event to consumer at ${opts.routePath}: ${enqueueError instanceof Error ? enqueueError.message : String(enqueueError)}`);
6077
6322
  }
@@ -6105,7 +6350,6 @@ function anthropicSseStreamFromEvents(events, opts) {
6105
6350
  }
6106
6351
  });
6107
6352
  }
6108
-
6109
6353
  //#endregion
6110
6354
  //#region src/lib/reasoning-effort.ts
6111
6355
  /**
@@ -6160,7 +6404,6 @@ function clampEffort(bucketed, supported) {
6160
6404
  }
6161
6405
  return best ?? bucketed;
6162
6406
  }
6163
-
6164
6407
  //#endregion
6165
6408
  //#region src/lib/anthropic-translate/anthropic-request.ts
6166
6409
  /**
@@ -6502,7 +6745,6 @@ function parsedToResponsesPayload(parsed) {
6502
6745
  stream: parsed.stream
6503
6746
  });
6504
6747
  }
6505
-
6506
6748
  //#endregion
6507
6749
  //#region src/lib/anthropic-translate/chat-request.ts
6508
6750
  /** Build the `data:` URI (base64) or return the verbatim URL for an image part. */
@@ -6652,9 +6894,32 @@ function parsedToChatPayload(parsed) {
6652
6894
  if (parsed.parallelToolCalls === false) payload.parallel_tool_calls = false;
6653
6895
  return payload;
6654
6896
  }
6655
-
6656
6897
  //#endregion
6657
6898
  //#region src/lib/anthropic-translate/chat-egress.ts
6899
+ /**
6900
+ * Copilot `/chat/completions` → Anthropic Messages egress (the Gemini path).
6901
+ *
6902
+ * - `chatResponseToAnthropicMessage` maps a non-streaming chat/completions
6903
+ * object to an Anthropic Messages object (text + tool_use blocks, usage,
6904
+ * stop_reason).
6905
+ * - `synthAnthropicFromChat` is the streaming state machine: it consumes
6906
+ * Copilot's chat SSE (`choices[].delta`) and yields Anthropic stream events
6907
+ * (message_start → content_block_* → message_delta → message_stop), reusing
6908
+ * the shared `anthropic-sse.ts` frame builders.
6909
+ *
6910
+ * Chat streaming differs from the Responses path in one load-bearing way: there
6911
+ * is NO authoritative per-tool `.done` event carrying the full arguments — a
6912
+ * tool call's `id`/`name` arrive early (first delta for its array `index`) and
6913
+ * its `arguments` stream incrementally across later deltas keyed by that same
6914
+ * array `index`. So each tool is BUFFERED per OpenAI array index and its
6915
+ * Anthropic block is emitted ATOMICALLY at end-of-stream (content_block_start →
6916
+ * one input_json_delta with the full assembled args → content_block_stop). This
6917
+ * carries the Phase 1 C1 correctness lesson to the chat path: parallel/multiple
6918
+ * tools keep DISTINCT block indices and never lose their args to a clobbered
6919
+ * shared pointer. An open TEXT block is closed before any tool block opens (a
6920
+ * tool_use must not nest inside a text block on the wire). The Anthropic block
6921
+ * index is assigned at emit time so indices stay monotonic on the wire.
6922
+ */
6658
6923
  /** Synthesize a matchable Anthropic tool_use id when the upstream id is absent. */
6659
6924
  function makeToolUseId$1() {
6660
6925
  return `toolu_${randomUUID().replace(/-/g, "")}`;
@@ -6802,8 +7067,8 @@ async function* synthAnthropicFromChat(upstream, opts) {
6802
7067
  toolByIndex.set(tcd.index, entry);
6803
7068
  }
6804
7069
  if (typeof tcd.id === "string" && tcd.id.length > 0) entry.id = tcd.id;
6805
- const name$1 = tcd.function?.name;
6806
- if (typeof name$1 === "string" && name$1.length > 0) entry.name = name$1;
7070
+ const name = tcd.function?.name;
7071
+ if (typeof name === "string" && name.length > 0) entry.name = name;
6807
7072
  const argDelta = tcd.function?.arguments;
6808
7073
  if (typeof argDelta === "string" && argDelta.length > 0) entry.args += argDelta;
6809
7074
  }
@@ -6811,10 +7076,7 @@ async function* synthAnthropicFromChat(upstream, opts) {
6811
7076
  if (choice.finish_reason != null) finishReason = choice.finish_reason;
6812
7077
  }
6813
7078
  if (!sawDone) throw new Error("chat stream ended without a [DONE] sentinel (truncated)");
6814
- if (activeTextIndex != null) {
6815
- yield makeContentBlockStop(activeTextIndex);
6816
- activeTextIndex = null;
6817
- }
7079
+ if (activeTextIndex != null) yield makeContentBlockStop(activeTextIndex);
6818
7080
  let sawTool = false;
6819
7081
  const orderedTools = [...toolByIndex.entries()].sort((a, b) => a[0] - b[0]);
6820
7082
  for (const [, entry] of orderedTools) {
@@ -6837,10 +7099,38 @@ async function* synthAnthropicFromChat(upstream, opts) {
6837
7099
  });
6838
7100
  yield makeMessageStop();
6839
7101
  }
6840
-
6841
7102
  //#endregion
6842
7103
  //#region src/lib/anthropic-translate/responses-egress.ts
6843
7104
  /**
7105
+ * OpenAI `/responses` → Anthropic Messages egress.
7106
+ *
7107
+ * - `responsesResponseToAnthropicMessage` maps a non-streaming Responses object
7108
+ * to an Anthropic Messages object (content blocks, tool_use, usage,
7109
+ * stop_reason).
7110
+ * - `synthAnthropicFromResponses` is the streaming state machine: it consumes
7111
+ * Copilot's `/responses` SSE and yields Anthropic stream events
7112
+ * (message_start → content_block_* → message_delta → message_stop).
7113
+ *
7114
+ * The decode follows our own empirically-verified `/responses` handling
7115
+ * (`src/lib/worker-agent/stream-fn.ts`): the tool/reasoning block key is the
7116
+ * STABLE `output_index`, never the per-event `item_id` — Copilot re-encrypts
7117
+ * `item_id` on every event, so an id key makes every delta lookup miss and the
7118
+ * tool args drop to `{}`.
7119
+ *
7120
+ * Block lifecycle (mirrors stream-fn.ts): every output item is tracked
7121
+ * independently by its `output_index` key. Parallel/interleaved tool calls
7122
+ * never force-close a sibling — Responses emits ALL `output_item.added` for
7123
+ * parallel tools BEFORE the first `function_call_arguments.delta`, so opening (and
7124
+ * emitting) a tool at `added` time would ship it with empty args and drop the
7125
+ * later deltas. Instead a tool's args are BUFFERED and its Anthropic block is
7126
+ * emitted ATOMICALLY (content_block_start → single input_json_delta with the
7127
+ * full assembled args → content_block_stop) at that tool's OWN
7128
+ * `output_item.done` (or flushed at end-of-stream if the item was left
7129
+ * dangling). Only an open TEXT or THINKING block is closed on a type switch —
7130
+ * never a sibling tool. The Anthropic block index is assigned at emit time, so
7131
+ * indices stay monotonic on the wire regardless of item interleaving.
7132
+ */
7133
+ /**
6844
7134
  * Stable map key for a `/responses` output item: prefer `output_index`
6845
7135
  * (constant per item), fall back to the opaque id only when absent. Namespaced
6846
7136
  * so a numeric index and a string id can never collide.
@@ -6972,11 +7262,11 @@ async function* synthAnthropicFromResponses(upstream, opts) {
6972
7262
  if (existing != null && current?.kind === "text" && current.index === existing.index) return existing;
6973
7263
  closeCurrent();
6974
7264
  const index = nextIndex++;
6975
- const state$1 = {
7265
+ const state = {
6976
7266
  index,
6977
7267
  emitted: ""
6978
7268
  };
6979
- textByKey.set(key, state$1);
7269
+ textByKey.set(key, state);
6980
7270
  current = {
6981
7271
  index,
6982
7272
  kind: "text"
@@ -6985,7 +7275,7 @@ async function* synthAnthropicFromResponses(upstream, opts) {
6985
7275
  type: "text",
6986
7276
  text: ""
6987
7277
  }));
6988
- return state$1;
7278
+ return state;
6989
7279
  };
6990
7280
  const ensureThinking = (key) => {
6991
7281
  if (current?.kind === "thinking" && thinkingByKey.get(key) === current.index) return current.index;
@@ -7035,9 +7325,9 @@ async function* synthAnthropicFromResponses(upstream, opts) {
7035
7325
  case "response.output_text.delta": {
7036
7326
  const d = ev.delta;
7037
7327
  if (typeof d !== "string" || d.length === 0) break;
7038
- const state$1 = ensureTextState(responsesKey(ev.output_index, ev.item_id) ?? "text");
7039
- state$1.emitted += d;
7040
- q.push(makeTextDelta(state$1.index, d));
7328
+ const state = ensureTextState(responsesKey(ev.output_index, ev.item_id) ?? "text");
7329
+ state.emitted += d;
7330
+ q.push(makeTextDelta(state.index, d));
7041
7331
  break;
7042
7332
  }
7043
7333
  case "response.output_text.done": {
@@ -7046,9 +7336,9 @@ async function* synthAnthropicFromResponses(upstream, opts) {
7046
7336
  const existing = textByKey.get(key);
7047
7337
  if (existing == null) {
7048
7338
  if (fullText.length > 0) {
7049
- const state$1 = ensureTextState(key);
7050
- state$1.emitted = fullText;
7051
- q.push(makeTextDelta(state$1.index, fullText));
7339
+ const state = ensureTextState(key);
7340
+ state.emitted = fullText;
7341
+ q.push(makeTextDelta(state.index, fullText));
7052
7342
  closeCurrent();
7053
7343
  }
7054
7344
  } else if (currentKind() === "text" && currentIndex() === existing.index) {
@@ -7139,7 +7429,6 @@ async function* synthAnthropicFromResponses(upstream, opts) {
7139
7429
  break;
7140
7430
  }
7141
7431
  case "response.failed": throw new Error(ev.response?.error?.message ?? "response.failed");
7142
- default: break;
7143
7432
  }
7144
7433
  for (const e of q) yield e;
7145
7434
  q.length = 0;
@@ -7158,10 +7447,33 @@ async function* synthAnthropicFromResponses(upstream, opts) {
7158
7447
  for (const e of q) yield e;
7159
7448
  q.length = 0;
7160
7449
  }
7161
-
7162
7450
  //#endregion
7163
7451
  //#region src/lib/anthropic-translate/classifier.ts
7164
7452
  /**
7453
+ * Routing classifier for `POST /v1/messages`.
7454
+ *
7455
+ * Claude Code speaks the Anthropic Messages wire format. Copilot only serves
7456
+ * Claude models on its native `/v1/messages` endpoint; a gpt/gemini request
7457
+ * sent there 400s. This classifier decides, from the RESOLVED model id and its
7458
+ * catalog metadata, whether a request stays on the native passthrough
7459
+ * (`createMessages`) or is diverted to the translation shim.
7460
+ *
7461
+ * Non-regression is the whole point — every Claude model (opus / sonnet / haiku /
7462
+ * any `claude-*` or Anthropic-vendored id) MUST return "claude-passthrough" so
7463
+ * its bytes reach `createMessages` unchanged — even if future catalog metadata
7464
+ * were to (wrongly) advertise a `/responses` endpoint for it. The Claude check
7465
+ * is therefore keyed off identity (id / vendor / family), NOT the endpoint.
7466
+ *
7467
+ * Non-Claude models are diverted to the translation shim by the endpoint the
7468
+ * catalog says they serve: `/responses` models (gpt-5.5, gpt-5.3-codex, …) take
7469
+ * the Responses path (`responses-shim`), `/chat/completions` models (gemini,
7470
+ * and any chat-default model) take the chat path (`chat-shim`). The decision is
7471
+ * derived from `pickEndpoint` (catalog `supported_endpoints`), never a
7472
+ * hardcoded slug list, so it generalizes. Copilot only serves Claude models on
7473
+ * its native `/v1/messages`, so diverting every non-Claude model to a shim is
7474
+ * correct — a non-Claude request sent to `/v1/messages` would 400.
7475
+ */
7476
+ /**
7165
7477
  * Match "claude"/"anthropic" as a delimiter-bounded segment anywhere in a model
7166
7478
  * id: at the start, or after a `/ _ . : -` path/version delimiter, and followed
7167
7479
  * by end-of-string, another such delimiter, or a digit. This catches catalog
@@ -7214,7 +7526,6 @@ function classifyMessagesRoute(modelId, model, originalModelId) {
7214
7526
  if (endpoint === "chat") return "chat-shim";
7215
7527
  return "claude-passthrough";
7216
7528
  }
7217
-
7218
7529
  //#endregion
7219
7530
  //#region src/lib/anthropic-translate/index.ts
7220
7531
  const STREAM_HEADERS = {
@@ -7338,7 +7649,6 @@ async function handleNonClaudeChat(c, opts) {
7338
7649
  }, opts.model, opts.startTime);
7339
7650
  return c.json(anthropic, 200);
7340
7651
  }
7341
-
7342
7652
  //#endregion
7343
7653
  //#region src/routes/messages/handler.ts
7344
7654
  const isWebSearchTool$1 = (tool) => !!tool && typeof tool === "object" && (typeof tool.type === "string" && tool.type.startsWith("web_search") || tool.name === "web_search");
@@ -7431,7 +7741,7 @@ function stripAdvisorTool(rawBody) {
7431
7741
  const original = body.tools;
7432
7742
  const tools = original.filter((tool) => {
7433
7743
  if (typeof tool !== "object" || tool === null) return true;
7434
- if (tool.name === ADVISOR_INTERNAL_TOOL_NAME) return false;
7744
+ if (tool.name === "__anthropic_advisor") return false;
7435
7745
  const type = tool.type;
7436
7746
  return typeof type !== "string" || !type.startsWith("advisor_");
7437
7747
  });
@@ -7500,7 +7810,8 @@ async function handleCompletion(c) {
7500
7810
  }
7501
7811
  if (state.manualApprove) await awaitApproval();
7502
7812
  const betaHeaders = extractBetaHeaders(c);
7503
- const advisorEnabled = isAdvisorRequested(c.req.header("anthropic-beta"));
7813
+ const incomingBeta = c.req.header("anthropic-beta");
7814
+ const advisorEnabled = isAdvisorRequested(incomingBeta);
7504
7815
  let finalBody = await processWebSearch(rawBody);
7505
7816
  finalBody = sanitizeAnthropicBody(finalBody);
7506
7817
  if (advisorEnabled) {
@@ -7812,7 +8123,7 @@ function stripAnthropicOnlyFields$1(body) {
7812
8123
  if (body.output_config !== void 0) {
7813
8124
  if (body.output_config && typeof body.output_config === "object") {
7814
8125
  const oc = body.output_config;
7815
- const PROXY_OWNED_FIELDS = new Set(["effort"]);
8126
+ const PROXY_OWNED_FIELDS = /* @__PURE__ */ new Set(["effort"]);
7816
8127
  const schema = oc.schema;
7817
8128
  const ocType = oc.type;
7818
8129
  let strippedAny = false;
@@ -7869,7 +8180,6 @@ function appendStructuredOutputInstruction(body, schema, ocType) {
7869
8180
  }];
7870
8181
  else body.system = instruction.trimStart();
7871
8182
  }
7872
-
7873
8183
  //#endregion
7874
8184
  //#region src/routes/messages/count-tokens-handler.ts
7875
8185
  const isWebSearchTool = (tool) => typeof tool.type === "string" && tool.type.startsWith("web_search") || tool.name === "web_search";
@@ -8005,7 +8315,7 @@ function stripAnthropicOnlyFields(body) {
8005
8315
  if (body.output_config !== void 0) {
8006
8316
  if (body.output_config && typeof body.output_config === "object") {
8007
8317
  const oc = body.output_config;
8008
- const PROXY_OWNED_FIELDS = new Set(["effort"]);
8318
+ const PROXY_OWNED_FIELDS = /* @__PURE__ */ new Set(["effort"]);
8009
8319
  let strippedAny = false;
8010
8320
  for (const key of Object.keys(oc)) if (!PROXY_OWNED_FIELDS.has(key)) {
8011
8321
  delete oc[key];
@@ -8039,7 +8349,6 @@ function stripAnthropicOnlyFields(body) {
8039
8349
  }
8040
8350
  return stripped;
8041
8351
  }
8042
-
8043
8352
  //#endregion
8044
8353
  //#region src/routes/messages/route.ts
8045
8354
  const messageRoutes = new Hono();
@@ -8057,15 +8366,14 @@ messageRoutes.post("/count_tokens", async (c) => {
8057
8366
  return await forwardError(c, error);
8058
8367
  }
8059
8368
  });
8060
-
8061
8369
  //#endregion
8062
8370
  //#region src/routes/models/route.ts
8063
8371
  const modelRoutes = new Hono();
8064
8372
  modelRoutes.get("/", async (c) => {
8065
8373
  try {
8066
8374
  if (!state.models) await cacheModels();
8067
- const models$1 = state.models?.data.map((model) => {
8068
- const { requestHeaders,...rest } = model;
8375
+ const models = state.models?.data.map((model) => {
8376
+ const { requestHeaders, ...rest } = model;
8069
8377
  return {
8070
8378
  ...rest,
8071
8379
  object: "model",
@@ -8078,14 +8386,13 @@ modelRoutes.get("/", async (c) => {
8078
8386
  });
8079
8387
  return c.json({
8080
8388
  object: "list",
8081
- data: models$1,
8389
+ data: models,
8082
8390
  has_more: false
8083
8391
  });
8084
8392
  } catch (error) {
8085
8393
  return await forwardError(c, error);
8086
8394
  }
8087
8395
  });
8088
-
8089
8396
  //#endregion
8090
8397
  //#region src/routes/responses/handler.ts
8091
8398
  const ENCODER = new TextEncoder();
@@ -8379,7 +8686,6 @@ async function syntheticCompact(c, body, startTime) {
8379
8686
  }
8380
8687
  });
8381
8688
  }
8382
-
8383
8689
  //#endregion
8384
8690
  //#region src/routes/responses/route.ts
8385
8691
  const responsesRoutes = new Hono();
@@ -8397,7 +8703,6 @@ responsesRoutes.post("/compact", async (c) => {
8397
8703
  return await forwardError(c, error);
8398
8704
  }
8399
8705
  });
8400
-
8401
8706
  //#endregion
8402
8707
  //#region src/routes/search/route.ts
8403
8708
  const searchRoutes = new Hono();
@@ -8411,7 +8716,6 @@ searchRoutes.post("/", async (c) => {
8411
8716
  return await forwardError(c, error);
8412
8717
  }
8413
8718
  });
8414
-
8415
8719
  //#endregion
8416
8720
  //#region src/routes/token/route.ts
8417
8721
  const tokenRoute = new Hono();
@@ -8422,7 +8726,6 @@ tokenRoute.get("/", (c) => {
8422
8726
  } }, 403);
8423
8727
  return c.json({ token: state.copilotToken });
8424
8728
  });
8425
-
8426
8729
  //#endregion
8427
8730
  //#region src/routes/usage/route.ts
8428
8731
  const usageRoute = new Hono();
@@ -8435,7 +8738,6 @@ usageRoute.get("/", async (c) => {
8435
8738
  return await forwardError(c, error);
8436
8739
  }
8437
8740
  });
8438
-
8439
8741
  //#endregion
8440
8742
  //#region src/server.ts
8441
8743
  assertMcpToolSurfaceConsistent();
@@ -8477,7 +8779,6 @@ server.notFound((c) => c.json({
8477
8779
  message: `${c.req.method} ${c.req.path} not found`
8478
8780
  }
8479
8781
  }, 404));
8480
-
8481
8782
  //#endregion
8482
8783
  //#region src/lib/server-setup.ts
8483
8784
  const MAX_PORT_RETRIES = 10;
@@ -8646,7 +8947,7 @@ const sharedServerArgs = {
8646
8947
  description: "Update github-router itself to the latest npm version on launch (throttled once/hour). Best-effort and non-blocking: the proxy serves immediately and a detached updater applies the new version after this process exits (it takes effect on the NEXT launch; the running process keeps its current build). Disable with --no-self-update or GH_ROUTER_NO_SELF_UPDATE=1. Skipped silently if npm/network unavailable."
8647
8948
  }
8648
8949
  };
8649
- const allowedAccountTypes = new Set([
8950
+ const allowedAccountTypes = /* @__PURE__ */ new Set([
8650
8951
  "individual",
8651
8952
  "business",
8652
8953
  "enterprise"
@@ -8782,17 +9083,17 @@ function nativeSelectableModelsInCatalog() {
8782
9083
  * reject, dropping the rows). Rename-over-existing is atomic on POSIX and
8783
9084
  * Windows (libuv MoveFileEx REPLACE_EXISTING).
8784
9085
  */
8785
- function seedGatewayModelCache(serverUrl, models$1, configDir = PATHS.CLAUDE_CONFIG_DIR) {
8786
- if (models$1.length === 0) return false;
8787
- const cacheDir = nodePath$1.join(configDir, "cache");
8788
- const target = nodePath$1.join(cacheDir, "gateway-models.json");
8789
- const tmp = nodePath$1.join(cacheDir, `gateway-models.json.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
9086
+ function seedGatewayModelCache(serverUrl, models, configDir = PATHS.CLAUDE_CONFIG_DIR) {
9087
+ if (models.length === 0) return false;
9088
+ const cacheDir = nodePath.join(configDir, "cache");
9089
+ const target = nodePath.join(cacheDir, "gateway-models.json");
9090
+ const tmp = nodePath.join(cacheDir, `gateway-models.json.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
8790
9091
  try {
8791
9092
  fs$2.mkdirSync(cacheDir, { recursive: true });
8792
9093
  const payload = {
8793
9094
  baseUrl: serverUrl,
8794
9095
  fetchedAt: Date.now(),
8795
- models: models$1.map((m) => ({
9096
+ models: models.map((m) => ({
8796
9097
  id: m.id,
8797
9098
  display_name: m.display_name
8798
9099
  }))
@@ -8817,7 +9118,7 @@ function seedGatewayModelCache(serverUrl, models$1, configDir = PATHS.CLAUDE_CON
8817
9118
  */
8818
9119
  function clearGatewayModelCache(configDir = PATHS.CLAUDE_CONFIG_DIR) {
8819
9120
  try {
8820
- fs$2.rmSync(nodePath$1.join(configDir, "cache", "gateway-models.json"), { force: true });
9121
+ fs$2.rmSync(nodePath.join(configDir, "cache", "gateway-models.json"), { force: true });
8821
9122
  } catch {}
8822
9123
  }
8823
9124
  /**
@@ -8912,11 +9213,10 @@ function getCodexEnvVars(serverUrl) {
8912
9213
  if (toolbeltEnabled()) Object.assign(vars, toolbeltPathOverride(process.env, PATHS.TOOLBELT_BIN_DIR));
8913
9214
  return vars;
8914
9215
  }
8915
-
8916
9216
  //#endregion
8917
9217
  //#region src/claude.ts
8918
- function isFirstMateSkillName(name$1) {
8919
- return name$1 === "gh-first-mate" || name$1 === "gh-first-mate-scaffold" || name$1 === "gh-first-mate-operate" || name$1 === "gh-first-mate-conduct";
9218
+ function isFirstMateSkillName(name) {
9219
+ return name === "gh-first-mate" || name === "gh-first-mate-scaffold" || name === "gh-first-mate-operate" || name === "gh-first-mate-conduct";
8920
9220
  }
8921
9221
  const claudeArgs = {
8922
9222
  ...sharedServerArgs,
@@ -8987,10 +9287,10 @@ const claudeArgs = {
8987
9287
  function collectChildPassthroughArgs(rawArgs, argsDef) {
8988
9288
  const known = /* @__PURE__ */ new Set();
8989
9289
  const stringTyped = /* @__PURE__ */ new Set();
8990
- for (const [name$1, def] of Object.entries(argsDef)) {
9290
+ for (const [name, def] of Object.entries(argsDef)) {
8991
9291
  const rawAlias = "alias" in def ? def.alias : void 0;
8992
9292
  const aliases = rawAlias === void 0 ? [] : Array.isArray(rawAlias) ? rawAlias : [rawAlias];
8993
- for (const n of [name$1, ...aliases]) {
9293
+ for (const n of [name, ...aliases]) {
8994
9294
  known.add(n);
8995
9295
  if (def.type === "string") stringTyped.add(n);
8996
9296
  }
@@ -9051,7 +9351,7 @@ const claude = defineCommand({
9051
9351
  } catch (err) {
9052
9352
  consola.debug("Claude version check failed:", err);
9053
9353
  }
9054
- let server$1;
9354
+ let server;
9055
9355
  let serverUrl;
9056
9356
  try {
9057
9357
  const result = await setupAndServe({
@@ -9059,7 +9359,7 @@ const claude = defineCommand({
9059
9359
  port: parsed.port,
9060
9360
  silent: true
9061
9361
  });
9062
- server$1 = result.server;
9362
+ server = result.server;
9063
9363
  serverUrl = result.serverUrl;
9064
9364
  } catch (error) {
9065
9365
  consola.error("Failed to start server:", error instanceof Error ? error.message : error);
@@ -9175,12 +9475,14 @@ const claude = defineCommand({
9175
9475
  let skillsWritten = 0;
9176
9476
  for (const s of skillsToWrite) if ((await writeInjectedSkill(s.name, s.md).catch(() => ({ written: false }))).written) skillsWritten++;
9177
9477
  try {
9178
- await injectStopHookIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), buildPromptSubmitHookCommand(process$1.execPath, process$1.argv[1]), "UserPromptSubmit", 45);
9478
+ const settingsPath = path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9479
+ const cmd = buildPromptSubmitHookCommand(process$1.execPath, process$1.argv[1]);
9480
+ await injectStopHookIntoSettingsFile(settingsPath, cmd, "UserPromptSubmit", 45);
9179
9481
  } catch (err) {
9180
9482
  consola.warn(`Could not register the UserPromptSubmit hook: ${String(err)}`);
9181
9483
  }
9182
9484
  try {
9183
- const res = await injectAllowRules(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), planModeAllowRules(injected.ok ? injected.serversAdded : [...Object.values(groupKeys).filter((k) => Boolean(k)), ...backend === "cli" ? ["codex-cli"] : []]));
9485
+ const res = await injectAllowRules(path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), planModeAllowRules(injected.ok ? injected.serversAdded : [...Object.values(groupKeys).filter((k) => Boolean(k)), ...backend === "cli" ? ["codex-cli"] : []]));
9184
9486
  if (res.added.length) consola.debug(`Auto-approved for plan mode: ${res.added.join(", ")}`);
9185
9487
  } catch (err) {
9186
9488
  consola.warn(`Could not auto-approve injected tools: ${String(err)}`);
@@ -9188,10 +9490,12 @@ const claude = defineCommand({
9188
9490
  if (!injected.ok) consola.warn("Workers non-blocking guard NOT registered: subagent MCP injection fell back to parent-only (--mcp-config), so worker-* dispatchers cannot reach the workers server. Raw (blocking) worker tools remain usable on the main thread this session.");
9189
9491
  else if (process$1.env.GH_ROUTER_DISABLE_WORKER_GUARD === "1") consola.info("Workers non-blocking guard disabled via GH_ROUTER_DISABLE_WORKER_GUARD=1 — raw mcp__workers__* is callable on the main thread (blocking); the worker-* background agents remain the steered, non-blocking default.");
9190
9492
  else try {
9191
- const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9493
+ const settingsPath = path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9192
9494
  const workersKey = workersKeyOf(groupKeys);
9193
9495
  const modes = activeDispatchModes({ browse: browseAgentEnabled() });
9194
- await injectStopHookIntoSettingsFile(settingsPath, buildWorkerGuardHookCommand(process$1.execPath, process$1.argv[1], workersKey, modes), "PreToolUse", 10, guardToolMatcher(workersKey, modes));
9496
+ const cmd = buildWorkerGuardHookCommand(process$1.execPath, process$1.argv[1], workersKey, modes);
9497
+ const matcher = guardToolMatcher(workersKey, modes);
9498
+ await injectStopHookIntoSettingsFile(settingsPath, cmd, "PreToolUse", 10, matcher);
9195
9499
  } catch (err) {
9196
9500
  consola.warn(`Could not register the workers PreToolUse guard hook: ${String(err)}`);
9197
9501
  }
@@ -9203,7 +9507,9 @@ const claude = defineCommand({
9203
9507
  if (agentToolsEnabled()) {
9204
9508
  let shapingInstalled = false;
9205
9509
  try {
9206
- await injectStopHookIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), buildFirstMateGuardHookCommand(process$1.execPath, process$1.argv[1]), "PreToolUse", void 0, FIRST_MATE_GUARD_MATCHER);
9510
+ const settingsPath = path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9511
+ const guardCmd = buildFirstMateGuardHookCommand(process$1.execPath, process$1.argv[1]);
9512
+ await injectStopHookIntoSettingsFile(settingsPath, guardCmd, "PreToolUse", void 0, FIRST_MATE_GUARD_MATCHER);
9207
9513
  shapingInstalled = true;
9208
9514
  } catch (err) {
9209
9515
  consola.error(`Could not register the operator worker/orchestrate guard hook: ${String(err)}`);
@@ -9212,7 +9518,7 @@ const claude = defineCommand({
9212
9518
  }
9213
9519
  const aiordieSidecar = (process$1.env.AIORDIE_CLAUDE_BIND ?? "").trim();
9214
9520
  if (aiordieSidecar.length > 0 && !aiordieSidecar.includes("\"")) try {
9215
- const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9521
+ const settingsPath = path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9216
9522
  const command = buildSessionBindHookCommand(process$1.execPath, process$1.argv[1], aiordieSidecar);
9217
9523
  await injectStopHookIntoSettingsFile(settingsPath, command, "SessionStart");
9218
9524
  await injectStopHookIntoSettingsFile(settingsPath, command, "SessionEnd");
@@ -9228,13 +9534,17 @@ const claude = defineCommand({
9228
9534
  }
9229
9535
  try {
9230
9536
  await writeArtifactCredsToMirror(shouldUseInsecureTls(process$1.env.AIORDIE_BASE_URL ?? ""));
9231
- await injectStopHookIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), buildArtifactOpenHookCommand(process$1.execPath, process$1.argv[1]), "PostToolUse", void 0, "ExitPlanMode");
9537
+ const settingsPath = path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9538
+ const cmd = buildArtifactOpenHookCommand(process$1.execPath, process$1.argv[1]);
9539
+ await injectStopHookIntoSettingsFile(settingsPath, cmd, "PostToolUse", void 0, "ExitPlanMode");
9232
9540
  } catch (err) {
9233
9541
  consola.warn(`Could not register the artifact auto-open hook: ${String(err)}`);
9234
9542
  }
9235
9543
  }
9236
9544
  if (hookMcpRuntimeFromEnv(envVars) && planReviewEnabled()) try {
9237
- await injectStopHookIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"), buildPlanReviewHookCommand(process$1.execPath, process$1.argv[1]), "PostToolUse", void 0, "ExitPlanMode");
9545
+ const settingsPath = path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9546
+ const command = buildPlanReviewHookCommand(process$1.execPath, process$1.argv[1]);
9547
+ await injectStopHookIntoSettingsFile(settingsPath, command, "PostToolUse", void 0, "ExitPlanMode");
9238
9548
  } catch (err) {
9239
9549
  consola.warn(`Could not register the advisory plan-review hook: ${String(err)}`);
9240
9550
  }
@@ -9281,7 +9591,7 @@ const claude = defineCommand({
9281
9591
  if (gateEnabled && descriptor) {
9282
9592
  const armed = descriptor;
9283
9593
  try {
9284
- const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9594
+ const settingsPath = path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
9285
9595
  const command = buildStopHookCommand(process$1.execPath, process$1.argv[1]);
9286
9596
  if (armed.kind === "sealed") envVars.GH_ROUTER_STOP_GATE_ID = armed.gateId;
9287
9597
  else if (armed.kind === "parsed") envVars.GH_ROUTER_STOP_GATE_PARSED = "1";
@@ -9300,7 +9610,7 @@ const claude = defineCommand({
9300
9610
  exec: liveExec,
9301
9611
  descriptorKey,
9302
9612
  token,
9303
- baseline: fileBaselineStore(nodePath.join(tmpdir(), "gh-router-stopgate-baseline"))
9613
+ baseline: fileBaselineStore(path.join(tmpdir(), "gh-router-stopgate-baseline"))
9304
9614
  });
9305
9615
  })().catch(() => {});
9306
9616
  }
@@ -9381,7 +9691,7 @@ const claude = defineCommand({
9381
9691
  consola.warn(`Operating-defaults CLAUDE.md prepend failed: ${err instanceof Error ? err.message : String(err)}`);
9382
9692
  }
9383
9693
  if (process$1.env.GH_ROUTER_DISABLE_NO_ATTRIBUTION !== "1") try {
9384
- await injectAttributionSuppressionIntoSettingsFile(nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"));
9694
+ await injectAttributionSuppressionIntoSettingsFile(path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json"));
9385
9695
  } catch (err) {
9386
9696
  consola.warn(`No-attribution settings backstop skipped: ${err instanceof Error ? err.message : String(err)}`);
9387
9697
  }
@@ -9390,10 +9700,9 @@ const claude = defineCommand({
9390
9700
  envVars,
9391
9701
  extraArgs,
9392
9702
  model: chosenSlug
9393
- }, server$1, { onShutdown });
9703
+ }, server, { onShutdown });
9394
9704
  }
9395
9705
  });
9396
-
9397
9706
  //#endregion
9398
9707
  //#region src/codex.ts
9399
9708
  const codex = defineCommand({
@@ -9415,7 +9724,7 @@ const codex = defineCommand({
9415
9724
  process$1.exit(1);
9416
9725
  }
9417
9726
  const parsed = parseSharedArgs(args);
9418
- let server$1;
9727
+ let server;
9419
9728
  let serverUrl;
9420
9729
  try {
9421
9730
  const result = await setupAndServe({
@@ -9423,7 +9732,7 @@ const codex = defineCommand({
9423
9732
  port: parsed.port,
9424
9733
  silent: true
9425
9734
  });
9426
- server$1 = result.server;
9735
+ server = result.server;
9427
9736
  serverUrl = result.serverUrl;
9428
9737
  } catch (error) {
9429
9738
  consola.error("Failed to start server:", error instanceof Error ? error.message : error);
@@ -9436,7 +9745,7 @@ const codex = defineCommand({
9436
9745
  startKeepAwake();
9437
9746
  if ((state.browseEnabled || process$1.env.GH_ROUTER_ENABLE_BROWSE === "1") && hasSupportedBrowserInstalled()) provisionBrowserAssets().catch((err) => consola.debug("Browser extension provisioning failed:", err));
9438
9747
  const usingDefault = !args.model;
9439
- const requestedModel = args.model ?? DEFAULT_CODEX_MODEL;
9748
+ const requestedModel = args.model ?? "gpt-5.6-sol";
9440
9749
  enableFileLogging();
9441
9750
  let codexModel = resolveCodexModel(requestedModel);
9442
9751
  if (codexModel !== requestedModel) consola.info(`Model "${requestedModel}" resolved to "${codexModel}"`);
@@ -9463,13 +9772,12 @@ const codex = defineCommand({
9463
9772
  extraArgs: args._ ?? [],
9464
9773
  model: codexModel,
9465
9774
  serverUrl
9466
- }, server$1);
9775
+ }, server);
9467
9776
  }
9468
9777
  });
9469
-
9470
9778
  //#endregion
9471
9779
  //#region src/debug.ts
9472
- async function getPackageVersion$1() {
9780
+ async function getPackageVersion() {
9473
9781
  try {
9474
9782
  const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
9475
9783
  return JSON.parse(await fs.readFile(packageJsonPath)).version;
@@ -9495,9 +9803,9 @@ async function checkTokenExists() {
9495
9803
  }
9496
9804
  }
9497
9805
  async function getDebugInfo() {
9498
- const [version$2, tokenExists] = await Promise.all([getPackageVersion$1(), checkTokenExists()]);
9806
+ const [version, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
9499
9807
  return {
9500
- version: version$2,
9808
+ version,
9501
9809
  runtime: getRuntimeInfo(),
9502
9810
  paths: {
9503
9811
  APP_DIR: PATHS.APP_DIR,
@@ -9540,10 +9848,27 @@ const debug = defineCommand({
9540
9848
  return runDebug({ json: args.json });
9541
9849
  }
9542
9850
  });
9543
-
9544
9851
  //#endregion
9545
9852
  //#region src/internal-prompt-submit.ts
9546
9853
  /**
9854
+ * The internal `internal-prompt-submit` subcommand: the executable a spawned
9855
+ * Claude Code session's `UserPromptSubmit` hook invokes (registered into the
9856
+ * mirrored settings.json by the launcher). It serves the front-end of the
9857
+ * floor-raising surface on one event:
9858
+ * 1. resets the Stop-gate's per-session block budget (making `maxBlocks`
9859
+ * per-prompt);
9860
+ * 2. stashes the prompt + surfaces the prior turn's advisory review findings;
9861
+ * 3. for a non-trivial prompt, injects a GROUNDED, user-derived scope/goal
9862
+ * note (one gpt-5.6-sol call over the prompt + a parallel lexical+semantic code
9863
+ * search) — or, when the proxy URL/nonce isn't wired or anything errors,
9864
+ * falls open to the v1 regex goal.
9865
+ *
9866
+ * ALWAYS exits 0 (never blocks the prompt): the steer is additive context, and a
9867
+ * UserPromptSubmit hook that exit-2'd would refuse the user's prompt. The pure
9868
+ * v1 path (`decidePromptSubmit`) is the fail-open fallback; V2
9869
+ * (`decidePromptSubmitV2`) layers the grounded enrichment on top via injected IO.
9870
+ */
9871
+ /**
9547
9872
  * Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`). An async
9548
9873
  * stdin read leaves an in-flight libuv FS request that, on Windows, races the
9549
9874
  * process teardown and trips a `uv_async_send` assertion; a synchronous read has
@@ -9626,15 +9951,25 @@ const internalPromptSubmit = defineCommand({
9626
9951
  stdin,
9627
9952
  steerEnabled
9628
9953
  });
9629
- if (decision.resetSession) await fileBlockBudget(nodePath.join(tmpdir(), "gh-router-stopgate")).reset(decision.resetSession).catch(() => {});
9954
+ if (decision.resetSession) await fileBlockBudget(path.join(tmpdir(), "gh-router-stopgate")).reset(decision.resetSession).catch(() => {});
9630
9955
  if (decision.inject.length > 0) await new Promise((resolve) => process.stdout.write(`${decision.inject}\n`, () => resolve()));
9631
9956
  } catch {}
9632
9957
  process.exitCode = 0;
9633
9958
  }
9634
9959
  });
9635
-
9636
9960
  //#endregion
9637
9961
  //#region src/internal-plan-review.ts
9962
+ /**
9963
+ * The internal `internal-plan-review` subcommand: a PostToolUse(ExitPlanMode)
9964
+ * hook that spawns a bounded advisory critic review when the main agent finalizes
9965
+ * a substantive plan.
9966
+ *
9967
+ * It never blocks ExitPlanMode: the hook process exits 0 quickly after deciding
9968
+ * whether to spawn a detached reviewer. The detached child calls a cross-lab plan
9969
+ * critic and writes material findings to the SAME per-session findings store used
9970
+ * by the Stop review, so the next UserPromptSubmit surfaces them with the existing
9971
+ * non-authoritative findings path.
9972
+ */
9638
9973
  function readStdin$4() {
9639
9974
  try {
9640
9975
  if (process.stdin.isTTY) return "";
@@ -9669,7 +10004,7 @@ function spawnDetachedPlanReview(ctx) {
9669
10004
  try {
9670
10005
  const dir = stopReviewStateDir();
9671
10006
  mkdirSync(dir, { recursive: true });
9672
- payloadPath = nodePath.join(dir, `plan-review-${process.pid}-${randomBytes(4).toString("hex")}.json`);
10007
+ payloadPath = path.join(dir, `plan-review-${process.pid}-${randomBytes(4).toString("hex")}.json`);
9673
10008
  writeFileSync(payloadPath, JSON.stringify(ctx), { mode: 384 });
9674
10009
  const scriptArgs = process.argv[1] && process.argv[1] !== process.execPath ? [process.argv[1]] : [];
9675
10010
  const child = spawn(process.execPath, [...scriptArgs, "internal-plan-review"], {
@@ -9725,10 +10060,30 @@ const internalPlanReview = defineCommand({
9725
10060
  process.exitCode = 0;
9726
10061
  }
9727
10062
  });
9728
-
9729
10063
  //#endregion
9730
10064
  //#region src/internal-session-bind.ts
9731
10065
  /**
10066
+ * The internal `internal-session-bind` subcommand: the executable a spawned Claude
10067
+ * Code session's SessionStart / SessionEnd hooks invoke (registered into the
10068
+ * mirrored settings.json by the launcher when `AIORDIE_CLAUDE_BIND` is set —
10069
+ * i.e. when github-router runs inside an ai-or-die Terminal tab).
10070
+ *
10071
+ * It reads Claude Code's hook payload from stdin and ATOMICALLY writes a tiny
10072
+ * "sidecar" JSON file (path passed via `--out`) describing the CURRENTLY active
10073
+ * claude session: its sessionId and the absolute transcript path. ai-or-die owns
10074
+ * the sidecar path and watches it, so it can bind that browser tab's session
10075
+ * summariser to the exact transcript — surviving in-session `/resume`, `/clear`,
10076
+ * `/compact`, and exit→relaunch, without guessing from the filesystem.
10077
+ *
10078
+ * Side-effect only: writes the file and exits 0. It NEVER writes to stdout (a
10079
+ * SessionStart hook's stdout is injected into the model's context) and NEVER
10080
+ * blocks (any error is swallowed → exit 0).
10081
+ *
10082
+ * Top-level filter: a SessionStart fired by a subagent/teammate carries
10083
+ * `agent_id`/`agent_type`; those are skipped so only the tab's top-level session
10084
+ * drives the binding (mirrors the Stop / UserPromptSubmit hooks).
10085
+ */
10086
+ /**
9732
10087
  * Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`). An async
9733
10088
  * stdin read leaves an in-flight libuv FS request that, on Windows, races process
9734
10089
  * teardown and trips a `uv_async_send` assertion; a synchronous read has no such
@@ -9759,11 +10114,11 @@ function realTranscriptPath(tp) {
9759
10114
  const missing = [];
9760
10115
  let cur = tp;
9761
10116
  for (let i = 0; i < 64; i++) {
9762
- missing.unshift(nodePath.basename(cur));
9763
- const parent = nodePath.dirname(cur);
10117
+ missing.unshift(path.basename(cur));
10118
+ const parent = path.dirname(cur);
9764
10119
  if (parent === cur) break;
9765
10120
  try {
9766
- return nodePath.join(realpathSync.native(parent), ...missing);
10121
+ return path.join(realpathSync.native(parent), ...missing);
9767
10122
  } catch {
9768
10123
  cur = parent;
9769
10124
  }
@@ -9804,7 +10159,7 @@ function decodeSessionBind(stdin) {
9804
10159
  /** Atomically write the sidecar (temp + rename) so the reader never sees a partial file. */
9805
10160
  function writeSidecar(out, record) {
9806
10161
  try {
9807
- mkdirSync(nodePath.dirname(out), { recursive: true });
10162
+ mkdirSync(path.dirname(out), { recursive: true });
9808
10163
  } catch {}
9809
10164
  const tmp = `${out}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
9810
10165
  writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
@@ -9830,7 +10185,6 @@ const internalSessionBind = defineCommand({
9830
10185
  process.exitCode = 0;
9831
10186
  }
9832
10187
  });
9833
-
9834
10188
  //#endregion
9835
10189
  //#region src/internal-workspace-header.ts
9836
10190
  function workspaceHeaderJsonForCwd(cwd = process.cwd()) {
@@ -9850,9 +10204,22 @@ const internalWorkspaceHeader = defineCommand({
9850
10204
  process.exitCode = 0;
9851
10205
  }
9852
10206
  });
9853
-
9854
10207
  //#endregion
9855
10208
  //#region src/lib/artifact/plan-html.ts
10209
+ /**
10210
+ * Render a plan's markdown into a self-contained, styled HTML document for the
10211
+ * ai-or-die Artifact panel. HTML is the canonical review artifact (lavish's
10212
+ * model); converting here means the hands-off auto-open shows a formatted,
10213
+ * annotatable page instead of raw markdown.
10214
+ *
10215
+ * Security: the iframe ai-or-die serves this into runs with
10216
+ * `allow-scripts allow-same-origin`, so a `<script>` smuggled into the plan
10217
+ * markdown could read the per-session asset token. Two defenses:
10218
+ * 1. marked is configured to ESCAPE raw HTML tokens (render as visible text).
10219
+ * 2. `link`/`image` hrefs with script-bearing schemes (javascript:, vbscript:,
10220
+ * non-image data:) are neutralised before rendering.
10221
+ * No separate sanitizer/dep needed.
10222
+ */
9856
10223
  function escapeHtml(s) {
9857
10224
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9858
10225
  }
@@ -9937,7 +10304,7 @@ function renderMarkdownBody(source) {
9937
10304
  let line = 1;
9938
10305
  let out = "";
9939
10306
  for (const tok of tokens) {
9940
- let html = "";
10307
+ let html;
9941
10308
  try {
9942
10309
  html = m.parser([tok]);
9943
10310
  } catch {
@@ -10010,9 +10377,30 @@ ${body}
10010
10377
  </html>
10011
10378
  `;
10012
10379
  }
10013
-
10014
10380
  //#endregion
10015
10381
  //#region src/internal-artifact-open.ts
10382
+ /**
10383
+ * The internal `internal-artifact-open` subcommand: invoked by a spawned Claude
10384
+ * Code session's `PostToolUse` hook (matcher `ExitPlanMode`), registered into the
10385
+ * mirrored settings.json by the launcher when running inside an ai-or-die tab
10386
+ * (`AIORDIE_SESSION_ID` set). It auto-opens the just-finalized plan in the
10387
+ * ai-or-die Artifact review panel so the human reviews it WITHOUT the model
10388
+ * having to call artifact_open itself.
10389
+ *
10390
+ * Auth: AIORDIE_TOKEN is stripped from the child env, so the token can't come via
10391
+ * env or argv (argv leaks to `ps`). The launcher writes a mode-600
10392
+ * `.aiordie-artifact.json` into CLAUDE_CONFIG_DIR; this hook reads it.
10393
+ *
10394
+ * Plan: the EXACT plan for THIS session comes from the ExitPlanMode payload —
10395
+ * `tool_input.planFilePath` (per-session; never the globally-newest file in the
10396
+ * shared plans dir) with `tool_input.plan` as the markdown fallback. The plan
10397
+ * markdown is rendered to a self-contained styled HTML file (HTML is the
10398
+ * canonical artifact, lavish's model — formatted + annotatable, not raw md), and
10399
+ * THAT `.html` is opened.
10400
+ *
10401
+ * Side-effect only: never writes stdout, never throws, always exits 0. Skips
10402
+ * subagent payloads (agent_id/agent_type) like the other hooks.
10403
+ */
10016
10404
  function readStdin$2() {
10017
10405
  try {
10018
10406
  if (process.stdin.isTTY) return "";
@@ -10022,8 +10410,8 @@ function readStdin$2() {
10022
10410
  }
10023
10411
  }
10024
10412
  function credsPath() {
10025
- const dir = process.env.CLAUDE_CONFIG_DIR || nodePath.join(homedir(), ".claude");
10026
- return nodePath.join(dir, ".aiordie-artifact.json");
10413
+ const dir = process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), ".claude");
10414
+ return path.join(dir, ".aiordie-artifact.json");
10027
10415
  }
10028
10416
  function isSubagent(payload) {
10029
10417
  try {
@@ -10068,7 +10456,7 @@ function resolvePlanMarkdown(p) {
10068
10456
  const md = readFileSync(p.planFilePath, "utf8");
10069
10457
  if (md.trim()) return {
10070
10458
  markdown: md,
10071
- title: nodePath.basename(p.planFilePath, nodePath.extname(p.planFilePath)),
10459
+ title: path.basename(p.planFilePath, path.extname(p.planFilePath)),
10072
10460
  nearPath: p.planFilePath
10073
10461
  };
10074
10462
  } catch {}
@@ -10086,8 +10474,8 @@ function writePlanHtml(html, nearPath) {
10086
10474
  let target;
10087
10475
  if (nearPath) target = `${nearPath.replace(/\.(md|markdown)$/i, "")}.aiordie.html`;
10088
10476
  else {
10089
- const dir = nodePath.join(process.env.CLAUDE_CONFIG_DIR || nodePath.join(homedir(), ".claude"), "plans");
10090
- target = nodePath.join(dir, `aiordie-plan-${process.pid}.html`);
10477
+ const dir = path.join(process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), ".claude"), "plans");
10478
+ target = path.join(dir, `aiordie-plan-${process.pid}.html`);
10091
10479
  }
10092
10480
  try {
10093
10481
  writeFileSync(target, html, "utf8");
@@ -10125,10 +10513,23 @@ const internalArtifactOpen = defineCommand({
10125
10513
  } catch {}
10126
10514
  }
10127
10515
  });
10128
-
10129
10516
  //#endregion
10130
10517
  //#region src/internal-stop-hook.ts
10131
10518
  /**
10519
+ * The internal `internal-stop-hook` subcommand: the executable a spawned Claude
10520
+ * Code session's Stop hook invokes (registered into the mirrored settings.json by
10521
+ * the launcher when `GH_ROUTER_ENABLE_STOP_GATE` is set). It reads Claude Code's
10522
+ * hook payload from stdin, runs the SEALED structural gate over the session's
10523
+ * working-tree diff, and maps the result to the hook exit contract: exit 2 (with
10524
+ * the reason on stderr) blocks the stop so the model fixes the failure; exit 0
10525
+ * allows it. The `stop_hook_active` loop guard (in `decideStopHook`) guarantees
10526
+ * it can never wedge the session.
10527
+ *
10528
+ * All decision logic lives in `decideStopHook` (pure, unit-tested); this wrapper
10529
+ * only does stdin read + the live `git diff` capture + the exit. The live firing
10530
+ * is verified by the gated E2E (it needs a real spawned session).
10531
+ */
10532
+ /**
10132
10533
  * Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`). An async
10133
10534
  * stdin read leaves an in-flight libuv FS request that, on Windows, races the
10134
10535
  * process teardown and trips a `uv_async_send` assertion; a synchronous read has
@@ -10145,7 +10546,7 @@ function readStdin$1() {
10145
10546
  }
10146
10547
  /** Max diff bytes scanned for gate-weakening: a hard cap so a huge generated diff
10147
10548
  * (e.g. a lockfile) can never OOM or stall the hook. */
10148
- const MAX_DIFF_BYTES = 2 * 1024 * 1024;
10549
+ const MAX_DIFF_BYTES = 2097152;
10149
10550
  /** Capture the working-tree diff WITHOUT mutating the user's index (no
10150
10551
  * `git add -N`): `git diff HEAD` covers modified tracked files, which is where
10151
10552
  * gate-weakening edits live. A git failure rejects, so the decision layer can run
@@ -10184,7 +10585,7 @@ function spawnStopReview(ctx, extras) {
10184
10585
  try {
10185
10586
  const dir = stopReviewStateDir();
10186
10587
  mkdirSync(dir, { recursive: true });
10187
- payloadPath = nodePath.join(dir, `payload-${process.pid}-${randomBytes(4).toString("hex")}.json`);
10588
+ payloadPath = path.join(dir, `payload-${process.pid}-${randomBytes(4).toString("hex")}.json`);
10188
10589
  writeFileSync(payloadPath, JSON.stringify({
10189
10590
  session_id: ctx.sessionId,
10190
10591
  cwd: ctx.cwd,
@@ -10246,11 +10647,12 @@ function buildResolveChecks(mode) {
10246
10647
  if (checks.length === 0) return null;
10247
10648
  const descriptorKey = descriptorHash(descriptor);
10248
10649
  const workdir = descriptor.workdir || root;
10650
+ const token = process.env.GH_ROUTER_STOP_GATE_BASELINE_TOKEN || void 0;
10249
10651
  return {
10250
10652
  checks,
10251
10653
  workdir,
10252
10654
  descriptorKey,
10253
- baselineKey: launchBaselineKey(workdir, descriptorKey, process.env.GH_ROUTER_STOP_GATE_BASELINE_TOKEN || void 0)
10655
+ baselineKey: launchBaselineKey(workdir, descriptorKey, token)
10254
10656
  };
10255
10657
  };
10256
10658
  }
@@ -10288,8 +10690,8 @@ const internalStopHook = defineCommand({
10288
10690
  exec: liveExec,
10289
10691
  captureDiff,
10290
10692
  fallbackCwd: process.cwd(),
10291
- budget: fileBlockBudget(nodePath.join(tmpdir(), "gh-router-stopgate")),
10292
- baseline: fileBaselineStore(nodePath.join(tmpdir(), "gh-router-stopgate-baseline")),
10693
+ budget: fileBlockBudget(path.join(tmpdir(), "gh-router-stopgate")),
10694
+ baseline: fileBaselineStore(path.join(tmpdir(), "gh-router-stopgate-baseline")),
10293
10695
  isEnabledForRepo: (cwd) => stopGateEnabledForRepo(cwd),
10294
10696
  resolveChecks: mode ? buildResolveChecks(mode) : void 0,
10295
10697
  timeoutMs: timeoutEnv,
@@ -10308,10 +10710,29 @@ const internalStopHook = defineCommand({
10308
10710
  process.exitCode = decision.exitCode;
10309
10711
  }
10310
10712
  });
10311
-
10312
10713
  //#endregion
10313
10714
  //#region src/internal-stop-review.ts
10314
10715
  /**
10716
+ * The internal `internal-stop-review` subcommand: the DETACHED background process
10717
+ * the Stop hook spawns (fire-and-forget, unref'd) after a GREEN deterministic
10718
+ * gate. It runs the advisory cross-lab review described in
10719
+ * `docs/hook-v2-design.md` — ADVISORY ONLY, it never blocks anything (the Stop
10720
+ * hook already returned exit 0 before this process even started).
10721
+ *
10722
+ * It reads a JSON payload from stdin (`{session_id, cwd, diff, prompt,
10723
+ * transcript_path}`), calls the read-only `workers/review` MCP tool over loopback
10724
+ * HTTP (gpt-5.6-sol, the live working tree, an accountability brief: judge the diff
10725
+ * against the user's actual ask for wrong-spec / vacuous-tests / incompleteness),
10726
+ * and writes the findings to the per-session findings file. The next
10727
+ * `UserPromptSubmit` surfaces them to Claude NON-authoritatively, and the user
10728
+ * sees them as an accountability log.
10729
+ *
10730
+ * Everything is best-effort: a missing proxy URL/nonce, an unavailable model, a
10731
+ * review error, or an empty result all end the process with NO findings written
10732
+ * and exit 0. It NEVER authors or runs tests (that path was rejected in design as
10733
+ * non-monotone — see the handoff).
10734
+ */
10735
+ /**
10315
10736
  * Read the JSON payload. The Stop hook writes it to a temp file (synchronously,
10316
10737
  * before spawning) and passes the path via `GH_ROUTER_STOP_REVIEW_PAYLOAD` — this
10317
10738
  * avoids the stdin-flush-before-parent-exit race a pipe would have for a large
@@ -10340,12 +10761,12 @@ async function readPayload() {
10340
10761
  /** Embed at most this many diff bytes in the review brief; the reviewer reads the
10341
10762
  * live tree itself for anything beyond it, so a giant diff never blows the model
10342
10763
  * window. The Stop hook already caps the captured diff at 2 MiB. */
10343
- const MAX_EMBEDDED_DIFF_BYTES = 200 * 1024;
10764
+ const MAX_EMBEDDED_DIFF_BYTES = 204800;
10344
10765
  /** Wall-clock the stop-gate reviewer may take. This is INDEPENDENT of the
10345
10766
  * autonomous worker's wall-clock cap (`DEFAULT_MAX_WALLCLOCK_MS`, now 6h) —
10346
10767
  * it bounds this one detached review request. Nothing waits on this process,
10347
10768
  * so the bound only stops a hung request from lingering forever. */
10348
- const REVIEW_TIMEOUT_MS = 2100 * 1e3;
10769
+ const REVIEW_TIMEOUT_MS = 21e5;
10349
10770
  function buildReviewBrief(payload) {
10350
10771
  const diff = payload.diff.length > MAX_EMBEDDED_DIFF_BYTES ? `${payload.diff.slice(0, MAX_EMBEDDED_DIFF_BYTES)}\n\n[diff truncated at ${MAX_EMBEDDED_DIFF_BYTES} bytes — read the files directly for the rest]` : payload.diff;
10351
10772
  return `You are an INDEPENDENT accountability reviewer. A coding agent just finished a turn and its working-tree diff passed the deterministic checks (typecheck/test/lint). Your job is to judge whether the change ACTUALLY does what the user asked — passing checks does not prove that.
@@ -10403,10 +10824,28 @@ const internalStopReview = defineCommand({
10403
10824
  } catch {}
10404
10825
  }
10405
10826
  });
10406
-
10407
10827
  //#endregion
10408
10828
  //#region src/internal-worker-guard.ts
10409
10829
  /**
10830
+ * The internal `internal-worker-guard` subcommand: the executable a spawned
10831
+ * Claude Code session's `PreToolUse` hook invokes for the workers tools
10832
+ * (registered into the mirrored settings.json by the launcher, with a matcher
10833
+ * scoped to `^mcp__<workersKey>__(explore|…)$`).
10834
+ *
10835
+ * It enforces the "workers are non-blocking" invariant: the MAIN agent (and any
10836
+ * non-dispatcher subagent) is DENIED a raw `mcp__<workersKey>__<mode>` call and
10837
+ * redirected to the matching background `worker-<mode>` dispatcher subagent; the
10838
+ * dispatcher's own call (identified by `agent_type` in the payload) is ALLOWED.
10839
+ * All the logic lives in the pure `decideWorkerGuard` (worker-dispatch.ts); this
10840
+ * entry is just stdin → decide → stdout.
10841
+ *
10842
+ * Output contract (Claude Code PreToolUse):
10843
+ * - ALLOW → print nothing, exit 0 (normal permission flow proceeds).
10844
+ * - DENY → print the `{hookSpecificOutput:{permissionDecision:"deny",…}}`
10845
+ * JSON to stdout, exit 0 (the JSON, not the exit code, blocks it).
10846
+ * Fail CLOSED: an unparseable payload for a matched worker tool is denied.
10847
+ */
10848
+ /**
10410
10849
  * Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`) — same
10411
10850
  * rationale as `internal-prompt-submit`: an async stdin read leaves an in-flight
10412
10851
  * libuv FS request that races Windows process teardown. Hooks always receive
@@ -10453,7 +10892,6 @@ const internalWorkerGuard = defineCommand({
10453
10892
  process.exitCode = 0;
10454
10893
  }
10455
10894
  });
10456
-
10457
10895
  //#endregion
10458
10896
  //#region src/models.ts
10459
10897
  const models = defineCommand({
@@ -10513,9 +10951,9 @@ const models = defineCommand({
10513
10951
  process.stdout.write(lines.join("\n"));
10514
10952
  }
10515
10953
  });
10516
- function filterModels(models$1, pattern) {
10954
+ function filterModels(models, pattern) {
10517
10955
  const needle = pattern.toLowerCase();
10518
- return models$1.filter((m) => {
10956
+ return models.filter((m) => {
10519
10957
  return [
10520
10958
  m.id,
10521
10959
  m.name,
@@ -10526,9 +10964,9 @@ function filterModels(models$1, pattern) {
10526
10964
  ].join(" ").toLowerCase().includes(needle);
10527
10965
  });
10528
10966
  }
10529
- function groupByVendor(models$1) {
10967
+ function groupByVendor(models) {
10530
10968
  const map = /* @__PURE__ */ new Map();
10531
- for (const m of models$1) {
10969
+ for (const m of models) {
10532
10970
  const key = m.vendor || "(unknown vendor)";
10533
10971
  const bucket = map.get(key);
10534
10972
  if (bucket) bucket.push(m);
@@ -10605,12 +11043,9 @@ function formatTokens(n) {
10605
11043
  if (n >= 1e3 && n % 1e3 === 0) return `${n / 1e3}k`;
10606
11044
  return `${n}`;
10607
11045
  }
10608
-
10609
11046
  //#endregion
10610
11047
  //#region src/lib/serve/cloudcli.ts
10611
11048
  const CLOUDCLI_PACKAGE = "@cloudcli-ai/cloudcli";
10612
- /** Pinned, tested known-good version. Never `@latest` (see docs/serve-control-plane.md). */
10613
- const CLOUDCLI_PINNED_VERSION = "1.36.1";
10614
11049
  /**
10615
11050
  * Build the env handed to the CloudCLI child. Starts from `buildEnv()` — the
10616
11051
  * vetted secret-stripping ALLOWLIST used for worker bash (drops GITHUB_TOKEN,
@@ -10626,10 +11061,10 @@ function composeCloudCliChildEnv(anthropicVars) {
10626
11061
  ...anthropicVars
10627
11062
  };
10628
11063
  }
10629
- const SERVER_ENTRY_REL = nodePath.join("dist-server", "server", "index.js");
11064
+ const SERVER_ENTRY_REL = path.join("dist-server", "server", "index.js");
10630
11065
  /** Where the package's server entry lives under a given install root. */
10631
11066
  function serverEntryUnderRoot(root) {
10632
- return nodePath.join(root, "node_modules", CLOUDCLI_PACKAGE, SERVER_ENTRY_REL);
11067
+ return path.join(root, "node_modules", CLOUDCLI_PACKAGE, SERVER_ENTRY_REL);
10633
11068
  }
10634
11069
  async function exists(p) {
10635
11070
  try {
@@ -10641,7 +11076,7 @@ async function exists(p) {
10641
11076
  }
10642
11077
  async function readVersion(pkgRoot) {
10643
11078
  try {
10644
- const raw = await fs.readFile(nodePath.join(pkgRoot, "node_modules", CLOUDCLI_PACKAGE, "package.json"), "utf8");
11079
+ const raw = await fs.readFile(path.join(pkgRoot, "node_modules", CLOUDCLI_PACKAGE, "package.json"), "utf8");
10645
11080
  return JSON.parse(raw).version ?? null;
10646
11081
  } catch {
10647
11082
  return null;
@@ -10655,9 +11090,9 @@ async function readVersion(pkgRoot) {
10655
11090
  * Never installs globally, never `@latest`.
10656
11091
  */
10657
11092
  async function resolveCloudCli(opts) {
10658
- const version$2 = opts.version ?? CLOUDCLI_PINNED_VERSION;
11093
+ const version = opts.version ?? "1.36.1";
10659
11094
  if (opts.cliPath) {
10660
- const direct = nodePath.join(opts.cliPath, SERVER_ENTRY_REL);
11095
+ const direct = path.join(opts.cliPath, SERVER_ENTRY_REL);
10661
11096
  const underRoot = serverEntryUnderRoot(opts.cliPath);
10662
11097
  if (await exists(direct)) return {
10663
11098
  serverEntry: direct,
@@ -10675,42 +11110,42 @@ async function resolveCloudCli(opts) {
10675
11110
  serverEntry: entry,
10676
11111
  version: await readVersion(root)
10677
11112
  };
10678
- if (opts.noInstall) throw new Error(`CloudCLI is not installed and --no-install was given. Install it with:\n npm i --prefix "${root}" ${CLOUDCLI_PACKAGE}@${version$2}`);
10679
- await installCloudCli(root, version$2);
11113
+ if (opts.noInstall) throw new Error(`CloudCLI is not installed and --no-install was given. Install it with:\n npm i --prefix "${root}" ${CLOUDCLI_PACKAGE}@${version}`);
11114
+ await installCloudCli(root, version);
10680
11115
  if (!await exists(entry)) throw new Error(`CloudCLI install completed but the server entry is missing (${entry}).`);
10681
11116
  return {
10682
11117
  serverEntry: entry,
10683
11118
  version: await readVersion(root)
10684
11119
  };
10685
11120
  }
10686
- async function installCloudCli(root, version$2) {
11121
+ async function installCloudCli(root, version) {
10687
11122
  const npm = resolveExecutable("npm");
10688
11123
  if (!npm) throw new Error("npm not found on PATH — cannot install CloudCLI. Install Node.js/npm, or pass --cloudcli-path to an existing install.");
10689
11124
  await fs.mkdir(root, {
10690
11125
  recursive: true,
10691
11126
  mode: 448
10692
11127
  });
10693
- const pkgJson = nodePath.join(root, "package.json");
11128
+ const pkgJson = path.join(root, "package.json");
10694
11129
  if (!await exists(pkgJson)) await fs.writeFile(pkgJson, JSON.stringify({
10695
11130
  name: "gh-router-cloudcli-host",
10696
11131
  private: true
10697
11132
  }, null, 2));
10698
- consola.info(`Installing ${CLOUDCLI_PACKAGE}@${version$2} (one-time, ~a minute; builds native deps)…`);
11133
+ consola.info(`Installing ${CLOUDCLI_PACKAGE}@${version} (one-time, ~a minute; builds native deps)…`);
10699
11134
  const { code, stderr } = await runCommandCapture([
10700
11135
  npm,
10701
11136
  "install",
10702
- `${CLOUDCLI_PACKAGE}@${version$2}`,
11137
+ `${CLOUDCLI_PACKAGE}@${version}`,
10703
11138
  "--prefix",
10704
11139
  root,
10705
11140
  "--no-audit",
10706
11141
  "--no-fund",
10707
11142
  "--loglevel=error"
10708
- ], { timeoutMs: 10 * 6e4 });
10709
- if (code !== 0) throw new Error(`CloudCLI install failed (npm exit ${code}). This often means no network, a corporate proxy, or missing Windows build tools for native deps.\nInstall manually then re-run with --cloudcli-path:\n npm i --prefix "${root}" ${CLOUDCLI_PACKAGE}@${version$2}\n${stderr.slice(0, 400)}`);
11143
+ ], { timeoutMs: 6e5 });
11144
+ if (code !== 0) throw new Error(`CloudCLI install failed (npm exit ${code}). This often means no network, a corporate proxy, or missing Windows build tools for native deps.\nInstall manually then re-run with --cloudcli-path:\n npm i --prefix "${root}" ${CLOUDCLI_PACKAGE}@${version}\n${stderr.slice(0, 400)}`);
10710
11145
  }
10711
11146
  /** Spawn CloudCLI on a loopback port with the given (already-filtered) env. */
10712
11147
  function spawnCloudCli(opts) {
10713
- const databasePath = nodePath.join(PATHS.CLOUDCLI_HOME, "auth.db");
11148
+ const databasePath = path.join(PATHS.CLOUDCLI_HOME, "auth.db");
10714
11149
  const node = resolveExecutable("node") ?? process.execPath;
10715
11150
  const logPath = process.env.GH_ROUTER_CLOUDCLI_LOG;
10716
11151
  let stdio = "ignore";
@@ -10776,7 +11211,7 @@ async function waitForCloudCliReady(port, timeoutMs = 6e4) {
10776
11211
  }
10777
11212
  throw new Error(`CloudCLI did not become ready within ${timeoutMs}ms`);
10778
11213
  }
10779
- const secretFile = () => nodePath.join(PATHS.CLOUDCLI_HOME, ".serve-secret.json");
11214
+ const secretFile = () => path.join(PATHS.CLOUDCLI_HOME, ".serve-secret.json");
10780
11215
  async function loadPassword(username) {
10781
11216
  try {
10782
11217
  const raw = await fs.readFile(secretFile(), "utf8");
@@ -10803,21 +11238,21 @@ async function persistPassword(username, password) {
10803
11238
  async function mintJwt(port, username, needsSetup) {
10804
11239
  const existing = await loadPassword(username);
10805
11240
  if (needsSetup) {
10806
- const password$1 = existing ?? randomBytes(24).toString("base64url");
10807
- const r$1 = await req(port, "POST", "/api/auth/register", {
11241
+ const password = existing ?? randomBytes(24).toString("base64url");
11242
+ const r = await req(port, "POST", "/api/auth/register", {
10808
11243
  username,
10809
- password: password$1
11244
+ password
10810
11245
  });
10811
- if (r$1.status === 200) {
10812
- const token = JSON.parse(r$1.body).token;
11246
+ if (r.status === 200) {
11247
+ const token = JSON.parse(r.body).token;
10813
11248
  if (token) {
10814
- await persistPassword(username, password$1);
11249
+ await persistPassword(username, password);
10815
11250
  return token;
10816
11251
  }
10817
11252
  }
10818
11253
  }
10819
11254
  const password = existing;
10820
- if (!password) throw new Error(`CloudCLI already has a user but no stored password for "${username}". Delete ${nodePath.join(PATHS.CLOUDCLI_HOME, "auth.db")} and retry.`);
11255
+ if (!password) throw new Error(`CloudCLI already has a user but no stored password for "${username}". Delete ${path.join(PATHS.CLOUDCLI_HOME, "auth.db")} and retry.`);
10821
11256
  const r = await req(port, "POST", "/api/auth/login", {
10822
11257
  username,
10823
11258
  password
@@ -10826,9 +11261,8 @@ async function mintJwt(port, username, needsSetup) {
10826
11261
  const token = JSON.parse(r.body).token;
10827
11262
  if (token) return token;
10828
11263
  }
10829
- throw new Error(`Could not obtain a CloudCLI session token (status ${r.status}). If a stale isolated DB exists, delete ${nodePath.join(PATHS.CLOUDCLI_HOME, "auth.db")} and retry.`);
11264
+ throw new Error(`Could not obtain a CloudCLI session token (status ${r.status}). If a stale isolated DB exists, delete ${path.join(PATHS.CLOUDCLI_HOME, "auth.db")} and retry.`);
10830
11265
  }
10831
-
10832
11266
  //#endregion
10833
11267
  //#region src/lib/serve/devtunnel.ts
10834
11268
  const SERVE_TUNNEL_LABEL = "github-router-serve";
@@ -11095,7 +11529,6 @@ async function startDevtunnel(port, opts = {}) {
11095
11529
  "github-router-serve-control-plane"
11096
11530
  ], timeoutMs);
11097
11531
  }
11098
-
11099
11532
  //#endregion
11100
11533
  //#region src/lib/serve/enhancements.ts
11101
11534
  const NOOP = { cleanup: async () => {} };
@@ -11176,7 +11609,7 @@ async function provisionServeEnhancements(serverUrl, opts = {}) {
11176
11609
  if (s.name.startsWith("gh-first-mate") || s.name === "gh-artifact-review") continue;
11177
11610
  if ((await writeInjectedSkill(s.name, s.md).catch(() => ({ written: false }))).written) skillsWritten++;
11178
11611
  }
11179
- const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
11612
+ const settingsPath = path.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
11180
11613
  await injectAttributionSuppressionIntoSettingsFile(settingsPath).catch(() => {});
11181
11614
  const strip = await sanitizeServeSettingsEnv(settingsPath).catch((err) => {
11182
11615
  consola.warn(`Could not sanitize serve settings env: ${String(err)}`);
@@ -11186,16 +11619,20 @@ async function provisionServeEnhancements(serverUrl, opts = {}) {
11186
11619
  if (process.env.GH_ROUTER_SERVE_NO_AUTO_APPROVE !== "1") await configureServeDefaultPermissionMode(settingsPath).catch((err) => consola.warn(`Could not set serve permission defaults: ${String(err)}`));
11187
11620
  await injectAllowRules(settingsPath, planModeAllowRules(injected.ok ? injected.serversAdded : [...Object.values(groupKeys).filter((k) => Boolean(k)), ...opts.codexCli === true ? ["codex-cli"] : []])).catch((err) => consola.warn(`Could not auto-approve injected tools: ${String(err)}`));
11188
11621
  if (workerToolsEnabled()) {
11189
- await injectStopHookIntoSettingsFile(settingsPath, buildPromptSubmitHookCommand(process.execPath, process.argv[1]), "UserPromptSubmit", 45).catch((err) => consola.warn(`Could not register the UserPromptSubmit hook: ${String(err)}`));
11622
+ const promptCmd = buildPromptSubmitHookCommand(process.execPath, process.argv[1]);
11623
+ await injectStopHookIntoSettingsFile(settingsPath, promptCmd, "UserPromptSubmit", 45).catch((err) => consola.warn(`Could not register the UserPromptSubmit hook: ${String(err)}`));
11190
11624
  if (!injected.ok) consola.warn("Workers non-blocking guard NOT registered: subagent MCP injection fell back to parent-only, so worker-* dispatchers cannot reach the workers server. Raw worker tools remain usable on the main thread.");
11191
11625
  else if (process.env.GH_ROUTER_DISABLE_WORKER_GUARD !== "1") {
11192
11626
  const workersKey = workersKeyOf(groupKeys);
11193
11627
  const modes = activeDispatchModes({ browse: browseAllowed && browseAgentEnabled() });
11194
- await injectStopHookIntoSettingsFile(settingsPath, buildWorkerGuardHookCommand(process.execPath, process.argv[1], workersKey, modes), "PreToolUse", 10, guardToolMatcher(workersKey, modes)).catch((err) => consola.warn(`Could not register the workers PreToolUse guard hook: ${String(err)}`));
11628
+ const cmd = buildWorkerGuardHookCommand(process.execPath, process.argv[1], workersKey, modes);
11629
+ const matcher = guardToolMatcher(workersKey, modes);
11630
+ await injectStopHookIntoSettingsFile(settingsPath, cmd, "PreToolUse", 10, matcher).catch((err) => consola.warn(`Could not register the workers PreToolUse guard hook: ${String(err)}`));
11195
11631
  } else consola.info("Workers non-blocking guard disabled via GH_ROUTER_DISABLE_WORKER_GUARD=1; raw worker tools remain callable on the main thread.");
11196
11632
  }
11197
11633
  if (planReviewEnabled()) try {
11198
- await injectStopHookIntoSettingsFile(settingsPath, buildPlanReviewHookCommand(process.execPath, process.argv[1]), "PostToolUse", void 0, "ExitPlanMode");
11634
+ const command = buildPlanReviewHookCommand(process.execPath, process.argv[1]);
11635
+ await injectStopHookIntoSettingsFile(settingsPath, command, "PostToolUse", void 0, "ExitPlanMode");
11199
11636
  } catch (err) {
11200
11637
  consola.warn(`Could not register the advisory plan-review hook: ${String(err)}`);
11201
11638
  }
@@ -11211,7 +11648,6 @@ async function provisionServeEnhancements(serverUrl, opts = {}) {
11211
11648
  return NOOP;
11212
11649
  }
11213
11650
  }
11214
-
11215
11651
  //#endregion
11216
11652
  //#region src/lib/serve/provider-facade.ts
11217
11653
  function isObject(value) {
@@ -11274,9 +11710,9 @@ function rewriteModels(upstreamJson, ctx) {
11274
11710
  if (!isObject(upstreamJson)) return null;
11275
11711
  const data = envelopedData(upstreamJson);
11276
11712
  if (!data || !isObject(data.models) || data.cache === void 0) return null;
11277
- const models$1 = ctx.getModels()?.data;
11278
- if (!Array.isArray(models$1) || models$1.length === 0) return null;
11279
- const options = models$1.filter(isPickerModel).map((m) => {
11713
+ const models = ctx.getModels()?.data;
11714
+ if (!Array.isArray(models) || models.length === 0) return null;
11715
+ const options = models.filter(isPickerModel).map((m) => {
11280
11716
  const option = {
11281
11717
  value: m.id,
11282
11718
  label: m.name || m.id,
@@ -11310,21 +11746,21 @@ async function rewriteMcp(upstreamJson, ctx, query) {
11310
11746
  if (!isObject(upstreamJson) || query.get("scope") !== "user") return null;
11311
11747
  const data = envelopedData(upstreamJson);
11312
11748
  if (!data || !Array.isArray(data.servers)) return null;
11313
- const mirror = await readJson(nodePath.join(ctx.claudeConfigDir, ".claude.json"));
11749
+ const mirror = await readJson(path.join(ctx.claudeConfigDir, ".claude.json"));
11314
11750
  if (!isObject(mirror) || !isObject(mirror.mcpServers)) return null;
11315
- const existing = new Set(data.servers.filter(isObject).map((s) => s.name).filter((name$1) => typeof name$1 === "string"));
11751
+ const existing = new Set(data.servers.filter(isObject).map((s) => s.name).filter((name) => typeof name === "string"));
11316
11752
  const additions = [];
11317
- for (const [name$1, entry] of Object.entries(mirror.mcpServers)) {
11318
- if (existing.has(name$1) || !isObject(entry)) continue;
11319
- const server$1 = {
11753
+ for (const [name, entry] of Object.entries(mirror.mcpServers)) {
11754
+ if (existing.has(name) || !isObject(entry)) continue;
11755
+ const server = {
11320
11756
  provider: "claude",
11321
- name: name$1,
11757
+ name,
11322
11758
  scope: "user",
11323
11759
  transport: entry.type === "http" ? "http" : "stdio"
11324
11760
  };
11325
- if (typeof entry.url === "string") server$1.url = entry.url;
11326
- additions.push(server$1);
11327
- existing.add(name$1);
11761
+ if (typeof entry.url === "string") server.url = entry.url;
11762
+ additions.push(server);
11763
+ existing.add(name);
11328
11764
  }
11329
11765
  if (additions.length === 0) return null;
11330
11766
  const nextData = cloneObject(data);
@@ -11338,7 +11774,7 @@ async function rewriteSkills(upstreamJson, ctx) {
11338
11774
  if (!isObject(upstreamJson)) return null;
11339
11775
  const data = envelopedData(upstreamJson);
11340
11776
  if (!data || !Array.isArray(data.skills)) return null;
11341
- const skills = await readSkillDirs(nodePath.join(ctx.claudeConfigDir, "skills"));
11777
+ const skills = await readSkillDirs(path.join(ctx.claudeConfigDir, "skills"));
11342
11778
  if (skills.length === 0) return null;
11343
11779
  const existing = new Set(data.skills.filter(isObject).map((s) => s.command).filter((command) => typeof command === "string"));
11344
11780
  const additions = skills.filter((skill) => !existing.has(skill.name)).map((skill) => ({
@@ -11358,10 +11794,10 @@ async function rewriteSkills(upstreamJson, ctx) {
11358
11794
  }
11359
11795
  async function rewriteCommands(upstreamJson, ctx) {
11360
11796
  if (!isObject(upstreamJson) || !Array.isArray(upstreamJson.custom)) return null;
11361
- const dirs = [nodePath.join(ctx.claudeConfigDir, "commands"), nodePath.join(ctx.claudeConfigDir, ".claude", "commands")];
11797
+ const dirs = [path.join(ctx.claudeConfigDir, "commands"), path.join(ctx.claudeConfigDir, ".claude", "commands")];
11362
11798
  const commandFiles = (await Promise.all(dirs.map(readCommandFiles))).flat();
11363
11799
  if (commandFiles.length === 0) return null;
11364
- const existing = new Set(upstreamJson.custom.filter(isObject).map((c) => c.name).filter((name$1) => typeof name$1 === "string"));
11800
+ const existing = new Set(upstreamJson.custom.filter(isObject).map((c) => c.name).filter((name) => typeof name === "string"));
11365
11801
  const additions = commandFiles.filter((command) => !existing.has(command.name)).map((command) => ({
11366
11802
  name: command.name,
11367
11803
  path: command.relativePath,
@@ -11402,7 +11838,7 @@ async function readSkillDirs(skillsDir) {
11402
11838
  try {
11403
11839
  const entries = await fs.readdir(skillsDir, { withFileTypes: true });
11404
11840
  return await Promise.all(entries.filter((entry) => entry.isDirectory() && entry.name.startsWith("gh-")).map(async (entry) => {
11405
- const sourcePath = nodePath.join(skillsDir, entry.name, "SKILL.md");
11841
+ const sourcePath = path.join(skillsDir, entry.name, "SKILL.md");
11406
11842
  let description = "";
11407
11843
  try {
11408
11844
  description = parseFrontmatterField(await fs.readFile(sourcePath, "utf8"), "description") ?? "";
@@ -11434,21 +11870,21 @@ async function readCommandFilesRecursive(root, dir) {
11434
11870
  }
11435
11871
  const out = [];
11436
11872
  for (const entry of entries) {
11437
- const filePath = nodePath.join(dir, entry.name);
11873
+ const filePath = path.join(dir, entry.name);
11438
11874
  if (entry.isDirectory()) {
11439
11875
  out.push(...await readCommandFilesRecursive(root, filePath));
11440
11876
  continue;
11441
11877
  }
11442
11878
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
11443
- const relativePath = normalizeRelative(nodePath.relative(root, filePath));
11444
- const name$1 = relativePath.replace(/\.md$/i, "");
11879
+ const relativePath = normalizeRelative(path.relative(root, filePath));
11880
+ const name = relativePath.replace(/\.md$/i, "");
11445
11881
  let description = "";
11446
11882
  try {
11447
11883
  const md = await fs.readFile(filePath, "utf8");
11448
11884
  description = parseFrontmatterField(md, "description") ?? firstMarkdownLine(md);
11449
11885
  } catch {}
11450
11886
  out.push({
11451
- name: name$1,
11887
+ name,
11452
11888
  relativePath,
11453
11889
  filePath,
11454
11890
  description
@@ -11477,12 +11913,11 @@ function firstMarkdownLine(markdown) {
11477
11913
  return "";
11478
11914
  }
11479
11915
  function normalizeRelative(value) {
11480
- return value.split(nodePath.sep).join("/");
11916
+ return value.split(path.sep).join("/");
11481
11917
  }
11482
11918
  function countCommands(builtIn) {
11483
11919
  return Array.isArray(builtIn) ? builtIn.length : 0;
11484
11920
  }
11485
-
11486
11921
  //#endregion
11487
11922
  //#region src/lib/serve/reverse-proxy.ts
11488
11923
  /**
@@ -11504,7 +11939,7 @@ const CLAUDE_SETTINGS_KEY = "claude-settings";
11504
11939
  * the question widget / plan card. Stripped defensively at the seed chokepoint
11505
11940
  * so no upstream list (mcp names, a future caller) can reintroduce them.
11506
11941
  */
11507
- const NEVER_AUTO_APPROVE = new Set(["AskUserQuestion", "ExitPlanMode"]);
11942
+ const NEVER_AUTO_APPROVE = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
11508
11943
  /**
11509
11944
  * Embed the token as a JS string literal. JSON.stringify escapes quotes and
11510
11945
  * backslashes; we additionally neutralize `<` so the value can never break out
@@ -11540,18 +11975,18 @@ async function startReverseProxy(opts) {
11540
11975
  const { targetHost, targetPort, bindHost, bindPort, authToken } = opts;
11541
11976
  const ownOrigin = `http://${bindHost}:${bindPort}`;
11542
11977
  const injection = buildInjection(authToken, opts.seedToolSettings === true, opts.seedAllowedTools ?? []);
11543
- const allowedHosts = new Set([
11978
+ const allowedHosts = /* @__PURE__ */ new Set([
11544
11979
  `127.0.0.1:${bindPort}`,
11545
11980
  `localhost:${bindPort}`,
11546
11981
  `[::1]:${bindPort}`,
11547
11982
  ...(opts.extraAllowedHosts ?? []).map((h) => h.toLowerCase())
11548
11983
  ]);
11549
- const loopbackHosts = new Set([
11984
+ const loopbackHosts = /* @__PURE__ */ new Set([
11550
11985
  `127.0.0.1:${bindPort}`,
11551
11986
  `localhost:${bindPort}`,
11552
11987
  `[::1]:${bindPort}`
11553
11988
  ]);
11554
- const allowedOrigins = new Set([
11989
+ const allowedOrigins = /* @__PURE__ */ new Set([
11555
11990
  `http://127.0.0.1:${bindPort}`,
11556
11991
  `http://localhost:${bindPort}`,
11557
11992
  `http://[::1]:${bindPort}`,
@@ -11571,7 +12006,7 @@ async function startReverseProxy(opts) {
11571
12006
  const hostAllowed = (host) => !!host && (allowedHosts.has(host.toLowerCase()) || isDevtunnelHost(host.toLowerCase()));
11572
12007
  const originAllowed = (origin) => !origin || allowedOrigins.has(origin.toLowerCase()) || isDevtunnelOrigin(origin);
11573
12008
  const liveSockets = /* @__PURE__ */ new Set();
11574
- const server$1 = http.createServer((clientReq, clientRes) => {
12009
+ const server = http.createServer((clientReq, clientRes) => {
11575
12010
  clientReq.on("error", () => clientRes.destroy());
11576
12011
  clientRes.on("error", () => clientReq.destroy());
11577
12012
  if (!hostAllowed(clientReq.headers.host) || !originAllowed(clientReq.headers.origin)) {
@@ -11579,7 +12014,7 @@ async function startReverseProxy(opts) {
11579
12014
  clientRes.end("Forbidden");
11580
12015
  return;
11581
12016
  }
11582
- if ((clientReq.url ?? "").split("?")[0] === SERVE_IDENTITY_PATH) {
12017
+ if ((clientReq.url ?? "").split("?")[0] === "/__github-router-serve__") {
11583
12018
  if (!loopbackHosts.has(String(clientReq.headers.host ?? "").toLowerCase())) {
11584
12019
  clientRes.writeHead(404, { "content-type": "text/plain" });
11585
12020
  clientRes.end("Not found");
@@ -11588,7 +12023,7 @@ async function startReverseProxy(opts) {
11588
12023
  clientRes.writeHead(200, { "content-type": "application/json" });
11589
12024
  clientRes.end(JSON.stringify({
11590
12025
  service: SERVE_IDENTITY_SERVICE,
11591
- version: getPackageVersion()
12026
+ version: getPackageVersion$1()
11592
12027
  }));
11593
12028
  return;
11594
12029
  }
@@ -11673,7 +12108,7 @@ async function startReverseProxy(opts) {
11673
12108
  });
11674
12109
  clientReq.pipe(proxyReq);
11675
12110
  });
11676
- server$1.on("upgrade", (clientReq, clientSock, clientHead) => {
12111
+ server.on("upgrade", (clientReq, clientSock, clientHead) => {
11677
12112
  clientSock.on("error", () => clientSock.destroy());
11678
12113
  liveSockets.add(clientSock);
11679
12114
  clientSock.once("close", () => liveSockets.delete(clientSock));
@@ -11718,15 +12153,15 @@ async function startReverseProxy(opts) {
11718
12153
  });
11719
12154
  await new Promise((resolve, reject) => {
11720
12155
  const onError = (err) => reject(err);
11721
- server$1.once("error", onError);
11722
- server$1.listen(bindPort, bindHost, () => {
11723
- server$1.removeListener("error", onError);
12156
+ server.once("error", onError);
12157
+ server.listen(bindPort, bindHost, () => {
12158
+ server.removeListener("error", onError);
11724
12159
  resolve();
11725
12160
  });
11726
12161
  });
11727
- const addr = server$1.address();
12162
+ const addr = server.address();
11728
12163
  if (addr && addr.address !== "127.0.0.1" && addr.address !== "::1") {
11729
- await new Promise((r) => server$1.close(() => r()));
12164
+ await new Promise((r) => server.close(() => r()));
11730
12165
  throw new Error(`reverse proxy refused non-loopback bind (got ${addr.address})`);
11731
12166
  }
11732
12167
  return {
@@ -11736,14 +12171,13 @@ async function startReverseProxy(opts) {
11736
12171
  for (const s of liveSockets) s.destroy();
11737
12172
  liveSockets.clear();
11738
12173
  const t = setTimeout(resolve, 1500);
11739
- server$1.close(() => {
12174
+ server.close(() => {
11740
12175
  clearTimeout(t);
11741
12176
  resolve();
11742
12177
  });
11743
12178
  })
11744
12179
  };
11745
12180
  }
11746
-
11747
12181
  //#endregion
11748
12182
  //#region src/serve.ts
11749
12183
  async function getFreePort() {
@@ -11777,7 +12211,7 @@ function isPortFree(port) {
11777
12211
  */
11778
12212
  function probeExistingServe(port) {
11779
12213
  return new Promise((resolve) => {
11780
- const req$1 = http.get({
12214
+ const req = http.get({
11781
12215
  host: "127.0.0.1",
11782
12216
  port,
11783
12217
  path: SERVE_IDENTITY_PATH,
@@ -11800,8 +12234,8 @@ function probeExistingServe(port) {
11800
12234
  });
11801
12235
  res.on("error", () => resolve(false));
11802
12236
  });
11803
- req$1.on("timeout", () => req$1.destroy());
11804
- req$1.on("error", () => resolve(false));
12237
+ req.on("timeout", () => req.destroy());
12238
+ req.on("error", () => resolve(false));
11805
12239
  });
11806
12240
  }
11807
12241
  /**
@@ -11910,7 +12344,7 @@ const serve$1 = defineCommand({
11910
12344
  if (args["no-open"] !== true) openBrowser(existingUrl);
11911
12345
  return;
11912
12346
  }
11913
- const { server: server$1, serverUrl } = await setupAndServe({
12347
+ const { server, serverUrl } = await setupAndServe({
11914
12348
  ...parsed,
11915
12349
  port: void 0,
11916
12350
  silent: true
@@ -11993,7 +12427,7 @@ const serve$1 = defineCommand({
11993
12427
  } catch {}
11994
12428
  if (reverseClose) await reverseClose().catch(() => {});
11995
12429
  try {
11996
- server$1.close?.();
12430
+ server.close?.();
11997
12431
  } catch {}
11998
12432
  await stopKeepAwake().catch(() => {});
11999
12433
  await enhancements.cleanup().catch(() => {});
@@ -12094,12 +12528,11 @@ const serve$1 = defineCommand({
12094
12528
  if (args["no-open"] !== true) openBrowser(reverse.url);
12095
12529
  }
12096
12530
  });
12097
-
12098
12531
  //#endregion
12099
12532
  //#region src/lib/shell.ts
12100
12533
  function getShell() {
12101
- const { platform: platform$1, env } = process$1;
12102
- if (platform$1 === "win32") {
12534
+ const { platform, env } = process$1;
12535
+ if (platform === "win32") {
12103
12536
  if (env.SHELL) {
12104
12537
  if (env.SHELL.endsWith("zsh")) return "zsh";
12105
12538
  if (env.SHELL.endsWith("fish")) return "fish";
@@ -12157,7 +12590,6 @@ function generateEnvScript(envVars, commandToRun = "") {
12157
12590
  if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : shell === "powershell" ? "; " : " && "}${commandToRun}`;
12158
12591
  return commandBlock || commandToRun;
12159
12592
  }
12160
-
12161
12593
  //#endregion
12162
12594
  //#region src/start.ts
12163
12595
  function printAndCopyCommand(command, label) {
@@ -12173,7 +12605,7 @@ function generateClaudeCodeCommand(serverUrl, model) {
12173
12605
  printAndCopyCommand(generateEnvScript(getClaudeCodeEnvVars(serverUrl, model), "claude --dangerously-skip-permissions"), "Claude Code");
12174
12606
  }
12175
12607
  function generateCodexCommand(serverUrl, model) {
12176
- const codexModel = model ?? DEFAULT_CODEX_MODEL;
12608
+ const codexModel = model ?? "gpt-5.6-sol";
12177
12609
  printAndCopyCommand(generateEnvScript(getCodexEnvVars(serverUrl), `codex -m ${codexModel}`), "Codex CLI");
12178
12610
  }
12179
12611
  const start = defineCommand({
@@ -12203,7 +12635,7 @@ const start = defineCommand({
12203
12635
  const parsed = parseSharedArgs(args);
12204
12636
  const { serverUrl } = await setupAndServe({
12205
12637
  ...parsed,
12206
- port: parsed.port ?? DEFAULT_PORT,
12638
+ port: parsed.port ?? 8787,
12207
12639
  silent: false
12208
12640
  });
12209
12641
  runSelfUpdate({ selfUpdate: args["self-update"] !== false });
@@ -12216,7 +12648,6 @@ const start = defineCommand({
12216
12648
  consola.box(`🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`);
12217
12649
  }
12218
12650
  });
12219
-
12220
12651
  //#endregion
12221
12652
  //#region src/main.ts
12222
12653
  process.on("unhandledRejection", (error) => {
@@ -12226,12 +12657,12 @@ process.on("uncaughtException", (error) => {
12226
12657
  consola.error("Uncaught exception:", error);
12227
12658
  process.exit(1);
12228
12659
  });
12229
- const version = getPackageVersion();
12660
+ const version = getPackageVersion$1();
12230
12661
  const argv = process.argv.slice(2);
12231
12662
  const isVersionFlag = argv.includes("--version");
12232
12663
  const isInternalHook = argv[0] === "internal-stop-hook" || argv[0] === "internal-prompt-submit" || argv[0] === "internal-stop-review" || argv[0] === "internal-plan-review" || argv[0] === "internal-session-bind" || argv[0] === "internal-workspace-header" || argv[0] === "internal-artifact-open" || argv[0] === "internal-first-mate-guard" || argv[0] === "internal-worker-guard";
12233
12664
  if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
12234
- await runMain(defineCommand({
12665
+ const main = defineCommand({
12235
12666
  meta: {
12236
12667
  name: "github-router",
12237
12668
  version,
@@ -12256,8 +12687,9 @@ await runMain(defineCommand({
12256
12687
  "internal-first-mate-guard": internalFirstMateGuard,
12257
12688
  "internal-worker-guard": internalWorkerGuard
12258
12689
  }
12259
- }));
12260
-
12690
+ });
12691
+ await runMain(main);
12261
12692
  //#endregion
12262
- export { };
12693
+ export {};
12694
+
12263
12695
  //# sourceMappingURL=main.js.map